dyolo-kya 1.0.0

Know Your Agent (KYA): cryptographic chain-of-custody for recursive AI delegation with provable scope narrowing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
use blake3::Hasher;
use ed25519_dalek::VerifyingKey;

use crate::cert::DelegationCert;
use crate::crypto::DOMAIN_CHAIN_FP;
use crate::error::KyaError;
use crate::intent::{IntentHash, MerkleProof};
use crate::registry::{NonceStore, RevocationStore};

// ── Clock ─────────────────────────────────────────────────────────────────────

/// Time source for certificate validity checks.
///
/// Implementing this trait lets you inject deterministic time in tests
/// without coupling the verifier to `SystemTime`.
pub trait Clock {
    fn unix_now(&self) -> u64;
}

/// Wall-clock implementation that reads from the operating system.
pub struct SystemClock;

impl Clock for SystemClock {
    fn unix_now(&self) -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("system clock is before the Unix epoch")
            .as_secs()
    }
}

// ── Verification Receipt ──────────────────────────────────────────────────────

/// The audit record produced by a successful [`DyoloChain::authorize`] call.
///
/// Every field is deterministic. Two identical authorizations over the same
/// chain produce identical receipts. The `chain_fingerprint` commits to the
/// full delegation path and is suitable as an append-only compliance log key.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VerificationReceipt {
    /// Number of delegation hops from principal to executing agent.
    pub chain_depth: usize,
    /// Merkle root of the scope the executing agent was authorized within.
    pub verified_scope_root: IntentHash,
    /// The exact intent that was authorized.
    pub intent: IntentHash,
    /// Unix timestamp at which this authorization was verified.
    pub verified_at_unix: u64,
    /// A 32-byte commitment to the complete delegation path.
    pub chain_fingerprint: [u8; 32],
}

// ── Zero-Knowledge Protocol ───────────────────────────────────────────────────

#[cfg(feature = "prove")]
pub use zk_rollup::*;

#[cfg(feature = "prove")]
mod zk_rollup {
    use super::*;
    use risc0_zkvm::{Receipt, sha::Digest};
    use subtle::{ConstantTimeEq, Choice};

    /// STARK-based recursive authorization primitive.
    ///
    /// Compresses an N-depth Ed25519 signature chain into an O(1) verifiable
    /// zero-knowledge state transition, supporting arbitrarily nested agent
    /// delegation without leaking chain depth or intermediate identities.
    #[derive(Debug, Clone)]
    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    pub struct DyoloZkRollup {
        pub receipt: Receipt,
        pub principal_pk: VerifyingKey,
        pub executor_pk: VerifyingKey,
        pub intent: IntentHash,
        pub state_root: [u8; 32],
    }

    impl DyoloZkRollup {
        #[inline(always)]
        pub fn verify_state_transition(&self, image_id: impl Into<Digest>) -> Result<(), KyaError> {
            self.receipt.verify(image_id).map_err(|_| KyaError::InvalidSubScopeProof)?;

            let journal = self.receipt.journal.bytes.as_slice();

            if journal.len() != 128 {
                return Err(KyaError::InvalidSubScopeProof);
            }

            let p_match = journal[0..32].ct_eq(self.principal_pk.as_bytes());
            let e_match = journal[32..64].ct_eq(self.executor_pk.as_bytes());
            let i_match = journal[64..96].ct_eq(self.intent.as_slice());
            let s_match = journal[96..128].ct_eq(&self.state_root);

            let valid_transition: Choice = p_match & e_match & i_match & s_match;

            if valid_transition.unwrap_u8() == 0 {
                return Err(KyaError::InvalidSubScopeProof);
            }

            Ok(())
        }
    }
}

// ── Authorized Action ─────────────────────────────────────────────────────────

/// The type-level proof that an action has been cryptographically authorized.
///
/// The only way to construct an `AuthorizedAction` is via
/// [`DyoloChain::authorize`]. There is no public constructor. This means the
/// Rust type system statically prevents any action from being executed
/// without first passing full chain verification — you cannot even instantiate
/// this type from outside the crate.
///
/// Pass this value into your execution layer rather than accepting raw intent
/// hashes, and your execution layer becomes trivially audit-complete by
/// construction.
///
/// # Example
///
/// ```rust,ignore
/// fn execute_trade(action: AuthorizedAction, engine: &TradingEngine) {
///     engine.submit(action.receipt());
/// }
/// ```
#[must_use]
pub struct AuthorizedAction {
    /// The audit record for this authorization.
    pub receipt: VerificationReceipt,
    _sealed: (),
}

