Skip to content

Commit 5116130

Browse files
[leader] drive guardian committee handoff (#568)
1 parent 58be14d commit 5116130

9 files changed

Lines changed: 524 additions & 6 deletions

File tree

crates/hashi-types/proto/sui/hashi/v1alpha/bridge_service.proto

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ service BridgeService {
2525
rpc SignWithdrawalTxSigning(SignWithdrawalTxSigningRequest) returns (SignWithdrawalTxSigningResponse);
2626
// Step 4: Sign committee approval to confirm a processed withdrawal on-chain.
2727
rpc SignWithdrawalConfirmation(SignWithdrawalConfirmationRequest) returns (SignWithdrawalConfirmationResponse);
28+
// Sign a committee transition for the guardian's `UpdateCommittee`.
29+
rpc SignCommitteeTransition(SignCommitteeTransitionRequest) returns (SignCommitteeTransitionResponse);
2830
}
2931

3032
message GetServiceInfoRequest {}
@@ -186,3 +188,15 @@ message SignGuardianWithdrawalRequestRequest {
186188
message SignGuardianWithdrawalRequestResponse {
187189
MemberSignature member_signature = 1;
188190
}
191+
192+
// Peers rebuild the transition from on-chain state; no committee bytes on the wire.
193+
message SignCommitteeTransitionRequest {
194+
// Outgoing committee epoch. The new committee is the next entry in the
195+
// on-chain `committees` map after `from_epoch` — committee epochs are
196+
// sparse, so it is not generally `from_epoch + 1`.
197+
uint64 from_epoch = 1;
198+
}
199+
200+
message SignCommitteeTransitionResponse {
201+
MemberSignature member_signature = 1;
202+
}
310 Bytes
Binary file not shown.

crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,20 @@ pub struct SignGuardianWithdrawalRequestResponse {
175175
#[prost(message, optional, tag = "1")]
176176
pub member_signature: ::core::option::Option<MemberSignature>,
177177
}
178+
/// Peers rebuild the transition from on-chain state; no committee bytes on the wire.
179+
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
180+
pub struct SignCommitteeTransitionRequest {
181+
/// Outgoing committee epoch. The new committee is the next entry in the
182+
/// on-chain `committees` map after `from_epoch` — committee epochs are
183+
/// sparse, so it is not generally `from_epoch + 1`.
184+
#[prost(uint64, tag = "1")]
185+
pub from_epoch: u64,
186+
}
187+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
188+
pub struct SignCommitteeTransitionResponse {
189+
#[prost(message, optional, tag = "1")]
190+
pub member_signature: ::core::option::Option<MemberSignature>,
191+
}
178192
/// Generated client implementations.
179193
pub mod bridge_service_client {
180194
#![allow(
@@ -505,6 +519,36 @@ pub mod bridge_service_client {
505519
);
506520
self.inner.unary(req, path, codec).await
507521
}
522+
/// Sign a committee transition for the guardian's `UpdateCommittee`.
523+
pub async fn sign_committee_transition(
524+
&mut self,
525+
request: impl tonic::IntoRequest<super::SignCommitteeTransitionRequest>,
526+
) -> std::result::Result<
527+
tonic::Response<super::SignCommitteeTransitionResponse>,
528+
tonic::Status,
529+
> {
530+
self.inner
531+
.ready()
532+
.await
533+
.map_err(|e| {
534+
tonic::Status::unknown(
535+
format!("Service was not ready: {}", e.into()),
536+
)
537+
})?;
538+
let codec = tonic_prost::ProstCodec::default();
539+
let path = http::uri::PathAndQuery::from_static(
540+
"/sui.hashi.v1alpha.BridgeService/SignCommitteeTransition",
541+
);
542+
let mut req = request.into_request();
543+
req.extensions_mut()
544+
.insert(
545+
GrpcMethod::new(
546+
"sui.hashi.v1alpha.BridgeService",
547+
"SignCommitteeTransition",
548+
),
549+
);
550+
self.inner.unary(req, path, codec).await
551+
}
508552
}
509553
}
510554
/// Generated server implementations.
@@ -586,6 +630,14 @@ pub mod bridge_service_server {
586630
tonic::Response<super::SignWithdrawalConfirmationResponse>,
587631
tonic::Status,
588632
>;
633+
/// Sign a committee transition for the guardian's `UpdateCommittee`.
634+
async fn sign_committee_transition(
635+
&self,
636+
request: tonic::Request<super::SignCommitteeTransitionRequest>,
637+
) -> std::result::Result<
638+
tonic::Response<super::SignCommitteeTransitionResponse>,
639+
tonic::Status,
640+
>;
589641
}
590642
#[derive(Debug)]
591643
pub struct BridgeServiceServer<T> {
@@ -1075,6 +1127,57 @@ pub mod bridge_service_server {
10751127
};
10761128
Box::pin(fut)
10771129
}
1130+
"/sui.hashi.v1alpha.BridgeService/SignCommitteeTransition" => {
1131+
#[allow(non_camel_case_types)]
1132+
struct SignCommitteeTransitionSvc<T: BridgeService>(pub Arc<T>);
1133+
impl<
1134+
T: BridgeService,
1135+
> tonic::server::UnaryService<super::SignCommitteeTransitionRequest>
1136+
for SignCommitteeTransitionSvc<T> {
1137+
type Response = super::SignCommitteeTransitionResponse;
1138+
type Future = BoxFuture<
1139+
tonic::Response<Self::Response>,
1140+
tonic::Status,
1141+
>;
1142+
fn call(
1143+
&mut self,
1144+
request: tonic::Request<
1145+
super::SignCommitteeTransitionRequest,
1146+
>,
1147+
) -> Self::Future {
1148+
let inner = Arc::clone(&self.0);
1149+
let fut = async move {
1150+
<T as BridgeService>::sign_committee_transition(
1151+
&inner,
1152+
request,
1153+
)
1154+
.await
1155+
};
1156+
Box::pin(fut)
1157+
}
1158+
}
1159+
let accept_compression_encodings = self.accept_compression_encodings;
1160+
let send_compression_encodings = self.send_compression_encodings;
1161+
let max_decoding_message_size = self.max_decoding_message_size;
1162+
let max_encoding_message_size = self.max_encoding_message_size;
1163+
let inner = self.inner.clone();
1164+
let fut = async move {
1165+
let method = SignCommitteeTransitionSvc(inner);
1166+
let codec = tonic_prost::ProstCodec::default();
1167+
let mut grpc = tonic::server::Grpc::new(codec)
1168+
.apply_compression_config(
1169+
accept_compression_encodings,
1170+
send_compression_encodings,
1171+
)
1172+
.apply_max_message_size_config(
1173+
max_decoding_message_size,
1174+
max_encoding_message_size,
1175+
);
1176+
let res = grpc.unary(method, req).await;
1177+
Ok(res)
1178+
};
1179+
Box::pin(fut)
1180+
}
10781181
_ => {
10791182
Box::pin(async move {
10801183
let mut response = http::Response::new(

crates/hashi/src/grpc/bridge_service.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::withdrawals::WithdrawalTxSigning;
1717
use hashi_types::bitcoin_txid::BitcoinTxid;
1818
use hashi_types::proto::GetServiceInfoRequest;
1919
use hashi_types::proto::GetServiceInfoResponse;
20+
use hashi_types::proto::SignCommitteeTransitionRequest;
21+
use hashi_types::proto::SignCommitteeTransitionResponse;
2022
use hashi_types::proto::SignDepositConfirmationRequest;
2123
use hashi_types::proto::SignDepositConfirmationResponse;
2224
use hashi_types::proto::SignGuardianWithdrawalRequestRequest;
@@ -133,6 +135,30 @@ impl BridgeService for HttpService {
133135
}))
134136
}
135137

138+
/// Validate and BLS-sign a `CommitteeTransition` for the guardian.
139+
#[tracing::instrument(
140+
level = "info",
141+
skip_all,
142+
fields(from_epoch = tracing::field::Empty, caller = tracing::field::Empty),
143+
)]
144+
async fn sign_committee_transition(
145+
&self,
146+
request: Request<SignCommitteeTransitionRequest>,
147+
) -> Result<Response<SignCommitteeTransitionResponse>, Status> {
148+
let caller = authenticate_caller(&request)?;
149+
tracing::Span::current().record("caller", tracing::field::display(&caller));
150+
let from_epoch = request.get_ref().from_epoch;
151+
tracing::Span::current().record("from_epoch", from_epoch);
152+
let member_signature = self
153+
.inner
154+
.validate_and_sign_committee_transition(from_epoch)
155+
.map_err(|e| Status::failed_precondition(e.to_string()))?;
156+
tracing::info!(from_epoch, "Signed committee transition");
157+
Ok(Response::new(SignCommitteeTransitionResponse {
158+
member_signature: Some(member_signature),
159+
}))
160+
}
161+
136162
/// Validate and BLS-sign a `StandardWithdrawalRequest` for the guardian.
137163
#[tracing::instrument(
138164
level = "info",

