Skip to content
Draft
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
13 changes: 13 additions & 0 deletions crates/hashi-guardian/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ where
self.inner.update_committee(request).await
}

async fn update_committee_chain(
&self,
request: Request<proto::UpdateCommitteeChainRequest>,
) -> Result<Response<proto::UpdateCommitteeResponse>, Status> {
self.inner.update_committee_chain(request).await
}

async fn rotate_kps(
&self,
request: Request<proto::RotateKpsRequest>,
Expand Down Expand Up @@ -243,6 +250,12 @@ mod tests {
) -> Result<Response<proto::UpdateCommitteeResponse>, Status> {
unimplemented!("not exercised by tests")
}
async fn update_committee_chain(
&self,
_: Request<proto::UpdateCommitteeChainRequest>,
) -> Result<Response<proto::UpdateCommitteeResponse>, Status> {
unimplemented!("not exercised by tests")
}
async fn rotate_kps(
&self,
_: Request<proto::RotateKpsRequest>,
Expand Down
23 changes: 23 additions & 0 deletions crates/hashi-guardian/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,27 @@ impl proto::guardian_service_server::GuardianService for GuardianGrpc {
current_committee_epoch: Some(current_committee_epoch),
}))
}

async fn update_committee_chain(
&self,
request: Request<proto::UpdateCommitteeChainRequest>,
) -> Result<Response<proto::UpdateCommitteeResponse>, Status> {
self.require_normal_mode("update_committee_chain")?;

let transitions = request
.into_inner()
.transitions
.into_iter()
.map(pb_to_signed_committee_transition)
.collect::<Result<Vec<_>, _>>()
.map_err(to_status)?;
let current_committee_epoch =
committee_update::update_committee_chain(self.enclave.clone(), transitions)
.await
.map_err(to_status)?;

Ok(Response::new(proto::UpdateCommitteeResponse {
current_committee_epoch: Some(current_committee_epoch),
}))
}
}
46 changes: 46 additions & 0 deletions crates/hashi-guardian/src/withdraw_mode/committee_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ pub async fn update_committee(
Ok(proposed_epoch)
}

pub async fn update_committee_chain(
enclave: Arc<Enclave>,
transitions: Vec<HashiSigned<CommitteeTransitionRequest>>,
) -> GuardianResult<u64> {
let mut current_epoch = enclave.state.get_committee()?.epoch();
for signed in transitions {
current_epoch = update_committee(enclave.clone(), signed).await?;
}
Ok(current_epoch)
}

