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
2 changes: 2 additions & 0 deletions crates/hashi-guardian/src/getters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ pub async fn get_guardian_info(enclave: Arc<Enclave>) -> GuardianResult<GetGuard

let signing_pub_key = enclave.signing_pubkey();
let attestation = get_attestation(&signing_pub_key)?;
let limiter_state = enclave.state.limiter_state().await;
Ok(GetGuardianInfoResponse {
attestation,
signing_pub_key,
signed_info: enclave.sign(enclave.info()),
limiter_state,
})
}

Expand Down
6 changes: 6 additions & 0 deletions crates/hashi-guardian/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ impl EnclaveState {
// Initialization Status
// ========================================================================

/// Snapshot the current limiter state (if initialized).
pub async fn limiter_state(&self) -> Option<LimiterState> {
let limiter = self.rate_limiter.get()?;
Some(*limiter.lock().await.state())
}

fn status_check_inner(&self) -> (bool, bool) {
let committee_init = self
.committee
Expand Down
20 changes: 20 additions & 0 deletions crates/hashi-types/proto/sui/hashi/v1alpha/bridge_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ service BridgeService {
rpc SignWithdrawalTxSigning(SignWithdrawalTxSigningRequest) returns (SignWithdrawalTxSigningResponse);
// Step 4: Sign committee approval to confirm a processed withdrawal on-chain.
rpc SignWithdrawalConfirmation(SignWithdrawalConfirmationRequest) returns (SignWithdrawalConfirmationResponse);
// Sign a guardian withdrawal request so the leader can forward it to the guardian.
rpc SignGuardianWithdrawalRequest(SignGuardianWithdrawalRequestRequest) returns (SignGuardianWithdrawalRequestResponse);
}

message GetServiceInfoRequest {}
Expand Down Expand Up @@ -159,3 +161,21 @@ message SignWithdrawalConfirmationRequest {
message SignWithdrawalConfirmationResponse {
MemberSignature member_signature = 1;
}

// The leader sends the pending withdrawal id along with the guardian-specific
// fields (timestamp, seq) so each validator can independently reconstruct and
// sign the same StandardWithdrawalRequest.
message SignGuardianWithdrawalRequestRequest {
// Pending withdrawal id on Sui (32 bytes).
bytes pending_withdrawal_id = 1;

// Timestamp in unix seconds (used for guardian rate limiting).
uint64 timestamp_secs = 2;

// Monotonic sequence number (used by guardian for replay prevention).
uint64 seq = 3;
}

message SignGuardianWithdrawalRequestResponse {
MemberSignature member_signature = 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ message GetGuardianInfoResponse {

// Signed guardian info (includes server version, encryption pubkey, and optional S3/bucket info).
SignedGuardianInfo signed_info = 3;

// Current rate limiter state (if initialized). Includes next_seq.
LimiterState limiter_state = 4;
}

// Guardian-signed wrapper around `GuardianInfoData`.
Expand Down
2 changes: 2 additions & 0 deletions crates/hashi-types/src/guardian/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ pub struct GetGuardianInfoResponse {
pub signing_pub_key: GuardianPubKey,
/// Signed guardian info
pub signed_info: GuardianSigned<GuardianInfo>,
/// Current rate limiter state (if initialized). Includes `next_seq`.
pub limiter_state: Option<LimiterState>,
}

/// TODO: Add network?
Expand Down
4 changes: 4 additions & 0 deletions crates/hashi-types/src/guardian/proto_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,13 @@ impl TryFrom<pb::GetGuardianInfoResponse> for GetGuardianInfoResponse {
let signed_info_pb = resp.signed_info.ok_or_else(|| missing("signed_info"))?;
let signed_info = pb_to_signed_guardian_info(signed_info_pb)?;

let limiter_state = resp.limiter_state.map(pb_to_limiter_state).transpose()?;

Ok(GetGuardianInfoResponse {
attestation: attestation.to_vec(),
signing_pub_key,
signed_info,
limiter_state,
})
}
}
Expand Down Expand Up @@ -331,6 +334,7 @@ pub fn get_guardian_info_response_to_pb(r: GetGuardianInfoResponse) -> pb::GetGu
attestation: Some(r.attestation.into()),
signing_pub_key: Some(r.signing_pub_key.to_bytes().to_vec().into()),
signed_info: Some(signed_guardian_info_to_pb(r.signed_info)),
limiter_state: r.limiter_state.map(limiter_state_to_pb),
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/hashi-types/src/guardian/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl GetGuardianInfoResponse {
attestation: "abcd".as_bytes().to_vec(),
signing_pub_key,
signed_info: GuardianSigned::new(info, &signing_key, 1234),
limiter_state: None,
}
}
}
Expand Down
Binary file modified crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin
Binary file not shown.
115 changes: 115 additions & 0 deletions crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ pub struct SignWithdrawalConfirmationResponse {
#[prost(message, optional, tag = "1")]
pub member_signature: ::core::option::Option<MemberSignature>,
}
/// The leader sends the pending withdrawal id along with the guardian-specific
/// fields (timestamp, seq) so each validator can independently reconstruct and
/// sign the same StandardWithdrawalRequest.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SignGuardianWithdrawalRequestRequest {
/// Pending withdrawal id on Sui (32 bytes).
#[prost(bytes = "bytes", tag = "1")]
pub pending_withdrawal_id: ::prost::bytes::Bytes,
/// Timestamp in unix seconds (used for guardian rate limiting).
#[prost(uint64, tag = "2")]
pub timestamp_secs: u64,
/// Monotonic sequence number (used by guardian for replay prevention).
#[prost(uint64, tag = "3")]
pub seq: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SignGuardianWithdrawalRequestResponse {
#[prost(message, optional, tag = "1")]
pub member_signature: ::core::option::Option<MemberSignature>,
}
/// Generated client implementations.
pub mod bridge_service_client {
#![allow(
Expand Down Expand Up @@ -448,6 +468,36 @@ pub mod bridge_service_client {
);
self.inner.unary(req, path, codec).await
}
/// Sign a guardian withdrawal request so the leader can forward it to the guardian.
pub async fn sign_guardian_withdrawal_request(
&mut self,
request: impl tonic::IntoRequest<super::SignGuardianWithdrawalRequestRequest>,
) -> std::result::Result<
tonic::Response<super::SignGuardianWithdrawalRequestResponse>,
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.BridgeService/SignGuardianWithdrawalRequest",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"sui.hashi.v1alpha.BridgeService",
"SignGuardianWithdrawalRequest",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
Expand Down Expand Up @@ -520,6 +570,14 @@ pub mod bridge_service_server {
tonic::Response<super::SignWithdrawalConfirmationResponse>,
tonic::Status,
>;
/// Sign a guardian withdrawal request so the leader can forward it to the guardian.
async fn sign_guardian_withdrawal_request(
&self,
request: tonic::Request<super::SignGuardianWithdrawalRequestRequest>,
) -> std::result::Result<
tonic::Response<super::SignGuardianWithdrawalRequestResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct BridgeServiceServer<T> {
Expand Down Expand Up @@ -955,6 +1013,60 @@ pub mod bridge_service_server {
};
Box::pin(fut)
}
"/sui.hashi.v1alpha.BridgeService/SignGuardianWithdrawalRequest" => {
#[allow(non_camel_case_types)]
struct SignGuardianWithdrawalRequestSvc<T: BridgeService>(
pub Arc<T>,
);
impl<
T: BridgeService,
> tonic::server::UnaryService<
super::SignGuardianWithdrawalRequestRequest,
> for SignGuardianWithdrawalRequestSvc<T> {
type Response = super::SignGuardianWithdrawalRequestResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<
super::SignGuardianWithdrawalRequestRequest,
>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as BridgeService>::sign_guardian_withdrawal_request(
&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 = SignGuardianWithdrawalRequestSvc(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 Expand Up @@ -1008,6 +1120,9 @@ pub struct GetGuardianInfoResponse {
/// Signed guardian info (includes server version, encryption pubkey, and optional S3/bucket info).
#[prost(message, optional, tag = "3")]
pub signed_info: ::core::option::Option<SignedGuardianInfo>,
/// Current rate limiter state (if initialized). Includes next_seq.
#[prost(message, optional, tag = "4")]
pub limiter_state: ::core::option::Option<LimiterState>,
}
/// Guardian-signed wrapper around `GuardianInfoData`.
#[derive(Clone, PartialEq, ::prost::Message)]
Expand Down
9 changes: 9 additions & 0 deletions crates/hashi/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ pub struct Config {
#[serde(skip_serializing_if = "Option::is_none")]
pub screener_endpoint: Option<String>,

/// URL of the guardian gRPC service endpoint (e.g. `http://guardian:3000`).
/// When not set, guardian rate limiting is skipped during withdrawals.
#[serde(skip_serializing_if = "Option::is_none")]
pub guardian_endpoint: Option<String>,

/// Maximum gRPC decoding message size in bytes.
///
/// Defaults to 16 MiB if not specified. Tonic's built-in default is 4 MiB,
Expand Down Expand Up @@ -344,6 +349,10 @@ impl Config {
self.screener_endpoint.as_deref()
}

pub fn guardian_endpoint(&self) -> Option<&str> {
self.guardian_endpoint.as_deref()
}

pub fn grpc_max_decoding_message_size(&self) -> usize {
self.grpc_max_decoding_message_size
.unwrap_or(16 * 1024 * 1024)
Expand Down
29 changes: 29 additions & 0 deletions crates/hashi/src/grpc/bridge_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use hashi_types::proto::GetServiceInfoRequest;
use hashi_types::proto::GetServiceInfoResponse;
use hashi_types::proto::SignDepositConfirmationRequest;
use hashi_types::proto::SignDepositConfirmationResponse;
use hashi_types::proto::SignGuardianWithdrawalRequestRequest;
use hashi_types::proto::SignGuardianWithdrawalRequestResponse;
use hashi_types::proto::SignWithdrawalConfirmationRequest;
use hashi_types::proto::SignWithdrawalConfirmationResponse;
use hashi_types::proto::SignWithdrawalRequestApprovalRequest;
Expand Down Expand Up @@ -183,6 +185,33 @@ impl BridgeService for HttpService {
member_signature: Some(member_signature),
}))
}

async fn sign_guardian_withdrawal_request(
&self,
request: Request<SignGuardianWithdrawalRequestRequest>,
) -> Result<Response<SignGuardianWithdrawalRequestResponse>, Status> {
authenticate_caller(&request)?;
let req = request.into_inner();
let pending_withdrawal_id = parse_address(&req.pending_withdrawal_id)
.map_err(|e| Status::invalid_argument(format!("invalid pending_withdrawal_id: {e}")))?;
let timestamp_secs = req.timestamp_secs;
let seq = req.seq;
let member_signature = self
.inner
.validate_and_sign_guardian_withdrawal_request(
&pending_withdrawal_id,
timestamp_secs,
seq,
)
.map_err(|e| Status::failed_precondition(e.to_string()))?;
tracing::info!(
pending_withdrawal_id = %pending_withdrawal_id,
"Signed guardian withdrawal request",
);
Ok(Response::new(SignGuardianWithdrawalRequestResponse {
member_signature: Some(member_signature),
}))
}
}

fn authenticate_caller<T>(request: &Request<T>) -> Result<Address, Status> {
Expand Down
56 changes: 56 additions & 0 deletions crates/hashi/src/grpc/guardian_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::time::Duration;

use hashi_types::proto;
use hashi_types::proto::guardian_service_client::GuardianServiceClient;
use tonic::transport::Channel;
use tonic::transport::Endpoint;

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

#[derive(Clone, Debug)]
pub struct GuardianClient {
endpoint: String,
channel: Channel,
}

impl GuardianClient {
pub fn new(endpoint: &str) -> Result<Self, tonic::Status> {
let channel = Endpoint::from_shared(endpoint.to_string())
.map_err(Into::<BoxError>::into)
.map_err(tonic::Status::from_error)?
.connect_timeout(Duration::from_secs(5))
.http2_keep_alive_interval(Duration::from_secs(5))
.connect_lazy();
Ok(Self {
endpoint: endpoint.to_string(),
channel,
})
}

pub fn endpoint(&self) -> &str {
&self.endpoint
}

fn client(&self) -> GuardianServiceClient<Channel> {
GuardianServiceClient::new(self.channel.clone())
}

pub async fn get_guardian_info(&self) -> Result<proto::GetGuardianInfoResponse, tonic::Status> {
let response = self
.client()
.get_guardian_info(proto::GetGuardianInfoRequest {})
.await?;
Ok(response.into_inner())
}

pub async fn standard_withdrawal(
&self,
request: proto::SignedStandardWithdrawalRequest,
) -> Result<proto::SignedStandardWithdrawalResponse, tonic::Status> {
let response = self.client().standard_withdrawal(request).await?;
Ok(response.into_inner())
}
}
1 change: 1 addition & 0 deletions crates/hashi/src/grpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod client;
pub use client::Client;

pub mod bridge_service;
pub mod guardian_client;
pub mod screener_client;

/// Wrapper that triggers graceful HTTP server shutdown on drop.
Expand Down
Loading
Loading