jcp-wire: the endpoint decides at its own clock, and a captured request is decided once #322

Merged
dcharlot merged 1 commit from fix/jcp-wire-endpoint-clock-and-replay into main 2026-10-18 19:02:35 -04:00
5 changed files with 218 additions and 28 deletions

View file

@ -1166,7 +1166,9 @@ A conforming implementation passes, with real checks:
decision enforces an inherited caveat; and a chain whose child dropped one does decision enforces an inherited caveat; and a chain whose child dropped one does
not verify. not verify.
- **C8 — wire binding:** a request round-trips the `jcp/1` framing canonically - **C8 — wire binding:** a request round-trips the `jcp/1` framing canonically
and an enforcement endpoint decides + seals over a live socket. and an enforcement endpoint decides + seals over a live socket; a replayed
request is refused `replayed`, and one stamped outside the window
`stale-request`.
- **C9 — energy honesty:** the metering ladder always yields a figure and its - **C9 — energy honesty:** the metering ladder always yields a figure and its
provenance is internally consistent. provenance is internally consistent.
- **C10 — delegation control:** depth narrows per hop; a leaf-only grant cannot - **C10 — delegation control:** depth narrows per hop; a leaf-only grant cannot
@ -1231,10 +1233,16 @@ WireResponse { decision, receipt: bytes }
``` ```
`chain` is the full delegation chain, root first (a single un-delegated grant is `chain` is the full delegation chain, root first (a single un-delegated grant is
a one-link chain). `now` makes decision time an explicit input — a decision is a one-link chain). The response carries the deterministic decision and the
replayable, not ambient. The response carries the deterministic decision and the
sealed JCR-1 receipt, deny and allow alike. sealed JCR-1 receipt, deny and allow alike.
**Time is the enforcement point's.** The endpoint MUST decide at its own clock.
`now` is the time the caller signed the request, a freshness stamp: the endpoint
MUST deny a request whose `now` differs from its clock by more than its
freshness window as `stale-request`, and MUST NOT decide any request as of the
time it names. A caller that chose the decision time could stamp a request
before its grant expired, or inside a time caveat it has since left.
**Caller authentication.** `signature` is Ed25519 by `caller` over the **Caller authentication.** `signature` is Ed25519 by `caller` over the
canonical bytes of every other field. The endpoint MUST verify it before canonical bytes of every other field. The endpoint MUST verify it before
deciding; a request that fails is denied `bad-signature` and sealed like any deciding; a request that fails is denied `bad-signature` and sealed like any
@ -1242,10 +1250,20 @@ other decision — the refused impersonation is evidence too. A threshold actor
(§2.2) signs the same field with a k-of-n aggregate, indistinguishable on the (§2.2) signs the same field with a k-of-n aggregate, indistinguishable on the
wire. wire.
The wire never carries a secret: grants are signed public objects, and a **A request is decided once.** The wire never carries a secret: grants are
captured request replays into the same deterministic decision and the same signed public objects, and a request is a signed public object too. The endpoint
budget accounting it produced the first time. The endpoint behind the MUST remember every request it decided while that request's stamp is inside the
enforcement point is invoked only on Allow. window, and MUST deny one presented again as `replayed`, so a captured request
is neither decided nor debited a second time. Once a request's stamp has left
the window, a replay of it is stale. Two byte-identical requests are one
request, so a caller that means the same action twice stamps the second later.
**The cost is the holder's measurement.** `cost` is the requester's own figure,
and the endpoint debits it as given, so a budget binds only as honestly as that
figure was measured. The requester MUST therefore be the party that metered the
action: the grant's holder is the enforcement point that ran it, as a steward is
in Mesh, rather than the agent the grant governs reporting its own consumption.
The endpoint behind the enforcement point is invoked only on Allow.
## 12. Non-goals ## 12. Non-goals

View file

