1use std::collections::BTreeMap;
2
3use crate::crypto::{hasher_intent_leaf, hasher_subscope, merkle_node};
4use crate::error::A1Error;
5
6pub type IntentHash = [u8; 32];
8
9pub const MAX_ACTION_LEN: usize = 256;
11pub const MAX_PARAM_KEY_LEN: usize = 128;
13pub const MAX_PARAM_VALUE_LEN: usize = 4096;
15pub const MAX_INTENT_PARAMS: usize = 64;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct Intent {
43 pub action: String,
45 pub params: BTreeMap<String, String>,
47}
48
49impl Intent {
50 pub fn new(action: impl Into<String>) -> Result<Self, A1Error> {
65 let action_str = action.into();
66 if action_str.is_empty() {
67 return Err(A1Error::WireFormatError(
68 "Intent action cannot be empty".into(),
69 ));
70 }
71 if action_str.len() > MAX_ACTION_LEN {
72 return Err(A1Error::WireFormatError(format!(
73 "Intent action exceeds maximum length of {MAX_ACTION_LEN}"
74 )));
75 }
76 Ok(Self {
77 action: action_str,
78 params: BTreeMap::new(),
79 })
80 }
81
82 #[inline]
84 pub fn try_new(action: impl Into<String>) -> Result<Self, A1Error> {
85 Self::new(action)
86 }
87
88 pub fn param(self, key: impl Into<String>, value: impl Into<String>) -> Self {
92 self.try_param(key, value)
93 .expect("invalid intent parameter")
94 }
95
96 pub fn try_param(
102 mut self,
103 key: impl Into<String>,
104 value: impl Into<String>,
105 ) -> Result<Self, A1Error> {
106 if self.params.len() >= MAX_INTENT_PARAMS {
107 return Err(A1Error::WireFormatError(format!(
108 "Intent exceeds maximum parameter count of {}",
109 MAX_INTENT_PARAMS
110 )));
111 }
112 let normalized_key = key.into().trim().to_lowercase();
113 let normalized_value = value.into().trim().to_lowercase();
114 if normalized_key.len() > MAX_PARAM_KEY_LEN {
115 return Err(A1Error::WireFormatError(format!(
116 "Intent parameter key exceeds maximum length of {}",
117 MAX_PARAM_KEY_LEN
118 )));
119 }
120 if normalized_value.len() > MAX_PARAM_VALUE_LEN {
121 return Err(A1Error::WireFormatError(format!(
122 "Intent parameter value exceeds maximum length of {}",
123 MAX_PARAM_VALUE_LEN
124 )));
125 }
126 self.params.insert(normalized_key, normalized_value);
127 Ok(self)
128 }
129
130 pub fn hash(&self) -> IntentHash {
135 let mut h = hasher_intent_leaf(crate::cert::CERT_VERSION);
136 h.update(b"a1::dyolo::intent::v2.8.0");
137 h.update(&(self.action.len() as u64).to_le_bytes());
138 h.update(self.action.as_bytes());
139 h.update(&(self.params.len() as u64).to_le_bytes());
140 for (k, v) in self.params.iter() {
141 h.update(&(k.len() as u64).to_le_bytes());
142 h.update(k.as_bytes());
143 h.update(&(v.len() as u64).to_le_bytes());
144 h.update(v.as_bytes());
145 }
146 h.finalize().into()
147 }
148}
149
150impl std::fmt::Display for Intent {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "{}", self.action)?;
153 if !self.params.is_empty() {
154 write!(f, "[")?;
155 let mut iter = self.params.iter().peekable();
156 while let Some((k, v)) = iter.next() {
157 write!(f, "{k}={v}")?;
158 if iter.peek().is_some() {
159 write!(f, ",")?;
160 }
161 }
162 write!(f, "]")?;
163 }
164 Ok(())
165 }
166}
167
168#[deprecated(
180 since = "2.0.0",
181 note = "Use `Intent::new` and `Intent::hash` to avoid serialization mismatches. This function will be removed in v3.0."
182)]
183pub fn intent_hash(action: &str, params: &[u8]) -> IntentHash {
184 let mut h = hasher_intent_leaf(crate::cert::CERT_VERSION);
185 h.update(&(action.len() as u64).to_le_bytes());
186 h.update(action.as_bytes());
187 h.update(&(params.len() as u64).to_le_bytes());
188 h.update(params);
189 h.finalize().into()
190}
191
192#[derive(Clone, Debug, Default, PartialEq)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub struct MerkleProof {
198 pub siblings: Vec<SiblingNode>,
199}
200
201#[derive(Clone, Debug, PartialEq)]
203#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
204pub struct SiblingNode {
205 pub hash: IntentHash,
206 pub is_left: bool,
208}
209
210impl MerkleProof {
211 pub fn verify(&self, leaf: &IntentHash, expected_root: &IntentHash) -> bool {
214 let mut current = *leaf;
215 for node in &self.siblings {
216 current = if node.is_left {
217 merkle_node(&node.hash, ¤t)
218 } else {
219 merkle_node(¤t, &node.hash)
220 };
221 }
222 use subtle::ConstantTimeEq;
223 current.ct_eq(expected_root).into()
224 }
225}
226
227pub struct IntentTree {
235 leaves: Vec<IntentHash>,
236 layers: Vec<Vec<IntentHash>>,
237}
238
239impl IntentTree {
240 pub fn build(mut intents: Vec<IntentHash>) -> Result<Self, A1Error> {
243 if intents.is_empty() {
244 return Err(A1Error::EmptyTree);
245 }
246 intents.sort_unstable();
247 intents.dedup();
248
249 let depth = (usize::BITS - intents.len().leading_zeros()) as usize;
250 let mut layers: Vec<Vec<IntentHash>> = Vec::with_capacity(depth);
251 layers.push(intents.clone());
252
253 let mut current = intents;
254 while current.len() > 1 {
255 let next_len = current.len().div_ceil(2);
256 let mut next = Vec::with_capacity(next_len);
257 for chunk in current.chunks(2) {
258 if chunk.len() == 2 {
259 next.push(merkle_node(&chunk[0], &chunk[1]));
260 } else {
261 next.push(chunk[0]);
262 }
263 }
264 layers.push(next.clone());
265 current = next;
266 }
267
268 let leaves = layers.first().expect("layers is never empty").clone();
269 Ok(Self { leaves, layers })
270 }
271
272 pub fn root(&self) -> IntentHash {
274 self.layers.last().unwrap()[0]
275 }
276
277 pub fn prove(&self, intent: &IntentHash) -> Result<MerkleProof, A1Error> {
280 let mut pos = self
281 .leaves
282 .binary_search(intent)
283 .map_err(|_| A1Error::IntentNotFound)?;
284
285 let mut siblings = Vec::new();
286 for layer in self.layers.iter().take(self.layers.len() - 1) {
287 let sibling_pos = if pos.is_multiple_of(2) {
288 pos + 1
289 } else {
290 pos - 1
291 };
292 if sibling_pos < layer.len() {
293 siblings.push(SiblingNode {
294 hash: layer[sibling_pos],
295 is_left: !pos.is_multiple_of(2),
296 });
297 }
298 pos /= 2;
299 }
300
301 Ok(MerkleProof { siblings })
302 }
303
304 pub fn contains(&self, intent: &IntentHash) -> bool {
305 self.leaves.binary_search(intent).is_ok()
306 }
307
308 pub fn leaf_count(&self) -> usize {
309 self.leaves.len()
310 }
311}
312
313#[derive(Clone, Debug, Default)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
328pub struct SubScopeProof {
329 pub subset_intents: Vec<IntentHash>,
330 pub proofs: Vec<MerkleProof>,
331}
332
333impl SubScopeProof {
334 pub fn full_passthrough() -> Self {
336 Self::default()
337 }
338
339 pub fn build(parent_tree: &IntentTree, intents: &[IntentHash]) -> Result<Self, A1Error> {
342 let proofs = intents
343 .iter()
344 .map(|intent| parent_tree.prove(intent))
345 .collect::<Result<Vec<_>, _>>()?;
346 Ok(Self {
347 subset_intents: intents.to_vec(),
348 proofs,
349 })
350 }
351
352 pub fn verify_and_derive_root(&self, parent_root: &IntentHash) -> Result<IntentHash, A1Error> {
357 if self.subset_intents.is_empty() {
358 return Ok(*parent_root);
359 }
360 if self.subset_intents.len() != self.proofs.len() {
361 return Err(A1Error::InvalidSubScopeProof);
362 }
363 for (intent, proof) in self.subset_intents.iter().zip(self.proofs.iter()) {
364 if !proof.verify(intent, parent_root) {
365 return Err(A1Error::InvalidSubScopeProof);
366 }
367 }
368 let sub_tree = IntentTree::build(self.subset_intents.clone())?;
369 Ok(sub_tree.root())
370 }
371
372 pub fn commitment(&self) -> [u8; 32] {
377 let mut h = hasher_subscope(crate::cert::CERT_VERSION);
378 h.update(b"a1::dyolo::subscope::v2.8.0");
379 h.update(&(self.subset_intents.len() as u64).to_le_bytes());
380 for intent in &self.subset_intents {
381 h.update(intent);
382 }
383 h.update(&(self.proofs.len() as u64).to_le_bytes());
384 for proof in &self.proofs {
385 h.update(&(proof.siblings.len() as u64).to_le_bytes());
386 for node in &proof.siblings {
387 h.update(&node.hash);
388 h.update(&[node.is_left as u8]);
389 }
390 }
391 h.finalize().into()
392 }
393}
394
395#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[allow(deprecated)]
402 fn sample_intents() -> Vec<IntentHash> {
403 (0..8u8)
404 .map(|i| intent_hash(&format!("action_{i}"), &[i]))
405 .collect()
406 }
407
408 #[test]
409 fn tree_root_is_deterministic() {
410 let a = IntentTree::build(sample_intents()).unwrap();
411 let mut reversed = sample_intents();
412 reversed.reverse();
413 let b = IntentTree::build(reversed).unwrap();
414 assert_eq!(a.root(), b.root());
415 }
416
417 #[test]
418 fn proofs_verify_for_all_leaves() {
419 let intents = sample_intents();
420 let tree = IntentTree::build(intents.clone()).unwrap();
421 let root = tree.root();
422 for intent in &intents {
423 let proof = tree.prove(intent).unwrap();
424 assert!(proof.verify(intent, &root));
425 }
426 }
427
428 #[test]
429 #[allow(deprecated)]
430 fn unknown_intent_proof_fails() {
431 let tree = IntentTree::build(sample_intents()).unwrap();
432 let unknown = intent_hash("unknown", b"");
433 assert_eq!(tree.prove(&unknown), Err(A1Error::IntentNotFound));
434 }
435
436 #[test]
437 fn sub_scope_derives_correct_root() {
438 let intents = sample_intents();
439 let tree = IntentTree::build(intents.clone()).unwrap();
440 let subset = &intents[..3];
441
442 let proof = SubScopeProof::build(&tree, subset).unwrap();
443 let derived = proof.verify_and_derive_root(&tree.root()).unwrap();
444
445 let expected = IntentTree::build(subset.to_vec()).unwrap().root();
446 assert_eq!(derived, expected);
447 }
448
449 #[test]
450 fn full_passthrough_returns_parent_root() {
451 let tree = IntentTree::build(sample_intents()).unwrap();
452 let root = tree.root();
453 let derived = SubScopeProof::full_passthrough()
454 .verify_and_derive_root(&root)
455 .unwrap();
456 assert_eq!(derived, root);
457 }
458
459 #[test]
460 fn intent_struct_hash_is_order_independent() {
461 let a = Intent::new("trade")
462 .unwrap()
463 .param("symbol", "AAPL")
464 .param("qty", "100");
465 let b = Intent::new("trade")
466 .unwrap()
467 .param("qty", "100")
468 .param("symbol", "AAPL");
469 assert_eq!(a.hash(), b.hash());
470 }
471
472 #[test]
473 fn intent_display() {
474 let s = Intent::new("trade.equity")
475 .unwrap()
476 .param("symbol", "AAPL")
477 .to_string();
478 assert!(s.contains("trade.equity"));
479 assert!(s.contains("symbol=aapl"));
480 }
481}