serai/message-queue/src/client.rs

237 lines
6.9 KiB
Rust
Raw Normal View History

use core::ops::Deref;
use zeroize::{Zeroize, Zeroizing};
use rand_core::OsRng;
use ciphersuite::{
group::ff::{Field, PrimeField},
Ciphersuite, Ristretto,
};
use schnorr_signatures::SchnorrSignature;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use serai_env as env;
#[rustfmt::skip]
use crate::{Service, Metadata, QueuedMessage, MessageQueueRequest, message_challenge, ack_challenge};
pub struct MessageQueue {
pub service: Service,
priv_key: Zeroizing<<Ristretto as Ciphersuite>::F>,
pub_key: <Ristretto as Ciphersuite>::G,
url: String,
}
impl MessageQueue {
2023-07-22 05:12:15 +00:00
pub fn new(
service: Service,
mut url: String,
priv_key: Zeroizing<<Ristretto as Ciphersuite>::F>,
) -> MessageQueue {
// Allow MESSAGE_QUEUE_RPC to either be a full URL or just a hostname
2023-07-22 05:12:15 +00:00
// While we could stitch together multiple variables, our control over this service makes this
// fine
if !url.contains(':') {
url += ":2287";
}
MessageQueue { service, pub_key: Ristretto::generator() * priv_key.deref(), priv_key, url }
2023-07-22 05:12:15 +00:00
}
pub fn from_env(service: Service) -> MessageQueue {
let url = env::var("MESSAGE_QUEUE_RPC").expect("message-queue RPC wasn't specified");
let priv_key: Zeroizing<<Ristretto as Ciphersuite>::F> = {
let key_str =
Zeroizing::new(env::var("MESSAGE_QUEUE_KEY").expect("message-queue key wasn't specified"));
let key_bytes = Zeroizing::new(
hex::decode(&key_str).expect("invalid message-queue key specified (wasn't hex)"),
);
let mut bytes = <<Ristretto as Ciphersuite>::F as PrimeField>::Repr::default();
bytes.copy_from_slice(&key_bytes);
let key = Zeroizing::new(
Option::from(<<Ristretto as Ciphersuite>::F as PrimeField>::from_repr(bytes))
.expect("invalid message-queue key specified"),
);
bytes.zeroize();
key
};
2023-07-22 05:12:15 +00:00
Self::new(service, url, priv_key)
}
#[must_use]
async fn send(socket: &mut TcpStream, msg: MessageQueueRequest) -> bool {
let msg = borsh::to_vec(&msg).unwrap();
2023-12-17 01:54:24 +00:00
let Ok(()) = socket.write_all(&u32::try_from(msg.len()).unwrap().to_le_bytes()).await else {
log::warn!("couldn't send the message len");
return false;
};
let Ok(()) = socket.write_all(&msg).await else {
log::warn!("couldn't write the message");
return false;
};
true
}
pub async fn queue(&self, metadata: Metadata, msg: Vec<u8>) {
// TODO: Should this use OsRng? Deterministic or deterministic + random may be better.
let nonce = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let nonce_pub = Ristretto::generator() * nonce.deref();
let sig = SchnorrSignature::<Ristretto>::sign(
&self.priv_key,
nonce,
message_challenge(
metadata.from,
self.pub_key,
metadata.to,
&metadata.intent,
&msg,
nonce_pub,
),
)
.serialize();
let msg = MessageQueueRequest::Queue { meta: metadata, msg, sig };
let mut first = true;
loop {
// Sleep, so we don't hammer re-attempts
if !first {
tokio::time::sleep(core::time::Duration::from_secs(5)).await;
}
first = false;
let Ok(mut socket) = TcpStream::connect(&self.url).await else { continue };
if !Self::send(&mut socket, msg.clone()).await {
continue;
}
if socket.read_u8().await.ok() != Some(1) {
continue;
}
break;
}
}
pub async fn next(&self, from: Service) -> QueuedMessage {
let msg = MessageQueueRequest::Next { from, to: self.service };
let mut first = true;
'outer: loop {
if !first {
tokio::time::sleep(core::time::Duration::from_secs(5)).await;
}
first = false;
log::trace!("opening socket to message-queue for next");
let mut socket = match TcpStream::connect(&self.url).await {
Ok(socket) => socket,
Err(e) => {
log::warn!("couldn't connect to message-queue server: {e:?}");
continue;
}
};
log::trace!("opened socket for next");
loop {
if !Self::send(&mut socket, msg.clone()).await {
continue 'outer;
}
let status = match socket.read_u8().await {
Ok(status) => status,
Err(e) => {
log::warn!("couldn't read status u8: {e:?}");
continue 'outer;
}
};
// If there wasn't a message, check again in 1s
// TODO: Use a notification system here
if status == 0 {
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
continue;
}
assert_eq!(status, 1);
break;
}
// Timeout after 5 seconds in case there's an issue with the length handling
let Ok(msg) = tokio::time::timeout(core::time::Duration::from_secs(5), async {
// Read the message length
let len = match socket.read_u32_le().await {
Ok(len) => len,
Err(e) => {
log::warn!("couldn't read len: {e:?}");
return vec![];
}
};
let mut buf = vec![0; usize::try_from(len).unwrap()];
// Read the message
let Ok(_) = socket.read_exact(&mut buf).await else {
log::warn!("couldn't read the message");
return vec![];
};
buf
})
.await
else {
continue;
};
if msg.is_empty() {
continue;
}
let msg: QueuedMessage = borsh::from_slice(msg.as_slice()).unwrap();
// Verify the message
// Verify the sender is sane
if matches!(self.service, Service::Processor(_)) {
assert_eq!(
msg.from,
Service::Coordinator,
"non-coordinator sent us (a processor) a message"
);
} else {
assert!(
matches!(msg.from, Service::Processor(_)),
"non-processor sent us (coordinator) a message"
);
}
// TODO: Verify the sender's signature
return msg;
}
}
pub async fn ack(&self, from: Service, id: u64) {
// TODO: Should this use OsRng? Deterministic or deterministic + random may be better.
let nonce = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let nonce_pub = Ristretto::generator() * nonce.deref();
let sig = SchnorrSignature::<Ristretto>::sign(
&self.priv_key,
nonce,
ack_challenge(self.service, self.pub_key, from, id, nonce_pub),
)
.serialize();
let msg = MessageQueueRequest::Ack { from, to: self.service, id, sig };
let mut first = true;
loop {
if !first {
tokio::time::sleep(core::time::Duration::from_secs(5)).await;
}
first = false;
let Ok(mut socket) = TcpStream::connect(&self.url).await else { continue };
if !Self::send(&mut socket, msg.clone()).await {
continue;
}
if socket.read_u8().await.ok() != Some(1) {
continue;
}
break;
}
}
}