@ -49,7 +49,7 @@ pub fn checks() -> Vec<Check> {
Check { id: "C5 receipt seal + verify", run: c5_receipt }, Check { id: "C5 receipt seal + verify", run: c5_receipt },
Check { id: "C6 revocation authority + subtree kill", run: c6_revocation }, Check { id: "C6 revocation authority + subtree kill", run: c6_revocation },
Check { id: "C7 chain verification + budget conservation + inherited caveats", run: c7_chain }, Check { id: "C7 chain verification + budget conservation + inherited caveats", run: c7_chain },
Check { id: "C8 wire binding round-trip + live decide", run: c8_wire }, Check { id: "C8 wire binding round-trip + live decide + no replay, endpoint clock", run: c8_wire },
Check { id: "C9 energy provenance honesty", run: c9_energy }, Check { id: "C9 energy provenance honesty", run: c9_energy },
Check { id: "C10 delegation control (depth narrows, leaf-only holds)", run: c10_delegation_control }, Check { id: "C10 delegation control (depth narrows, leaf-only holds)", run: c10_delegation_control },
Check { id: "C11 sharded budgets conserve the root at mint", run: c11_shard }, Check { id: "C11 sharded budgets conserve the root at mint", run: c11_shard },
@ -583,7 +583,7 @@ fn c8_wire() -> Result<(), String> {
let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| e.to_string())?; let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| e.to_string())?;
let addr = listener.local_addr().map_err(|e| e.to_string())?; let addr = listener.local_addr().map_err(|e| e.to_string())?;
let server = std::thread::spawn(move || { let server = std::thread::spawn(move || {
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), steward_sk, "did:example:steward"); let mut ep = jcp_wire::Endpoint::new(Runtime::new(), steward_sk, "did:example:steward").with_clock(|| 1);
let (mut stream, _) = listener.accept().unwrap(); let (mut stream, _) = listener.accept().unwrap();
let _ = ep.serve_stream(&mut stream); let _ = ep.serve_stream(&mut stream);
}); });
@ -594,8 +594,28 @@ fn c8_wire() -> Result<(), String> {
} }
jcp_receipt::verify(&req.frame, &sg.grant, &resp.decision, &resp.receipt, "did:example:steward", &steward_vk.to_bytes()) jcp_receipt::verify(&req.frame, &sg.grant, &resp.decision, &resp.receipt, "did:example:steward", &steward_vk.to_bytes())
.map_err(|e| format!("wire receipt failed to verify: {e}"))?; .map_err(|e| format!("wire receipt failed to verify: {e}"))?;
// The same request again is refused, not decided and debited twice.
let again = client.call(&req).map_err(|e| e.to_string())?;
if again.decision.verdict != Verdict::Deny(DenyReason::Replayed) {
return Err(format!("a replayed wire request was decided again: {:?}", again.decision.verdict));
}
drop(client); drop(client);
server.join().map_err(|_| "wire server panicked".to_string())?; server.join().map_err(|_| "wire server panicked".to_string())?;
// The endpoint decides at its own clock: a request stamped outside its
// window is refused rather than decided as of the time it names.
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(|| 1_000).with_skew(60);
if ep.handle(&req).decision.verdict != Verdict::Deny(DenyReason::StaleRequest) {
return Err("a wire request stamped outside the window was decided".into());
}
// Inside the window, the clock still decides: a grant that expires between
// the request's stamp and the endpoint's clock is expired.
let short = grant(&issuer, holder, r, &["read"], 100_000, 999);
let late = jcp_wire::WireRequest::sign(frame(&short, r, "read", Trust::Control), vec![short.clone()], &sk(2), Energy::estimator(1), 998)
.map_err(|e| e.to_string())?;
if ep.handle(&late).decision.verdict != Verdict::Deny(DenyReason::Expired) {
return Err("a wire request was decided at its own stamp rather than the endpoint's clock".into());
}
Ok(()) Ok(())
} }
@ -854,7 +874,7 @@ fn c13_signed_caller() -> Result<(), String> {
// two can — and the endpoint treats it as any signed caller // two can — and the endpoint treats it as any signed caller
let sig = team.sign(&bytes, &[1, 3], &mut rng).map_err(|e| e.to_string())?; let sig = team.sign(&bytes, &[1, 3], &mut rng).map_err(|e| e.to_string())?;
let req = jcp_wire::WireRequest::with_signature(f.clone(), chain.clone(), team.group_did(), cost, 1, sig); let req = jcp_wire::WireRequest::with_signature(f.clone(), chain.clone(), team.group_did(), cost, 1, sig);
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), sk(7), "did:example:steward"); let mut ep = jcp_wire::Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(|| 1);
if !ep.handle(&req).decision.is_allow() { if !ep.handle(&req).decision.is_allow() {
return Err("a properly threshold-signed request was denied".into()); return Err("a properly threshold-signed request was denied".into());
} }

