mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-11 05:14:41 +00:00
337e54c672
Moves from concatted Dockerfiles to pseudo-templated Dockerfiles via a dedicated Rust program. Removes the unmaintained kubernetes, not because we shouldn't have/use it, but because it's unmaintained and needs to be reworked before it's present again. Replaces the compose with the work in the new orchestrator binary which spawns everything as expected. While this arguably re-invents the wheel, it correctly manages secrets and handles the variadic Dockerfiles. Also adds an unrelated patch for zstd and simplifies running services a bit by greater utilizing the existing infrastructure. --- * Delete all Dockerfile fragments, add new orchestator to generate Dockerfiles Enables greater templating. Also delete the unmaintained kubernetes folder *for now*. This should be restored in the future. * Use Dockerfiles from the orchestator * Ignore Dockerfiles in the git repo * Remove CI job to check Dockerfiles are as expected now that they're no longer committed * Remove old Dockerfiles from repo * Use Debian for monero-wallet-rpc * Remove replace_cmds for proper usage of entry-dev Consolidates ports a bit. Updates serai-docker-tests from "compose" to "build". * Only write a new dockerfile if it's distinct Preserves the updated time metadata. * Update serai-docker-tests * Correct the path Dockerfiles are built from * Correct inclusion of orchestration folder in Docker builds * Correct debug/release flagging in the cargo command Apparently, --debug isn't an effective NOP yet an error. * Correct path used to run the Serai node within a Dockerfile * Correct path in Monero Dockerfile * Attempt storing monerod in /usr/bin * Use sudo to move into /usr/bin in CI * Correct 18.3.0 to 18.3.1 * Escape * with quotes * Update deny.toml, ADD orchestration in runtime Dockerfile * Add --detach to the Monero GH CI * Diversify dockerfiles by network * Fixes to network-diversified orchestration * Bitcoin and Monero testnet scripts * Permissions and tweaks * Flatten scripts folders * Add missing folder specification to Monero Dockerfile * Have monero-wallet-rpc specify the monerod login * Have the Docker CMD specify env variables inserted at time of Dockerfile generation They're overrideable with the global enviornment as for tests. This enables variable generation in orchestrator and output to productionized Docker files without creating a life-long file within the Docker container. * Don't add Dockerfiles into Docker containers now that they have secrets Solely add the source code for them as needed to satisfy the workspace bounds. * Download arm64 Monero on arm64 * Ensure constant host architecture when reproducibly building the wasm Host architecture, for some reason, can effect the generated code despite the target architecture always being foreign to the host architecture. * Randomly generate infrastructure keys * Have orchestrator generate a key, be able to create/start containers * Ensure bash is used over sh * Clean dated docs * Change how quoting occurs * Standardize to sh * Have Docker test build the dev Dockerfiles * Only key_gen once * cargo update Adds a patch for zstd and reconciles the breaking nightly change which just occurred. * Use a dedicated network for Serai Also fixes SERAI_HOSTNAME passed to coordinator. * Support providing a key over the env for the Serai node * Enable and document running daemons for tests via serai-orchestrator Has running containers under the dev network port forward the RPC ports. * Use volumes for bitcoin/monero * Use bitcoin's run.sh in GH CI * Only use the volume for testnet (not dev)
318 lines
10 KiB
Rust
318 lines
10 KiB
Rust
use core::ops::Deref;
|
|
use std_shims::{sync::OnceLock, collections::HashSet};
|
|
|
|
use zeroize::Zeroizing;
|
|
use rand_core::OsRng;
|
|
|
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
|
|
|
use tokio::sync::Mutex;
|
|
|
|
use monero_serai::{
|
|
random_scalar,
|
|
rpc::{HttpRpc, Rpc},
|
|
wallet::{
|
|
ViewPair, Scanner,
|
|
address::{Network, AddressType, AddressSpec, AddressMeta, MoneroAddress},
|
|
SpendableOutput, Fee,
|
|
},
|
|
transaction::Transaction,
|
|
};
|
|
|
|
pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) {
|
|
let spend = random_scalar(&mut OsRng);
|
|
let spend_pub = &spend * ED25519_BASEPOINT_TABLE;
|
|
let view = Zeroizing::new(random_scalar(&mut OsRng));
|
|
(
|
|
spend,
|
|
ViewPair::new(spend_pub, view.clone()),
|
|
MoneroAddress {
|
|
meta: AddressMeta::new(Network::Mainnet, AddressType::Standard),
|
|
spend: spend_pub,
|
|
view: view.deref() * ED25519_BASEPOINT_TABLE,
|
|
},
|
|
)
|
|
}
|
|
|
|
// TODO: Support transactions already on-chain
|
|
// TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions
|
|
// TODO: mine as much as needed instead of default 10 blocks
|
|
pub async fn mine_until_unlocked(rpc: &Rpc<HttpRpc>, addr: &str, tx_hash: [u8; 32]) {
|
|
// mine until tx is in a block
|
|
let mut height = rpc.get_height().await.unwrap();
|
|
let mut found = false;
|
|
while !found {
|
|
let block = rpc.get_block_by_number(height - 1).await.unwrap();
|
|
found = match block.txs.iter().find(|&&x| x == tx_hash) {
|
|
Some(_) => true,
|
|
None => {
|
|
rpc.generate_blocks(addr, 1).await.unwrap();
|
|
height += 1;
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
// mine 9 more blocks to unlock the tx
|
|
rpc.generate_blocks(addr, 9).await.unwrap();
|
|
}
|
|
|
|
// Mines 60 blocks and returns an unlocked miner TX output.
|
|
#[allow(dead_code)]
|
|
pub async fn get_miner_tx_output(rpc: &Rpc<HttpRpc>, view: &ViewPair) -> SpendableOutput {
|
|
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
|
|
|
|
// Mine 60 blocks to unlock a miner TX
|
|
let start = rpc.get_height().await.unwrap();
|
|
rpc
|
|
.generate_blocks(&view.address(Network::Mainnet, AddressSpec::Standard).to_string(), 60)
|
|
.await
|
|
.unwrap();
|
|
|
|
let block = rpc.get_block_by_number(start).await.unwrap();
|
|
scanner.scan(rpc, &block).await.unwrap().swap_remove(0).ignore_timelock().swap_remove(0)
|
|
}
|
|
|
|
/// Make sure the weight and fee match the expected calculation.
|
|
pub fn check_weight_and_fee(tx: &Transaction, fee_rate: Fee) {
|
|
let fee = tx.rct_signatures.base.fee;
|
|
|
|
let weight = tx.weight();
|
|
let expected_weight = fee_rate.calculate_weight_from_fee(fee);
|
|
assert_eq!(weight, expected_weight);
|
|
|
|
let expected_fee = fee_rate.calculate_fee_from_weight(weight);
|
|
assert_eq!(fee, expected_fee);
|
|
}
|
|
|
|
pub async fn rpc() -> Rpc<HttpRpc> {
|
|
let rpc = HttpRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap();
|
|
|
|
// Only run once
|
|
if rpc.get_height().await.unwrap() != 1 {
|
|
return rpc;
|
|
}
|
|
|
|
let addr = MoneroAddress {
|
|
meta: AddressMeta::new(Network::Mainnet, AddressType::Standard),
|
|
spend: &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
|
view: &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
|
}
|
|
.to_string();
|
|
|
|
// Mine 40 blocks to ensure decoy availability
|
|
rpc.generate_blocks(&addr, 40).await.unwrap();
|
|
|
|
// Make sure we recognize the protocol
|
|
rpc.get_protocol().await.unwrap();
|
|
|
|
rpc
|
|
}
|
|
|
|
pub static SEQUENTIAL: OnceLock<Mutex<()>> = OnceLock::new();
|
|
|
|
#[macro_export]
|
|
macro_rules! async_sequential {
|
|
($(async fn $name: ident() $body: block)*) => {
|
|
$(
|
|
#[tokio::test]
|
|
async fn $name() {
|
|
let guard = runner::SEQUENTIAL.get_or_init(|| tokio::sync::Mutex::new(())).lock().await;
|
|
let local = tokio::task::LocalSet::new();
|
|
local.run_until(async move {
|
|
if let Err(err) = tokio::task::spawn_local(async move { $body }).await {
|
|
drop(guard);
|
|
Err(err).unwrap()
|
|
}
|
|
}).await;
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! test {
|
|
(
|
|
$name: ident,
|
|
(
|
|
$first_tx: expr,
|
|
$first_checks: expr,
|
|
),
|
|
$((
|
|
$tx: expr,
|
|
$checks: expr,
|
|
)$(,)?),*
|
|
) => {
|
|
async_sequential! {
|
|
async fn $name() {
|
|
use core::{ops::Deref, any::Any};
|
|
use std::collections::HashSet;
|
|
#[cfg(feature = "multisig")]
|
|
use std::collections::HashMap;
|
|
|
|
use zeroize::Zeroizing;
|
|
use rand_core::OsRng;
|
|
|
|
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
|
|
|
|
#[cfg(feature = "multisig")]
|
|
use transcript::{Transcript, RecommendedTranscript};
|
|
#[cfg(feature = "multisig")]
|
|
use frost::{
|
|
curve::Ed25519,
|
|
Participant,
|
|
tests::{THRESHOLD, key_gen},
|
|
};
|
|
|
|
use monero_serai::{
|
|
random_scalar,
|
|
wallet::{
|
|
address::{Network, AddressSpec}, ViewPair, Scanner, Change, Decoys, FeePriority,
|
|
SignableTransaction, SignableTransactionBuilder,
|
|
},
|
|
};
|
|
|
|
use runner::{
|
|
random_address, rpc, mine_until_unlocked, get_miner_tx_output,
|
|
check_weight_and_fee,
|
|
};
|
|
|
|
type Builder = SignableTransactionBuilder;
|
|
|
|
// Run each function as both a single signer and as a multisig
|
|
#[allow(clippy::redundant_closure_call)]
|
|
for multisig in [false, true] {
|
|
// Only run the multisig variant if multisig is enabled
|
|
if multisig {
|
|
#[cfg(not(feature = "multisig"))]
|
|
continue;
|
|
}
|
|
|
|
let spend = Zeroizing::new(random_scalar(&mut OsRng));
|
|
#[cfg(feature = "multisig")]
|
|
let keys = key_gen::<_, Ed25519>(&mut OsRng);
|
|
|
|
let spend_pub = if !multisig {
|
|
spend.deref() * ED25519_BASEPOINT_TABLE
|
|
} else {
|
|
#[cfg(not(feature = "multisig"))]
|
|
panic!("Multisig branch called without the multisig feature");
|
|
#[cfg(feature = "multisig")]
|
|
keys[&Participant::new(1).unwrap()].group_key().0
|
|
};
|
|
|
|
let rpc = rpc().await;
|
|
|
|
let view = ViewPair::new(spend_pub, Zeroizing::new(random_scalar(&mut OsRng)));
|
|
let addr = view.address(Network::Mainnet, AddressSpec::Standard);
|
|
|
|
let miner_tx = get_miner_tx_output(&rpc, &view).await;
|
|
|
|
let protocol = rpc.get_protocol().await.unwrap();
|
|
|
|
let builder = SignableTransactionBuilder::new(
|
|
protocol,
|
|
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
|
Change::new(
|
|
&ViewPair::new(
|
|
&random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
|
Zeroizing::new(random_scalar(&mut OsRng))
|
|
),
|
|
false
|
|
),
|
|
);
|
|
|
|
let sign = |tx: SignableTransaction| {
|
|
let spend = spend.clone();
|
|
#[cfg(feature = "multisig")]
|
|
let keys = keys.clone();
|
|
async move {
|
|
if !multisig {
|
|
tx.sign(&mut OsRng, &spend).unwrap()
|
|
} else {
|
|
#[cfg(not(feature = "multisig"))]
|
|
panic!("Multisig branch called without the multisig feature");
|
|
#[cfg(feature = "multisig")]
|
|
{
|
|
let mut machines = HashMap::new();
|
|
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
|
machines.insert(
|
|
i,
|
|
tx
|
|
.clone()
|
|
.multisig(
|
|
&keys[&i],
|
|
RecommendedTranscript::new(b"Monero Serai Test Transaction"),
|
|
)
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
frost::tests::sign_without_caching(&mut OsRng, machines, &[])
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: Generate a distinct wallet for each transaction to prevent overlap
|
|
let next_addr = addr;
|
|
|
|
let temp = Box::new({
|
|
let mut builder = builder.clone();
|
|
|
|
let decoys = Decoys::select(
|
|
&mut OsRng,
|
|
&rpc,
|
|
protocol.ring_len(),
|
|
rpc.get_height().await.unwrap() - 1,
|
|
&[miner_tx.clone()]
|
|
)
|
|
.await
|
|
.unwrap();
|
|
builder.add_input((miner_tx, decoys.first().unwrap().clone()));
|
|
|
|
let (tx, state) = ($first_tx)(rpc.clone(), builder, next_addr).await;
|
|
let fee_rate = tx.fee_rate().clone();
|
|
let signed = sign(tx).await;
|
|
rpc.publish_transaction(&signed).await.unwrap();
|
|
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
|
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
|
check_weight_and_fee(&tx, fee_rate);
|
|
let scanner =
|
|
Scanner::from_view(view.clone(), Some(HashSet::new()));
|
|
($first_checks)(rpc.clone(), tx, scanner, state).await
|
|
});
|
|
#[allow(unused_variables, unused_mut, unused_assignments)]
|
|
let mut carried_state: Box<dyn Any> = temp;
|
|
|
|
$(
|
|
let (tx, state) = ($tx)(
|
|
protocol,
|
|
rpc.clone(),
|
|
builder.clone(),
|
|
next_addr,
|
|
*carried_state.downcast().unwrap()
|
|
).await;
|
|
let fee_rate = tx.fee_rate().clone();
|
|
let signed = sign(tx).await;
|
|
rpc.publish_transaction(&signed).await.unwrap();
|
|
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
|
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
|
if stringify!($name) != "spend_one_input_to_two_outputs_no_change" {
|
|
// Skip weight and fee check for the above test because when there is no change,
|
|
// the change is added to the fee
|
|
check_weight_and_fee(&tx, fee_rate);
|
|
}
|
|
#[allow(unused_assignments)]
|
|
{
|
|
let scanner =
|
|
Scanner::from_view(view.clone(), Some(HashSet::new()));
|
|
carried_state =
|
|
Box::new(($checks)(rpc.clone(), tx, scanner, state).await);
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|