Racing Competition¶
How nodes compete to serve queries and earn rewards.
Overview¶
Every query triggers a racing competition between 3-5 nodes:
- Query is broadcast to selected nodes
- Nodes race to provide the fastest correct response
- First correct answer wins 70% of payment
- Two verifiers confirm correctness, earn 15% each
How Racing Works¶
sequenceDiagram
participant C as Client
participant R as Query Router
participant N1 as Node 1
participant N2 as Node 2
participant N3 as Node 3
participant V1 as Verifier 1
participant V2 as Verifier 2
C->>R: Query Request<br/>(max 10ms, 100 STRM)
Note over R: Select 3-5 racing nodes<br/>based on capability & reputation
par Race Execution
R->>N1: Execute Query
R->>N2: Execute Query
R->>N3: Execute Query
end
N1-->>R: Result (4ms) 🏆
N2-->>R: Result (7ms)
N3-->>R: Result (11ms)
Note over R: N1 wins the race!
par Verification
R->>V1: Verify N1's result
R->>V2: Verify N1's result
end
V1-->>R: Confirmed ✓
V2-->>R: Confirmed ✓
R-->>C: Result + Proof
Note over R: Reward Distribution:<br/>N1: 70 STRM<br/>V1: 15 STRM<br/>V2: 15 STRM
Node Selection¶
Nodes are selected for racing based on:
Capability Matching¶
pub fn find_capable_nodes(&self, query: &Query) -> Vec<&Node> {
self.nodes.iter()
.filter(|node| {
// Must support query type
node.supports_query_type(&query.query_type)
// Must have capacity
&& node.current_load < node.max_capacity
// Must be active
&& node.is_active()
// Must have required specialization (if any)
&& query.required_spec.map_or(true, |s| node.specialization == s)
})
.collect()
}
Weighted Selection¶
Better nodes are more likely to be selected:
| Factor | Weight | Description |
|---|---|---|
| Performance | 40% | Recent latency scores |
| Accuracy | 30% | Correct response rate |
| Uptime | 20% | Availability history |
| Stake | 10% | Economic commitment (log scale) |
fn calculate_selection_weight(node: &Node) -> f64 {
let perf = node.avg_latency_score(); // 0.0 - 1.0
let acc = node.accuracy_rate(); // 0.0 - 1.0
let up = node.uptime_percentage(); // 0.0 - 1.0
let stake = (node.stake as f64).ln() / 20.0; // Log scale
(perf * 0.4) + (acc * 0.3) + (up * 0.2) + (stake * 0.1)
}
Race Execution¶
Parallel Query Dispatch¶
pub async fn execute_race(&self, query: Query) -> Result<RaceResult> {
let candidates = self.select_racing_candidates(3..=5)?;
let (tx, mut rx) = mpsc::channel(candidates.len());
// Dispatch to all candidates in parallel
for node in &candidates {
let tx = tx.clone();
let query = query.clone();
let node_id = node.id;
tokio::spawn(async move {
let start = Instant::now();
let result = node.execute_query(&query).await;
let latency = start.elapsed();
let _ = tx.send(RaceEntry {
node_id,
result,
latency,
}).await;
});
}
// Await first valid response
self.await_winner(&mut rx, query.max_latency).await
}
Winner Determination¶
async fn await_winner(&self, rx: &mut Receiver<RaceEntry>, max_latency: Duration)
-> Result<RaceResult>
{
let deadline = Instant::now() + max_latency;
while Instant::now() < deadline {
match rx.try_recv() {
Ok(entry) => {
// Validate result quickly
if self.quick_validate(&entry.result) {
return Ok(RaceResult {
winner: entry.node_id,
result: entry.result,
latency: entry.latency,
});
}
}
Err(_) => tokio::time::sleep(Duration::from_micros(100)).await,
}
}
Err(RaceError::Timeout)
}
Verification¶
After a winner is determined, the result is verified:
Verifier Selection¶
- 2 verifiers selected from non-racing nodes
- Weighted by accuracy history
- Cannot be same operator as winner
Verification Process¶
pub async fn verify_result(&self, result: &QueryResult, winner: NodeId)
-> Result<VerificationResult>
{
let verifiers = self.select_verifiers(2, winner)?;
let verifications = futures::join_all(
verifiers.iter().map(|v| v.verify(result))
).await;
// Require 2/2 confirmations
let confirmed = verifications.iter()
.filter(|v| v.is_ok() && v.as_ref().unwrap().confirmed)
.count();
if confirmed >= 2 {
Ok(VerificationResult::Confirmed(verifiers))
} else {
// Dispute resolution
self.handle_verification_failure(result, verifications).await
}
}
Reward Distribution¶
Standard Distribution¶
| Recipient | Share | Example (100 STRM query) |
|---|---|---|
| Winner | 70% | 70 STRM |
| Verifier 1 | 15% | 15 STRM |
| Verifier 2 | 15% | 15 STRM |
Performance Bonuses¶
Winners can earn additional bonuses:
| Performance | Bonus |
|---|---|
| Sub-1ms response | +50% |
| Sub-5ms response | +20% |
| Perfect accuracy | +10% |
fn calculate_winner_reward(&self, base_reward: u64, latency: Duration) -> u64 {
let mut reward = base_reward;
// Speed bonuses
if latency < Duration::from_millis(1) {
reward = (reward as f64 * 1.5) as u64;
} else if latency < Duration::from_millis(5) {
reward = (reward as f64 * 1.2) as u64;
}
reward
}
Batch Settlement¶
Rewards are batched for gas efficiency:
graph LR
Q1[Query 1: 100 STRM] --> B[Settlement Batch]
Q2[Query 2: 50 STRM] --> B
Q3[Query 3: 200 STRM] --> B
Q4[Query 4: 75 STRM] --> B
B -->|Every 5 min| S[Solana Transaction]
S --> N1[Node A: 285 STRM]
S --> N2[Node B: 95 STRM]
S --> N3[Node C: 45 STRM]
Why Batch?¶
| Approach | Gas Cost | Latency |
|---|---|---|
| Per-query settlement | ~0.00001 SOL each | Immediate |
| Batch settlement | ~0.000005 SOL each | 5 min max |
Batching reduces gas costs by ~50% while maintaining acceptable settlement latency.
SLA Enforcement¶
Automatic Refunds¶
pub async fn handle_query_result(&self, result: QueryResult, request: QueryRequest)
-> SettlementAction
{
match (result.latency <= request.max_latency, result.is_valid) {
(true, true) => {
// SLA met: distribute rewards
SettlementAction::DistributeRewards {
winner: result.winner,
verifiers: result.verifiers,
amount: request.payment,
}
}
(false, _) | (_, false) => {
// SLA missed: refund customer
SettlementAction::RefundCustomer {
customer: request.customer,
amount: request.payment,
reason: "SLA not met",
}
}
}
}
Customer Guarantee¶
- Miss latency target → Full refund
- Invalid result → Full refund
- No response → Full refund
Anti-Gaming Measures¶
Preventing Collusion¶
// Verifiers cannot be from same operator as winner
fn select_verifiers(&self, count: usize, winner: NodeId) -> Vec<NodeId> {
let winner_operator = self.get_operator(winner);
self.nodes.iter()
.filter(|n| {
n.id != winner
&& self.get_operator(n.id) != winner_operator
&& n.accuracy_rate() > 0.95
})
.take(count)
.map(|n| n.id)
.collect()
}
Stake Slashing¶
Misbehavior results in stake slashing:
| Violation | Penalty |
|---|---|
| Wrong result (proven) | 2% of stake |
| Verification collusion | 5% of stake |
| Repeated timeouts | 0.5% of stake |
Summary¶
Racing competition ensures:
- ✅ Fastest response wins - incentivizes performance
- ✅ Verification prevents cheating - trustless correctness
- ✅ Automatic SLA enforcement - customers protected
- ✅ Efficient settlement - low gas costs
- ✅ Anti-gaming measures - honest behavior rewarded