View file

@ -602,6 +602,13 @@ pub enum DenyReason {
/// (SPEC §4.10) — a tighter wallet for work that has touched an untrusted /// (SPEC §4.10) — a tighter wallet for work that has touched an untrusted
/// source, distinct from the cumulative `BudgetExceeded`. /// source, distinct from the cumulative `BudgetExceeded`.
TaintBudgetExceeded, TaintBudgetExceeded,
/// A wire request stamped with a `now` outside the enforcement point's
/// freshness window (SPEC §11). The point decides at its own clock, and a
/// request signed for another time is not decided.
StaleRequest,
/// A wire request the enforcement point has already decided, presented
/// again (SPEC §11). It is decided once, and debited once.
Replayed,
} }
impl DenyReason { impl DenyReason {
@ -623,6 +630,8 @@ impl DenyReason {
DenyReason::UntrustedRoot => "untrusted-root", DenyReason::UntrustedRoot => "untrusted-root",
DenyReason::TaintedFlow => "tainted-flow", DenyReason::TaintedFlow => "tainted-flow",
DenyReason::TaintBudgetExceeded => "taint-budget-exceeded", DenyReason::TaintBudgetExceeded => "taint-budget-exceeded",
DenyReason::StaleRequest => "stale-request",
DenyReason::Replayed => "replayed",
} }
} }
} }

View file

@ -292,7 +292,8 @@ mod tests {
jcp_runtime::Runtime::new(), jcp_runtime::Runtime::new(),
ed25519_dalek::SigningKey::from_bytes(&[7; 32]), ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
"did:example:steward", "did:example:steward",
); )
.with_clock(|| 5);
let resp = ep.handle(&req); let resp = ep.handle(&req);
assert!(resp.decision.is_allow()); assert!(resp.decision.is_allow());
} }

View file

