mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Use dedicated Queues for each from-to pair
Prevents one Processor's message from halting the entire pipeline.
This commit is contained in:
@@ -140,9 +140,9 @@ impl MessageQueue {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn next(&self) -> QueuedMessage {
|
||||
pub async fn next(&self, from: Service) -> QueuedMessage {
|
||||
loop {
|
||||
let json = self.json_call("next", serde_json::json!([self.service])).await;
|
||||
let json = self.json_call("next", serde_json::json!([from, self.service])).await;
|
||||
|
||||
// Convert from a Value to a type via reserialization
|
||||
let msg: Option<QueuedMessage> = serde_json::from_str(
|
||||
@@ -179,18 +179,18 @@ impl MessageQueue {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ack(&self, id: u64) {
|
||||
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, id, nonce_pub),
|
||||
ack_challenge(self.service, self.pub_key, from, id, nonce_pub),
|
||||
)
|
||||
.serialize();
|
||||
|
||||
let json = self.json_call("ack", serde_json::json!([self.service, id, sig])).await;
|
||||
let json = self.json_call("ack", serde_json::json!([from, self.service, id, sig])).await;
|
||||
if json.get("result") != Some(&serde_json::Value::Bool(true)) {
|
||||
panic!("failed to ack message {id}: {json}");
|
||||
}
|
||||
|
||||
@@ -25,12 +25,17 @@ mod binaries {
|
||||
|
||||
pub(crate) type Db = serai_db::RocksDB;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub(crate) static ref KEYS: Arc<RwLock<HashMap<Service, <Ristretto as Ciphersuite>::G>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
pub(crate) static ref QUEUES: Arc<RwLock<HashMap<Service, RwLock<Queue<Db>>>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
#[allow(clippy::type_complexity)]
|
||||
mod clippy {
|
||||
use super::*;
|
||||
lazy_static::lazy_static! {
|
||||
pub(crate) static ref KEYS: Arc<RwLock<HashMap<Service, <Ristretto as Ciphersuite>::G>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
pub(crate) static ref QUEUES: Arc<RwLock<HashMap<(Service, Service), RwLock<Queue<Db>>>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
}
|
||||
}
|
||||
pub(crate) use self::clippy::*;
|
||||
|
||||
// queue RPC method
|
||||
/*
|
||||
@@ -71,16 +76,17 @@ mod binaries {
|
||||
fn key(domain: &'static [u8], key: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
[&[u8::try_from(domain.len()).unwrap()], domain, key.as_ref()].concat()
|
||||
}
|
||||
fn intent_key(from: Service, intent: &[u8]) -> Vec<u8> {
|
||||
key(b"intent_seen", bincode::serialize(&(from, intent)).unwrap())
|
||||
fn intent_key(from: Service, to: Service, intent: &[u8]) -> Vec<u8> {
|
||||
key(b"intent_seen", bincode::serialize(&(from, to, intent)).unwrap())
|
||||
}
|
||||
let mut db = db.write().unwrap();
|
||||
let mut txn = db.txn();
|
||||
let intent_key = intent_key(meta.from, &meta.intent);
|
||||
let intent_key = intent_key(meta.from, meta.to, &meta.intent);
|
||||
if Get::get(&txn, &intent_key).is_some() {
|
||||
log::warn!(
|
||||
"Prior queued message attempted to be queued again. From: {:?} Intent: {}",
|
||||
"Prior queued message attempted to be queued again. From: {:?} To: {:?} Intent: {}",
|
||||
meta.from,
|
||||
meta.to,
|
||||
hex::encode(&meta.intent)
|
||||
);
|
||||
return;
|
||||
@@ -88,7 +94,7 @@ mod binaries {
|
||||
DbTxn::put(&mut txn, intent_key, []);
|
||||
|
||||
// Queue it
|
||||
let id = (*QUEUES).read().unwrap()[&meta.to].write().unwrap().queue_message(
|
||||
let id = (*QUEUES).read().unwrap()[&(meta.from, meta.to)].write().unwrap().queue_message(
|
||||
&mut txn,
|
||||
QueuedMessage {
|
||||
from: meta.from,
|
||||
@@ -105,15 +111,15 @@ mod binaries {
|
||||
|
||||
// next RPC method
|
||||
/*
|
||||
Gets the next message in queue for this service.
|
||||
Gets the next message in queue for the named services.
|
||||
|
||||
This is not authenticated due to the fact every nonce would have to be saved to prevent
|
||||
replays, or a challenge-response protocol implemented. Neither are worth doing when there
|
||||
should be no sensitive data on this server.
|
||||
*/
|
||||
pub(crate) fn get_next_message(service: Service) -> Option<QueuedMessage> {
|
||||
pub(crate) fn get_next_message(from: Service, to: Service) -> Option<QueuedMessage> {
|
||||
let queue_outer = (*QUEUES).read().unwrap();
|
||||
let queue = queue_outer[&service].read().unwrap();
|
||||
let queue = queue_outer[&(from, to)].read().unwrap();
|
||||
let next = queue.last_acknowledged().map(|i| i + 1).unwrap_or(0);
|
||||
queue.get_message(next)
|
||||
}
|
||||
@@ -123,10 +129,10 @@ mod binaries {
|
||||
Acknowledges a message as received and handled, meaning it'll no longer be returned as the next
|
||||
message.
|
||||
*/
|
||||
pub(crate) fn ack_message(service: Service, id: u64, sig: SchnorrSignature<Ristretto>) {
|
||||
pub(crate) fn ack_message(from: Service, to: Service, id: u64, sig: SchnorrSignature<Ristretto>) {
|
||||
{
|
||||
let from = (*KEYS).read().unwrap()[&service];
|
||||
assert!(sig.verify(from, ack_challenge(service, from, id, sig.R)));
|
||||
let to_key = (*KEYS).read().unwrap()[&to];
|
||||
assert!(sig.verify(to_key, ack_challenge(to, to_key, from, id, sig.R)));
|
||||
}
|
||||
|
||||
// Is it:
|
||||
@@ -136,9 +142,9 @@ mod binaries {
|
||||
// It's the second if we acknowledge messages before saving them as acknowledged
|
||||
// TODO: Check only a proper message is being acked
|
||||
|
||||
log::info!("{:?} is acknowledging {}", service, id);
|
||||
log::info!("{:?} is acknowledging {:?} {}", from, to, id);
|
||||
|
||||
(*QUEUES).read().unwrap()[&service].write().unwrap().ack_message(id)
|
||||
(*QUEUES).read().unwrap()[&(from, to)].write().unwrap().ack_message(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,13 +183,29 @@ async fn main() {
|
||||
Some(<Ristretto as Ciphersuite>::G::from_bytes(&repr).unwrap())
|
||||
};
|
||||
|
||||
const ALL_EXT_NETWORKS: [NetworkId; 3] =
|
||||
[NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero];
|
||||
|
||||
let register_service = |service, key| {
|
||||
(*KEYS).write().unwrap().insert(service, key);
|
||||
(*QUEUES).write().unwrap().insert(service, RwLock::new(Queue(db.clone(), service)));
|
||||
let mut queues = (*QUEUES).write().unwrap();
|
||||
if service == Service::Coordinator {
|
||||
for network in ALL_EXT_NETWORKS {
|
||||
queues.insert(
|
||||
(service, Service::Processor(network)),
|
||||
RwLock::new(Queue(db.clone(), service, Service::Processor(network))),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
queues.insert(
|
||||
(service, Service::Coordinator),
|
||||
RwLock::new(Queue(db.clone(), service, Service::Coordinator)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Make queues for each NetworkId, other than Serai
|
||||
for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] {
|
||||
for network in ALL_EXT_NETWORKS {
|
||||
// Use a match so we error if the list of NetworkIds changes
|
||||
let Some(key) = read_key(match network {
|
||||
NetworkId::Serai => unreachable!(),
|
||||
@@ -224,17 +246,18 @@ async fn main() {
|
||||
.unwrap();
|
||||
module
|
||||
.register_method("next", |args, _| {
|
||||
let args = args.parse::<Service>().unwrap();
|
||||
Ok(get_next_message(args))
|
||||
let (from, to) = args.parse::<(Service, Service)>().unwrap();
|
||||
Ok(get_next_message(from, to))
|
||||
})
|
||||
.unwrap();
|
||||
module
|
||||
.register_method("ack", |args, _| {
|
||||
let args = args.parse::<(Service, u64, Vec<u8>)>().unwrap();
|
||||
let args = args.parse::<(Service, Service, u64, Vec<u8>)>().unwrap();
|
||||
ack_message(
|
||||
args.0,
|
||||
args.1,
|
||||
SchnorrSignature::<Ristretto>::read(&mut args.2.as_slice()).unwrap(),
|
||||
args.2,
|
||||
SchnorrSignature::<Ristretto>::read(&mut args.3.as_slice()).unwrap(),
|
||||
);
|
||||
Ok(true)
|
||||
})
|
||||
|
||||
@@ -48,15 +48,17 @@ pub fn message_challenge(
|
||||
}
|
||||
|
||||
pub fn ack_challenge(
|
||||
to: Service,
|
||||
to_key: <Ristretto as Ciphersuite>::G,
|
||||
from: Service,
|
||||
from_key: <Ristretto as Ciphersuite>::G,
|
||||
id: u64,
|
||||
nonce: <Ristretto as Ciphersuite>::G,
|
||||
) -> <Ristretto as Ciphersuite>::F {
|
||||
let mut transcript = RecommendedTranscript::new(b"Serai Message Queue v0.1 Ackowledgement");
|
||||
transcript.domain_separate(b"metadata");
|
||||
transcript.append_message(b"to", bincode::serialize(&to).unwrap());
|
||||
transcript.append_message(b"to_key", to_key.to_bytes());
|
||||
transcript.append_message(b"from", bincode::serialize(&from).unwrap());
|
||||
transcript.append_message(b"from_key", from_key.to_bytes());
|
||||
transcript.domain_separate(b"message");
|
||||
transcript.append_message(b"id", id.to_le_bytes());
|
||||
transcript.domain_separate(b"signature");
|
||||
|
||||
@@ -3,14 +3,14 @@ use serai_db::{DbTxn, Db};
|
||||
use crate::messages::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Queue<D: Db>(pub(crate) D, pub(crate) Service);
|
||||
pub(crate) struct Queue<D: Db>(pub(crate) D, pub(crate) Service, pub(crate) Service);
|
||||
impl<D: Db> Queue<D> {
|
||||
fn key(domain: &'static [u8], key: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
[&[u8::try_from(domain.len()).unwrap()], domain, key.as_ref()].concat()
|
||||
}
|
||||
|
||||
fn message_count_key(&self) -> Vec<u8> {
|
||||
Self::key(b"message_count", serde_json::to_vec(&self.1).unwrap())
|
||||
Self::key(b"message_count", bincode::serialize(&(self.1, self.2)).unwrap())
|
||||
}
|
||||
pub(crate) fn message_count(&self) -> u64 {
|
||||
self
|
||||
@@ -21,7 +21,7 @@ impl<D: Db> Queue<D> {
|
||||
}
|
||||
|
||||
fn last_acknowledged_key(&self) -> Vec<u8> {
|
||||
Self::key(b"last_acknowledged", serde_json::to_vec(&self.1).unwrap())
|
||||
Self::key(b"last_acknowledged", bincode::serialize(&(self.1, self.2)).unwrap())
|
||||
}
|
||||
pub(crate) fn last_acknowledged(&self) -> Option<u64> {
|
||||
self
|
||||
@@ -31,7 +31,7 @@ impl<D: Db> Queue<D> {
|
||||
}
|
||||
|
||||
fn message_key(&self, id: u64) -> Vec<u8> {
|
||||
Self::key(b"message", serde_json::to_vec(&(self.1, id)).unwrap())
|
||||
Self::key(b"message", bincode::serialize(&(self.1, self.2, id)).unwrap())
|
||||
}
|
||||
// TODO: This is fine as-used, yet gets from the DB while having a txn. It should get from the
|
||||
// txn
|
||||
|
||||
Reference in New Issue
Block a user