crates/hashi/src/grpc/guardian_client.rs

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
11
// Copyright (c) Mysten Labs, Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
use std::sync::Arc;
45
use std::time::Duration;
56

6-
use hashi_types::proto::guardian_service_client::GuardianServiceClient;
7+
use axum::http;
8+
use sui_http::middleware::callback::CallbackLayer;
9+
use tonic::body::Body;
710
use tonic::transport::Channel;
811
use tonic::transport::Endpoint;
12+
use tower::ServiceBuilder;
13+
use tower::util::BoxCloneService;
14+
15+
use crate::grpc::metrics_layer::RpcMetricsMakeCallbackHandler;
16+
use crate::metrics::Metrics;
17+
use hashi_types::proto::guardian_service_client::GuardianServiceClient;
918

1019
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
1120

21+
/// Boxed transport handed to the tonic-generated `GuardianServiceClient`.
22+
/// Same shape as `crate::grpc::Client::BoxedChannel`, so the metrics
23+
/// callback layer wraps validator-validator and validator-guardian RPCs
24+
/// identically.
25+
pub type BoxedChannel = BoxCloneService<http::Request<Body>, http::Response<Body>, tonic::Status>;
26+
1227
/// Lazy gRPC channel to a `hashi-guardian`.
13-
#[derive(Clone, Debug)]
28+
#[derive(Clone)]
1429
pub struct GuardianClient {
1530
endpoint: String,
1631
channel: Channel,
32+
metrics: Option<Arc<Metrics>>,
33+
}
34+
35+
impl std::fmt::Debug for GuardianClient {
36+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37+
f.debug_struct("GuardianClient")
38+
.field("endpoint", &self.endpoint)
39+
.field("metrics_enabled", &self.metrics.is_some())
40+
.finish()
41+
}
1742
}
1843

