Skip to content

Commit 03b25f2

Browse files
authored
Merge branch 'main' into feat/sparse-fieldset-allowlist
2 parents 0a83bc8 + d8bae43 commit 03b25f2

10 files changed

Lines changed: 449 additions & 0 deletions

File tree

backend/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ pub mod contract_ops;
1010
pub mod distributed_lock;
1111
pub mod event_indexer;
1212
pub mod field_selection;
13+
pub mod network;
1314
pub mod observability;
1415
pub mod realtime;
1516
pub mod reconciliation;
17+
pub mod replay;
18+
pub mod snapshot;

backend/src/network/client.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use super::identity::Network;
2+
3+
#[derive(Clone, Debug, PartialEq, Eq)]
4+
pub struct NetworkClient {
5+
pub network: Network,
6+
pub rpc_url: String,
7+
pub horizon_url: String,
8+
}
9+
10+
impl NetworkClient {
11+
pub fn for_network(network: Network) -> Self {
12+
let (rpc_url, horizon_url) = match network {
13+
Network::Mainnet => (
14+
"https://horizon-mainnet.stellar.org".to_string(),
15+
"https://horizon-mainnet.stellar.org".to_string(),
16+
),
17+
Network::Testnet => (
18+
"https://horizon-testnet.stellar.org".to_string(),
19+
"https://horizon-testnet.stellar.org".to_string(),
20+
),
21+
Network::Futurenet => (
22+
"https://horizon-futurenet.stellar.org".to_string(),
23+
"https://horizon-futurenet.stellar.org".to_string(),
24+
),
25+
};
26+
27+
Self {
28+
network,
29+
rpc_url,
30+
horizon_url,
31+
}
32+
}
33+
}

backend/src/network/identity.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::fmt;
2+
3+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
4+
pub enum Network {
5+
Mainnet,
6+
Testnet,
7+
Futurenet,
8+
}
9+
10+
impl fmt::Display for Network {
11+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12+
match self {
13+
Network::Mainnet => write!(f, "mainnet"),
14+
Network::Testnet => write!(f, "testnet"),
15+
Network::Futurenet => write!(f, "futurenet"),
16+
}
17+
}
18+
}
19+
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
pub struct TableSchema {
22+
pub name: String,
23+
pub fields: Vec<String>,
24+
}
25+
26+
#[derive(Debug, thiserror::Error)]
27+
pub enum NetworkSchemaError {
28+
#[error("schema missing network discriminator: {0}")]
29+
MissingNetworkDiscriminator(String),
30+
}
31+
32+
pub struct NetworkSchema;
33+
34+
impl NetworkSchema {
35+
pub fn validate(schemas: &[TableSchema]) -> Result<(), String> {
36+
for schema in schemas {
37+
let has_network = schema
38+
.fields
39+
.iter()
40+
.any(|field| field.eq_ignore_ascii_case("network"));
41+
if !has_network {
42+
return Err(format!(
43+
"schema missing network discriminator for table {}",
44+
schema.name
45+
));
46+
}
47+
}
48+
49+
Ok(())
50+
}
51+
52+
pub fn assert_startup_schema(schemas: &[TableSchema]) {
53+
match Self::validate(schemas) {
54+
Ok(()) => {}
55+
Err(message) => panic!("startup schema check failed: {message}"),
56+
}
57+
}
58+
}

backend/src/network/mod.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
pub mod client;
2+
pub mod identity;
3+
4+
use std::collections::HashMap;
5+
6+
pub use client::NetworkClient;
7+
pub use identity::{Network, NetworkSchema, TableSchema};
8+
9+
#[derive(Clone, Debug, PartialEq)]
10+
pub struct MetricRecord {
11+
pub corridor: String,
12+
pub network: Network,
13+
pub reliability: f64,
14+
pub volume: f64,
15+
}
16+
17+
#[derive(Clone, Debug, Default)]
18+
pub struct NetworkStore {
19+
by_network: HashMap<Network, Vec<MetricRecord>>,
20+
}
21+
22+
impl NetworkStore {
23+
pub fn new() -> Self {
24+
Self::default()
25+
}
26+
27+
pub fn ingest(&mut self, corridor: String, network: Network, reliability: f64, volume: f64) {
28+
self.by_network
29+
.entry(network)
30+
.or_default()
31+
.push(MetricRecord {
32+
corridor,
33+
network,
34+
reliability,
35+
volume,
36+
});
37+
}
38+
39+
pub fn for_network(&self, network: Network) -> Vec<MetricRecord> {
40+
self.by_network.get(&network).cloned().unwrap_or_default()
41+
}
42+
}

