Skip to content

Commit b3a2eec

Browse files
committed
Add support for multiple OPRF instances in a single server, use duration strings instead of seconds for epoch durations
1 parent b206ded commit b3a2eec

File tree

8 files changed

+469
-202
lines changed

8 files changed

+469
-202
lines changed

Cargo.lock

Lines changed: 9 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ edition = "2021"
1010
axum = "0.6.20"
1111
axum-prometheus = "0.4.0"
1212
base64 = "0.21.2"
13+
calendar-duration = { git = "https://github.com/brave-experiments/calendar-duration", branch = "init" }
1314
clap = { version = "4.3.24", features = ["derive"] }
1415
metrics-exporter-prometheus = "0.12.1"
1516
ppoprf = "0.3.1"

src/handler.rs

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
//! STAR Randomness web service route implementation
22
3-
use axum::extract::{Json, State};
3+
use std::sync::RwLockReadGuard;
4+
5+
use axum::extract::{Json, Path, State};
46
use axum::http::StatusCode;
57
use base64::prelude::{Engine as _, BASE64_STANDARD as BASE64};
68
use serde::{Deserialize, Serialize};
7-
use tracing::debug;
9+
use tracing::{debug, instrument};
810

9-
use crate::OPRFState;
11+
use crate::state::{OPRFInstance, OPRFState};
1012
use ppoprf::ppoprf;
1113

