mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 21:49:26 +00:00
Merge branch 'develop' into HEAD
This commit is contained in:
@@ -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 }
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user