use clap::Parser;
use siglog::witness::{handlers, LogConfig, Witness};
use sea_orm::{ConnectOptions, ConnectionTrait, Database as SeaDatabase, DatabaseConnection};
use sea_orm_migration::MigratorTrait;
use std::sync::Arc;
use std::time::Duration;
#[derive(Parser, Debug)]
#[command(name = "witness")]
#[command(about = "A witness server for transparency logs")]
struct Args {
#[arg(long, env = "DATABASE_URL", default_value = "sqlite:./witness.db")]
database_url: String,
#[arg(long, env = "WITNESS_PRIVATE_KEY")]
private_key: String,
#[arg(long = "log", env = "WITNESS_LOGS", value_parser = parse_log_config)]
logs: Vec<LogConfig>,
#[arg(long, env = "LISTEN_ADDR", default_value = "0.0.0.0:2026")]
listen: String,
}
fn parse_log_config(s: &str) -> Result<LogConfig, String> {
let parts: Vec<&str> = s.splitn(2, '=').collect();
if parts.len() != 2 {
return Err(format!(
"invalid log config format: expected 'origin=vkey', got '{}'",
s
));
}
LogConfig::new(parts[0].to_string(), parts[1]).map_err(|e| format!("invalid log config: {}", e))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("witness=info".parse()?)
.add_directive("tower_http=debug".parse()?),
)
.init();
let args = Args::parse();
tracing::info!("Starting Siglog Witness");
if args.logs.is_empty() {
anyhow::bail!("At least one log must be configured with --log");
}
for log in &args.logs {
tracing::info!("Configured to witness log: {}", log.origin);
}
tracing::info!("Connecting to database: {}", args.database_url);
let conn = connect_database(&args.database_url).await?;
let conn = Arc::new(conn);
tracing::info!("Database connected and migrations complete");
let signer = Arc::new(
siglog::checkpoint::CheckpointSigner::from_note_key(&args.private_key)
.map_err(|e| anyhow::anyhow!("invalid private key: {}", e))?,
);
tracing::info!("Witness signer initialized: {}", signer.name());
let witness = Arc::new(Witness::new(signer, conn, args.logs));
tracing::info!("Witness name: {}", witness.name());
let app = axum::Router::new()
.route(
"/add-checkpoint",
axum::routing::post(handlers::add_checkpoint),
)
.route("/health", axum::routing::get(handlers::health))
.with_state(witness)
.layer(
tower_http::trace::TraceLayer::new_for_http()
.make_span_with(
tower_http::trace::DefaultMakeSpan::new().level(tracing::Level::INFO),
)
.on_response(
tower_http::trace::DefaultOnResponse::new().level(tracing::Level::INFO),
),
);
let listener = tokio::net::TcpListener::bind(&args.listen).await?;
tracing::info!("Witness server listening on {}", args.listen);
let shutdown_signal = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
tracing::info!("Shutdown signal received");
};
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal)
.await?;
tracing::info!("Witness server stopped");
Ok(())
}
async fn connect_database(database_url: &str) -> anyhow::Result<DatabaseConnection> {
let mut opts = ConnectOptions::new(database_url);
opts.max_connections(10)
.min_connections(1)
.connect_timeout(Duration::from_secs(10))
.idle_timeout(Duration::from_secs(300))
.sqlx_logging(false);
let conn = SeaDatabase::connect(opts).await?;
if matches!(
conn.get_database_backend(),
sea_orm::DatabaseBackend::Sqlite
) {
conn.execute_unprepared("PRAGMA journal_mode=WAL").await?;
conn.execute_unprepared("PRAGMA busy_timeout=5000").await?;
}
siglog::migration::Migrator::up(&conn, None).await?;
Ok(conn)
}