From e201ffb1dc8e0e6f203618c4de361ba730a1c55d Mon Sep 17 00:00:00 2001 From: petarjuki7 Date: Thu, 21 Nov 2024 23:04:13 +0100 Subject: [PATCH] nonce comes from caller --- crates/crypto/Cargo.toml | 2 +- crates/crypto/src/lib.rs | 51 +++++++++++--------------- crates/node/src/lib.rs | 7 ++-- crates/node/src/sync.rs | 37 ++++++++----------- crates/node/src/sync/blobs.rs | 43 +++++++++++++++------- crates/node/src/sync/key.rs | 59 +++++++++++++++++++++++------- crates/node/src/sync/state.rs | 69 +++++++++++++++++++++++++---------- crates/node/src/types.rs | 4 +- 8 files changed, 170 insertions(+), 102 deletions(-) diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index 49e5c564b..4370af111 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -10,12 +10,12 @@ license.workspace = true curve25519-dalek.workspace = true ed25519-dalek = { workspace = true, features = ["rand_core"] } ring.workspace = true -rand.workspace = true calimero-primitives = { path = "../primitives", features = ["rand"] } [dev-dependencies] eyre.workspace = true +rand.workspace = true [lints] workspace = true diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index a292c586b..9d3929bcb 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -1,43 +1,33 @@ use calimero_primitives::identity::{PrivateKey, PublicKey}; use ed25519_dalek::{SecretKey, SigningKey}; -use rand::{thread_rng, Rng}; use ring::aead; pub const NONCE_LEN: usize = 12; +pub type Nonce = [u8; NONCE_LEN]; + #[derive(Copy, Clone, Debug)] pub struct SharedKey { key: SecretKey, } -#[derive(Debug)] -pub struct Record { - pub token: Vec, - pub nonce: [u8; NONCE_LEN], -} - impl SharedKey { - pub fn new(sk: &PrivateKey, pk: &PublicKey) -> (Self, [u8; NONCE_LEN]) { - let nonce = thread_rng().gen::<[u8; NONCE_LEN]>(); - ( - SharedKey { - key: (SigningKey::from_bytes(sk).to_scalar() - * curve25519_dalek::edwards::CompressedEdwardsY(**pk) - .decompress() - .expect("pk should be guaranteed to be the y coordinate")) - .compress() - .to_bytes(), - }, - nonce, - ) + pub fn new(sk: &PrivateKey, pk: &PublicKey) -> Self { + SharedKey { + key: (SigningKey::from_bytes(sk).to_scalar() + * curve25519_dalek::edwards::CompressedEdwardsY(**pk) + .decompress() + .expect("pk should be guaranteed to be the y coordinate")) + .compress() + .to_bytes(), + } } - pub fn from_sk(sk: &PrivateKey) -> (Self, [u8; NONCE_LEN]) { - let nonce = thread_rng().gen::<[u8; NONCE_LEN]>(); - (SharedKey { key: **sk }, nonce) + pub fn from_sk(sk: &PrivateKey) -> Self { + SharedKey { key: **sk } } - pub fn encrypt(&self, payload: Vec, nonce: [u8; NONCE_LEN]) -> Option> { + pub fn encrypt(&self, payload: Vec, nonce: Nonce) -> Option> { let encryption_key = aead::LessSafeKey::new(aead::UnboundKey::new(&aead::AES_256_GCM, &self.key).ok()?); @@ -53,7 +43,7 @@ impl SharedKey { Some(cipher_text) } - pub fn decrypt(&self, cipher_text: Vec, nonce: [u8; NONCE_LEN]) -> Option> { + pub fn decrypt(&self, cipher_text: Vec, nonce: Nonce) -> Option> { let decryption_key = aead::LessSafeKey::new(aead::UnboundKey::new(&aead::AES_256_GCM, &self.key).ok()?); @@ -76,6 +66,7 @@ impl SharedKey { #[cfg(test)] mod tests { use eyre::OptionExt; + use rand::thread_rng; use super::*; @@ -86,10 +77,11 @@ mod tests { let signer = PrivateKey::random(&mut csprng); let verifier = PrivateKey::random(&mut csprng); - let (signer_shared_key, nonce) = SharedKey::new(&signer, &verifier.public_key()); - let (verifier_shared_key, _nonce) = SharedKey::new(&verifier, &signer.public_key()); + let signer_shared_key = SharedKey::new(&signer, &verifier.public_key()); + let verifier_shared_key = SharedKey::new(&verifier, &signer.public_key()); let payload = b"privacy is important"; + let nonce = [0u8; NONCE_LEN]; let encrypted_payload = signer_shared_key .encrypt(payload.to_vec(), nonce) @@ -113,10 +105,11 @@ mod tests { let verifier = PrivateKey::random(&mut csprng); let invalid = PrivateKey::random(&mut csprng); - let (signer_shared_key, nonce) = SharedKey::new(&signer, &verifier.public_key()); - let (invalid_shared_key, _nonce) = SharedKey::new(&invalid, &invalid.public_key()); + let signer_shared_key = SharedKey::new(&signer, &verifier.public_key()); + let invalid_shared_key = SharedKey::new(&invalid, &invalid.public_key()); let token = b"privacy is important"; + let nonce = [0u8; NONCE_LEN]; let encrypted_token = signer_shared_key .encrypt(token.to_vec(), nonce) diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 101aa62cb..fecb5e625 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -16,7 +16,7 @@ use calimero_context::config::ContextConfig; use calimero_context::ContextManager; use calimero_context_config::repr::ReprTransmute; use calimero_context_config::ProposalAction; -use calimero_crypto::{SharedKey, NONCE_LEN}; +use calimero_crypto::{Nonce, SharedKey, NONCE_LEN}; use calimero_network::client::NetworkClient; use calimero_network::config::NetworkConfig; use calimero_network::types::{NetworkEvent, PeerId}; @@ -344,7 +344,7 @@ impl Node { return self.initiate_sync(context_id, source).await; }; - let (shared_key, _) = SharedKey::from_sk(&sender_key); + let shared_key = SharedKey::from_sk(&sender_key); let artifact = &shared_key .decrypt(artifact, nonce) @@ -388,7 +388,8 @@ impl Node { .get_sender_key(&context.id, &executor_public_key)? .ok_or_eyre("expected own identity to have sender key")?; - let (shared_key, nonce) = SharedKey::from_sk(&sender_key); + let shared_key = SharedKey::from_sk(&sender_key); + let nonce = thread_rng().gen::(); let artifact_encrypted = shared_key .encrypt(outcome.artifact.clone(), nonce) diff --git a/crates/node/src/sync.rs b/crates/node/src/sync.rs index 068aaf2bc..d46898792 100644 --- a/crates/node/src/sync.rs +++ b/crates/node/src/sync.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use calimero_crypto::{SharedKey, NONCE_LEN}; +use calimero_crypto::{Nonce, SharedKey}; use calimero_network::stream::{Message, Stream}; use calimero_primitives::context::ContextId; use eyre::{bail, eyre, OptionExt, Result as EyreResult}; @@ -28,20 +28,15 @@ pub struct SyncConfig { async fn send( stream: &mut Stream, message: &StreamMessage<'_>, - shared_key: Option, - nonce: Option<[u8; NONCE_LEN]>, + shared_key: Option<(SharedKey, Nonce)>, ) -> EyreResult<()> { let base_data = borsh::to_vec(message)?; - let data = match (shared_key, nonce) { - (Some(key), Some(nonce)) => key + let data = match shared_key { + Some((key, nonce)) => key .encrypt(base_data, nonce) - .ok_or_eyre("encryption failed")? - .into_iter() - .chain(nonce.into_iter()) - .collect(), - (Some(_), None) => bail!("nonce must be provided when encrypting"), - (None, _) => base_data, + .ok_or_eyre("encryption failed")?, + None => base_data, }; stream.send(Message::new(data)).await?; @@ -51,7 +46,7 @@ async fn send( async fn recv( stream: &mut Stream, duration: Duration, - shared_key: Option, + shared_key: Option<(SharedKey, Nonce)>, ) -> EyreResult>> { let Some(message) = timeout(duration, stream.next()).await? else { return Ok(None); @@ -60,10 +55,9 @@ async fn recv( let message_data = message?.data.into_owned(); let data = match shared_key { - Some(key) => { - let (message, nonce) = message_data.split_at(message_data.len() - NONCE_LEN); + Some((key, nonce)) => { match key.decrypt( - message.to_vec(), + message_data, nonce .try_into() .map_err(|_| eyre!("nonce must be 12 bytes"))?, @@ -157,9 +151,7 @@ impl Node { Err(err) => { error!(%err, "Failed to handle stream message"); - if let Err(err) = - send(&mut stream, &StreamMessage::OpaqueError, None, None).await - { + if let Err(err) = send(&mut stream, &StreamMessage::OpaqueError, None).await { error!(%err, "Failed to send error message"); } } @@ -172,12 +164,14 @@ impl Node { return Ok(None); }; - let (context_id, their_identity, payload) = match message { + let (context_id, their_identity, payload, nonce) = match message { StreamMessage::Init { context_id, party_id, payload, - } => (context_id, party_id, payload), + nonce, + .. + } => (context_id, party_id, payload, nonce), unexpected @ (StreamMessage::Message { .. } | StreamMessage::OpaqueError) => { bail!("expected initialization handshake, got {:?}", unexpected) } @@ -215,7 +209,7 @@ impl Node { match payload { InitPayload::KeyShare => { - self.handle_key_share_request(&context, our_identity, their_identity, stream) + self.handle_key_share_request(&context, our_identity, their_identity, stream, nonce) .await? } InitPayload::BlobShare { blob_id } => { @@ -247,6 +241,7 @@ impl Node { their_root_hash, their_application_id, stream, + nonce, ) .await? } diff --git a/crates/node/src/sync/blobs.rs b/crates/node/src/sync/blobs.rs index 69e4694ba..bf44c3496 100644 --- a/crates/node/src/sync/blobs.rs +++ b/crates/node/src/sync/blobs.rs @@ -1,4 +1,4 @@ -use calimero_crypto::SharedKey; +use calimero_crypto::{Nonce, SharedKey}; use calimero_network::stream::Stream; use calimero_primitives::blobs::BlobId; use calimero_primitives::context::Context; @@ -6,6 +6,7 @@ use calimero_primitives::identity::PublicKey; use eyre::{bail, OptionExt}; use futures_util::stream::poll_fn; use futures_util::TryStreamExt; +use rand::{thread_rng, Rng}; use tokio::sync::mpsc; use tracing::{debug, warn}; @@ -29,15 +30,17 @@ impl Node { "Initiating blob share", ); + let nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Init { context_id: context.id, party_id: our_identity, payload: InitPayload::BlobShare { blob_id }, + nonce, }, None, - None, ) .await?; @@ -45,13 +48,14 @@ impl Node { bail!("connection closed while awaiting blob share handshake"); }; - let their_identity = match ack { + let (their_identity, mut nonce) = match ack { StreamMessage::Init { party_id, payload: InitPayload::BlobShare { blob_id: ack_blob_id, }, + nonce, .. } => { if ack_blob_id != blob_id { @@ -62,7 +66,7 @@ impl Node { ); } - party_id + (party_id, nonce) } unexpected @ (StreamMessage::Init { .. } | StreamMessage::Message { .. } @@ -76,7 +80,7 @@ impl Node { .get_private_key(context.id, our_identity)? .ok_or_eyre("expected own identity to have private key")?; - let (shared_key, _) = SharedKey::new(&private_key, &their_identity); + let shared_key = SharedKey::new(&private_key, &their_identity); let (tx, mut rx) = mpsc::channel(1); @@ -89,13 +93,16 @@ impl Node { let read_task = async { let mut sequencer = Sequencer::default(); - while let Some(msg) = recv(stream, self.sync_config.timeout, Some(shared_key)).await? { - let (sequence_id, chunk) = match msg { + while let Some(msg) = + recv(stream, self.sync_config.timeout, Some((shared_key, nonce))).await? + { + let (sequence_id, chunk, new_nonce) = match msg { StreamMessage::OpaqueError => bail!("other peer ran into an error"), StreamMessage::Message { sequence_id, payload: MessagePayload::BlobShare { chunk }, - } => (sequence_id, chunk), + nonce, + } => (sequence_id, chunk, nonce), unexpected @ (StreamMessage::Init { .. } | StreamMessage::Message { .. }) => { bail!("unexpected message: {:?}", unexpected) } @@ -108,6 +115,8 @@ impl Node { } tx.send(Ok(chunk)).await?; + + nonce = new_nonce; } drop(tx); @@ -163,7 +172,8 @@ impl Node { .get_private_key(context.id, our_identity)? .ok_or_eyre("expected own identity to have private key")?; - let (shared_key, nonce) = SharedKey::new(&private_key, &their_identity); + let shared_key = SharedKey::new(&private_key, &their_identity); + let mut nonce = thread_rng().gen::(); send( stream, @@ -171,15 +181,16 @@ impl Node { context_id: context.id, party_id: our_identity, payload: InitPayload::BlobShare { blob_id }, + nonce, }, None, - None, ) .await?; let mut sequencer = Sequencer::default(); while let Some(chunk) = blob.try_next().await? { + let new_nonce = thread_rng().gen::(); send( stream, &StreamMessage::Message { @@ -187,21 +198,25 @@ impl Node { payload: MessagePayload::BlobShare { chunk: chunk.into_vec().into(), }, + nonce: new_nonce, }, - Some(shared_key), - Some(nonce), + Some((shared_key, nonce)), ) .await?; + + nonce = new_nonce; } + let new_nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Message { sequence_id: sequencer.next(), payload: MessagePayload::BlobShare { chunk: b"".into() }, + nonce: new_nonce, }, - Some(shared_key), - Some(nonce), + Some((shared_key, nonce)), ) .await?; diff --git a/crates/node/src/sync/key.rs b/crates/node/src/sync/key.rs index d3b3e3001..adeb28746 100644 --- a/crates/node/src/sync/key.rs +++ b/crates/node/src/sync/key.rs @@ -1,8 +1,9 @@ -use calimero_crypto::SharedKey; +use calimero_crypto::{Nonce, SharedKey}; use calimero_network::stream::Stream; use calimero_primitives::context::Context; use calimero_primitives::identity::PublicKey; use eyre::{bail, OptionExt}; +use rand::{thread_rng, Rng}; use tracing::debug; use crate::sync::{recv, send, Sequencer}; @@ -22,15 +23,17 @@ impl Node { "Initiating key share", ); + let nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Init { context_id: context.id, party_id: our_identity, payload: InitPayload::KeyShare, + nonce, }, None, - None, ) .await?; @@ -38,12 +41,13 @@ impl Node { bail!("connection closed while awaiting state sync handshake"); }; - let their_identity = match ack { + let (their_identity, their_nonce) = match ack { StreamMessage::Init { party_id, payload: InitPayload::KeyShare, + nonce, .. - } => party_id, + } => (party_id, nonce), unexpected @ (StreamMessage::Init { .. } | StreamMessage::Message { .. } | StreamMessage::OpaqueError) => { @@ -51,8 +55,15 @@ impl Node { } }; - self.bidirectional_key_share(context, our_identity, their_identity, stream) - .await + self.bidirectional_key_share( + context, + our_identity, + their_identity, + stream, + nonce, + their_nonce, + ) + .await } pub(super) async fn handle_key_share_request( @@ -61,6 +72,7 @@ impl Node { our_identity: PublicKey, their_identity: PublicKey, stream: &mut Stream, + nonce: Nonce, ) -> eyre::Result<()> { debug!( context_id=%context.id, @@ -68,20 +80,31 @@ impl Node { "Received key share request", ); + let their_nonce = nonce; + + let nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Init { context_id: context.id, party_id: our_identity, payload: InitPayload::KeyShare, + nonce: nonce, }, None, - None, ) .await?; - self.bidirectional_key_share(context, our_identity, their_identity, stream) - .await + self.bidirectional_key_share( + context, + our_identity, + their_identity, + stream, + nonce, + their_nonce, + ) + .await } async fn bidirectional_key_share( @@ -90,6 +113,8 @@ impl Node { our_identity: PublicKey, their_identity: PublicKey, stream: &mut Stream, + sending_nonce: Nonce, + receiving_nonce: Nonce, ) -> eyre::Result<()> { debug!( context_id=%context.id, @@ -103,7 +128,8 @@ impl Node { .get_private_key(context.id, our_identity)? .ok_or_eyre("expected own identity to have private key")?; - let (shared_key, nonce) = SharedKey::new(&private_key, &their_identity); + let shared_key = SharedKey::new(&private_key, &their_identity); + let new_nonce = thread_rng().gen::(); let sender_key = self .ctx_manager @@ -117,13 +143,19 @@ impl Node { &StreamMessage::Message { sequence_id: sqx_out.next(), payload: MessagePayload::KeyShare { sender_key }, + nonce: new_nonce, }, - Some(shared_key), - Some(nonce), + Some((shared_key, sending_nonce)), ) .await?; - let Some(msg) = recv(stream, self.sync_config.timeout, Some(shared_key)).await? else { + let Some(msg) = recv( + stream, + self.sync_config.timeout, + Some((shared_key, receiving_nonce)), + ) + .await? + else { bail!("connection closed while awaiting key share"); }; @@ -131,6 +163,7 @@ impl Node { StreamMessage::Message { sequence_id, payload: MessagePayload::KeyShare { sender_key }, + .. } => (sequence_id, sender_key), unexpected @ (StreamMessage::Init { .. } | StreamMessage::Message { .. } diff --git a/crates/node/src/sync/state.rs b/crates/node/src/sync/state.rs index 1b7b813a3..b4b1b35c3 100644 --- a/crates/node/src/sync/state.rs +++ b/crates/node/src/sync/state.rs @@ -1,12 +1,13 @@ use std::borrow::Cow; -use calimero_crypto::{SharedKey, NONCE_LEN}; +use calimero_crypto::{Nonce, SharedKey}; use calimero_network::stream::Stream; use calimero_primitives::application::ApplicationId; use calimero_primitives::context::Context; use calimero_primitives::hash::Hash; use calimero_primitives::identity::PublicKey; use eyre::{bail, OptionExt}; +use rand::{thread_rng, Rng}; use tracing::debug; use crate::sync::{recv, send, Sequencer}; @@ -28,6 +29,8 @@ impl Node { "Initiating state sync", ); + let nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Init { @@ -37,20 +40,20 @@ impl Node { root_hash: context.root_hash, application_id: context.application_id, }, + nonce, }, None, - None, ) .await?; - let mut pair = None; + let mut triple = None; for _ in 1..=2 { let Some(ack) = recv(stream, self.sync_config.timeout, None).await? else { bail!("connection closed while awaiting state sync handshake"); }; - let (root_hash, their_identity) = match ack { + let (root_hash, their_identity, their_nonce) = match ack { StreamMessage::Init { party_id, payload: @@ -58,6 +61,7 @@ impl Node { root_hash, application_id, }, + nonce, .. } => { if application_id != context.application_id { @@ -68,7 +72,7 @@ impl Node { ); } - (root_hash, party_id) + (root_hash, party_id, nonce) } StreamMessage::Init { party_id: their_identity, @@ -93,12 +97,12 @@ impl Node { } }; - pair = Some((root_hash, their_identity)); + triple = Some((root_hash, their_identity, their_nonce)); break; } - let Some((root_hash, their_identity)) = pair else { + let Some((root_hash, their_identity, their_nonce)) = triple else { bail!("expected two state sync handshakes, got none"); }; @@ -113,7 +117,8 @@ impl Node { .get_private_key(context.id, our_identity)? .ok_or_eyre("expected own identity to have private key")?; - let (shared_key, nonce) = SharedKey::new(&private_key, &their_identity); + let shared_key = SharedKey::new(&private_key, &their_identity); + let new_nonce = thread_rng().gen::(); send( stream, @@ -122,12 +127,11 @@ impl Node { payload: MessagePayload::StateSync { artifact: b"".into(), }, + nonce: new_nonce, }, - Some(shared_key), - Some(nonce), + Some((shared_key, nonce)), ) .await?; - self.bidirectional_sync( context, our_identity, @@ -135,7 +139,8 @@ impl Node { &mut sqx_out, stream, shared_key, - nonce, + new_nonce, + their_nonce, ) .await?; @@ -150,6 +155,7 @@ impl Node { their_root_hash: Hash, their_application_id: ApplicationId, stream: &mut Stream, + nonce: Nonce, ) -> eyre::Result<()> { debug!( context_id=%context.id, @@ -194,6 +200,10 @@ impl Node { debug!(context_id=%context.id, "Resuming state sync"); } + let their_nonce = nonce; + + let nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Init { @@ -203,9 +213,9 @@ impl Node { root_hash: context.root_hash, application_id: context.application_id, }, + nonce, }, None, - None, ) .await?; @@ -218,7 +228,8 @@ impl Node { .get_private_key(context.id, our_identity)? .ok_or_eyre("expected own identity to have private key")?; - let (shared_key, nonce) = SharedKey::new(&private_key, &their_identity); + let shared_key = SharedKey::new(&private_key, &their_identity); + let nonce = thread_rng().gen::(); let mut sqx_out = Sequencer::default(); @@ -230,6 +241,7 @@ impl Node { stream, shared_key, nonce, + their_nonce, ) .await @@ -244,7 +256,8 @@ impl Node { sqx_out: &mut Sequencer, stream: &mut Stream, shared_key: SharedKey, - nonce: [u8; NONCE_LEN], + sending_nonce: Nonce, + receiving_nonce: Nonce, ) -> eyre::Result<()> { debug!( context_id=%context.id, @@ -255,18 +268,30 @@ impl Node { let mut sqx_in = Sequencer::default(); - while let Some(msg) = recv(stream, self.sync_config.timeout, Some(shared_key)).await? { - let (sequence_id, artifact) = match msg { + let mut sending_nonce = sending_nonce; + let mut receiving_nonce = receiving_nonce; + + while let Some(msg) = recv( + stream, + self.sync_config.timeout, + Some((shared_key, receiving_nonce)), + ) + .await? + { + let (sequence_id, artifact, new_receiving_nonce) = match msg { StreamMessage::OpaqueError => bail!("other peer ran into an error"), StreamMessage::Message { sequence_id, payload: MessagePayload::StateSync { artifact }, - } => (sequence_id, artifact), + nonce, + } => (sequence_id, artifact, nonce), unexpected @ (StreamMessage::Init { .. } | StreamMessage::Message { .. }) => { bail!("unexpected message: {:?}", unexpected) } }; + receiving_nonce = new_receiving_nonce; + sqx_in.test(sequence_id)?; if artifact.is_empty() && sqx_out.current() != 0 { @@ -289,6 +314,8 @@ impl Node { "State sync outcome", ); + let new_sending_nonce = thread_rng().gen::(); + send( stream, &StreamMessage::Message { @@ -296,11 +323,13 @@ impl Node { payload: MessagePayload::StateSync { artifact: Cow::from(&outcome.artifact), }, + nonce: new_sending_nonce, }, - Some(shared_key), - Some(nonce), + Some((shared_key, sending_nonce)), ) .await?; + + sending_nonce = new_sending_nonce; } debug!( diff --git a/crates/node/src/types.rs b/crates/node/src/types.rs index 33c889a81..f089d18ee 100644 --- a/crates/node/src/types.rs +++ b/crates/node/src/types.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use borsh::{BorshDeserialize, BorshSerialize}; -use calimero_crypto::NONCE_LEN; +use calimero_crypto::{Nonce, NONCE_LEN}; use calimero_primitives::application::ApplicationId; use calimero_primitives::blobs::BlobId; use calimero_primitives::context::ContextId; @@ -30,10 +30,12 @@ pub enum StreamMessage<'a> { party_id: PublicKey, // nonce: usize, payload: InitPayload, + nonce: Nonce, }, Message { sequence_id: usize, payload: MessagePayload<'a>, + nonce: Nonce, }, /// Other peers must not learn anything about the node's state if anything goes wrong. OpaqueError,