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}