impl AuthorizedAction {
    pub(crate) fn new(receipt: VerificationReceipt) -> Self {
        Self { receipt, _sealed: () }
    }

    pub fn receipt(&self) -> &VerificationReceipt {
        &self.receipt
    }
}

impl std::fmt::Debug for AuthorizedAction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AuthorizedAction")
            .field("receipt", &self.receipt)
            .finish()
    }
}

// ── Chain of Custody ──────────────────────────────────────────────────────────

/// The verifiable delegation chain from a principal to an executing agent.
///
/// A `DyoloChain` is a pure value type: it holds certs and carries no
/// mutable state. All side-effects (nonce consumption, receipt logging) are
/// delegated to the caller-supplied [`NonceStore`] and [`RevocationStore`]
/// on each call to [`authorize`].
///
/// This design means `DyoloChain` is `Clone`, and the same chain value can
/// be verified against multiple execution requests without any shared-state
/// concerns.
///
/// # Invariants enforced on every [`authorize`] call
///
/// 1. **Anchor integrity** — the first cert's delegator equals `principal_pk`.
/// 2. **Linkage** — each cert's delegator key equals the prior cert's delegate key.
/// 3. **Signature validity** — every Ed25519 signature verifies against its declared signer.
/// 4. **Temporal validity** — every cert is within its `[issued_at, expiration_unix]` window.
/// 5. **Temporal monotonicity** — no child cert expires after its parent.
/// 6. **Depth cap** — chain length respects the most restrictive `max_depth` seen.
/// 7. **Revocation** — no cert fingerprint appears in the provided [`RevocationStore`].
/// 8. **Nonce uniqueness** — no nonce appears twice, preventing replay attacks.
/// 9. **Scope containment** — every delegated scope is proven to be within the delegator's scope.
/// 10. **Leaf authorization** — the executing agent is the terminal delegate.
/// 11. **Intent membership** — the execution intent is within the terminal scope.
///
/// [`authorize`]: DyoloChain::authorize
pub struct DyoloChain {
    /// The public key of the root human principal.
    pub principal_pk: VerifyingKey,
    /// The Merkle root of the principal's authorized intent set.
    pub principal_scope: IntentHash,
    certs: Vec<DelegationCert>,
}

impl DyoloChain {
    pub fn new(principal_pk: VerifyingKey, principal_scope: IntentHash) -> Self {
        Self { principal_pk, principal_scope, certs: Vec::new() }
    }

    /// Append a delegation cert to the chain.
    pub fn push(&mut self, cert: DelegationCert) -> &mut Self {
        self.certs.push(cert);
        self
    }

    /// The number of delegation hops in this chain.
    pub fn len(&self) -> usize {
        self.certs.len()
    }

    pub fn is_empty(&self) -> bool {
        self.certs.is_empty()
    }

    /// Read-only access to the raw certificate sequence.
    pub fn certs(&self) -> &[DelegationCert] {
        &self.certs
    }

    /// A 32-byte digest committing to the complete delegation path.
    ///
    /// Deterministic for a given chain. Stable across calls.
    /// Suitable as an audit log key or for on-chain anchoring.
    pub fn fingerprint(&self) -> [u8; 32] {
        let mut base_hasher = Hasher::new_derive_key(DOMAIN_CHAIN_FP);
        base_hasher.update(self.principal_pk.as_bytes());
        base_hasher.update(&self.principal_scope);

        self.certs
            .iter()
            .fold(base_hasher, |mut h, cert| {
                h.update(&cert.fingerprint());
                h
            })
            .finalize()
            .into()
    }

