Skip to main content

urma_node_client/
model.rs

1use bitcoin::BlockHash;
2use bitcoin::hashes::Hash;
3use serde::{Deserialize, Serialize};
4use urma_chain::observation::Chain;
5
6pub const API_VERSION: u32 = 1;
7pub const SCAN_RULES_VERSION: u32 = 1;
8
9/// Acknowledges a local pin operation at its commit point. This is not a
10/// promise of current chain selection, object availability forever, or peer trust.
11#[derive(Clone, Debug, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct PinReceipt {
14    pub root: bitcoin::Txid,
15    pub pinned: bool,
16}
17
18/// Explicit local cache maintenance. Counts removed child records, not raw
19/// transactions or bytes; unpruned archives return zero without eviction.
20#[derive(Clone, Debug, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct PruneReceipt {
23    pub records_removed: u64,
24}
25
26/// Transport data, never trusted merely because it came from another node.
27/// The receiver selects the header independently before verifying this proof.
28/// `commit` authenticates the exact prevout through its TXID; this package does
29/// NOT assert the commit's inclusion, ordering, or witness authenticity. Names
30/// must obtain independently anchored commit placement before applying a record.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct RecordPackage {
34    pub version: u32,
35    pub chain_genesis: BlockHash,
36    pub block: ChainPoint,
37    pub reveal: urma_chain::inclusion::TransactionProof,
38    pub commit: Vec<u8>,
39}
40
41/// Local evidence at one archive generation. Selective admission establishes
42/// membership and the URMA proof, never a full-block scan or interval coverage.
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct RecordReceipt {
45    pub generation: u64,
46    pub block: ChainPoint,
47    pub position: u32,
48    pub txid: bitcoin::Txid,
49    pub wtxid: bitcoin::Wtxid,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct ArchivedRecord {
54    pub receipt: RecordReceipt,
55    pub package: RecordPackage,
56}
57
58/// A successful complete reconstruction, distinct from interval coverage.
59/// Counts describe the current archive snapshot. A reorg can leave all bytes
60/// available locally while some memberships no longer belong to the selected chain.
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct ObjectStatus {
63    pub root: bitcoin::Txid,
64    pub verified_generation: u64,
65    pub payload_bytes: u64,
66    pub members_total: u64,
67    pub members_local: u64,
68    /// Previously verified placements still selected, independent of eviction.
69    pub members_selected: u64,
70    pub pinned: bool,
71}
72
73impl ObjectStatus {
74    pub fn locally_complete(&self) -> bool {
75        self.members_total > 0 && self.members_local == self.members_total
76    }
77
78    pub fn selected_complete(&self) -> bool {
79        self.locally_complete() && self.members_selected == self.members_total
80    }
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
84pub struct ChainPoint {
85    pub height: u64,
86    pub hash: BlockHash,
87}
88
89#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "kebab-case")]
91pub enum Retention {
92    Pruned,
93    #[default]
94    Unpruned,
95}
96
97/// The application start is independent of the chain genesis and header anchor.
98/// There is deliberately no implicit tip-minus-N or checkpoint-based default.
99#[derive(Clone, Debug, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct ArchiveConfig {
102    pub chain: Chain,
103    pub application_start: ChainPoint,
104    pub retention: Retention,
105}
106
107/// Status structs are `non_exhaustive`: build them from `Default` and assign
108/// fields, so additive fields stay non-breaking.
109#[derive(Clone, Debug, Serialize, Deserialize)]
110#[non_exhaustive]
111pub struct ArchiveStatus {
112    pub api_version: u32,
113    pub chain_genesis: BlockHash,
114    pub application_start: ChainPoint,
115    pub retention: Retention,
116    pub generation: u64,
117    pub selected_tip: Option<ChainPoint>,
118    /// Contiguous fully scanned and dependency-resolved prefix from app start.
119    /// This does not assert full consensus validation or local object retention.
120    pub indexed_through: Option<ChainPoint>,
121    pub scanned_blocks: u64,
122    pub unresolved_dependencies: u64,
123    pub stored_transactions: u64,
124    pub scan_rules_version: u32,
125    /// Highest height whose prefix from app start lies in current peer coverage.
126    pub peer_synced_through: Option<ChainPoint>,
127    /// Selected placements a covering peer did not list, found by a later scan.
128    pub peer_omissions: u64,
129    /// Selected records admitted selectively whose block has not been scanned.
130    pub unaudited_records: u64,
131}
132
133impl Default for ArchiveStatus {
134    fn default() -> Self {
135        let zero = BlockHash::all_zeros();
136        Self {
137            api_version: API_VERSION,
138            chain_genesis: zero,
139            application_start: ChainPoint {
140                height: 0,
141                hash: zero,
142            },
143            retention: Retention::default(),
144            generation: 0,
145            selected_tip: None,
146            indexed_through: None,
147            scanned_blocks: 0,
148            unresolved_dependencies: 0,
149            stored_transactions: 0,
150            scan_rules_version: SCAN_RULES_VERSION,
151            peer_synced_through: None,
152            peer_omissions: 0,
153            unaudited_records: 0,
154        }
155    }
156}
157
158/// Historical receipt of one atomic cache maintenance step. Later admissions,
159/// pins and reorgs can change usage. Protected evidence and SQLite/WAL overhead
160/// are outside this metric; disabled enforcement does not imply an empty archive.
161#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
162#[non_exhaustive]
163pub struct CacheReceipt {
164    pub enabled: bool,
165    pub limit_bytes: u64,
166    pub before_bytes: u64,
167    pub remaining_bytes: u64,
168    pub removed_records: u64,
169    pub removed_transactions: u64,
170    pub over_budget: bool,
171}
172
173/// Operator-reported service state. Every `last_*_error` describes the latest
174/// attempt of that worker; `*_at` is when it was recorded, in Unix seconds.
175#[derive(Clone, Debug, Default, Serialize, Deserialize)]
176#[non_exhaustive]
177pub struct ServiceStatus {
178    pub archive: ArchiveStatus,
179    pub last_sync_error: Option<String>,
180    pub last_sync_error_at: Option<u64>,
181    pub last_retention_error: Option<String>,
182    pub last_retention_error_at: Option<u64>,
183    pub last_names_error: Option<String>,
184    pub last_names_error_at: Option<u64>,
185    pub last_dependency_error: Option<String>,
186    pub last_dependency_error_at: Option<u64>,
187    pub last_inventory_error: Option<String>,
188    pub last_inventory_error_at: Option<u64>,
189    pub last_discovery_error: Option<String>,
190    pub last_discovery_error_at: Option<u64>,
191    pub commit_sources: Vec<CommitSourceStatus>,
192    pub cache_limit_bytes: u64,
193    pub last_cache_error: Option<String>,
194    pub last_cache_error_at: Option<u64>,
195    pub last_cache_maintenance: Option<CacheReceipt>,
196    pub overlay: Option<OverlayStatus>,
197    /// Litecoin P2P peers the light client holds now; 0 when offline.
198    #[serde(default)]
199    pub chain_peers: u64,
200    /// Absolute height header sync heads for: the best height chain peers
201    /// announced, never below the local tip. Announced, not proven; null while
202    /// unknown (no chain peer yet, or offline).
203    #[serde(default)]
204    pub chain_target: Option<u64>,
205    pub retention_configured: bool,
206    pub record_sources_configured: u64,
207    pub capabilities: Vec<String>,
208}
209
210#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
211#[non_exhaustive]
212pub struct CommitSourceStatus {
213    pub label: String,
214    pub last_error: Option<String>,
215    pub last_error_at: Option<u64>,
216}
217
218/// Public overlay facts; never secrets. Counters cover this service run.
219#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
220#[non_exhaustive]
221pub struct OverlayStatus {
222    pub peer_id: String,
223    #[serde(default)]
224    pub listen: Vec<String>,
225    #[serde(default)]
226    pub announce: Vec<String>,
227    pub peer_book: u64,
228    pub connected: u64,
229    pub admitted: u64,
230    pub rejected: u64,
231    pub peers: Vec<OverlayPeerStatus>,
232    pub discovery: Option<DiscoveryStatus>,
233}
234
235/// Peer book and discovery counters. `reached` counts entries whose last dial
236/// succeeded, `reached_ever` entries with any past successful dial; a dial
237/// proves an identity answered, not honesty.
238#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
239#[non_exhaustive]
240pub struct DiscoveryStatus {
241    pub seeds: u64,
242    pub book: u64,
243    pub reached: u64,
244    pub reached_ever: u64,
245    pub gossip: u64,
246    pub last_seed_round_error: Option<String>,
247    pub last_seed_round_error_at: Option<u64>,
248    pub last_gossip_error: Option<String>,
249    pub last_gossip_error_at: Option<u64>,
250}
251
252/// A selected record placement a peer's coverage claimed but did not list.
253#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
254pub struct Omission {
255    pub peer: String,
256    pub block: ChainPoint,
257    pub position: u32,
258    pub txid: bitcoin::Txid,
259    pub generation: u64,
260}
261
262/// How a peer-book entry entered the book. Ordered by precedence: learning
263/// never downgrades an entry, and manual and seed entries are never evicted.
264#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum BookOrigin {
267    Gossip,
268    Seed,
269    Manual,
270}
271
272/// One peer-book entry as served by `GET /v1/overlay/book`: an untrusted
273/// routing hint. Addresses carry no terminal `/p2p`; `peer_id` is set once a
274/// dial has authenticated it. Times are Unix seconds.
275#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
276pub struct BookEntry {
277    pub peer_id: Option<String>,
278    pub addrs: Vec<String>,
279    pub origin: BookOrigin,
280    pub last_success: Option<u64>,
281    pub failures: u32,
282    pub banned_until: Option<u64>,
283}
284
285#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
286#[non_exhaustive]
287pub struct OverlayPeerStatus {
288    pub label: String,
289    pub cursor: u64,
290    pub synced_through: Option<u64>,
291    pub last_error: Option<String>,
292    pub last_error_at: Option<u64>,
293    /// Unix second until which sync skips this peer after a peer error.
294    pub backoff_until: Option<u64>,
295    /// Unix second of this peer's last turn with an admission or coverage.
296    pub last_progress_at: Option<u64>,
297}
298
299#[cfg(test)]
300#[path = "model_tests.rs"]
301mod model_tests;