Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions crates/hashi/src/mpc/mpc_except_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub struct MpcManager {
pub dkg_messages: HashMap<Address, avss::Message>,
pub rotation_messages: HashMap<Address, RotationMessages>,
pub nonce_messages: HashMap<Address, NonceMessage>,
pub message_responses: HashMap<Address, SendMessagesResponse>,
pub message_responses: HashMap<Address, MpcResult<SendMessagesResponse>>,
pub complaints_to_process: HashMap<ComplaintsToProcessKey, complaint::Complaint>,
pub complaint_responses: HashMap<(Address, ProtocolTypeIndicator), ComplaintResponses>,
pub public_messages_store: Box<dyn PublicMessagesStore>,
Expand Down Expand Up @@ -262,35 +262,35 @@ impl MpcManager {
reason: "Dealer sent different messages".to_string(),
});
}
if let Some(response) = self.message_responses.get(&sender) {
return Ok(response.clone());
if let Some(cached) = self.message_responses.get(&sender) {
return cached.clone();
}
tracing::info!(
"handle_send_messages_request: existing message from {sender:?} but no \
cached response (e.g. post-restart), re-processing"
);
}
let signature = match &request.messages {
let result = match &request.messages {
Messages::Dkg(msg) => {
self.store_dkg_message(self.mpc_config.epoch, sender, msg)?;
self.try_sign_dkg_message(sender, &request.messages)?
self.try_sign_dkg_message(sender, &request.messages)
}
Messages::Rotation(msgs) => {
let previous = self
.previous_output
.clone()
.ok_or_else(|| MpcError::NotReady("Rotation not started".into()))?;
self.store_rotation_messages(self.mpc_config.epoch, sender, msgs)?;
self.try_sign_rotation_messages(&previous, sender, &request.messages)?
self.try_sign_rotation_messages(&previous, sender, &request.messages)
}
Messages::NonceGeneration(nonce) => {
self.store_nonce_message(self.mpc_config.epoch, sender, nonce)?;
self.try_sign_nonce_message(sender, &request.messages)?
self.try_sign_nonce_message(sender, &request.messages)
}
};
let response = SendMessagesResponse { signature };
self.message_responses.insert(sender, response.clone());
Ok(response)
}
.map(|signature| SendMessagesResponse { signature });
self.message_responses.insert(sender, result.clone());
result
}

pub fn handle_retrieve_messages_request(
Expand Down
26 changes: 13 additions & 13 deletions crates/hashi/src/mpc/mpc_except_signing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4294,7 +4294,7 @@ async fn test_handle_send_messages_request_equivocation() {
}

#[tokio::test]
async fn test_handle_send_messages_request_invalid_shares_no_panic_on_retry() {
async fn test_handle_send_messages_request_invalid_shares_cached_on_retry() {
// Second RPC call with invalid shares should not panic.

let mut rng = rand::thread_rng();
Expand Down Expand Up @@ -4328,20 +4328,16 @@ async fn test_handle_send_messages_request_invalid_shares_no_panic_on_retry() {
_ => panic!("Expected InvalidMessage error"),
}

// Second call: same message — re-processes and returns the same
// "Invalid shares" error. The point of this test is that the second
// call must NOT panic; the specific error message is allowed to be
// either the original Complaint reason or a short-circuit reason.
// Second call: same message — returns the cached error immediately
// without re-processing.
let result2 = receiver_manager.handle_send_messages_request(dealer_addr, &request);
assert!(result2.is_err(), "Second call should also return error");
assert!(result2.is_err(), "Second call should return cached error");
match result2.unwrap_err() {
MpcError::InvalidMessage { sender, reason } => {
assert_eq!(sender, dealer_addr);
assert!(
reason.contains("Invalid shares")
|| reason.contains("previously received but no valid response"),
"Second call should return either the original Complaint error or a \
'previously received' short-circuit, got: {reason}"
reason.contains("Invalid shares"),
"Should return the cached original error, got: {reason}"
);
}
_ => panic!("Expected InvalidMessage error"),
Expand All @@ -4353,12 +4349,16 @@ async fn test_handle_send_messages_request_invalid_shares_no_panic_on_retry() {
"Message should be stored even if invalid"
);

// Verify no response was cached (since we returned error)
// Verify the error was cached so subsequent retries don't re-process.
assert!(
!receiver_manager
receiver_manager
.message_responses
.contains_key(&dealer_addr),
"Response should not be cached for invalid shares"
"Error should be cached for invalid shares"
);
assert!(
receiver_manager.message_responses[&dealer_addr].is_err(),
"Cached response should be an error"
);

// Verify receiver can still serve the message via RetrieveMessagesRequest
Expand Down
2 changes: 1 addition & 1 deletion crates/hashi/src/mpc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ impl CertificateV1 {

pub type MpcResult<T> = Result<T, MpcError>;

#[derive(Debug, thiserror::Error)]
#[derive(Clone, Debug, thiserror::Error)]
pub enum MpcError {
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
Expand Down
Loading