Merge branch 'develop' into HEAD

This commit is contained in:
Luke Parker
2024-06-06 02:43:33 -04:00
261 changed files with 10394 additions and 3812 deletions

View File

@@ -20,7 +20,6 @@ workspace = true
hex = "0.4"
async-trait = "0.1"
async-recursion = "1"
zeroize = { version = "1", default-features = false }
rand_core = { version = "0.6", default-features = false }

View File

@@ -5,7 +5,10 @@ use std::{
time::Duration,
};
use tokio::{task::AbortHandle, sync::Mutex as AsyncMutex};
use tokio::{
task::AbortHandle,
sync::{Mutex as AsyncMutex, mpsc},
};
use rand_core::{RngCore, OsRng};
@@ -58,21 +61,21 @@ pub fn coordinator_instance(
}
pub fn serai_composition(name: &str, fast_epoch: bool) -> TestBodySpecification {
if fast_epoch {
(if fast_epoch {
serai_docker_tests::build("serai-fast-epoch".to_string());
TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
)
.replace_env([("SERAI_NAME".to_string(), name.to_lowercase())].into())
.set_publish_all_ports(true)
} else {
serai_docker_tests::build("serai".to_string());
TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never),
)
.replace_env([("SERAI_NAME".to_string(), name.to_lowercase())].into())
.set_publish_all_ports(true)
}
})
.replace_env(
[("SERAI_NAME".to_string(), name.to_lowercase()), ("KEY".to_string(), " ".to_string())].into(),
)
.set_publish_all_ports(true)
}
fn is_cosign_message(msg: &CoordinatorMessage) -> bool {
@@ -104,7 +107,6 @@ pub struct Handles {
pub(crate) message_queue: String,
}
#[derive(Clone)]
pub struct Processor {
network: NetworkId,
@@ -112,7 +114,8 @@ pub struct Processor {
#[allow(unused)]
handles: Handles,
queue: Arc<AsyncMutex<(u64, u64, MessageQueue)>>,
msgs: mpsc::UnboundedReceiver<messages::CoordinatorMessage>,
queue_for_sending: MessageQueue,
abort_handle: Option<Arc<AbortHandle>>,
substrate_key: Arc<AsyncMutex<Option<Zeroizing<<Ristretto as Ciphersuite>::F>>>>,
@@ -153,156 +156,173 @@ impl Processor {
// The Serai RPC may or may not be started
// Assume it is and continue, so if it's a few seconds late, it's still within tolerance
// Create the queue
let mut queue = (
0,
Arc::new(MessageQueue::new(
Service::Processor(network),
message_queue_rpc.clone(),
Zeroizing::new(processor_key),
)),
);
let (msg_send, msg_recv) = mpsc::unbounded_channel();
let substrate_key = Arc::new(AsyncMutex::new(None));
let mut res = Processor {
network,
serai_rpc,
handles,
queue: Arc::new(AsyncMutex::new((
0,
0,
MessageQueue::new(
Service::Processor(network),
message_queue_rpc,
Zeroizing::new(processor_key),
),
))),
queue_for_sending: MessageQueue::new(
Service::Processor(network),
message_queue_rpc,
Zeroizing::new(processor_key),
),
msgs: msg_recv,
abort_handle: None,
substrate_key: Arc::new(AsyncMutex::new(None)),
substrate_key: substrate_key.clone(),
};
// Handle any cosigns which come up
res.abort_handle = Some(Arc::new(
tokio::spawn({
let mut res = res.clone();
async move {
loop {
tokio::task::yield_now().await;
// Spawn a task to handle cosigns and forward messages as appropriate
let abort_handle = tokio::spawn({
async move {
loop {
// Get new messages
let (next_recv_id, queue) = &mut queue;
let msg = queue.next(Service::Coordinator).await;
assert_eq!(msg.from, Service::Coordinator);
assert_eq!(msg.id, *next_recv_id);
queue.ack(Service::Coordinator, msg.id).await;
*next_recv_id += 1;
let msg = {
let mut queue_lock = res.queue.lock().await;
let (_, next_recv_id, queue) = &mut *queue_lock;
let Ok(msg) =
tokio::time::timeout(Duration::from_secs(1), queue.next(Service::Coordinator))
.await
else {
continue;
};
assert_eq!(msg.from, Service::Coordinator);
assert_eq!(msg.id, *next_recv_id);
let msg_msg = borsh::from_slice(&msg.msg).unwrap();
let msg_msg = borsh::from_slice(&msg.msg).unwrap();
// Remove any BatchReattempts clogging the pipe
// TODO: Set up a wrapper around serai-client so we aren't throwing this away yet
// leave it for the tests
if matches!(
msg_msg,
messages::CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::BatchReattempt { .. }
)
) {
queue.ack(Service::Coordinator, msg.id).await;
*next_recv_id += 1;
continue;
}
if !is_cosign_message(&msg_msg) {
continue;
};
queue.ack(Service::Coordinator, msg.id).await;
*next_recv_id += 1;
msg_msg
};
// Remove any BatchReattempts clogging the pipe
// TODO: Set up a wrapper around serai-client so we aren't throwing this away yet
// leave it for the tests
if matches!(
msg_msg,
messages::CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::BatchReattempt { .. }
)
) {
continue;
}
struct CurrentCosign {
block_number: u64,
block: [u8; 32],
}
static CURRENT_COSIGN: OnceLock<AsyncMutex<Option<CurrentCosign>>> = OnceLock::new();
let mut current_cosign =
CURRENT_COSIGN.get_or_init(|| AsyncMutex::new(None)).lock().await;
match msg {
// If this is a CosignSubstrateBlock, reset the CurrentCosign
// While technically, each processor should individually track the current cosign,
// this is fine for current testing purposes
CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::CosignSubstrateBlock {
id,
block_number,
if !is_cosign_message(&msg_msg) {
msg_send.send(msg_msg).unwrap();
continue;
}
let msg = msg_msg;
let send_message = |msg: ProcessorMessage| async move {
queue
.queue(
Metadata {
from: Service::Processor(network),
to: Service::Coordinator,
intent: msg.intent(),
},
) => {
let SubstrateSignId {
id: SubstrateSignableId::CosigningSubstrateBlock(block), ..
} = id
else {
panic!("CosignSubstrateBlock didn't have CosigningSubstrateBlock ID")
};
borsh::to_vec(&msg).unwrap(),
)
.await;
};
let new_cosign = CurrentCosign { block_number, block };
if current_cosign.is_none() || (current_cosign.as_ref().unwrap().block != block) {
*current_cosign = Some(new_cosign);
struct CurrentCosign {
block_number: u64,
block: [u8; 32],
}
static CURRENT_COSIGN: OnceLock<AsyncMutex<Option<CurrentCosign>>> = OnceLock::new();
let mut current_cosign =
CURRENT_COSIGN.get_or_init(|| AsyncMutex::new(None)).lock().await;
match msg {
// If this is a CosignSubstrateBlock, reset the CurrentCosign
// While technically, each processor should individually track the current cosign,
// this is fine for current testing purposes
CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::CosignSubstrateBlock { id, block_number },
) => {
let SubstrateSignId {
id: SubstrateSignableId::CosigningSubstrateBlock(block), ..
} = id
else {
panic!("CosignSubstrateBlock didn't have CosigningSubstrateBlock ID")
};
let new_cosign = CurrentCosign { block_number, block };
if current_cosign.is_none() || (current_cosign.as_ref().unwrap().block != block) {
*current_cosign = Some(new_cosign);
}
send_message(
messages::coordinator::ProcessorMessage::CosignPreprocess {
id: id.clone(),
preprocesses: vec![[raw_i; 64]],
}
res
.send_message(messages::coordinator::ProcessorMessage::CosignPreprocess {
id: id.clone(),
preprocesses: vec![[raw_i; 64]],
})
.await;
}
CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::SubstratePreprocesses { id, .. },
) => {
// TODO: Assert the ID matches CURRENT_COSIGN
// TODO: Verify the received preprocesses
res
.send_message(messages::coordinator::ProcessorMessage::SubstrateShare {
id,
shares: vec![[raw_i; 32]],
})
.await;
}
CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::SubstrateShares { .. },
) => {
// TODO: Assert the ID matches CURRENT_COSIGN
// TODO: Verify the shares
let block_number = current_cosign.as_ref().unwrap().block_number;
let block = current_cosign.as_ref().unwrap().block;
let substrate_key = res.substrate_key.lock().await.clone().unwrap();
// Expand to a key pair as Schnorrkel expects
// It's the private key + 32-bytes of entropy for nonces + the public key
let mut schnorrkel_key_pair = [0; 96];
schnorrkel_key_pair[.. 32].copy_from_slice(&substrate_key.to_repr());
OsRng.fill_bytes(&mut schnorrkel_key_pair[32 .. 64]);
schnorrkel_key_pair[64 ..].copy_from_slice(
&(<Ristretto as Ciphersuite>::generator() * *substrate_key).to_bytes(),
);
let signature = Signature(
schnorrkel::keys::Keypair::from_bytes(&schnorrkel_key_pair)
.unwrap()
.sign_simple(b"substrate", &cosign_block_msg(block_number, block))
.to_bytes(),
);
res
.send_message(messages::coordinator::ProcessorMessage::CosignedBlock {
block_number,
block,
signature: signature.0.to_vec(),
})
.await;
}
_ => panic!("unexpected message passed is_cosign_message"),
.into(),
)
.await;
}
CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::SubstratePreprocesses { id, .. },
) => {
// TODO: Assert the ID matches CURRENT_COSIGN
// TODO: Verify the received preprocesses
send_message(
messages::coordinator::ProcessorMessage::SubstrateShare {
id,
shares: vec![[raw_i; 32]],
}
.into(),
)
.await;
}
CoordinatorMessage::Coordinator(
messages::coordinator::CoordinatorMessage::SubstrateShares { .. },
) => {
// TODO: Assert the ID matches CURRENT_COSIGN
// TODO: Verify the shares
let block_number = current_cosign.as_ref().unwrap().block_number;
let block = current_cosign.as_ref().unwrap().block;
let substrate_key = substrate_key.lock().await.clone().unwrap();
// Expand to a key pair as Schnorrkel expects
// It's the private key + 32-bytes of entropy for nonces + the public key
let mut schnorrkel_key_pair = [0; 96];
schnorrkel_key_pair[.. 32].copy_from_slice(&substrate_key.to_repr());
OsRng.fill_bytes(&mut schnorrkel_key_pair[32 .. 64]);
schnorrkel_key_pair[64 ..].copy_from_slice(
&(<Ristretto as Ciphersuite>::generator() * *substrate_key).to_bytes(),
);
let signature = Signature(
schnorrkel::keys::Keypair::from_bytes(&schnorrkel_key_pair)
.unwrap()
.sign_simple(b"substrate", &cosign_block_msg(block_number, block))
.to_bytes(),
);
send_message(
messages::coordinator::ProcessorMessage::CosignedBlock {
block_number,
block,
signature: signature.0.to_vec(),
}
.into(),
)
.await;
}
_ => panic!("unexpected message passed is_cosign_message"),
}
}
})
.abort_handle(),
));
}
})
.abort_handle();
res.abort_handle = Some(Arc::new(abort_handle));
res
}
@@ -315,9 +335,8 @@ impl Processor {
pub async fn send_message(&mut self, msg: impl Into<ProcessorMessage>) {
let msg: ProcessorMessage = msg.into();
let mut queue_lock = self.queue.lock().await;
let (next_send_id, _, queue) = &mut *queue_lock;
queue
self
.queue_for_sending
.queue(
Metadata {
from: Service::Processor(self.network),
@@ -327,36 +346,13 @@ impl Processor {
borsh::to_vec(&msg).unwrap(),
)
.await;
*next_send_id += 1;
}
async fn recv_message_inner(&mut self) -> CoordinatorMessage {
loop {
tokio::task::yield_now().await;
let mut queue_lock = self.queue.lock().await;
let (_, next_recv_id, queue) = &mut *queue_lock;
let msg = queue.next(Service::Coordinator).await;
assert_eq!(msg.from, Service::Coordinator);
assert_eq!(msg.id, *next_recv_id);
// If this is a cosign message, let the cosign task handle it
let msg_msg = borsh::from_slice(&msg.msg).unwrap();
if is_cosign_message(&msg_msg) {
continue;
}
queue.ack(Service::Coordinator, msg.id).await;
*next_recv_id += 1;
return msg_msg;
}
}
/// Receive a message from the coordinator as a processor.
pub async fn recv_message(&mut self) -> CoordinatorMessage {
// Set a timeout of 30 minutes to allow effectively any protocol to occur without a fear of
// an arbitrary timeout cutting it short
tokio::time::timeout(Duration::from_secs(30 * 60), self.recv_message_inner()).await.unwrap()
tokio::time::timeout(Duration::from_secs(20 * 60), self.msgs.recv()).await.unwrap().unwrap()
}
pub async fn set_substrate_key(

View File

@@ -245,7 +245,7 @@ pub async fn batch(
)
);
// Send the ack as expected, though it shouldn't trigger any observable behavior
// Send the ack as expected
processor
.send_message(messages::ProcessorMessage::Coordinator(
messages::coordinator::ProcessorMessage::SubstrateBlockAck {

View File

@@ -137,7 +137,6 @@ pub(crate) async fn new_test(test_body: impl TestBody, fast_epoch: bool) {
*OUTER_OPS.get_or_init(|| Mutex::new(None)).lock().await = None;
// Spawns a coordinator, if one has yet to be spawned, or else runs the test.
#[async_recursion::async_recursion]
async fn spawn_coordinator_or_run_test(inner_ops: DockerOperations) {
// If the outer operations have yet to be set, these *are* the outer operations
let outer_ops = OUTER_OPS.get().unwrap();
@@ -180,7 +179,10 @@ pub(crate) async fn new_test(test_body: impl TestBody, fast_epoch: bool) {
test.provide_container(composition);
drop(context_lock);
test.run_async(spawn_coordinator_or_run_test).await;
fn recurse(ops: DockerOperations) -> core::pin::Pin<Box<impl Send + Future<Output = ()>>> {
Box::pin(spawn_coordinator_or_run_test(ops))
}
test.run_async(recurse).await;
} else {
let outer_ops = outer_ops.lock().await.take().unwrap();

View File

@@ -85,7 +85,7 @@ pub fn build(name: String) {
}
let mut dockerfile_path = orchestration_path.clone();
if HashSet::from(["bitcoin", "ethereum", "monero"]).contains(name.as_str()) {
if HashSet::from(["bitcoin", "ethereum", "ethereum-relayer", "monero"]).contains(name.as_str()) {
dockerfile_path = dockerfile_path.join("coins");
}
if name.contains("-processor") {
@@ -124,7 +124,8 @@ pub fn build(name: String) {
// Check any additionally specified paths
let meta = |path: PathBuf| (path.clone(), fs::metadata(path));
let mut metadatas = match name.as_str() {
"bitcoin" | "monero" => vec![],
"bitcoin" | "ethereum" | "monero" => vec![],
"ethereum-relayer" => vec![meta(repo_path.join("common")), meta(repo_path.join("coins"))],
"message-queue" => vec![
meta(repo_path.join("common")),
meta(repo_path.join("crypto")),

View File

@@ -20,7 +20,6 @@ workspace = true
hex = "0.4"
async-trait = "0.1"
async-recursion = "1"
zeroize = { version = "1", default-features = false }
rand_core = { version = "0.6", default-features = false }

View File

@@ -57,7 +57,7 @@ async fn mint_and_burn_test() {
};
let addr = Address::p2pkh(
&PublicKey::from_private_key(
PublicKey::from_private_key(
SECP256K1,
&PrivateKey::new(SecretKey::from_slice(&[0x01; 32]).unwrap(), Network::Bitcoin),
),
@@ -266,14 +266,13 @@ async fn mint_and_burn_test() {
script::{PushBytesBuf, Script, ScriptBuf, Builder},
absolute::LockTime,
transaction::{Version, Transaction},
address::Payload,
Sequence, Witness, OutPoint, TxIn, Amount, TxOut, Network,
Sequence, Witness, OutPoint, TxIn, Amount, TxOut, Network, Address,
};
let private_key =
PrivateKey::new(SecretKey::from_slice(&[0x01; 32]).unwrap(), Network::Bitcoin);
let public_key = PublicKey::from_private_key(SECP256K1, &private_key);
let addr = Payload::p2pkh(&public_key);
let addr = Address::p2pkh(public_key, Network::Bitcoin);
// Use the first block's coinbase
let rpc = handles[0].bitcoin(&ops).await;
@@ -284,7 +283,7 @@ async fn mint_and_burn_test() {
version: Version(2),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: tx.txid(), vout: 0 },
previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 },
script_sig: Script::new().into(),
sequence: Sequence(u32::MAX),
witness: Witness::default(),
@@ -292,17 +291,23 @@ async fn mint_and_burn_test() {
output: vec![
TxOut {
value: Amount::from_sat(1_100_000_00),
script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&bitcoin_key_pair.1[1 ..]).unwrap(),
))
script_pubkey: Address::p2tr_tweaked(
TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&bitcoin_key_pair.1[1 ..]).unwrap(),
),
Network::Bitcoin,
)
.script_pubkey(),
},
TxOut {
// change = amount spent - fee
value: Amount::from_sat(tx.output[0].value.to_sat() - 1_100_000_00 - 1_000_00),
script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&public_key.inner.serialize()[1 ..]).unwrap(),
))
script_pubkey: Address::p2tr_tweaked(
TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&public_key.inner.serialize()[1 ..]).unwrap(),
),
Network::Bitcoin,
)
.script_pubkey(),
},
TxOut {
@@ -316,12 +321,14 @@ async fn mint_and_burn_test() {
let mut der = SECP256K1
.sign_ecdsa_low_r(
&Message::from(
&Message::from_digest_slice(
SighashCache::new(&tx)
.legacy_signature_hash(0, &addr.script_pubkey(), EcdsaSighashType::All.to_u32())
.unwrap()
.to_raw_hash(),
),
.to_raw_hash()
.as_ref(),
)
.unwrap(),
&private_key.inner,
)
.serialize_der()
@@ -447,19 +454,17 @@ async fn mint_and_burn_test() {
// Create a random Bitcoin/Monero address
let bitcoin_addr = {
use bitcoin_serai::bitcoin::{network::Network, key::PublicKey, address::Address};
// Uses Network::Bitcoin since it doesn't actually matter, Serai strips it out
// TODO: Move Serai to Payload from Address
Address::p2pkh(
&loop {
use bitcoin_serai::bitcoin::{key::PublicKey, ScriptBuf};
ScriptBuf::new_p2pkh(
&(loop {
let mut bytes = [0; 33];
OsRng.fill_bytes(&mut bytes);
bytes[0] %= 4;
if let Ok(key) = PublicKey::from_slice(&bytes) {
break key;
}
},
Network::Bitcoin,
})
.pubkey_hash(),
)
};
@@ -552,7 +557,7 @@ async fn mint_and_burn_test() {
let received_output = block.txdata[1]
.output
.iter()
.find(|output| output.script_pubkey == bitcoin_addr.script_pubkey())
.find(|output| output.script_pubkey == bitcoin_addr)
.unwrap();
let tx_fee = 1_100_000_00 -

View File

@@ -57,12 +57,16 @@ pub(crate) async fn new_test(test_body: impl TestBody) {
let (coord_key, message_queue_keys, message_queue_composition) = message_queue_instance();
let (bitcoin_composition, bitcoin_port) = network_instance(NetworkId::Bitcoin);
let bitcoin_processor_composition =
let mut bitcoin_processor_composition =
processor_instance(NetworkId::Bitcoin, bitcoin_port, message_queue_keys[&NetworkId::Bitcoin]);
assert_eq!(bitcoin_processor_composition.len(), 1);
let bitcoin_processor_composition = bitcoin_processor_composition.swap_remove(0);
let (monero_composition, monero_port) = network_instance(NetworkId::Monero);
let monero_processor_composition =
let mut monero_processor_composition =
processor_instance(NetworkId::Monero, monero_port, message_queue_keys[&NetworkId::Monero]);
assert_eq!(monero_processor_composition.len(), 1);
let monero_processor_composition = monero_processor_composition.swap_remove(0);
let coordinator_composition = coordinator_instance(name, coord_key);
let serai_composition = serai_composition(name, false);
@@ -161,54 +165,57 @@ pub(crate) async fn new_test(test_body: impl TestBody) {
*OUTER_OPS.get_or_init(|| Mutex::new(None)).lock().await = None;
// Spawns a coordinator, if one has yet to be spawned, or else runs the test.
#[async_recursion::async_recursion]
async fn spawn_coordinator_or_run_test(inner_ops: DockerOperations) {
// If the outer operations have yet to be set, these *are* the outer operations
let outer_ops = OUTER_OPS.get().unwrap();
if outer_ops.lock().await.is_none() {
*outer_ops.lock().await = Some(inner_ops);
}
pub(crate) fn spawn_coordinator_or_run_test(
inner_ops: DockerOperations,
) -> core::pin::Pin<Box<impl Send + Future<Output = ()>>> {
Box::pin(async {
// If the outer operations have yet to be set, these *are* the outer operations
let outer_ops = OUTER_OPS.get().unwrap();
if outer_ops.lock().await.is_none() {
*outer_ops.lock().await = Some(inner_ops);
}
let context_lock = CONTEXT.get().unwrap().lock().await;
let Context { pending_coordinator_compositions, handles, test_body } =
context_lock.as_ref().unwrap();
let context_lock = CONTEXT.get().unwrap().lock().await;
let Context { pending_coordinator_compositions, handles, test_body } =
context_lock.as_ref().unwrap();
// Check if there is a coordinator left
let maybe_coordinator = {
let mut remaining = pending_coordinator_compositions.lock().await;
let maybe_coordinator = if !remaining.is_empty() {
let handles = handles[handles.len() - remaining.len()].clone();
let composition = remaining.remove(0);
Some((composition, handles))
// Check if there is a coordinator left
let maybe_coordinator = {
let mut remaining = pending_coordinator_compositions.lock().await;
let maybe_coordinator = if !remaining.is_empty() {
let handles = handles[handles.len() - remaining.len()].clone();
let composition = remaining.remove(0);
Some((composition, handles))
} else {
None
};
drop(remaining);
maybe_coordinator
};
if let Some((mut composition, handles)) = maybe_coordinator {
let network = {
let outer_ops = outer_ops.lock().await;
let outer_ops = outer_ops.as_ref().unwrap();
// Spawn it by building another DockerTest which recursively calls this function
// TODO: Spawn this outside of DockerTest so we can remove the recursion
let serai_container = outer_ops.handle(&handles.serai);
composition.modify_env("SERAI_HOSTNAME", serai_container.ip());
let message_queue_container = outer_ops.handle(&handles.message_queue);
composition.modify_env("MESSAGE_QUEUE_RPC", message_queue_container.ip());
format!("container:{}", serai_container.name())
};
let mut test = DockerTest::new().with_network(dockertest::Network::External(network));
test.provide_container(composition);
drop(context_lock);
test.run_async(spawn_coordinator_or_run_test).await;
} else {
None
};
drop(remaining);
maybe_coordinator
};
if let Some((mut composition, handles)) = maybe_coordinator {
let network = {
let outer_ops = outer_ops.lock().await;
let outer_ops = outer_ops.as_ref().unwrap();
// Spawn it by building another DockerTest which recursively calls this function
// TODO: Spawn this outside of DockerTest so we can remove the recursion
let serai_container = outer_ops.handle(&handles.serai);
composition.modify_env("SERAI_HOSTNAME", serai_container.ip());
let message_queue_container = outer_ops.handle(&handles.message_queue);
composition.modify_env("MESSAGE_QUEUE_RPC", message_queue_container.ip());
format!("container:{}", serai_container.name())
};
let mut test = DockerTest::new().with_network(dockertest::Network::External(network));
test.provide_container(composition);
drop(context_lock);
test.run_async(spawn_coordinator_or_run_test).await;
} else {
let outer_ops = outer_ops.lock().await.take().unwrap();
test_body.body(outer_ops, handles.clone()).await;
}
let outer_ops = outer_ops.lock().await.take().unwrap();
test_body.body(outer_ops, handles.clone()).await;
}
})
}
test.run_async(spawn_coordinator_or_run_test).await;

View File

@@ -23,16 +23,21 @@ zeroize = { version = "1", default-features = false }
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
curve25519-dalek = "4"
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["ristretto"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["secp256k1", "ristretto"] }
dkg = { path = "../../crypto/dkg", default-features = false, features = ["tests"] }
bitcoin-serai = { path = "../../coins/bitcoin" }
k256 = "0.13"
ethereum-serai = { path = "../../coins/ethereum" }
monero-serai = { path = "../../coins/monero" }
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
scale = { package = "parity-scale-codec", version = "3" }
serai-client = { path = "../../substrate/client" }
serai-db = { path = "../../common/db", default-features = false }
serai-message-queue = { path = "../../message-queue" }
borsh = { version = "1", features = ["de_strict_order"] }
@@ -41,7 +46,7 @@ serde_json = { version = "1", default-features = false }
tokio = { version = "1", features = ["time"] }
processor = { package = "serai-processor", path = "../../processor", features = ["bitcoin", "monero"] }
processor = { package = "serai-processor", path = "../../processor", features = ["bitcoin", "ethereum", "monero"] }
dockertest = "0.4"
serai-docker-tests = { path = "../docker" }

View File

@@ -28,7 +28,7 @@ pub fn processor_instance(
network: NetworkId,
port: u32,
message_queue_key: <Ristretto as Ciphersuite>::F,
) -> TestBodySpecification {
) -> Vec<TestBodySpecification> {
let mut entropy = [0; 32];
OsRng.fill_bytes(&mut entropy);
@@ -41,7 +41,7 @@ pub fn processor_instance(
let image = format!("{network_str}-processor");
serai_docker_tests::build(image.clone());
TestBodySpecification::with_image(
let mut res = vec![TestBodySpecification::with_image(
Image::with_repository(format!("serai-dev-{image}")).pull_policy(PullPolicy::Never),
)
.replace_env(
@@ -55,19 +55,40 @@ pub fn processor_instance(
("RUST_LOG".to_string(), "serai_processor=trace,".to_string()),
]
.into(),
)
)];
if network == NetworkId::Ethereum {
serai_docker_tests::build("ethereum-relayer".to_string());
res.push(
TestBodySpecification::with_image(
Image::with_repository("serai-dev-ethereum-relayer".to_string())
.pull_policy(PullPolicy::Never),
)
.replace_env(
[
("DB_PATH".to_string(), "./ethereum-relayer-db".to_string()),
("RUST_LOG".to_string(), "serai_ethereum_relayer=trace,".to_string()),
]
.into(),
)
.set_publish_all_ports(true),
);
}
res
}
pub type Handles = (String, String, String);
pub type Handles = (String, String, String, String);
pub fn processor_stack(
network: NetworkId,
network_hostname_override: Option<String>,
) -> (Handles, <Ristretto as Ciphersuite>::F, Vec<TestBodySpecification>) {
let (network_composition, network_rpc_port) = network_instance(network);
let (coord_key, message_queue_keys, message_queue_composition) =
serai_message_queue_tests::instance();
let processor_composition =
let mut processor_compositions =
processor_instance(network, network_rpc_port, message_queue_keys[&network]);
// Give every item in this stack a unique ID
@@ -83,7 +104,7 @@ pub fn processor_stack(
let mut compositions = vec![];
let mut handles = vec![];
for (name, composition) in [
(
Some((
match network {
NetworkId::Serai => unreachable!(),
NetworkId::Bitcoin => "bitcoin",
@@ -91,10 +112,14 @@ pub fn processor_stack(
NetworkId::Monero => "monero",
},
network_composition,
),
("message_queue", message_queue_composition),
("processor", processor_composition),
] {
)),
Some(("message_queue", message_queue_composition)),
Some(("processor", processor_compositions.remove(0))),
processor_compositions.pop().map(|composition| ("relayer", composition)),
]
.into_iter()
.flatten()
{
let handle = format!("processor-{name}-{unique_id}");
compositions.push(
composition.set_start_policy(StartPolicy::Strict).set_handle(handle.clone()).set_log_options(
@@ -112,11 +137,27 @@ pub fn processor_stack(
handles.push(handle);
}
let processor_composition = compositions.last_mut().unwrap();
processor_composition.inject_container_name(handles[0].clone(), "NETWORK_RPC_HOSTNAME");
let processor_composition = compositions.get_mut(2).unwrap();
processor_composition.inject_container_name(
network_hostname_override.unwrap_or_else(|| handles[0].clone()),
"NETWORK_RPC_HOSTNAME",
);
if let Some(hostname) = handles.get(3) {
processor_composition.inject_container_name(hostname, "ETHEREUM_RELAYER_HOSTNAME");
processor_composition.modify_env("ETHEREUM_RELAYER_PORT", "20830");
}
processor_composition.inject_container_name(handles[1].clone(), "MESSAGE_QUEUE_RPC");
((handles[0].clone(), handles[1].clone(), handles[2].clone()), coord_key, compositions)
(
(
handles[0].clone(),
handles[1].clone(),
handles[2].clone(),
handles.get(3).cloned().unwrap_or(String::new()),
),
coord_key,
compositions,
)
}
#[derive(serde::Deserialize, Debug)]
@@ -130,6 +171,7 @@ pub struct Coordinator {
message_queue_handle: String,
#[allow(unused)]
processor_handle: String,
relayer_handle: String,
next_send_id: u64,
next_recv_id: u64,
@@ -140,7 +182,7 @@ impl Coordinator {
pub fn new(
network: NetworkId,
ops: &DockerOperations,
handles: (String, String, String),
handles: Handles,
coord_key: <Ristretto as Ciphersuite>::F,
) -> Coordinator {
let rpc = ops.handle(&handles.1).host_port(2287).unwrap();
@@ -152,6 +194,7 @@ impl Coordinator {
network_handle: handles.0,
message_queue_handle: handles.1,
processor_handle: handles.2,
relayer_handle: handles.3,
next_send_id: 0,
next_recv_id: 0,
@@ -181,7 +224,55 @@ impl Coordinator {
break;
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use std::sync::Arc;
use ethereum_serai::{
alloy::{
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
},
deployer::Deployer,
};
let provider = Arc::new(RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
));
if handle
.block_on(provider.raw_request::<_, ()>("evm_setAutomine".into(), [false]))
.is_ok()
{
handle.block_on(async {
// Deploy the deployer
let tx = Deployer::deployment_tx();
let signer = tx.recover_signer().unwrap();
let (tx, sig, _) = tx.into_parts();
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[signer.to_string(), (tx.gas_limit * tx.gas_price).to_string()],
)
.await
.unwrap();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let _ = provider.send_raw_transaction(&bytes).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
let _ = Deployer::new(provider.clone()).await.unwrap().unwrap();
// Sleep until the actual time is ahead of whatever time is in the epoch we just
// mined
tokio::time::sleep(core::time::Duration::from_secs(30)).await;
});
break;
}
}
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;
@@ -271,7 +362,45 @@ impl Coordinator {
block.consensus_encode(&mut block_buf).unwrap();
(hash, block_buf)
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_types::BlockNumberOrTag,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let start = provider
.get_block(BlockNumberOrTag::Latest.into(), false)
.await
.unwrap()
.unwrap()
.header
.number
.unwrap();
// We mine 96 blocks to mine one epoch, then cause its finalization
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
let end_of_epoch = start + 31;
let hash = provider
.get_block(BlockNumberOrTag::Number(end_of_epoch).into(), false)
.await
.unwrap()
.unwrap()
.header
.hash
.unwrap();
let state = provider
.raw_request::<_, String>("anvil_dumpState".into(), ())
.await
.unwrap()
.into_bytes();
(hash.into(), state)
}
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_serai::{
@@ -303,39 +432,6 @@ impl Coordinator {
}
}
pub async fn broadcast_block(&self, ops: &DockerOperations, block: &[u8]) {
let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network {
NetworkId::Bitcoin => {
use bitcoin_serai::rpc::Rpc;
let rpc =
Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC");
let res: Option<String> =
rpc.rpc_call("submitblock", serde_json::json!([hex::encode(block)])).await.unwrap();
if let Some(err) = res {
panic!("submitblock failed: {err}");
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;
let rpc =
HttpRpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Monero RPC");
let res: serde_json::Value = rpc
.json_rpc_call("submit_block", Some(serde_json::json!([hex::encode(block)])))
.await
.unwrap();
let err = res.get("error");
if err.is_some() && (err.unwrap() != &serde_json::Value::Null) {
panic!("failed to submit Monero block: {res}");
}
}
NetworkId::Serai => panic!("processor tests broadcasting block to Serai"),
}
}
pub async fn sync(&self, ops: &DockerOperations, others: &[Coordinator]) {
let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network {
@@ -345,13 +441,11 @@ impl Coordinator {
let rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC");
let to = rpc.get_latest_block_number().await.unwrap();
for coordinator in others {
let from = Rpc::new(network_rpc(self.network, ops, &coordinator.network_handle))
let other_rpc = Rpc::new(network_rpc(self.network, ops, &coordinator.network_handle))
.await
.expect("couldn't connect to the Bitcoin RPC")
.get_latest_block_number()
.await
.unwrap() +
1;
.expect("couldn't connect to the Bitcoin RPC");
let from = other_rpc.get_latest_block_number().await.unwrap() + 1;
for b in from ..= to {
let mut buf = vec![];
rpc
@@ -360,30 +454,92 @@ impl Coordinator {
.unwrap()
.consensus_encode(&mut buf)
.unwrap();
coordinator.broadcast_block(ops, &buf).await;
let res: Option<String> = other_rpc
.rpc_call("submitblock", serde_json::json!([hex::encode(buf)]))
.await
.unwrap();
if let Some(err) = res {
panic!("submitblock failed: {err}");
}
}
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_types::BlockNumberOrTag,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let (expected_number, state) = {
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let expected_number = provider
.get_block(BlockNumberOrTag::Latest.into(), false)
.await
.unwrap()
.unwrap()
.header
.number;
(
expected_number,
provider.raw_request::<_, String>("anvil_dumpState".into(), ()).await.unwrap(),
)
};
for coordinator in others {
let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle);
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
assert!(provider
.raw_request::<_, bool>("anvil_loadState".into(), &[&state])
.await
.unwrap());
let new_number = provider
.get_block(BlockNumberOrTag::Latest.into(), false)
.await
.unwrap()
.unwrap()
.header
.number;
// TODO: https://github.com/foundry-rs/foundry/issues/7955
let _ = expected_number;
let _ = new_number;
//assert_eq!(expected_number, new_number);
}
}
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;
let rpc = HttpRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC");
let to = rpc.get_height().await.unwrap();
for coordinator in others {
let from = HttpRpc::new(network_rpc(self.network, ops, &coordinator.network_handle))
.await
.expect("couldn't connect to the Monero RPC")
.get_height()
.await
.unwrap();
let other_rpc =
HttpRpc::new(network_rpc(coordinator.network, ops, &coordinator.network_handle))
.await
.expect("couldn't connect to the Monero RPC");
let from = other_rpc.get_height().await.unwrap();
for b in from .. to {
coordinator
.broadcast_block(
ops,
&rpc.get_block(rpc.get_block_hash(b).await.unwrap()).await.unwrap().serialize(),
)
.await;
let block =
rpc.get_block(rpc.get_block_hash(b).await.unwrap()).await.unwrap().serialize();
let res: serde_json::Value = other_rpc
.json_rpc_call("submit_block", Some(serde_json::json!([hex::encode(block)])))
.await
.unwrap();
let err = res.get("error");
if err.is_some() && (err.unwrap() != &serde_json::Value::Null) {
panic!("failed to submit Monero block: {res}");
}
}
}
}
@@ -391,7 +547,7 @@ impl Coordinator {
}
}
pub async fn publish_transacton(&self, ops: &DockerOperations, tx: &[u8]) {
pub async fn publish_transaction(&self, ops: &DockerOperations, tx: &[u8]) {
let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network {
NetworkId::Bitcoin => {
@@ -404,7 +560,19 @@ impl Coordinator {
Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC");
rpc.send_raw_transaction(&Transaction::consensus_decode(&mut &*tx).unwrap()).await.unwrap();
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ethereum_serai::alloy::{
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let _ = provider.send_raw_transaction(tx).await.unwrap();
}
NetworkId::Monero => {
use monero_serai::{transaction::Transaction, rpc::HttpRpc};
@@ -416,7 +584,19 @@ impl Coordinator {
}
}
pub async fn get_transaction(&self, ops: &DockerOperations, tx: &[u8]) -> Option<Vec<u8>> {
pub async fn publish_eventuality_completion(&self, ops: &DockerOperations, tx: &[u8]) {
match self.network {
NetworkId::Bitcoin | NetworkId::Monero => self.publish_transaction(ops, tx).await,
NetworkId::Ethereum => (),
NetworkId::Serai => panic!("processor tests broadcasting block to Serai"),
}
}
pub async fn get_published_transaction(
&self,
ops: &DockerOperations,
tx: &[u8],
) -> Option<Vec<u8>> {
let rpc_url = network_rpc(self.network, ops, &self.network_handle);
match self.network {
NetworkId::Bitcoin => {
@@ -424,8 +604,15 @@ impl Coordinator {
let rpc =
Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC");
// Bitcoin publishes a 0-byte TX ID to reduce variables
// Accordingly, read the mempool to find the (presumed relevant) TX
let entries: Vec<String> =
rpc.rpc_call("getrawmempool", serde_json::json!([false])).await.unwrap();
assert_eq!(entries.len(), 1, "more than one entry in the mempool, so unclear which to get");
let mut hash = [0; 32];
hash.copy_from_slice(tx);
hash.copy_from_slice(&hex::decode(&entries[0]).unwrap());
if let Ok(tx) = rpc.get_transaction(&hash).await {
let mut buf = vec![];
tx.consensus_encode(&mut buf).unwrap();
@@ -434,7 +621,56 @@ impl Coordinator {
None
}
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
/*
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
let mut hash = [0; 32];
hash.copy_from_slice(tx);
let tx = provider.get_transaction_by_hash(hash.into()).await.unwrap()?;
let (tx, sig, _) = Signed::<TxLegacy>::try_from(tx).unwrap().into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
Some(bytes)
*/
// This is being passed a signature. We need to check the relayer has a TX with this
// signature
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
let (ip, port) = ops.handle(&self.relayer_handle).host_port(20831).unwrap();
let relayer_url = format!("{ip}:{port}");
let mut socket = TcpStream::connect(&relayer_url).await.unwrap();
// Iterate over every published command
for i in 1 .. u32::MAX {
socket.write_all(&i.to_le_bytes()).await.unwrap();
let mut recvd_len = [0; 4];
socket.read_exact(&mut recvd_len).await.unwrap();
if recvd_len == [0; 4] {
break;
}
let mut msg = vec![0; usize::try_from(u32::from_le_bytes(recvd_len)).unwrap()];
socket.read_exact(&mut msg).await.unwrap();
for start_pos in 0 .. msg.len() {
if (start_pos + tx.len()) > msg.len() {
break;
}
if &msg[start_pos .. (start_pos + tx.len())] == tx {
return Some(msg);
}
}
}
None
}
NetworkId::Monero => {
use monero_serai::rpc::HttpRpc;

View File

@@ -19,6 +19,7 @@ pub const RPC_USER: &str = "serai";
pub const RPC_PASS: &str = "seraidex";
pub const BTC_PORT: u32 = 8332;
pub const ETH_PORT: u32 = 8545;
pub const XMR_PORT: u32 = 18081;
pub fn bitcoin_instance() -> (TestBodySpecification, u32) {
@@ -31,6 +32,17 @@ pub fn bitcoin_instance() -> (TestBodySpecification, u32) {
(composition, BTC_PORT)
}
pub fn ethereum_instance() -> (TestBodySpecification, u32) {
serai_docker_tests::build("ethereum".to_string());
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-ethereum").pull_policy(PullPolicy::Never),
)
.set_start_policy(StartPolicy::Strict)
.set_publish_all_ports(true);
(composition, ETH_PORT)
}
pub fn monero_instance() -> (TestBodySpecification, u32) {
serai_docker_tests::build("monero".to_string());
@@ -45,7 +57,7 @@ pub fn monero_instance() -> (TestBodySpecification, u32) {
pub fn network_instance(network: NetworkId) -> (TestBodySpecification, u32) {
match network {
NetworkId::Bitcoin => bitcoin_instance(),
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => ethereum_instance(),
NetworkId::Monero => monero_instance(),
NetworkId::Serai => {
panic!("Serai is not a valid network to spawn an instance of for a processor")
@@ -58,7 +70,7 @@ pub fn network_rpc(network: NetworkId, ops: &DockerOperations, handle: &str) ->
.handle(handle)
.host_port(match network {
NetworkId::Bitcoin => BTC_PORT,
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => ETH_PORT,
NetworkId::Monero => XMR_PORT,
NetworkId::Serai => panic!("getting port for external network yet it was Serai"),
})
@@ -70,7 +82,7 @@ pub fn confirmations(network: NetworkId) -> usize {
use processor::networks::*;
match network {
NetworkId::Bitcoin => Bitcoin::CONFIRMATIONS,
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => Ethereum::<serai_db::MemDb>::CONFIRMATIONS,
NetworkId::Monero => Monero::CONFIRMATIONS,
NetworkId::Serai => panic!("getting confirmations required for Serai"),
}
@@ -83,6 +95,11 @@ pub enum Wallet {
public_key: bitcoin_serai::bitcoin::PublicKey,
input_tx: bitcoin_serai::bitcoin::Transaction,
},
Ethereum {
rpc_url: String,
key: <ciphersuite::Secp256k1 as Ciphersuite>::F,
nonce: u64,
},
Monero {
handle: String,
spend_key: Zeroizing<curve25519_dalek::scalar::Scalar>,
@@ -109,7 +126,7 @@ impl Wallet {
let secret_key = SecretKey::new(&mut rand_core::OsRng);
let private_key = PrivateKey::new(secret_key, Network::Regtest);
let public_key = PublicKey::from_private_key(SECP256K1, &private_key);
let main_addr = Address::p2pkh(&public_key, Network::Regtest);
let main_addr = Address::p2pkh(public_key, Network::Regtest);
let rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC");
@@ -138,7 +155,37 @@ impl Wallet {
Wallet::Bitcoin { private_key, public_key, input_tx: funds }
}
NetworkId::Ethereum => todo!(),
NetworkId::Ethereum => {
use ciphersuite::{group::ff::Field, Secp256k1};
use ethereum_serai::alloy::{
primitives::{U256, Address},
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
};
let key = <Secp256k1 as Ciphersuite>::F::random(&mut OsRng);
let address =
ethereum_serai::crypto::address(&(<Secp256k1 as Ciphersuite>::generator() * key));
let provider = RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
);
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[Address(address.into()).to_string(), {
let nine_decimals = U256::from(1_000_000_000u64);
(U256::from(100u64) * nine_decimals * nine_decimals).to_string()
}],
)
.await
.unwrap();
Wallet::Ethereum { rpc_url: rpc_url.clone(), key, nonce: 0 }
}
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
@@ -211,7 +258,6 @@ impl Wallet {
consensus::Encodable,
sighash::{EcdsaSighashType, SighashCache},
script::{PushBytesBuf, Script, ScriptBuf, Builder},
address::Payload,
OutPoint, Sequence, Witness, TxIn, Amount, TxOut,
absolute::LockTime,
transaction::{Version, Transaction},
@@ -222,7 +268,7 @@ impl Wallet {
version: Version(2),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: input_tx.txid(), vout: 0 },
previous_output: OutPoint { txid: input_tx.compute_txid(), vout: 0 },
script_sig: Script::new().into(),
sequence: Sequence(u32::MAX),
witness: Witness::default(),
@@ -234,10 +280,11 @@ impl Wallet {
},
TxOut {
value: Amount::from_sat(AMOUNT),
script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(),
))
.script_pubkey(),
script_pubkey: ScriptBuf::new_p2tr_tweaked(
TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(),
),
),
},
],
};
@@ -256,7 +303,7 @@ impl Wallet {
let mut der = SECP256K1
.sign_ecdsa_low_r(
&Message::from(
&Message::from_digest_slice(
SighashCache::new(&tx)
.legacy_signature_hash(
0,
@@ -264,8 +311,10 @@ impl Wallet {
EcdsaSighashType::All.to_u32(),
)
.unwrap()
.to_raw_hash(),
),
.to_raw_hash()
.as_ref(),
)
.unwrap(),
&private_key.inner,
)
.serialize_der()
@@ -282,6 +331,109 @@ impl Wallet {
(buf, Balance { coin: Coin::Bitcoin, amount: Amount(AMOUNT) })
}
Wallet::Ethereum { rpc_url, key, ref mut nonce } => {
use std::sync::Arc;
use ethereum_serai::{
alloy::{
primitives::{U256, TxKind},
sol_types::SolCall,
simple_request_transport::SimpleRequest,
consensus::{TxLegacy, SignableTransaction},
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
network::Ethereum,
},
crypto::PublicKey,
deployer::Deployer,
};
let eight_decimals = U256::from(100_000_000u64);
let nine_decimals = eight_decimals * U256::from(10u64);
let eighteen_decimals = nine_decimals * nine_decimals;
let one_eth = eighteen_decimals;
let provider = Arc::new(RootProvider::<_, Ethereum>::new(
ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true),
));
let to_as_key = PublicKey::new(
<ciphersuite::Secp256k1 as Ciphersuite>::read_G(&mut to.as_slice()).unwrap(),
)
.unwrap();
let router_addr = {
// Find the deployer
let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap();
// Find the router, deploying if non-existent
let router = if let Some(router) =
deployer.find_router(provider.clone(), &to_as_key).await.unwrap()
{
router
} else {
let mut tx = deployer.deploy_router(&to_as_key);
tx.gas_price = 1_000_000_000u64.into();
let tx = ethereum_serai::crypto::deterministically_sign(&tx);
let signer = tx.recover_signer().unwrap();
let (tx, sig, _) = tx.into_parts();
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[signer.to_string(), (tx.gas_limit * tx.gas_price).to_string()],
)
.await
.unwrap();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let _ = provider.send_raw_transaction(&bytes).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
deployer.find_router(provider.clone(), &to_as_key).await.unwrap().unwrap()
};
router.address()
};
let tx = TxLegacy {
chain_id: None,
nonce: *nonce,
gas_price: 1_000_000_000u128,
gas_limit: 200_000u128,
to: TxKind::Call(router_addr.into()),
// 1 ETH
value: one_eth,
input: ethereum_serai::router::abi::inInstructionCall::new((
[0; 20].into(),
one_eth,
if let Some(instruction) = instruction {
Shorthand::Raw(RefundableInInstruction { origin: None, instruction }).encode().into()
} else {
vec![].into()
},
))
.abi_encode()
.into(),
};
*nonce += 1;
let sig =
k256::ecdsa::SigningKey::from(k256::elliptic_curve::NonZeroScalar::new(*key).unwrap())
.sign_prehash_recoverable(tx.signature_hash().as_ref())
.unwrap();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig.into(), &mut bytes);
// We drop the bottom 10 decimals
(
bytes,
Balance { coin: Coin::Ether, amount: Amount(u64::try_from(eight_decimals).unwrap()) },
)
}
Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut inputs } => {
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use monero_serai::{
@@ -366,14 +518,18 @@ impl Wallet {
match self {
Wallet::Bitcoin { public_key, .. } => {
use bitcoin_serai::bitcoin::{Network, Address};
use bitcoin_serai::bitcoin::ScriptBuf;
ExternalAddress::new(
networks::bitcoin::Address::new(Address::p2pkh(public_key, Network::Regtest))
networks::bitcoin::Address::new(ScriptBuf::new_p2pkh(&public_key.pubkey_hash()))
.unwrap()
.into(),
)
.unwrap()
}
Wallet::Ethereum { key, .. } => ExternalAddress::new(
ethereum_serai::crypto::address(&(ciphersuite::Secp256k1::generator() * key)).into(),
)
.unwrap(),
Wallet::Monero { view_pair, .. } => {
use monero_serai::wallet::address::{Network, AddressSpec};
ExternalAddress::new(

View File

@@ -17,7 +17,8 @@ use serai_client::{
validator_sets::primitives::Session,
};
use processor::networks::{Network, Bitcoin, Monero};
use serai_db::MemDb;
use processor::networks::{Network, Bitcoin, Ethereum, Monero};
use crate::{*, tests::*};
@@ -188,7 +189,7 @@ pub(crate) async fn substrate_block(
#[test]
fn batch_test() {
for network in [NetworkId::Bitcoin, NetworkId::Monero] {
for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] {
let (coordinators, test) = new_test(network);
test.run(|ops| async move {
@@ -228,7 +229,7 @@ fn batch_test() {
let (tx, balance_sent) =
wallet.send_to_address(&ops, &key_pair.1, instruction.clone()).await;
for coordinator in &mut coordinators {
coordinator.publish_transacton(&ops, &tx).await;
coordinator.publish_transaction(&ops, &tx).await;
}
// Put the TX past the confirmation depth
@@ -245,6 +246,8 @@ fn batch_test() {
// The scanner works on a 5s interval, so this leaves a few s for any processing/latency
tokio::time::sleep(Duration::from_secs(10)).await;
println!("sent in transaction. with in instruction: {}", instruction.is_some());
let expected_batch = Batch {
network,
id: i,
@@ -256,10 +259,11 @@ fn batch_test() {
coin: balance_sent.coin,
amount: Amount(
balance_sent.amount.0 -
(2 * if network == NetworkId::Bitcoin {
Bitcoin::COST_TO_AGGREGATE
} else {
Monero::COST_TO_AGGREGATE
(2 * match network {
NetworkId::Bitcoin => Bitcoin::COST_TO_AGGREGATE,
NetworkId::Ethereum => Ethereum::<MemDb>::COST_TO_AGGREGATE,
NetworkId::Monero => Monero::COST_TO_AGGREGATE,
NetworkId::Serai => panic!("minted for Serai?"),
}),
),
},
@@ -272,6 +276,8 @@ fn batch_test() {
},
};
println!("receiving batch preprocesses...");
// Make sure the processors picked it up by checking they're trying to sign a batch for it
let (mut id, mut preprocesses) =
recv_batch_preprocesses(&mut coordinators, Session(0), &expected_batch, 0).await;
@@ -291,6 +297,8 @@ fn batch_test() {
recv_batch_preprocesses(&mut coordinators, Session(0), &expected_batch, attempt).await;
}
println!("signing batch...");
// Continue with signing the batch
let batch = sign_batch(&mut coordinators, key_pair.0 .0, id, preprocesses).await;

View File

@@ -144,7 +144,7 @@ pub(crate) async fn key_gen(coordinators: &mut [Coordinator]) -> KeyPair {
#[test]
fn key_gen_test() {
for network in [NetworkId::Bitcoin, NetworkId::Monero] {
for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] {
let (coordinators, test) = new_test(network);
test.run(|ops| async move {

View File

@@ -20,8 +20,14 @@ pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1;
fn new_test(network: NetworkId) -> (Vec<(Handles, <Ristretto as Ciphersuite>::F)>, DockerTest) {
let mut coordinators = vec![];
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
let mut eth_handle = None;
for _ in 0 .. COORDINATORS {
let (handles, coord_key, compositions) = processor_stack(network);
let (handles, coord_key, compositions) = processor_stack(network, eth_handle.clone());
// TODO: Remove this once https://github.com/foundry-rs/foundry/issues/7955
// This has all processors share an Ethereum node until we can sync controlled nodes
if network == NetworkId::Ethereum {
eth_handle = eth_handle.or_else(|| Some(handles.0.clone()));
}
coordinators.push((handles, coord_key));
for composition in compositions {
test.provide_container(composition);

View File

@@ -8,12 +8,15 @@ use dkg::{Participant, tests::clone_without};
use messages::{sign::SignId, SubstrateContext};
use serai_client::{
primitives::{BlockHash, NetworkId},
primitives::{BlockHash, NetworkId, Amount, Balance, SeraiAddress},
coins::primitives::{OutInstruction, OutInstructionWithBalance},
in_instructions::primitives::Batch,
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
validator_sets::primitives::Session,
};
use serai_db::MemDb;
use processor::networks::{Network, Bitcoin, Ethereum, Monero};
use crate::{*, tests::*};
#[allow(unused)]
@@ -144,7 +147,7 @@ pub(crate) async fn sign_tx(
#[test]
fn send_test() {
for network in [NetworkId::Bitcoin, NetworkId::Monero] {
for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] {
let (coordinators, test) = new_test(network);
test.run(|ops| async move {
@@ -173,9 +176,13 @@ fn send_test() {
coordinators[0].sync(&ops, &coordinators[1 ..]).await;
// Send into the processor's wallet
let (tx, balance_sent) = wallet.send_to_address(&ops, &key_pair.1, None).await;
let mut serai_address = [0; 32];
OsRng.fill_bytes(&mut serai_address);
let instruction = InInstruction::Transfer(SeraiAddress(serai_address));
let (tx, balance_sent) =
wallet.send_to_address(&ops, &key_pair.1, Some(instruction.clone())).await;
for coordinator in &mut coordinators {
coordinator.publish_transacton(&ops, &tx).await;
coordinator.publish_transaction(&ops, &tx).await;
}
// Put the TX past the confirmation depth
@@ -192,8 +199,25 @@ fn send_test() {
// The scanner works on a 5s interval, so this leaves a few s for any processing/latency
tokio::time::sleep(Duration::from_secs(10)).await;
let expected_batch =
Batch { network, id: 0, block: BlockHash(block_with_tx.unwrap()), instructions: vec![] };
let amount_minted = Amount(
balance_sent.amount.0 -
(2 * match network {
NetworkId::Bitcoin => Bitcoin::COST_TO_AGGREGATE,
NetworkId::Ethereum => Ethereum::<MemDb>::COST_TO_AGGREGATE,
NetworkId::Monero => Monero::COST_TO_AGGREGATE,
NetworkId::Serai => panic!("minted for Serai?"),
}),
);
let expected_batch = Batch {
network,
id: 0,
block: BlockHash(block_with_tx.unwrap()),
instructions: vec![InInstructionWithBalance {
instruction,
balance: Balance { coin: balance_sent.coin, amount: amount_minted },
}],
};
// Make sure the proceessors picked it up by checking they're trying to sign a batch for it
let (id, preprocesses) =
@@ -221,7 +245,7 @@ fn send_test() {
block: substrate_block_num,
burns: vec![OutInstructionWithBalance {
instruction: OutInstruction { address: wallet.address(), data: None },
balance: balance_sent,
balance: Balance { coin: balance_sent.coin, amount: amount_minted },
}],
batches: vec![batch.batch.id],
},
@@ -261,17 +285,17 @@ fn send_test() {
let participating =
participating.iter().map(|p| usize::from(u16::from(*p) - 1)).collect::<HashSet<_>>();
for participant in &participating {
assert!(coordinators[*participant].get_transaction(&ops, &tx_id).await.is_some());
assert!(coordinators[*participant].get_published_transaction(&ops, &tx_id).await.is_some());
}
// Publish this transaction to the left out nodes
let tx = coordinators[*participating.iter().next().unwrap()]
.get_transaction(&ops, &tx_id)
.get_published_transaction(&ops, &tx_id)
.await
.unwrap();
for (i, coordinator) in coordinators.iter_mut().enumerate() {
if !participating.contains(&i) {
coordinator.publish_transacton(&ops, &tx).await;
coordinator.publish_eventuality_completion(&ops, &tx).await;
// Tell them of it as a completion of the relevant signing nodes
coordinator
.send_message(messages::sign::CoordinatorMessage::Completed {