1214
/// Request format for the randomness endpoint
@@ -63,6 +65,8 @@ struct ErrorResponse {
6365
/// handling requests.
6466
#[derive(thiserror::Error, Debug)]
6567
pub enum Error {
68+
#[error("instance '{0}' not found")]
69+
InstanceNotFound(String),
6670
#[error("Couldn't lock state: RwLock poisoned")]
6771
LockFailure,
6872
#[error("Invalid point")]
@@ -93,6 +97,7 @@ impl axum::response::IntoResponse for Error {
9397
/// Construct an http response from our error type
9498
fn into_response(self) -> axum::response::Response {
9599
let code = match self {
100+
Error::InstanceNotFound(_) => StatusCode::NOT_FOUND,
96101
// This indicates internal failure.
97102
Error::LockFailure => StatusCode::INTERNAL_SERVER_ERROR,
98103
// Other cases are the client's fault.
@@ -105,13 +110,28 @@ impl axum::response::IntoResponse for Error {
105110
}
106111
}
107112

113+
type Result<T> = std::result::Result<T, Error>;
114+
115+
fn get_server_from_state(
116+
state: &OPRFState,
117+
instance_name: String,
118+
) -> Result<RwLockReadGuard<'_, OPRFInstance>> {
119+
Ok(state
120+
.instances
121+
.get(&instance_name)
122+
.ok_or(Error::InstanceNotFound(instance_name))?
123+
.read()?)
124+
}
125+
108126
/// Process PPOPRF evaluation requests
109-
pub async fn randomness(
110-
State(state): State<OPRFState>,
111-
Json(request): Json<RandomnessRequest>,
112-
) -> Result<Json<RandomnessResponse>, Error> {
127+
#[instrument(skip(state, request))]
128+
async fn randomness(
129+
state: OPRFState,
130+
instance_name: String,
131+
request: RandomnessRequest,
132+
) -> Result<Json<RandomnessResponse>> {
113133
debug!("recv: {request:?}");
114-
let state = state.read()?;
134+
let state = get_server_from_state(&state, instance_name)?;
115135
let epoch = request.epoch.unwrap_or(state.epoch);
116136
if epoch != state.epoch {
117137
return Err(Error::BadEpoch(epoch));
@@ -138,12 +158,29 @@ pub async fn randomness(
138158
Ok(Json(response))
139159
}
140160

141-
/// Process PPOPRF epoch and key requests
142-
pub async fn info(
161+
/// Process PPOPRF evaluation requests using default instance
162+
pub async fn default_instance_randomness(
163+
State(state): State<OPRFState>,
164+
Json(request): Json<RandomnessRequest>,
165+
) -> Result<Json<RandomnessResponse>> {
166+
let instance_name = state.default_instance.clone();
167+
randomness(state, instance_name, request).await
168+
}
169+
170+
/// Process PPOPRF evaluation requests using specific instance
171+
pub async fn specific_instance_randomness(
143172
State(state): State<OPRFState>,
144-
) -> Result<Json<InfoResponse>, Error> {
173+
Path(instance_name): Path<String>,
174+
Json(request): Json<RandomnessRequest>,
175+
) -> Result<Json<RandomnessResponse>> {
176+
randomness(state, instance_name, request).await
177+
}
178+
179+
/// Provide PPOPRF epoch and key metadata
180+
#[instrument(skip(state))]
181+
async fn info(state: OPRFState, instance_name: String) -> Result<Json<InfoResponse>> {
145182
debug!("recv: info request");
146-
let state = state.read()?;
183+
let state = get_server_from_state(&state, instance_name)?;
147184
let public_key = state.server.get_public_key().serialize_to_bincode()?;
148185
let public_key = BASE64.encode(public_key);
149186
let response = InfoResponse {
@@ -155,3 +192,17 @@ pub async fn info(
155192
debug!("send: {response:?}");
156193
Ok(Json(response))
157194
}
195+
196+
/// Provide PPOPRF epoch and key metadata using default instance
197+
pub async fn default_instance_info(State(state): State<OPRFState>) -> Result<Json<InfoResponse>> {
198+
let instance_name = state.default_instance.clone();
199+
info(state, instance_name).await
200+
}
201+
202+
/// Provide PPOPRF epoch and key metadata using specific instance
203+
pub async fn specific_instance_info(
204+
State(state): State<OPRFState>,
205+
Path(instance_name): Path<String>,
206+
) -> Result<Json<InfoResponse>> {
207+
info(state, instance_name).await
208+
}

src/main.rs

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22
33
use axum::{routing::get, routing::post, Router};
44
use axum_prometheus::PrometheusMetricLayer;
5+
use calendar_duration::CalendarDuration;
56
use clap::Parser;
67
use metrics_exporter_prometheus::PrometheusHandle;
78
use rlimit::Resource;
8-
use std::sync::{Arc, RwLock};
9+
use state::{OPRFServer, OPRFState};
910
use tikv_jemallocator::Jemalloc;
10-
use time::format_description::well_known::Rfc3339;
1111
use time::OffsetDateTime;
1212
use tracing::{debug, info, metadata::LevelFilter};
1313
use tracing_subscriber::EnvFilter;
14+
use util::{assert_unique_names, parse_timestamp};
1415

1516
#[global_allocator]
1617
static GLOBAL: Jemalloc = Jemalloc;
1718

1819
mod handler;
1920
mod state;
20-
21-
pub use state::OPRFState;
21+
mod util;
2222

2323
#[cfg(test)]
2424
mod tests;
@@ -27,15 +27,22 @@ mod tests;
2727
const MAX_POINTS: usize = 1024;
2828

2929
/// Command line switches
30-
#[derive(Parser, Debug)]
30+
#[derive(Parser, Debug, Clone)]
3131
#[command(author, version, about, long_about = None)]
3232
pub struct Config {
3333
/// Host and port to listen for http connections
3434
#[arg(long, default_value = "127.0.0.1:8080")]
3535
listen: String,
36-
/// Duration of each randomness epoch
37-
#[arg(long, default_value_t = 5)]
38-
epoch_seconds: u32,
36+
/// Name of OPRF instance contained in server. Multiple instances may be defined
37+
/// by defining this switch multiple times. The first defined instance will
38+
/// become the default instance.
39+
#[arg(long = "instance-name", default_value = "main")]
40+
instance_names: Vec<String>,
41+
/// Duration of each randomness epoch. This switch may be defined multiple times
42+
/// to set the epoch length for each respective instance, if multiple instances
43+
/// are defined.
44+
#[arg(long = "epoch-duration", value_name = "Duration string i.e. 1mon5h2s", default_values = ["5s"])]
45+
epoch_durations: Vec<CalendarDuration>,
3946
/// First epoch tag to make available
4047
#[arg(long, default_value_t = 0)]
4148
first_epoch: u8,
@@ -56,20 +63,24 @@ pub struct Config {
5663
prometheus_listen: Option<String>,
5764
}
5865

59-
/// Parse a timestamp given as a config option
60-
fn parse_timestamp(stamp: &str) -> Result<OffsetDateTime, &'static str> {
61-
OffsetDateTime::parse(stamp, &Rfc3339).map_err(|_| "Try something like '2023-05-15T04:30:00Z'.")
62-
}
63-
6466
/// Initialize an axum::Router for our web service
6567
/// Having this as a separate function makes testing easier.
6668
fn app(oprf_state: OPRFState) -> Router {
6769
Router::new()
6870
// Friendly default route to identify the site
6971
.route("/", get(|| async { "STAR randomness server\n" }))
70-
// Main endpoints
71-
.route("/randomness", post(handler::randomness))
72-
.route("/info", get(handler::info))
72+
// Endpoints for all instances
73+
.route(
74+
"/instances/:instance/randomness",
75+
post(handler::specific_instance_randomness),
76+
)
77+
.route(
78+
"/instances/:instance/info",
79+
get(handler::specific_instance_info),
80+
)
81+
// Endpoints for default instance
82+
.route("/randomness", post(handler::default_instance_randomness))
83+
.route("/info", get(handler::default_instance_info))
7384
// Attach shared state
7485
.with_state(oprf_state)
7586
// Logging must come after active routes
@@ -126,22 +137,28 @@ async fn main() {
126137
increase_nofile_limit();
127138
}
128139

129-
// Oblivious function state
130-
info!("initializing OPRF state...");
131-
let server = state::OPRFServer::new(&config).expect("Could not initialize PPOPRF state");
132-
info!("epoch now {}", server.epoch);
133-
let oprf_state = Arc::new(RwLock::new(server));
140+
assert_unique_names(&config.instance_names);
141+
assert!(
142+
!config.epoch_durations.iter().any(|d| d.is_zero()),
143+
"all epoch lengths must be non-zero"
144+
);
145+
assert!(
146+
!config.instance_names.is_empty(),
147+
"at least one instance name must be defined"
148+
);
149+
assert!(
150+
config.instance_names.len() == config.epoch_durations.len(),
151+
"instance-name switch count must match epoch-seconds switch count"
152+
);
134153

135154
let metric_layer = config.prometheus_listen.as_ref().map(|listen| {
136155
let (layer, handle) = PrometheusMetricLayer::pair();
137156
start_prometheus_server(handle, listen.clone());
138157
layer
139158
});
140159

141-
// Spawn a background process to advance the epoch
142-
info!("Spawning background epoch rotation task...");
143-
let background_state = oprf_state.clone();
144-
tokio::spawn(async move { state::epoch_loop(background_state, &config).await });
160+
let oprf_state = OPRFServer::new(&config);
161+
oprf_state.start_background_tasks(&config);
145162

146163
// Set up routes and middleware
147164
info!("initializing routes...");

0 commit comments

Comments
 (0)