@ -15,10 +15,18 @@
//! //!
//! The decision stays on the enforcement side: the endpoint behind the server //! The decision stays on the enforcement side: the endpoint behind the server
//! is invoked only on Allow. The wire never carries a secret — grants are //! is invoked only on Allow. The wire never carries a secret — grants are
//! signed public objects, and a captured request replays into the same //! signed public objects. The endpoint decides at **its own clock**: a
//! deterministic decision (and the same budget accounting) it would have //! request's `now` is a freshness stamp, and one outside the window
//! produced the first time. //! ([`Endpoint::with_skew`]) is denied `stale-request`. Within the window the
//! endpoint remembers what it decided, so a captured request presented again
//! is denied `replayed` rather than decided, and debited, a second time.
//!
//! The `cost` a request declares is the requester's own figure, so the
//! requester is the grant's holder and the party that metered the action
//! (SPEC §11): an endpoint takes no figure from a party it does not hold to
//! account for it.
use std::collections::HashMap;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream, ToSocketAddrs}; use std::net::{TcpListener, TcpStream, ToSocketAddrs};
#[cfg(unix)] #[cfg(unix)]
@ -26,7 +34,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
use jcp_runtime::{Decision, Runtime}; use jcp_runtime::{Decision, Runtime};
use jcp_schema::{canonical_cbor, from_canonical_cbor, Did, Energy, Frame, SchemaError, SignedGrant}; use jcp_schema::{canonical_cbor, from_canonical_cbor, ContentId, Did, Energy, Frame, SchemaError, SignedGrant};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Cap on a single wire message. Generous for grant chains, hostile to a /// Cap on a single wire message. Generous for grant chains, hostile to a
@ -60,8 +68,9 @@ pub struct WireRequest {
pub chain: Vec<SignedGrant>, pub chain: Vec<SignedGrant>,
pub caller: Did, pub caller: Did,
pub cost: Energy, pub cost: Energy,
/// Decision-time seconds; the enforcement point treats time as input, not /// When the caller signed the request, unix seconds: a freshness stamp. The
/// ambient state, so a decision is replayable. /// enforcement point decides at its own clock and refuses a request whose
/// stamp is outside its window, so this cannot choose the decision time.
pub now: u64, pub now: u64,
/// Ed25519 (64 bytes) by `caller` over [`WireRequest::signable_bytes`]. /// Ed25519 (64 bytes) by `caller` over [`WireRequest::signable_bytes`].
pub signature: Vec<u8>, pub signature: Vec<u8>,
@ -137,6 +146,12 @@ pub struct WireResponse {
pub receipt: Vec<u8>, pub receipt: Vec<u8>,
} }
fn system_clock() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
/// Write one length-prefixed canonical-CBOR message. /// Write one length-prefixed canonical-CBOR message.
pub fn write_msg<T: Serialize>(w: &mut impl Write, msg: &T) -> Result<(), WireError> { pub fn write_msg<T: Serialize>(w: &mut impl Write, msg: &T) -> Result<(), WireError> {
let body = canonical_cbor(msg)?; let body = canonical_cbor(msg)?;
@ -171,17 +186,63 @@ pub fn read_msg<T: serde::de::DeserializeOwned>(r: &mut impl Read) -> Result<T,
Ok(from_canonical_cbor(&body)?) Ok(from_canonical_cbor(&body)?)
} }
/// How far, by default, a request's `now` may stray from the endpoint's clock.
pub const DEFAULT_SKEW_SECS: u64 = 300;
/// An enforcement point serving `jcp/1` over byte streams: the runtime that /// An enforcement point serving `jcp/1` over byte streams: the runtime that
/// decides, and the steward key that seals every decision. /// decides, and the steward key that seals every decision.
pub struct Endpoint { pub struct Endpoint {
pub runtime: Runtime, pub runtime: Runtime,
steward_sk: SigningKey, steward_sk: SigningKey,
steward: String, steward: String,
/// Unix seconds: the time every decision is made at.
clock: fn() -> u64,
/// How far a request's `now` may stray from `clock`.
skew_secs: u64,
/// The requests decided within the window, by content id, with their stamp.
decided: HashMap<ContentId, u64>,
} }
impl Endpoint { impl Endpoint {
/// An endpoint deciding at the system clock, with a
/// [`DEFAULT_SKEW_SECS`] freshness window.
pub fn new(runtime: Runtime, steward_sk: SigningKey, steward: impl Into<String>) -> Self { pub fn new(runtime: Runtime, steward_sk: SigningKey, steward: impl Into<String>) -> Self {
Self { runtime, steward_sk, steward: steward.into() } Self {
runtime,
steward_sk,
steward: steward.into(),
clock: system_clock,
skew_secs: DEFAULT_SKEW_SECS,
decided: HashMap::new(),
}
}
/// Decide at `clock` (unix seconds) instead of the system clock.
pub fn with_clock(mut self, clock: fn() -> u64) -> Self {
self.clock = clock;
self
}
/// Accept a request whose `now` is within `secs` of the clock.
pub fn with_skew(mut self, secs: u64) -> Self {
self.skew_secs = secs;
self
}
/// Whether `req` may be decided now: fresh, and not already decided. A
/// request that may be is remembered until its stamp leaves the window,
/// after which a replay of it is stale.
fn admit(&mut self, req: &WireRequest, now: u64) -> Result<(), jcp_schema::DenyReason> {
let skew = self.skew_secs;
self.decided.retain(|_, stamp| stamp.saturating_add(skew) >= now);
if now.abs_diff(req.now) > skew {
return Err(jcp_schema::DenyReason::StaleRequest);
}
let id = ContentId::of(req).map_err(|_| jcp_schema::DenyReason::BadSignature)?;
if self.decided.insert(id, req.now).is_some() {
return Err(jcp_schema::DenyReason::Replayed);
}
Ok(())
} }
/// Decide one request and seal the receipt — the whole protocol step, /// Decide one request and seal the receipt — the whole protocol step,
@ -189,18 +250,19 @@ impl Endpoint {
/// unauthenticated request is denied (`bad-signature`) and sealed like any /// unauthenticated request is denied (`bad-signature`) and sealed like any
/// other decision — the refused impersonation is evidence too. /// other decision — the refused impersonation is evidence too.
pub fn handle(&mut self, req: &WireRequest) -> WireResponse { pub fn handle(&mut self, req: &WireRequest) -> WireResponse {
let decision = if req.verify_caller() { let now = (self.clock)();
self.runtime.decide_chained(&req.frame, &req.chain, req.caller, req.cost, req.now) let refused = if req.verify_caller() { self.admit(req, now).err() } else { Some(jcp_schema::DenyReason::BadSignature) };
} else { let decision = match refused {
Decision { None => self.runtime.decide_chained(&req.frame, &req.chain, req.caller, req.cost, now),
verdict: jcp_schema::Verdict::Deny(jcp_schema::DenyReason::BadSignature), Some(reason) => Decision {
verdict: jcp_schema::Verdict::Deny(reason),
cost: req.cost, cost: req.cost,
budget_remaining_uj: req budget_remaining_uj: req
.chain .chain
.last() .last()
.map(|l| l.grant.budget_uj.saturating_sub(self.runtime.spent(&l.grant.id()))) .map(|l| l.grant.budget_uj.saturating_sub(self.runtime.spent(&l.grant.id())))
.unwrap_or(0), .unwrap_or(0),
} },
}; };
// The receipt names the leaf grant and — for a multi-hop chain — every // The receipt names the leaf grant and — for a multi-hop chain — every
// debited link, so the receipt stream replays exactly (SPEC §8.1). // debited link, so the receipt stream replays exactly (SPEC §8.1).
@ -318,6 +380,86 @@ mod tests {
(vec![root, leaf], did(&sk(3)), r) (vec![root, leaf], did(&sk(3)), r)
} }
/// The fixture's requests are stamped 10; the existing tests decide there.
fn at_10() -> u64 {
10
}
/// A request stamped `now`, signed by the fixture's caller.
fn stamped(chain: &[SignedGrant], r: ContentId, cost: u64, now: u64) -> WireRequest {
let frame = request(chain, did(&sk(3)), r, Trust::Control, cost).frame;
WireRequest::sign(frame, chain.to_vec(), &sk(3), Energy::estimator(cost), now).unwrap()
}
/// The fixture's chain with a root and leaf that expire at `at`.
fn expiring(at: u64) -> (Vec<SignedGrant>, ContentId) {
let (chain, _, r) = fixture();
let mut g = chain[0].grant.clone();
g.expiry = at;
let root = issue(&sk(1), g).unwrap();
let leaf = attenuate(&root, &sk(2), did(&sk(3)), Capability::new(r, ["read"]), 50_000, at, [1; 16]).unwrap();
(vec![root, leaf], r)
}
fn at_100() -> u64 {
100
}
/// The endpoint decides at its own clock. A caller used to choose the
/// decision time by stamping its request, so a grant that had expired was
/// decided as of a moment it had not.
#[test]
fn the_endpoint_decides_at_its_own_clock() {
// Expires at 99. A request stamped 97 is inside the window, and was
// valid at its stamp, but the endpoint's clock reads 100.
let (chain, r) = expiring(99);
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_100).with_skew(5);
let fresh = ep.handle(&stamped(&chain, r, 1, 97));
assert_eq!(fresh.decision.verdict, Verdict::Deny(DenyReason::Expired), "decided at 100, not at the stamp 97");
let backdated = ep.handle(&stamped(&chain, r, 1, 40));
assert_eq!(backdated.decision.verdict, Verdict::Deny(DenyReason::StaleRequest), "stamped outside the window");
assert!(!backdated.receipt.is_empty(), "sealed like any other denial");
assert_eq!(ep.runtime.spent(&chain[1].grant.id()), 0);
// The same chain at an endpoint whose clock is before the expiry: allowed.
let (chain, r) = expiring(200);
let mut ok = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_100).with_skew(5);
assert!(ok.handle(&stamped(&chain, r, 1, 97)).decision.is_allow());
}
/// A captured request presented again is refused, not decided and debited
/// a second time; a request stamped differently is a new request.
#[test]
fn a_captured_request_is_decided_once() {
let (chain, _, r) = fixture();
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_10);
let req = stamped(&chain, r, 10_000, 10);
assert!(ep.handle(&req).decision.is_allow());
let again = ep.handle(&req);
assert_eq!(again.decision.verdict, Verdict::Deny(DenyReason::Replayed));
assert_eq!(ep.runtime.spent(&chain[1].grant.id()), 10_000, "debited once");
assert!(ep.handle(&stamped(&chain, r, 10_000, 11)).decision.is_allow());
assert_eq!(ep.runtime.spent(&chain[1].grant.id()), 20_000);
}
static CLOCK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1_000);
fn moving() -> u64 {
CLOCK.load(std::sync::atomic::Ordering::SeqCst)
}
/// The endpoint forgets a request once its stamp leaves the window, and a
/// replay after that is stale rather than fresh again.
#[test]
fn a_replay_after_the_window_is_stale() {
let (chain, _, r) = fixture();
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(moving).with_skew(30);
let req = stamped(&chain, r, 1, 1_000);
assert!(ep.handle(&req).decision.is_allow());
CLOCK.store(1_031, std::sync::atomic::Ordering::SeqCst);
assert_eq!(ep.handle(&req).decision.verdict, Verdict::Deny(DenyReason::StaleRequest));
assert!(ep.decided.is_empty(), "nothing outside the window is remembered");
}
// the fixture's caller is sk(3) — sign as them // the fixture's caller is sk(3) — sign as them
fn request(chain: &[SignedGrant], _caller: Did, r: ContentId, trust: Trust, cost: u64) -> WireRequest { fn request(chain: &[SignedGrant], _caller: Did, r: ContentId, trust: Trust, cost: u64) -> WireRequest {
let frame = Frame { let frame = Frame {
@ -350,7 +492,7 @@ mod tests {
let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || { let server = std::thread::spawn(move || {
let mut ep = Endpoint::new(Runtime::new(), steward_sk, "did:example:steward"); let mut ep = Endpoint::new(Runtime::new(), steward_sk, "did:example:steward").with_clock(at_10);
let (mut stream, _) = listener.accept().unwrap(); let (mut stream, _) = listener.accept().unwrap();
ep.serve_stream(&mut stream).unwrap(); ep.serve_stream(&mut stream).unwrap();
}); });
@ -385,7 +527,7 @@ mod tests {
let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || { let server = std::thread::spawn(move || {
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward"); let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_10);
for _ in 0..2 { for _ in 0..2 {
let (mut stream, _) = listener.accept().unwrap(); let (mut stream, _) = listener.accept().unwrap();
ep.serve_stream(&mut stream).unwrap(); ep.serve_stream(&mut stream).unwrap();
@ -419,7 +561,7 @@ mod tests {
// the issuer revoked the ROOT before this endpoint came up // the issuer revoked the ROOT before this endpoint came up
let sr = revoke(&sk(1), &root_grant, [9; 16]).unwrap(); let sr = revoke(&sk(1), &root_grant, [9; 16]).unwrap();
rt.apply_revocation(&sr, &root_grant).unwrap(); rt.apply_revocation(&sr, &root_grant).unwrap();
let mut ep = Endpoint::new(rt, sk(7), "did:example:steward"); let mut ep = Endpoint::new(rt, sk(7), "did:example:steward").with_clock(at_10);
let (mut stream, _) = listener.accept().unwrap(); let (mut stream, _) = listener.accept().unwrap();
ep.serve_stream(&mut stream).unwrap(); ep.serve_stream(&mut stream).unwrap();
}); });
@ -443,7 +585,7 @@ mod tests {
#[test] #[test]
fn an_impersonated_caller_is_denied_and_sealed() { fn an_impersonated_caller_is_denied_and_sealed() {
let (chain, caller, r) = fixture(); let (chain, caller, r) = fixture();
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward"); let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_10);
// a request claiming the caller's did, signed by someone else // a request claiming the caller's did, signed by someone else
let frame = request(&chain, caller, r, Trust::Control, 1).frame; let frame = request(&chain, caller, r, Trust::Control, 1).frame;
let bytes = WireRequest::signable_bytes(&frame, &chain, &caller, &Energy::estimator(1), 10).unwrap(); let bytes = WireRequest::signable_bytes(&frame, &chain, &caller, &Energy::estimator(1), 10).unwrap();