backend/src/replay/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use crate::snapshot::{generate_snapshot, model_version_for_ledger_range, RawSnapshotRow};
2+
3+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4+
pub struct LedgerRange {
5+
pub start_ledger: u64,
6+
pub end_ledger: u64,
7+
}
8+
9+
#[derive(Debug, thiserror::Error)]
10+
pub enum ReplayError {
11+
#[error("invalid ledger range: start > end")]
12+
InvalidRange,
13+
#[error("snapshot generation failed: {0}")]
14+
Snapshot(#[from] crate::snapshot::SnapshotError),
15+
}
16+
17+
/// Historical replays are intentionally pinned to a model version and do not read
18+
/// wall-clock time. Re-running the same range against the same raw inputs must
19+
/// produce a byte-identical output payload.
20+
pub fn replay_historical_range(range: &LedgerRange, rows: &[RawSnapshotRow]) -> Result<Vec<u8>, ReplayError> {
21+
if range.start_ledger > range.end_ledger {
22+
return Err(ReplayError::InvalidRange);
23+
}
24+
25+
let _ = model_version_for_ledger_range(range.start_ledger, range.end_ledger);
26+
27+
let filtered: Vec<RawSnapshotRow> = rows
28+
.iter()
29+
.filter(|row| row.ledger_sequence >= range.start_ledger && row.ledger_sequence <= range.end_ledger)
30+
.cloned()
31+
.collect();
32+
33+
// Replay of an empty range is intentionally deterministic and yields an empty
34+
// JSON payload rather than reading time or state from the live process.
35+
if filtered.is_empty() {
36+
return Ok(b"{\"model_version\":\"reliability-v2025.01\",\"records\":[]}".to_vec());
37+
}
38+
39+
Ok(generate_snapshot(&filtered)?)
40+
}

backend/src/snapshot/generator.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use serde::{Deserialize, Serialize};
2+
use std::collections::BTreeMap;
3+
4+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
5+
pub struct RawSnapshotRow {
6+
pub ledger_sequence: u64,
7+
pub corridor: String,
8+
pub source: String,
9+
pub reliability: f64,
10+
pub volume: f64,
11+
pub latency_ms: f64,
12+
}
13+
14+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15+
pub struct SnapshotRecord {
16+
pub corridor: String,
17+
pub source: String,
18+
pub avg_reliability: f64,
19+
pub total_volume: f64,
20+
pub avg_latency_ms: f64,
21+
}
22+
23+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24+
pub struct SnapshotPayload {
25+
pub model_version: String,
26+
pub records: Vec<SnapshotRecord>,
27+
}
28+
29+
#[derive(Debug, thiserror::Error)]
30+
pub enum SnapshotError {
31+
#[error("snapshot input is empty")]
32+
EmptyInput,
33+
#[error("snapshot serialization failed: {0}")]
34+
Serialization(#[from] serde_json::Error),
35+
}
36+
37+
/// Pure, deterministic snapshot generation.
38+
///
39+
/// The function aggregates by corridor using a canonical BTreeMap ordering and
40+
/// sorts ledger rows before computing averages so the serialized output is
41+
/// stable across repeated replays of the same historical data.
42+
pub fn generate_snapshot(rows: &[RawSnapshotRow]) -> Result<Vec<u8>, SnapshotError> {
43+
if rows.is_empty() {
44+
return Err(SnapshotError::EmptyInput);
45+
}
46+
47+
let mut grouped: BTreeMap<String, Vec<&RawSnapshotRow>> = BTreeMap::new();
48+
for row in rows {
49+
grouped.entry(row.corridor.clone()).or_default().push(row);
50+
}
51+
52+
let mut records = Vec::with_capacity(grouped.len());
53+
for (corridor, entries) in grouped {
54+
let mut entries = entries;
55+
entries.sort_by_key(|entry| entry.ledger_sequence);
56+
57+
let total_count = entries.len() as f64;
58+
let avg_reliability = entries.iter().map(|entry| entry.reliability).sum::<f64>() / total_count;
59+
let total_volume = entries.iter().map(|entry| entry.volume).sum::<f64>();
60+
let avg_latency_ms = entries.iter().map(|entry| entry.latency_ms).sum::<f64>() / total_count;
61+
62+
records.push(SnapshotRecord {
63+
corridor,
64+
source: entries[0].source.clone(),
65+
avg_reliability: avg_reliability,
66+
total_volume,
67+
avg_latency_ms,
68+
});
69+
}
70+
71+
let payload = SnapshotPayload {
72+
model_version: crate::snapshot::model_version::PINNED_MODEL_VERSION.to_string(),
73+
records,
74+
};
75+
76+
Ok(serde_json::to_vec(&payload)?)
77+
}

backend/src/snapshot/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pub mod generator;
2+
pub mod model_version;
3+
4+
pub use generator::{generate_snapshot, RawSnapshotRow, SnapshotRecord, SnapshotPayload, SnapshotError};
5+
pub use model_version::{
6+
model_version_for_ledger_range, model_version_for_range, network_model_version, DETERMINISM_EFFECTIVE_DATE,
7+
PINNED_MODEL_VERSION,
8+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
pub const DETERMINISM_EFFECTIVE_DATE: &str = "2025-01-01";
4+
pub const PINNED_MODEL_VERSION: &str = "reliability-v2025.01";
5+
6+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7+
pub struct ModelVersion {
8+
pub name: String,
9+
pub effective_from: String,
10+
pub effective_until: Option<String>,
11+
}
12+
13+
/// The historical replay path is pinned to a single model version so that
14+
/// replaying the same ledger range produces identical output regardless of the
15+
/// model currently shipped in production.
16+
///
17+
/// This guarantee applies to data on or after 2025-01-01, which is the
18+
/// effective date of the snapshot determinism contract. Model changes after that
19+
/// date are not retroactively applied to historical replays.
20+
pub fn model_version_for_range(start_ledger: u64, end_ledger: u64) -> &'static str {
21+
let _ = (start_ledger, end_ledger);
22+
PINNED_MODEL_VERSION
23+
}
24+
25+
pub fn model_version_for_ledger_range(start_ledger: u64, end_ledger: u64) -> &'static str {
26+
model_version_for_range(start_ledger, end_ledger)
27+
}
28+
29+
pub fn network_model_version(network: &str) -> &'static str {
30+
let _ = network;
31+
PINNED_MODEL_VERSION
32+
}

0 commit comments

Comments
 (0)