Skip to main content

wickra_verify_core/
claim.rs

1//! [`Claim`] — the assertion to be checked: "this strategy on this data produced
2//! this report."
3
4use crate::error::{Error, Result};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::BTreeMap;
8
9pub use wickra_backtest_core::{BacktestReport, Candle, StrategySpec};
10
11/// Where the candle data behind a claim comes from.
12///
13/// `Inline` carries the candles verbatim, so a claim is fully self-contained and
14/// portable. `Files` names the symbols (and an optional content hash) the caller
15/// must resolve out of band — the verifier never fetches data itself. The tag is
16/// the `kind` field: `{"kind":"inline","data":{...}}` /
17/// `{"kind":"files","symbols":[...]}`.
18#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
19#[serde(tag = "kind", rename_all = "snake_case")]
20pub enum DatasetRef {
21    /// The candles, embedded per symbol.
22    Inline {
23        /// Candle series keyed by symbol.
24        data: BTreeMap<String, Vec<Candle>>,
25    },
26    /// A reference to external files the caller resolves.
27    Files {
28        /// The symbols the claim needs.
29        symbols: Vec<String>,
30        /// Optional content hash of the referenced data, for provenance.
31        #[serde(default, skip_serializing_if = "Option::is_none")]
32        hash: Option<String>,
33    },
34}
35
36/// A claim to verify: a strategy, the data it ran on, and the report it is said
37/// to have produced. `claimed_report` is untrusted — verification recomputes the
38/// report from `strategy` + data and compares, so a doctored `claimed_report`
39/// cannot pass.
40#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
41#[serde(deny_unknown_fields)]
42pub struct Claim {
43    /// The embedded wickra-backtest `StrategySpec`, kept as raw JSON so
44    /// wickra-verify-core stays decoupled from backtest struct internals across the FFI
45    /// boundary.
46    pub strategy: Value,
47    /// Where the candle data comes from.
48    pub dataset_ref: DatasetRef,
49    /// The report the claimant asserts this run produced (untrusted).
50    pub claimed_report: Value,
51}
52
53impl Claim {
54    /// Parse a `Claim` from JSON.
55    pub fn from_json(s: &str) -> Result<Self> {
56        let claim: Self = serde_json::from_str(s).map_err(|e| Error::BadClaim(e.to_string()))?;
57        claim.validate()?;
58        Ok(claim)
59    }
60
61    /// Parse a `Claim` from TOML.
62    pub fn from_toml(s: &str) -> Result<Self> {
63        let claim: Self = toml::from_str(s).map_err(|e| Error::Parse(e.to_string()))?;
64        claim.validate()?;
65        Ok(claim)
66    }
67
68    /// The inline candle data, if this claim carries it. `Files` claims return
69    /// `None` — their data must be supplied to [`verify`](crate::verify)
70    /// explicitly.
71    #[must_use]
72    pub fn inline_data(&self) -> Option<&BTreeMap<String, Vec<Candle>>> {
73        match &self.dataset_ref {
74            DatasetRef::Inline { data } => Some(data),
75            DatasetRef::Files { .. } => None,
76        }
77    }
78
79    /// Validate structural invariants: the embedded strategy must be a JSON
80    /// object (a `StrategySpec`), and the claimed report a JSON object.
81    pub(crate) fn validate(&self) -> Result<()> {
82        if !self.strategy.is_object() {
83            return Err(Error::BadClaim(
84                "strategy must be a JSON object (a StrategySpec)".to_string(),
85            ));
86        }
87        if !self.claimed_report.is_object() {
88            return Err(Error::BadReport(
89                "claimed_report must be a JSON object (a BacktestReport)".to_string(),
90            ));
91        }
92        Ok(())
93    }
94}