    /// Verify that `agent_pk` is authorized to execute `intent` under this chain,
    /// and return a sealed [`AuthorizedAction`] on success.
    ///
    /// All nonces seen in this call are atomically committed to `nonces` only after
    /// every other invariant passes. A failed verification leaves `nonces` unchanged.
    ///
    /// # Parameters
    ///
    /// - `agent_pk` — the public key of the agent requesting execution.
    /// - `intent` — the exact intent hash the agent wants to execute.
    /// - `proof` — a Merkle inclusion proof for `intent` against the terminal scope root.
    ///   For a single-intent scope, pass [`MerkleProof::default()`].
    /// - `clock` — time source for validity window checks.
    /// - `revocation` — store of revoked certificate fingerprints.
    /// - `nonces` — persistent store of consumed nonces; updated on success.
    pub fn authorize(
        &self,
        agent_pk: &VerifyingKey,
        intent: &IntentHash,
        proof: &MerkleProof,
        clock: &(dyn Clock + Send + Sync),
        revocation: &(dyn RevocationStore + Send + Sync),
        nonces: &(dyn NonceStore + Send + Sync),
    ) -> Result<AuthorizedAction, KyaError> {
        if self.certs.is_empty() {
            return Err(KyaError::EmptyChain);
        }
        if self.certs[0].delegator_pk != self.principal_pk {
            return Err(KyaError::RootMismatch);
        }

        static DRIFT_TOLERANCE: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
        let drift_tolerance = *DRIFT_TOLERANCE.get_or_init(|| {
            std::env::var("DYOLO_CLOCK_DRIFT_SEC")
                .ok()
                .and_then(|v| v.parse::<u64>().ok())
                .unwrap_or(15)
        });

        let now = clock.unix_now();
        let tolerated_now_early = now.saturating_add(drift_tolerance);
        let tolerated_now_late  = now.saturating_sub(drift_tolerance);

        let mut current_scope      = self.principal_scope;
        let mut expected_delegator = self.principal_pk;
        let mut depth: usize       = 0;
        let mut max_allowed_depth  = u8::MAX;
        let mut parent_expiry      = u64::MAX;

        let chain_len = self.certs.len();
        if chain_len > 255 {
            return Err(KyaError::MaxDepthExceeded(255, 255));
        }

        // Pre-allocated vectors bounded by the protocol depth cap (255).
        // Heap-allocated to remain safe on all target platforms including WASM
        // and embedded targets where large stack frames are dangerous.
        let mut seen_nonces:      Vec<[u8; 16]>                 = Vec::with_capacity(chain_len);
        let mut batch_signatures: Vec<ed25519_dalek::Signature> = Vec::with_capacity(chain_len);
        let mut batch_public_keys: Vec<VerifyingKey>            = Vec::with_capacity(chain_len);
        let mut compiled_messages: Vec<Vec<u8>>                 = Vec::with_capacity(chain_len);

        for (i, cert) in self.certs.iter().enumerate() {
            if cert.delegator_pk != expected_delegator {
                return Err(KyaError::BrokenLinkage(i));
            }

            compiled_messages.push(DelegationCert::signable_bytes(
                &cert.delegator_pk, &cert.delegate_pk, &cert.scope_root,
                &cert.scope_proof, &cert.nonce, cert.issued_at,
                cert.expiration_unix, cert.max_depth,
            ));

            batch_signatures.push(cert.signature);
            batch_public_keys.push(cert.delegator_pk);

            let is_early     = (tolerated_now_early < cert.issued_at)      as u8;
            let is_expired   = (cert.expiration_unix < tolerated_now_late)  as u8;
            let is_escalated = (cert.expiration_unix > parent_expiry)       as u8;

            if is_early     == 1 { return Err(KyaError::NotYetValid(i, cert.issued_at, now)); }
            if is_expired   == 1 { return Err(KyaError::Expired(i, cert.expiration_unix, now)); }
            if is_escalated == 1 { return Err(KyaError::TemporalViolation(i, cert.expiration_unix, parent_expiry)); }

            depth += 1;
            if depth > max_allowed_depth as usize {
                return Err(KyaError::MaxDepthExceeded(i, max_allowed_depth));
            }
            if cert.max_depth < max_allowed_depth {
                max_allowed_depth = cert.max_depth;
            }

            if revocation.is_revoked(&cert.fingerprint()).map_err(KyaError::StorageFailure)? {
                return Err(KyaError::Revoked);
            }

            let mut internal_replay = false;
            for seen in &seen_nonces {
                if seen == &cert.nonce {
                    internal_replay = true;
                    break;
                }
            }
            if internal_replay || nonces.is_consumed(&cert.nonce).map_err(KyaError::StorageFailure)? {
                return Err(KyaError::NonceReplay);
            }
            seen_nonces.push(cert.nonce);

            // Scope narrowing: passthrough is detected by proof emptiness, not scope equality.
            //
            // An empty SubScopeProof is an explicit full-scope passthrough; the scope root
            // must then equal the parent's unchanged scope. A non-empty proof must
            // cryptographically derive its stated scope root from the parent root.
            //
            // This ordering is correct even for the edge case where a single-intent
            // narrowing produces the same root as the parent — that case carries a
            // non-empty proof and correctly routes through the derivation path rather
            // than being erroneously rejected as a malformed passthrough.
            let is_declared_passthrough =
                cert.scope_proof.subset_intents.is_empty() && cert.scope_proof.proofs.is_empty();

            if is_declared_passthrough {
                use subtle::ConstantTimeEq;
                if cert.scope_root.ct_eq(&current_scope).unwrap_u8() == 0 {
                    return Err(KyaError::ScopeEscalation(i));
                }
            } else {
                let derived = cert
                    .scope_proof
                    .verify_and_derive_root(&current_scope)
                    .map_err(|_| KyaError::ScopeEscalation(i))?;

                use subtle::ConstantTimeEq;
                if derived.ct_eq(&cert.scope_root).unwrap_u8() == 0 {
                    return Err(KyaError::ScopeEscalation(i));
                }
            }

            parent_expiry      = cert.expiration_unix;
            current_scope      = cert.scope_root;
            expected_delegator = cert.delegate_pk;
        }

        if expected_delegator != *agent_pk {
            return Err(KyaError::UnauthorizedLeaf);
        }

        // Batched signature verification. If the batch call rejects, fall back
        // to a linear scan to pinpoint the exact failing hop for precise error
        // reporting. Both paths return an error; a batch failure is never silently
        // swallowed when all individual checks happen to pass.
        {
            let messages_refs: Vec<&[u8]> = compiled_messages.iter().map(|m| m.as_slice()).collect();
            if ed25519_dalek::verify_batch(&messages_refs, &batch_signatures, &batch_public_keys).is_err() {
                for (i, cert) in self.certs.iter().enumerate() {
                    if !cert.verify_signature() {
                        return Err(KyaError::InvalidSignature(i));
                    }
                }
                return Err(KyaError::InvalidSignature(0));
            }
        }

        let intent_authorized = if proof.siblings.is_empty() {
            use subtle::ConstantTimeEq;
            intent.ct_eq(&current_scope).into()
        } else {
            proof.verify(intent, &current_scope)
        };

        if !intent_authorized {
            return Err(KyaError::ScopeViolation);
        }

        for nonce in &seen_nonces {
            nonces.mark_consumed(nonce).map_err(KyaError::StorageFailure)?;
        }

        Ok(AuthorizedAction::new(VerificationReceipt {
            chain_depth:        depth,
            verified_scope_root: current_scope,
            intent:             *intent,
            verified_at_unix:   now,
            chain_fingerprint:  self.fingerprint(),
        }))
    }

