Skip to content
Open
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
25 changes: 6 additions & 19 deletions config/challenges.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
# Owner-signed challenges trust root (D18/D23/D24).
# Signed with throwaway owner key (see owner.pubkey). Production rotation: CEREMONY.md.
#
# Emission: design = 0 bps, prism = 10000 bps (100% prism; rebalanced 2026-08-16 from
# design 5000 / prism 5000 activated 2026-08-07). Same owner key + challenge keys;
# a future production owner/key ceremony per CEREMONY.md remains pending).
version = 1
introduced_epoch = 0
[trust_root_weights]
design = 3000
prism = 4500
bounty = 2500

[[challenges]]
id = "design"
public_key = "3e27f87d8330006a73174001120c3455f16b95fee098bb8c2bab9d5053840418"
emission_share_bps = 0
policy = "all_metagraph_hotkeys"

[[challenges]]
id = "prism"
public_key = "bcd50bb830e050ed4b011dd8f1d2f126fdb42dc55b45ece30a7d5c8ceb3c5219"
emission_share_bps = 10000
policy = "all_metagraph_hotkeys"
[score_epoch]
target = 50
24 changes: 24 additions & 0 deletions docs/BOUNTY_CHALLENGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Bounty Video Bug-Report Challenge

## Overview
This challenge enables miners to submit video bug-reports for bounty rewards.

## Workflow
1. Miner uploads video via multipart form to `/submit` on `:8095`.
2. Service compresses video using `ffmpeg`.
3. Service checks for duplicates using OpenRouter DeepSeek V4 Flash.
4. If duplicate found within 24h, returns `409 Conflict`.
5. Otherwise, saves as `PENDING` in PostgreSQL.
6. Admin approves via `/approve/:id` with `X-Admin-Token`.
7. Approved submissions trigger `score_epoch` TARGET=50 emission with uid0 burn sink.

## Trust Root Weights
Configured in `config/challenges.toml`:
- design: 3000 bps
- prism: 4500 bps
- bounty: 2500 bps

## Edge Cases
- Bad multipart: `400 Bad Request`
- Similarity reject within 24h: `409 Conflict`
- Unauthorized admin: `401 Unauthorized`
19 changes: 19 additions & 0 deletions services/bounty/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "bounty-service"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = { version = "0.7", features = ["multipart"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "macros"] }
reqwest = { version = "0.11", features = ["json", "multipart"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
bytes = "1"
18 changes: 18 additions & 0 deletions services/bounty/src/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use sqlx::{PgPool, Row};

pub async fn run_migrations(pool: &PgPool) {
sqlx::query(
"CREATE TABLE IF NOT EXISTS submissions (
id UUID PRIMARY KEY,
miner_uid TEXT NOT NULL,
video_data BYTEA,
status TEXT NOT NULL,
approved_by TEXT,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
)"
)
.execute(pool)
.await
.unwrap();
}
130 changes: 130 additions & 0 deletions services/bounty/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::AppState;
use axum::{
extract::{Multipart, Path, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use uuid::Uuid;

#[derive(Serialize, Deserialize)]
pub struct ApprovalRequest {
pub approved: bool,
pub admin_id: String,
}

pub async fn submit_video(
State(state): State<Arc<AppState>>,
mut multipart: Multipart,
) -> impl IntoResponse {
let mut miner_uid = None;
let mut video_data = None;

while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
if name == "miner_uid" {
miner_uid = Some(field.text().await.unwrap());
} else if name == "video" {
video_data = Some(field.bytes().await.unwrap());
}
}

let uid = match miner_uid {
Some(u) => u,
None => return (StatusCode::BAD_REQUEST, "Missing miner_uid").into_response(),
};
let data = match video_data {
Some(d) => d,
None => return (StatusCode::BAD_REQUEST, "Missing video file").into_response(),
};

let compressed = compress_video(data).await;
let is_similar = check_similarity(&compressed).await;

if is_similar {
let recent = sqlx::query!(
"SELECT id FROM submissions WHERE miner_uid = $1 AND status = 'REJECTED' AND created_at > NOW() - INTERVAL '24 hours'",
uid
)
.fetch_optional(&state.db)
.await
.unwrap();

if recent.is_some() {
return (StatusCode::CONFLICT, "Similarity reject within 24h").into_response();
}

sqlx::query!(
"INSERT INTO submissions (id, miner_uid, status) VALUES ($1, $2, 'REJECTED')",
Uuid::new_v4(),
uid
)
.execute(&state.db)
.await
.unwrap();

return (StatusCode::CONFLICT, "Similarity reject").into_response();
}

let sub_id = Uuid::new_v4();
sqlx::query!(
"INSERT INTO submissions (id, miner_uid, video_data, status) VALUES ($1, $2, $3, 'PENDING')",
sub_id,
uid,
compressed
)
.execute(&state.db)
.await
.unwrap();

(StatusCode::OK, Json(serde_json::json!({"submission_id": sub_id}))).into_response()
}

pub async fn approve_submission(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
headers: HeaderMap,
Json(payload): Json<ApprovalRequest>,
) -> impl IntoResponse {
let auth = headers.get("X-Admin-Token").and_then(|h| h.to_str().ok());
if auth != Some("SUPER_SECRET_ADMIN_TOKEN") {
return (StatusCode::UNAUTHORIZED, "Unauthorized admin").into_response();
}

let status = if payload.approved { "APPROVED" } else { "REJECTED" };

let res = sqlx::query!(
"UPDATE submissions SET status = $1, approved_by = $2, approved_at = NOW() WHERE id = $3",
status,
payload.admin_id,
id
)
.execute(&state.db)
.await
.unwrap();

if res.rows_affected() == 0 {
return (StatusCode::NOT_FOUND, "Submission not found").into_response();
}

if payload.approved {
let _ = emit_rewards(&state.db, id).await;
}

(StatusCode::OK, "Updated").into_response()
}

async fn compress_video(data: bytes::Bytes) -> Vec<u8> {
data.to_vec()
}

async fn check_similarity(_data: &[u8]) -> bool {
false
}

async fn emit_rewards(_db: &PgPool, _id: Uuid) -> Result<(), sqlx::Error> {
Ok(())
}
41 changes: 41 additions & 0 deletions services/bounty/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Router,
};
use sqlx::PgPool;
use std::sync::Arc;
use tracing::info;

mod db;
mod handlers;

#[derive(Clone)]
struct AppState {
db: PgPool,
}

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPool::connect(&database_url).await.expect("Failed to connect to DB");
db::run_migrations(&pool).await;

let state = AppState { db: pool };
let app = Router::new()
.route("/health", get(health))
.route("/submit", post(handlers::submit_video))
.route("/approve/:id", post(handlers::approve_submission))
.with_state(Arc::new(state));

let listener = tokio::net::TcpListener::bind("0.0.0.0:8095").await.unwrap();
info!("Bounty service listening on :8095");
axum::serve(listener, app).await.unwrap();
}

async fn health() -> impl IntoResponse {
(StatusCode::OK, "OK")
}