wickra_proof_core/
proof.rs1use crate::canonical::{canonicalize, hash_value};
4use crate::error::{Error, Result};
5use crate::spec::ProofSpec;
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use std::collections::BTreeMap;
9use wickra_backtest_core::{run, version as engine_version, Candle, StrategySpec};
10
11#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
14pub struct Proof {
15 pub report: Value,
17 pub inputs_hash: String,
20 pub report_hash: String,
22 pub engine_version: String,
24}
25
26pub fn prove(spec: &ProofSpec, data: &BTreeMap<String, Vec<Candle>>) -> Result<Proof> {
28 let linked = engine_version().to_string();
29 if let Some(expected) = &spec.engine_version {
30 if expected != &linked {
31 return Err(Error::EngineMismatch {
32 expected: expected.clone(),
33 linked,
34 });
35 }
36 }
37
38 let strategy: StrategySpec =
39 serde_json::from_value(spec.strategy.clone()).map_err(|e| Error::BadSpec(e.to_string()))?;
40 let candles = data
41 .get(&strategy.symbol)
42 .ok_or_else(|| Error::Data(format!("no candles for symbol {}", strategy.symbol)))?;
43
44 let report = run(&strategy, candles).map_err(|e| Error::Backtest(e.to_string()))?;
45 let report_value = serde_json::to_value(&report)?;
46 let report_hash = hash_value(&report_value)?;
47
48 let inputs = json!({
49 "strategy": spec.strategy,
50 "dataset_ref": spec.dataset_ref,
51 "candles": serde_json::to_value(data)?,
52 "engine_version": linked,
53 });
54 let inputs_hash = hash_value(&inputs)?;
55
56 Ok(Proof {
57 report: report_value,
58 inputs_hash,
59 report_hash,
60 engine_version: linked,
61 })
62}
63
64pub fn verify(
68 proof: &Proof,
69 spec: &ProofSpec,
70 data: &BTreeMap<String, Vec<Candle>>,
71) -> Result<bool> {
72 let fresh = prove(spec, data)?;
73 Ok(fresh.report_hash == proof.report_hash
74 && fresh.inputs_hash == proof.inputs_hash
75 && fresh.engine_version == proof.engine_version)
76}
77
78#[derive(Debug, Default, Clone, Copy)]
81pub struct Prover;
82
83#[derive(Deserialize)]
84struct ProveReq {
85 spec: ProofSpec,
86 data: BTreeMap<String, Vec<Candle>>,
87}
88
89#[derive(Deserialize)]
90struct VerifyReq {
91 proof: Proof,
92 spec: ProofSpec,
93 data: BTreeMap<String, Vec<Candle>>,
94}
95
96impl Prover {
97 #[must_use]
99 pub fn new() -> Self {
100 Prover
101 }
102
103 #[must_use]
105 pub fn version() -> &'static str {
106 env!("CARGO_PKG_VERSION")
107 }
108
109 pub fn command_json(&mut self, cmd_json: &str) -> Result<String> {
113 let value = dispatch(cmd_json);
114 canonicalize(&value)
115 }
116}
117
118fn dispatch(cmd_json: &str) -> Value {
119 match dispatch_inner(cmd_json) {
120 Ok(v) => v,
121 Err(e) => json!({ "ok": false, "error": e.to_string() }),
122 }
123}
124
125fn dispatch_inner(cmd_json: &str) -> Result<Value> {
126 let env: Value = serde_json::from_str(cmd_json)?;
127 let cmd = env.get("cmd").and_then(Value::as_str).unwrap_or("");
128 match cmd {
129 "prove" => {
130 let req: ProveReq = serde_json::from_value(env)?;
131 Ok(serde_json::to_value(prove(&req.spec, &req.data)?)?)
132 }
133 "verify" => {
134 let req: VerifyReq = serde_json::from_value(env)?;
135 let valid = verify(&req.proof, &req.spec, &req.data)?;
136 Ok(json!({ "ok": true, "valid": valid }))
137 }
138 "canonicalize" => {
139 let value = env.get("value").cloned().unwrap_or(Value::Null);
140 Ok(json!({ "ok": true, "canonical": canonicalize(&value)? }))
141 }
142 "version" => Ok(json!({
143 "version": Prover::version(),
144 "engine_version": engine_version(),
145 })),
146 other => Err(Error::Parse(format!("unknown cmd: {other}"))),
147 }
148}