    #[cfg(feature = "prove")]
    pub fn authorize_zk_rollup(
        &self,
        agent_pk: &VerifyingKey,
        intent: &IntentHash,
        proof: &MerkleProof,
        clock: &(dyn Clock + Send + Sync),
        revocation: &(dyn RevocationStore + Send + Sync),
        nonces: &(dyn NonceStore + Send + Sync),
    ) -> Result<DyoloZkRollup, KyaError> {
        use risc0_zkvm::{default_prover, ExecutorEnv};

        let action   = self.authorize(agent_pk, intent, proof, clock, revocation, nonces)?;
        let elf_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/dyolo_zk_guest.elf"));

        let env = ExecutorEnv::builder()
            .write(&self.principal_pk.to_bytes()).map_err(|_| KyaError::InvalidSubScopeProof)?
            .write(&self.principal_scope).map_err(|_| KyaError::InvalidSubScopeProof)?
            .write(&self.certs).map_err(|_| KyaError::InvalidSubScopeProof)?
            .write(&agent_pk.to_bytes()).map_err(|_| KyaError::InvalidSubScopeProof)?
            .write(intent).map_err(|_| KyaError::InvalidSubScopeProof)?
            .write(&action.receipt.chain_fingerprint).map_err(|_| KyaError::InvalidSubScopeProof)?
            .build()
            .map_err(|_| KyaError::InvalidSubScopeProof)?;

        let receipt = default_prover()
            .prove(env, elf_bytes)
            .map_err(|_| KyaError::InvalidSubScopeProof)?
            .receipt;

        Ok(DyoloZkRollup {
            receipt,
            principal_pk: self.principal_pk,
            executor_pk:  *agent_pk,
            intent:       *intent,
            state_root:   action.receipt.chain_fingerprint,
        })
    }
}

