Skip to content

Commit 0e5df2d

Browse files
committed
[aggregator-server] set up server crate with todos
1 parent a60cecf commit 0e5df2d

File tree

7 files changed

+301
-0
lines changed

7 files changed

+301
-0
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ prost-types = "0.14"
2525
sui-rpc = "0.1.0"
2626
tonic = "0.14"
2727

28+
axum = { version = "0.8", features = ["macros"] }
29+
tower = "0.5"
30+
tower-http = { version = "0.6.6", features = ["cors", "limit"] }
31+
tokio = { version = "1.46.1", features = ["full"] }
32+
futures = "0.3"
33+
reqwest = "0.12"
34+
tracing-subscriber = {version = "0.3.20", features = ["env-filter"]}
35+
semver = { version = "1.0.26", features = ["serde"] }
36+
serde_yaml = "0.9"
37+
2838
# Sui dependencies
2939
sui_types = { git = "https://github.com/mystenlabs/sui", rev = "22642cf", package = "sui-types"}
3040
mysten-service = { git = "https://github.com/mystenlabs/sui", rev = "22642cf" }

Dockerfile.aggregator

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Start with a Rust base image
2+
FROM rust:1.88-bullseye AS builder
3+
4+
ARG PROFILE=release
5+
6+
WORKDIR work
7+
8+
COPY ./crates ./crates
9+
COPY ./Cargo.toml ./
10+
11+
ARG GIT_REVISION
12+
ENV GIT_REVISION=$GIT_REVISION
13+
14+
RUN cargo build --bin aggregator-server --profile $PROFILE --config net.git-fetch-with-cli=true
15+
16+
FROM debian:bullseye-slim AS runtime
17+
18+
EXPOSE 2024
19+
20+
RUN apt-get update && apt-get install -y ca-certificates
21+
22+
COPY --from=builder /work/target/release/aggregator-server /opt/aggregator-server/bin/
23+
COPY ./crates/aggregator-server/aggregator-config.yaml /opt/aggregator-server/config/
24+
25+
# Set default CONFIG_PATH
26+
ENV CONFIG_PATH=/opt/aggregator-server/config/aggregator-config.yaml
27+
28+
# Handle all environment variables
29+
RUN echo '#!/bin/bash\n\
30+
# Export all environment variables\n\
31+
for var in $(env | cut -d= -f1); do\n\
32+
export "$var"\n\
33+
done\n\
34+
\n\
35+
exec /opt/aggregator-server/bin/aggregator-server "$@"' > /opt/aggregator-server/entrypoint.sh && \
36+
chmod +x /opt/aggregator-server/entrypoint.sh
37+
38+
ENTRYPOINT ["/opt/aggregator-server/entrypoint.sh"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "aggregator-server"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
8+
[[bin]]
9+
name = "aggregator-server"
10+
path = "src/main.rs"
11+
12+
[dependencies]
13+
seal-sdk = { path = "../seal-sdk" }
14+
seal-committee = { path = "../seal-committee" }
15+
crypto = { path = "../crypto" }
16+
17+
mysten-service.workspace = true
18+
sui-sdk-types.workspace = true
19+
sui-rpc.workspace = true
20+
fastcrypto.workspace = true
21+
22+
anyhow.workspace = true
23+
axum.workspace = true
24+
tower-http.workspace = true
25+
futures.workspace = true
26+
reqwest.workspace = true
27+
tracing.workspace = true
28+
tokio.workspace = true
29+
tracing-subscriber.workspace = true
30+
semver.workspace = true
31+
serde.workspace = true
32+
serde_yaml.workspace = true

crates/aggregator-server/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Aggregator Server
2+
3+
Aggregator server for Seal committee mode. It fetches encrypted partial keys from committee member servers, verifies encrypted signatures.
4+
5+
## Overview
6+
7+
The aggregator server:
8+
1. Loads committee configuration from onchain (threshold, member URLs, partial public keys)
9+
2. Receives fetch key requests from clients.
10+
3. Fans out requests to all committee member servers till threshold is met.
11+
5. Verifies encrypted signatures from each member using their partial public key.
12+
6. Aggregates encrypted responses using Lagrange interpolation.
13+
7. Returns the aggregated encrypted key to the client.
14+
15+
### Running the Server
16+
17+
See example configuration file at `aggregator-config.yaml` and modify as needed. Then run:
18+
19+
```shell
20+
CONFIG_PATH=crates/aggregator-server/aggregator-config.yaml cargo run --bin aggregator-server
21+
```
22+
23+
24+
## Running with Docker
25+
```shell
26+
docker build -f Dockerfile.aggregator -t aggregator-server:latest .
27+
28+
docker run -p 2024:2024 \
29+
-v $(pwd)/crates/aggregator-server/aggregator-config.yaml:/config/aggregator-config.yaml \
30+
-e CONFIG_PATH=/config/aggregator-config.yaml \
31+
aggregator-server
32+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
network: !Testnet
2+
key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000000'
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright (c), Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Aggregator server for Seal committee mode. It fetches encrypted partial keys from committee
5+
//! servers, verifies and aggregates them into a single response.
6+
7+
#![allow(dead_code)]
8+
#![allow(unused_variables)]
9+
10+
use anyhow::{Context, Result};
11+
use axum::{
12+
extract::State,
13+
http::{HeaderMap, StatusCode},
14+
response::{IntoResponse, Response},
15+
routing::post,
16+
Json, Router,
17+
};
18+
use mysten_service::{get_mysten_service, package_name, package_version};
19+
use seal_committee::{grpc_helper::create_grpc_client, move_types::PartialKeyServer, Network};
20+
use seal_sdk::{FetchKeyRequest, FetchKeyResponse};
21+
use serde::Deserialize;
22+
use std::env;
23+
use std::sync::Arc;
24+
use sui_rpc::client::Client as SuiGrpcClient;
25+
use sui_sdk_types::Address;
26+
use tower_http::cors::{Any, CorsLayer};
27+
use tracing::info;
28+
29+
/// Minimum required version for committee members' responses (matches typescript).
30+
const MIN_SERVER_VERSION: &str = ">=0.4.1";
31+
32+
/// Default port for aggregator server.
33+
const DEFAULT_PORT: u16 = 2024;
34+
35+
/// Configuration for aggregator server.
36+
#[derive(Deserialize)]
37+
struct Config {
38+
network: Network,
39+
key_server_object_id: Address,
40+
}
41+
42+
/// Application state.
43+
#[derive(Clone)]
44+
struct AppState {
45+
key_server_object_id: Address,
46+
network: Network,
47+
grpc_client: SuiGrpcClient,
48+
threshold: u16,
49+
committee_members: Arc<Vec<PartialKeyServer>>,
50+
// TODO: API storage and rotation.
51+
}
52+
53+
/// Custom error type for aggregator responses.
54+
struct AggregatorError {
55+
status: StatusCode,
56+
message: String,
57+
headers: HeaderMap,
58+
}
59+
60+
impl IntoResponse for AggregatorError {
61+
fn into_response(self) -> Response {
62+
let mut response = (self.status, self.message).into_response();
63+
*response.headers_mut() = self.headers;
64+
response
65+
}
66+
}
67+
68+
#[tokio::main]
69+
async fn main() -> Result<()> {
70+
tracing_subscriber::fmt::init();
71+
72+
// Load configuration from file.
73+
let config_path =
74+
env::var("CONFIG_PATH").context("CONFIG_PATH environment variable not set")?;
75+
info!("Loading config file: {}", config_path);
76+
77+
let config: Config = serde_yaml::from_reader(
78+
std::fs::File::open(&config_path)
79+
.context(format!("Cannot open configuration file {config_path}"))?,
80+
)
81+
.context("Failed to parse configuration file")?;
82+
let grpc_client = create_grpc_client(&config.network)?;
83+
84+
info!(
85+
"Starting aggregator for KeyServer {} on {:?}",
86+
config.key_server_object_id, config.network
87+
);
88+
89+
let state = AppState {
90+
key_server_object_id: config.key_server_object_id,
91+
network: config.network,
92+
grpc_client,
93+
threshold: 0, // TODO: Load from onchain
94+
committee_members: Arc::new(vec![]), // TODO: Load from onchain
95+
};
96+
97+
// TODO: Spawn background task to watch onchain for committee version updates:
98+
// 1. Every 30s, fetch KeyServerV2.version from onchain
99+
// 2. If version changes, refresh committee_members in AppState
100+
info!(
101+
"Loaded committee with {} members, threshold {}",
102+
state.committee_members.len(),
103+
state.threshold
104+
);
105+
106+
let port: u16 = env::var("PORT")
107+
.unwrap_or_else(|_| DEFAULT_PORT.to_string())
108+
.parse()
109+
.context("Invalid PORT")?;
110+
111+
let app = get_mysten_service::<AppState>(package_name!(), package_version!())
112+
.merge(Router::new().route("/v1/fetch_key", post(handle_fetch_key)))
113+
.with_state(state)
114+
.layer(CorsLayer::new().allow_origin(Any));
115+
116+
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
117+
let listener = tokio::net::TcpListener::bind(addr).await?;
118+
info!("Aggregator server listening on http://localhost:{}", port);
119+
120+
axum::serve(listener, app).await?;
121+
Ok(())
122+
}
123+
124+
/// Handle fetch_key request by fanning out to committee members and aggregating responses.
125+
async fn handle_fetch_key(
126+
State(state): State<AppState>,
127+
Json(request): Json<FetchKeyRequest>,
128+
) -> Result<(HeaderMap, Json<FetchKeyResponse>), AggregatorError> {
129+
// TODO:
130+
// 1. Call fetch_from_member for all committee members in parallel.
131+
// 2. Collect responses until we have t successful responses and abort others.
132+
// 3. Track versions from each response header (X-KeyServer-Version). Use the oldest version as
133+
// the aggregator's response version (?)
134+
// 4. Upon sufficient responses, Aaggregate encrypted responses using crypto::elgamal::aggregate_encrypted
135+
// 5. Return with appropriate headers
136+
137+
unimplemented!("depends on crypto code")
138+
}
139+
140+
/// Fetch encrypted partial key from a single committee member.
141+
async fn fetch_from_member(
142+
member: &PartialKeyServer,
143+
request: &FetchKeyRequest,
144+
) -> Result<(FetchKeyResponse, String), String> {
145+
// TODO:
146+
// 1. Implement HTTP client call to each member URL.
147+
// 2. Extract each and validate X-KeyServer-Version header with MIN_SERVER_VERSION
148+
// 3. Parse response body as FetchKeyResponse
149+
// 4. Verify encrypted signatures using crypto::ibe::verify_encrypted_signature.
150+
// 5. Return (response, version_string)
151+
152+
unimplemented!("not implemented yet")
153+
}
154+
155+
/// Load committee state from onchain KeyServerV2 object.
156+
async fn load_committee_state(key_server_obj_id: &Address, network: Network) -> Result<AppState> {
157+
// TODO:
158+
// 1. Fetch KeyServerV2 object from chain.
159+
// 2. Parse committee members and threshold.
160+
// 3. Return AppState with loaded data.
161+
162+
unimplemented!("not implemented yet")
163+
}

0 commit comments

Comments
 (0)