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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ miner-runtime/
!deploy/secrets/
deploy/secrets/*
!deploy/secrets/README.md
!deploy/secrets/lium/
deploy/secrets/lium/*
!deploy/secrets/lium/README.md


__pycache__/
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Match CI (`.github/workflows/ci.yml`):
| Doc authority vs evidence | [`docs/AGENTS.md`](docs/AGENTS.md) |
| Component status | [`docs/COMPLETENESS.md`](docs/COMPLETENESS.md) |
| Frozen contracts | [`docs/BUNDLE_SPEC.md`](docs/BUNDLE_SPEC.md), [`docs/DESIGN_CHALLENGE.md`](docs/DESIGN_CHALLENGE.md), [`docs/PRISM.md`](docs/PRISM.md) |
| Shared Lium GPU prepay (scaffolding) | [`docs/LIUM_FUNDING.md`](docs/LIUM_FUNDING.md) · crate `lium-funding` · **off** in prod until wallet live (`PRISM_REQUIRE_LIUM_FUNDING=0`) |
| Miner HTTP submit | [`docs/external-miner/`](docs/external-miner/) · public: [design-challenge](https://github.com/BaseIntelligence/design-challenge), [prism](https://github.com/BaseIntelligence/prism) |
| Threat / operator checklist | [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md), [`docs/OPERATOR_SECURITY.md`](docs/OPERATOR_SECURITY.md) |

Expand Down
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bins/prism-challenge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ challenge-keys = { path = "../../crates/challenge-keys" }
chain = { path = "../../crates/chain" }
chain-live = { path = "../../crates/chain-live" }
clap = { version = "4", features = ["derive", "env"] }
lium-funding = { path = "../../crates/lium-funding" }
prism-challenge = { path = "../../crates/prism-challenge" }
prism-lium = { path = "../../crates/prism-lium" }
prism-recipe = { path = "../../crates/prism-recipe" }
Expand All @@ -29,6 +30,7 @@ db = { path = "../../crates/db" }
telemetry = { path = "../../crates/telemetry" }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "sync", "time"] }
trustroot = { path = "../../crates/trustroot" }
async-trait = "0.1"
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
tracing = "0.1"

Expand Down
89 changes: 87 additions & 2 deletions bins/prism-challenge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use challenge_agentic::{AgenticBackend, OpenRouterAgent, SimAgent};
use challenge_keys::load_challenge_secret;
use clap::{Parser, Subcommand};
use lium_funding::{
funding_router, EligibilityChecker, EnvTaoOracle, FakeLiumAccount, FakeTaoVerifier,
FundingConfig, FundingError, FundingHttpState, FundingService, HttpLiumAccount,
LiumAccountClient, MemoryFundingStore, PrismFundingPolicy, TaoPaymentVerifier, TaoPriceOracle,
};
use prism_challenge::{
submission_router, AppState, DbPrismStore, MemoryPrismStore, Orchestrator, OrchestratorConfig,
PrismStore, CHALLENGE_ID, SCORING_VERSION,
Expand Down Expand Up @@ -361,6 +367,7 @@ fn build_topmodel() -> Option<Arc<prism_registry::TopModelPublisher>> {
p
}

#[allow(clippy::too_many_lines)]
async fn cmd_serve(cli: Cli) -> Result<(), String> {
let path = resolve_sk_path(cli.challenge_sk_file.as_ref())?;
if !path.is_file() {
Expand Down Expand Up @@ -398,7 +405,13 @@ async fn cmd_serve(cli: Cli) -> Result<(), String> {
gating: gating_enabled.then(|| Arc::clone(&gating)),
metagraph: gating_enabled.then(|| Arc::clone(&metagraph)),
});
let app = submission_router(Arc::clone(&state));
let funding = build_funding(
Arc::clone(&store),
gating_enabled.then(|| Arc::clone(&metagraph)),
);
let app = submission_router(Arc::clone(&state)).merge(funding_router(FundingHttpState {
service: Arc::clone(&funding),
}));

// Chain (for epoch/E): live only when endpoint configured; otherwise a
// fixed epoch-0 (local sim posture documented in PRISM.md).
Expand Down Expand Up @@ -454,7 +467,7 @@ async fn cmd_serve(cli: Cli) -> Result<(), String> {
Duration::from_secs(cli.gating_watch_secs.max(15)),
);
}
let orchestrator = Arc::new(orchestrator);
let orchestrator = Arc::new(orchestrator.with_funding(funding));
spawn_orchestrator(&cli, &orchestrator);

let listener = TcpListener::bind(cli.bind)
Expand Down Expand Up @@ -500,6 +513,78 @@ fn spawn_epoch_feed(chain_ep: &str, state: &Arc<AppState>) {
});
}

/// Prism funding eligibility: metagraph member + zero prior Prism submissions.
struct PrismFundingElig {
store: Arc<dyn PrismStore>,
metagraph: Option<Arc<MetagraphCache>>,
}

#[async_trait]
impl EligibilityChecker for PrismFundingElig {
async fn ensure_eligible(&self, hotkey: &str) -> Result<(), FundingError> {
let hk = hotkey.trim().to_ascii_lowercase();
if let Some(mg) = &self.metagraph {
match mg.snapshot() {
Some(view) if view.contains_hex(&hk) => {}
Some(_) => {
return Err(FundingError::Ineligible(
"hotkey not registered on subnet metagraph".into(),
));
}
None => {
return Err(FundingError::Ineligible(
"metagraph snapshot unavailable".into(),
));
}
}
}
let prior = self
.store
.list(None, Some(&hk), 1)
.await
.map_err(|e| FundingError::Store(e.to_string()))?;
if !prior.is_empty() {
return Err(FundingError::Ineligible(
"hotkey already has a Prism submission".into(),
));
}
Ok(())
}
}

fn build_funding(
store: Arc<dyn PrismStore>,
metagraph: Option<Arc<MetagraphCache>>,
) -> Arc<FundingService> {
// Always mount the funding HTTP surface; rent gate stays off until
// PRISM_REQUIRE_LIUM_FUNDING=1 (see docs/LIUM_FUNDING.md).
let cfg = FundingConfig::from_env();
let elig: Arc<dyn EligibilityChecker> = Arc::new(PrismFundingElig { store, metagraph });
let policy = Arc::new(PrismFundingPolicy::new(elig).with_economics(cfg.economics));
let oracle: Arc<dyn TaoPriceOracle> = Arc::new(EnvTaoOracle {
fallback_usd_per_tao: 400.0,
});
let payments: Arc<dyn TaoPaymentVerifier> = Arc::new(FakeTaoVerifier::default());
let lium: Arc<dyn LiumAccountClient> = load_lium_api_key()
.and_then(|k| HttpLiumAccount::new(k).ok())
.map_or_else(
|| Arc::new(FakeLiumAccount { balance_usd: 0.0 }) as Arc<dyn LiumAccountClient>,
|c| Arc::new(c) as Arc<dyn LiumAccountClient>,
);
tracing::info!(
require_funding = cfg.require_funding,
"lium-funding surface enabled (payment verifier=fake until on-chain watcher ships)"
);
Arc::new(FundingService::new(
cfg,
policy,
Arc::new(MemoryFundingStore::default()),
oracle,
payments,
lium,
))
}

fn spawn_orchestrator(cli: &Cli, orchestrator: &Arc<Orchestrator<chain_live::LiveChainClient>>) {
let permits = cli.max_concurrent_evals.max(1) as usize;
let sem = Arc::new(Semaphore::new(permits));
Expand Down
30 changes: 30 additions & 0 deletions crates/lium-funding/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "lium-funding"
description = "Shared challenge GPU prepay: quote TAO, verify deposit, grant/consume FundingCredit (Lium operator account)"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
publish = false

[dependencies]
async-trait = "0.1"
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json", "query"] }
hex = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
thiserror = "2"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] }
tracing = "0.1"
uuid = { version = "1", features = ["v4"] }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"

[lints]
workspace = true
106 changes: 106 additions & 0 deletions crates/lium-funding/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Env knobs for funding (no secrets in process env when avoidable).

use crate::types::FundingEconomics;

/// Runtime funding configuration.
#[derive(Debug, Clone)]
pub struct FundingConfig {
/// When false, rent gate is a no-op (default for Prism until wallet live).
pub require_funding: bool,
/// Operator SS58 deposit address (placeholder until secrets wired).
pub deposit_address: String,
/// Quote TTL seconds.
pub quote_ttl_secs: u64,
/// Economics (rate/hours/buffer).
pub economics: FundingEconomics,
/// Optional admin bearer for `/v1/funding/admin/*`.
pub admin_token: Option<String>,
}

impl Default for FundingConfig {
fn default() -> Self {
Self {
require_funding: false,
deposit_address: "REPLACE_ME_DEPOSIT_SS58".into(),
quote_ttl_secs: 900,
economics: FundingEconomics::prism_default(),
admin_token: None,
}
}
}

impl FundingConfig {
/// Load from env. Secrets (deposit address file, admin token file) optional.
///
/// `PRISM_REQUIRE_LIUM_FUNDING` — `1`/`true` enables the rent gate (default off).
#[must_use]
pub fn from_env() -> Self {
let mut cfg = Self::default();
cfg.require_funding = env_truthy("PRISM_REQUIRE_LIUM_FUNDING");
if let Ok(addr) = std::env::var("LIUM_FUNDING_DEPOSIT_ADDRESS") {
if !addr.trim().is_empty() {
cfg.deposit_address = addr.trim().to_owned();
}
} else if let Ok(path) = std::env::var("LIUM_FUNDING_DEPOSIT_ADDRESS_FILE") {
if let Ok(s) = std::fs::read_to_string(&path) {
let t = s.trim();
if !t.is_empty() {
cfg.deposit_address = t.to_owned();
}
}
}
if let Ok(v) = std::env::var("LIUM_FUNDING_QUOTE_TTL_SECS") {
if let Ok(n) = v.parse::<u64>() {
cfg.quote_ttl_secs = n;
}
}
cfg.economics = economics_from_env(cfg.economics);
cfg.admin_token =
read_secret_env("LIUM_FUNDING_ADMIN_TOKEN", "LIUM_FUNDING_ADMIN_TOKEN_FILE");
cfg
}
}

fn economics_from_env(mut e: FundingEconomics) -> FundingEconomics {
if let Some(v) = env_f64("PRISM_FUNDING_RATE_USD_PER_HOUR")
.or_else(|| env_f64("LIUM_FUNDING_RATE_USD_PER_HOUR"))
{
e.rate_usd_per_hour = v;
}
if let Some(v) = env_f64("PRISM_FUNDING_HOURS").or_else(|| env_f64("LIUM_FUNDING_HOURS")) {
e.hours = v;
}
if let Some(v) = env_f64("LIUM_FUNDING_BUFFER") {
e.buffer = v;
}
e
}

fn env_f64(key: &str) -> Option<f64> {
std::env::var(key).ok().and_then(|s| s.parse().ok())
}

fn env_truthy(key: &str) -> bool {
matches!(
std::env::var(key).as_deref(),
Ok("1" | "true" | "TRUE" | "yes" | "YES")
)
}

fn read_secret_env(env_key: &str, file_key: &str) -> Option<String> {
if let Ok(v) = std::env::var(env_key) {
let t = v.trim();
if !t.is_empty() {
return Some(t.to_owned());
}
}
if let Ok(path) = std::env::var(file_key) {
if let Ok(s) = std::fs::read_to_string(path) {
let t = s.trim();
if !t.is_empty() {
return Some(t.to_owned());
}
}
}
None
}
Loading