1944
impl GuardianClient {
@@ -27,15 +52,53 @@ impl GuardianClient {
2752
Ok(Self {
2853
endpoint: endpoint.to_string(),
2954
channel,
55+
metrics: None,
3056
})
3157
}
3258

59+
/// Attach the metrics registry so outbound guardian RPCs are observed
60+
/// by [`RpcMetricsMakeCallbackHandler`] via `sui_http`'s callback
61+
/// layer. Without this, the client emits no RPC traffic metrics.
62+
pub fn with_metrics(mut self, metrics: Arc<Metrics>) -> Self {
63+
self.metrics = Some(metrics);
64+
self
65+
}
66+
3367
pub fn endpoint(&self) -> &str {
3468
&self.endpoint
3569
}
3670

37-
pub fn guardian_service_client(&self) -> GuardianServiceClient<Channel> {
38-
GuardianServiceClient::new(self.channel.clone())
71+
/// Build a boxed transport, applying the metrics callback layer when
72+
/// a registry is configured. Mirrors `crate::grpc::client::Client::boxed_channel`
73+
/// so guardian RPCs surface under the same `hashi_requests_total` /
74+
/// `hashi_request_latency_seconds` metrics as validator-validator
75+
/// traffic.
76+
fn boxed_channel(&self) -> BoxedChannel {
77+
let channel = self.channel.clone();
78+
match &self.metrics {
79+
Some(metrics) => {
80+
let svc = ServiceBuilder::new()
81+
.map_err(tonic::Status::from_error)
82+
.map_response(|resp: http::Response<_>| resp.map(Body::new))
83+
.layer(CallbackLayer::new(RpcMetricsMakeCallbackHandler::client(
84+
metrics.clone(),
85+
)))
86+
.map_request(|req: http::Request<_>| req.map(Body::new))
87+
.map_err(|e: tonic::transport::Error| -> BoxError { Box::new(e) })
88+
.service(channel);
89+
BoxCloneService::new(svc)
90+
}
91+
None => {
92+
let svc = ServiceBuilder::new()
93+
.map_err(|e: tonic::transport::Error| tonic::Status::from_error(Box::new(e)))
94+
.service(channel);
95+
BoxCloneService::new(svc)
96+
}
97+
}
98+
}
99+
100+
pub fn guardian_service_client(&self) -> GuardianServiceClient<BoxedChannel> {
101+
GuardianServiceClient::new(self.boxed_channel())
39102
}
40103

41104
pub async fn get_guardian_info(
@@ -58,4 +121,15 @@ impl GuardianClient {
58121
.await?;
59122
Ok(response.into_inner())
60123
}
124+
125+
pub async fn update_committee(
126+
&self,
127+
request: hashi_types::proto::SignedCommitteeTransition,
128+
) -> Result<hashi_types::proto::UpdateCommitteeResponse, tonic::Status> {
129+
let response = self
130+
.guardian_service_client()
131+
.update_committee(request)
132+
.await?;
133+
Ok(response.into_inner())
134+
}
61135
}

0 commit comments

Comments
 (0)