impl Clone for DyoloChain {
    fn clone(&self) -> Self {
        Self {
            principal_pk:    self.principal_pk,
            principal_scope: self.principal_scope,
            certs:           self.certs.clone(),
        }
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        cert::CertBuilder,
        identity::DyoloIdentity,
        intent::{intent_hash, IntentTree, SubScopeProof},
        registry::{MemoryNonceStore, MemoryRevocationStore, RevocationStore, fresh_nonce},
    };

    struct FixedClock(u64);
    impl Clock for FixedClock {
        fn unix_now(&self) -> u64 { self.0 }
    }

    fn setup() -> (DyoloIdentity, DyoloIdentity, DyoloIdentity, IntentTree, u64) {
        let human   = DyoloIdentity::generate();
        let agent_a = DyoloIdentity::generate();
        let agent_b = DyoloIdentity::generate();
        let trade   = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let query   = intent_hash("QUERY_PORTFOLIO", b"");
        let tree    = IntentTree::build(vec![trade, query]).unwrap();
        let now     = 1_700_000_000u64;
        (human, agent_a, agent_b, tree, now)
    }

    fn build_two_hop_chain(
        human: &DyoloIdentity,
        agent_a: &DyoloIdentity,
        agent_b: &DyoloIdentity,
        scope_root: IntentHash,
        now: u64,
    ) -> DyoloChain {
        let expiry = now + 3600;
        let cert_a = CertBuilder::new(agent_a.verifying_key(), scope_root, now, expiry).sign(human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), scope_root, now, expiry).sign(agent_a);
        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_b);
        chain
    }

    #[test]
    fn full_delegation_chain_succeeds() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let scope_root = tree.root();
        let trade      = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof      = tree.prove(&trade).unwrap();
        let chain      = build_two_hop_chain(&human, &agent_a, &agent_b, scope_root, now);

        let action = chain
            .authorize(&agent_b.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new())
            .unwrap();

        assert_eq!(action.receipt.chain_depth, 2);
        assert_eq!(action.receipt.intent, trade);
        assert_ne!(action.receipt.chain_fingerprint, [0u8; 32]);
    }

    #[test]
    fn sub_scope_delegation_succeeds() {
        let (human, agent_a, agent_b, human_tree, now) = setup();
        let human_scope = human_tree.root();
        let trade       = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let expiry      = now + 3600;

        let sub_proof = SubScopeProof::build(&human_tree, &[trade]).unwrap();
        let sub_scope = IntentTree::build(vec![trade]).unwrap().root();

        let cert_a = CertBuilder::new(agent_a.verifying_key(), human_scope, now, expiry).sign(&human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), sub_scope, now, expiry)
            .scope_proof(sub_proof)
            .max_depth(5)
            .sign(&agent_a);

        let mut chain = DyoloChain::new(human.verifying_key(), human_scope);
        chain.push(cert_a).push(cert_b);

        assert!(chain
            .authorize(&agent_b.verifying_key(), &trade, &MerkleProof::default(), &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new())
            .is_ok());
    }

    #[test]
    fn scope_escalation_is_rejected() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let scope_root = tree.root();
        let drain      = intent_hash("DRAIN_ACCOUNT", b"all");
        let fake_scope = IntentTree::build(vec![drain]).unwrap().root();
        let expiry     = now + 3600;

        let cert_a = CertBuilder::new(agent_a.verifying_key(), scope_root, now, expiry).sign(&human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), fake_scope, now, expiry).sign(&agent_a);

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_b);

        assert_eq!(
            chain.authorize(&agent_b.verifying_key(), &drain, &MerkleProof::default(), &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new()),
            Err(KyaError::ScopeEscalation(1))
        );
    }

    #[test]
    fn temporal_monotonicity_is_enforced() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let scope_root = tree.root();

        let cert_a = CertBuilder::new(agent_a.verifying_key(), scope_root, now, now + 100).sign(&human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), scope_root, now, now + 9999).sign(&agent_a);

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_b);

        let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof = tree.prove(&trade).unwrap();

        assert!(matches!(
            chain.authorize(&agent_b.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new()),
            Err(KyaError::TemporalViolation(1, _, _))
        ));
    }

    #[test]
    fn expired_cert_is_rejected() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let scope_root = tree.root();

        let cert_a = CertBuilder::new(agent_a.verifying_key(), scope_root, now - 10, now - 1).sign(&human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), scope_root, now, now + 3600).sign(&agent_a);

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_b);

        let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof = tree.prove(&trade).unwrap();

        assert!(matches!(
            chain.authorize(&agent_b.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new()),
            Err(KyaError::Expired(0, _, _))
        ));
    }

    #[test]
    fn revoked_cert_is_rejected() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let scope_root = tree.root();
        let expiry     = now + 3600;

        let cert_a = CertBuilder::new(agent_a.verifying_key(), scope_root, now, expiry).sign(&human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), scope_root, now, expiry).sign(&agent_a);

        let rev = MemoryRevocationStore::new();
        rev.revoke(&cert_a.fingerprint()).unwrap();

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_b);

        let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof = tree.prove(&trade).unwrap();

        assert_eq!(
            chain.authorize(&agent_b.verifying_key(), &trade, &proof, &FixedClock(now), &rev, &MemoryNonceStore::new()),
            Err(KyaError::Revoked)
        );
    }

    #[test]
    fn replay_attack_is_rejected() {
        let (human, agent_a, _, tree, now) = setup();
        let scope_root = tree.root();
        let trade      = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof      = tree.prove(&trade).unwrap();
        let nonces     = MemoryNonceStore::new();

        let pinned = fresh_nonce();
        let cert   = CertBuilder::new(agent_a.verifying_key(), scope_root, now, now + 3600)
            .nonce(pinned)
            .sign(&human);

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert.clone());
        chain.authorize(&agent_a.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &nonces).unwrap();

        let mut chain2 = DyoloChain::new(human.verifying_key(), scope_root);
        chain2.push(cert);
        assert_eq!(
            chain2.authorize(&agent_a.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &nonces),
            Err(KyaError::NonceReplay)
        );
    }

    #[test]
    fn duplicate_nonce_within_chain_is_rejected() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let scope_root = tree.root();
        let shared     = fresh_nonce();
        let expiry     = now + 3600;

        let cert_a = CertBuilder::new(agent_a.verifying_key(), scope_root, now, expiry).nonce(shared).sign(&human);
        let cert_b = CertBuilder::new(agent_b.verifying_key(), scope_root, now, expiry).nonce(shared).sign(&agent_a);

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_b);

        let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof = tree.prove(&trade).unwrap();

        assert_eq!(
            chain.authorize(&agent_b.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new()),
            Err(KyaError::NonceReplay)
        );
    }

    #[test]
    fn broken_linkage_is_rejected() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let impostor   = DyoloIdentity::generate();
        let scope_root = tree.root();
        let expiry     = now + 3600;

        let cert_a    = CertBuilder::new(agent_a.verifying_key(), scope_root, now, expiry).sign(&human);
        let cert_fake = CertBuilder::new(agent_b.verifying_key(), scope_root, now, expiry).sign(&impostor);

        let mut chain = DyoloChain::new(human.verifying_key(), scope_root);
        chain.push(cert_a).push(cert_fake);

        let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
        let proof = tree.prove(&trade).unwrap();

        assert_eq!(
            chain.authorize(&agent_b.verifying_key(), &trade, &proof, &FixedClock(now), &MemoryRevocationStore::new(), &MemoryNonceStore::new()),
            Err(KyaError::BrokenLinkage(1))
        );
    }

    #[test]
    fn chain_fingerprint_is_stable() {
        let (human, agent_a, agent_b, tree, now) = setup();
        let chain = build_two_hop_chain(&human, &agent_a, &agent_b, tree.root(), now);
        assert_eq!(chain.fingerprint(), chain.clone().fingerprint());
    }

    #[test]
    fn identity_round_trip() {
        let id       = DyoloIdentity::generate();
        let bytes    = id.to_signing_bytes();
        let restored = DyoloIdentity::from_signing_bytes(&bytes);
        assert_eq!(id.verifying_key().as_bytes(), restored.verifying_key().as_bytes());
    }
}