async fn log_success(
enclave: &Enclave,
from_epoch: u64,
Expand Down Expand Up @@ -236,6 +247,41 @@ mod tests {
assert_eq!(enclave.state.get_committee().unwrap().epoch(), 7);
}

#[tokio::test]
async fn update_committee_chain_advances_multiple_handoffs() {
let enclave = enclave_at_epoch(5).await;
let transitions = vec![
sign_transition_at(5, committee_at(7)),
sign_transition_at(7, committee_at(9)),
];

let new_epoch = update_committee_chain(enclave.clone(), transitions)
.await
.unwrap();

assert_eq!(new_epoch, 9);
assert_eq!(enclave.state.get_committee().unwrap().epoch(), 9);
}

#[tokio::test]
async fn update_committee_chain_rejects_bad_middle_handoff() {
let enclave = enclave_at_epoch(5).await;
let transitions = vec![
sign_transition_at(5, committee_at(7)),
sign_transition_at(6, committee_at(9)),
];

let err = update_committee_chain(enclave.clone(), transitions)
.await
.expect_err("bad middle handoff must error");

assert!(
matches!(err, GuardianError::InvalidInputs(_)),
"expected InvalidInputs, got {err:?}"
);
assert_eq!(enclave.state.get_committee().unwrap().epoch(), 7);
}

#[tokio::test]
async fn wrong_signing_epoch_rejected() {
let enclave = enclave_at_epoch(5).await;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ service GuardianService {
// Standard withdrawal: request withdrawal signature.
rpc StandardWithdrawal(SignedStandardWithdrawalRequest) returns (SignedStandardWithdrawalResponse);

// Advance the guardian's committee one epoch (signed by the outgoing committee). Idempotent.
// Advance the guardian's committee one handoff (signed by the outgoing committee). Idempotent.
rpc UpdateCommittee(SignedCommitteeTransition) returns (UpdateCommitteeResponse);

// Advance the guardian by replaying a chain of stored committee handoffs.
rpc UpdateCommitteeChain(UpdateCommitteeChainRequest) returns (UpdateCommitteeResponse);
}

message GetGuardianInfoRequest {}
Expand Down Expand Up @@ -344,7 +347,8 @@ message StandardWithdrawalResponseData {
// UpdateCommittee
// ============================

// Handoff payload. `new_committee.epoch` must equal `current_epoch + 1`.
// Handoff payload. Hashi committee epochs are sparse, so `new_committee.epoch`
// only needs to be greater than the guardian's current committee epoch.
message CommitteeTransition {
Committee new_committee = 1;
}
Expand All @@ -356,6 +360,10 @@ message SignedCommitteeTransition {
CommitteeSignature committee_signature = 2;
}

message UpdateCommitteeChainRequest {
repeated SignedCommitteeTransition transitions = 1;
}

message UpdateCommitteeResponse {
// Guardian's committee epoch after the call (unchanged on no-op).
optional uint64 current_committee_epoch = 1;
Expand Down
10 changes: 10 additions & 0 deletions crates/hashi-types/src/move_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub struct CommitteeSet {
/// The current epoch.
pub epoch: u64,
pub committees: Bag,
pub guardian_handoffs: Bag,
pub pending_epoch_change: Option<u64>,

/// The MPC committee's threshold public key.
Expand Down Expand Up @@ -167,6 +168,13 @@ pub struct Committee {
pub mpc_max_faulty_in_basis_points: u64,
}

/// Rust version of the Move hashi::committee_set::GuardianCommitteeHandoff type.
#[derive(Debug, Clone, serde_derive::Deserialize, serde_derive::Serialize)]
pub struct GuardianCommitteeHandoff {
pub new_committee: Committee,
pub cert: CommitteeSignature,
}

/// Rust version of the Move hashi::config::Config type.
#[derive(Debug, serde_derive::Deserialize)]
pub struct Config {
Expand Down Expand Up @@ -1182,8 +1190,10 @@ impl From<StartReconfigEvent> for HashiEvent {

#[derive(Debug, serde_derive::Deserialize)]
pub struct EndReconfigEvent {
pub from_epoch: u64,
pub epoch: u64,
pub mpc_public_key: Vec<u8>,
pub guardian_handoff_cert: CommitteeSignature,
}

impl MoveType for EndReconfigEvent {
Expand Down
Binary file modified crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin
Binary file not shown.
99 changes: 96 additions & 3 deletions crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1565,7 +1565,8 @@ pub struct StandardWithdrawalResponseData {
#[prost(bytes = "bytes", repeated, tag = "1")]
pub enclave_signatures: ::prost::alloc::vec::Vec<::prost::bytes::Bytes>,
}
/// Handoff payload. `new_committee.epoch` must equal `current_epoch + 1`.
/// Handoff payload. Hashi committee epochs are sparse, so `new_committee.epoch`
/// only needs to be greater than the guardian's current committee epoch.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CommitteeTransition {
#[prost(message, optional, tag = "1")]
Expand All @@ -1579,6 +1580,11 @@ pub struct SignedCommitteeTransition {
#[prost(message, optional, tag = "2")]
pub committee_signature: ::core::option::Option<CommitteeSignature>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UpdateCommitteeChainRequest {
#[prost(message, repeated, tag = "1")]
pub transitions: ::prost::alloc::vec::Vec<SignedCommitteeTransition>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct UpdateCommitteeResponse {
/// Guardian's committee epoch after the call (unchanged on no-op).
Expand Down Expand Up @@ -1883,7 +1889,7 @@ pub mod guardian_service_client {
);
self.inner.unary(req, path, codec).await
}
/// Advance the guardian's committee one epoch (signed by the outgoing committee). Idempotent.
/// Advance the guardian's committee one handoff (signed by the outgoing committee). Idempotent.
pub async fn update_committee(
&mut self,
request: impl tonic::IntoRequest<super::SignedCommitteeTransition>,
Expand Down Expand Up @@ -1913,6 +1919,36 @@ pub mod guardian_service_client {
);
self.inner.unary(req, path, codec).await
}
/// Advance the guardian by replaying a chain of stored committee handoffs.
pub async fn update_committee_chain(
&mut self,
request: impl tonic::IntoRequest<super::UpdateCommitteeChainRequest>,
) -> std::result::Result<
tonic::Response<super::UpdateCommitteeResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/sui.hashi.v1alpha.GuardianService/UpdateCommitteeChain",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"sui.hashi.v1alpha.GuardianService",
"UpdateCommitteeChain",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
Expand Down Expand Up @@ -1980,14 +2016,22 @@ pub mod guardian_service_server {
tonic::Response<super::SignedStandardWithdrawalResponse>,
tonic::Status,
>;
/// Advance the guardian's committee one epoch (signed by the outgoing committee). Idempotent.
/// Advance the guardian's committee one handoff (signed by the outgoing committee). Idempotent.
async fn update_committee(
&self,
request: tonic::Request<super::SignedCommitteeTransition>,
) -> std::result::Result<
tonic::Response<super::UpdateCommitteeResponse>,
tonic::Status,
>;
/// Advance the guardian by replaying a chain of stored committee handoffs.
async fn update_committee_chain(
&self,
request: tonic::Request<super::UpdateCommitteeChainRequest>,
) -> std::result::Result<
tonic::Response<super::UpdateCommitteeResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct GuardianServiceServer<T> {
Expand Down Expand Up @@ -2386,6 +2430,55 @@ pub mod guardian_service_server {
};
Box::pin(fut)
}
"/sui.hashi.v1alpha.GuardianService/UpdateCommitteeChain" => {
#[allow(non_camel_case_types)]
struct UpdateCommitteeChainSvc<T: GuardianService>(pub Arc<T>);
impl<
T: GuardianService,
> tonic::server::UnaryService<super::UpdateCommitteeChainRequest>
for UpdateCommitteeChainSvc<T> {
type Response = super::UpdateCommitteeResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::UpdateCommitteeChainRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as GuardianService>::update_committee_chain(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = UpdateCommitteeChainSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
Expand Down
11 changes: 11 additions & 0 deletions crates/hashi/src/grpc/guardian_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,15 @@ impl GuardianClient {
.await?;
Ok(response.into_inner())
}

pub async fn update_committee_chain(
&self,
request: hashi_types::proto::UpdateCommitteeChainRequest,
) -> Result<hashi_types::proto::UpdateCommitteeResponse, tonic::Status> {
let response = self
.guardian_service_client()
.update_committee_chain(request)
.await?;
Ok(response.into_inner())
}
}
Loading
Loading