joule-code-joulecontract: an obligation nothing checked is not discharged #307
1 changed files with 275 additions and 26 deletions
|
|
@ -18,7 +18,7 @@
|
||||||
//! composing them to JR-3+ with sealed evidence, a key log, and anchored
|
//! composing them to JR-3+ with sealed evidence, a key log, and anchored
|
||||||
//! intervals is the deployment's verifier-of-record work, not this crate's.
|
//! intervals is the deployment's verifier-of-record work, not this crate's.
|
||||||
|
|
||||||
use joule_code_cargo::CodingOp;
|
use joule_code_cargo::{CodingOp, SignedOp};
|
||||||
use joule_code_receipt::{Energy, Provenance};
|
use joule_code_receipt::{Energy, Provenance};
|
||||||
use joule_code_schema::ContentId;
|
use joule_code_schema::ContentId;
|
||||||
use joulecontract_receipt::{
|
use joulecontract_receipt::{
|
||||||
|
|
@ -95,6 +95,77 @@ pub enum BindingError {
|
||||||
OutOfFrame { path: String },
|
OutOfFrame { path: String },
|
||||||
#[error("contract addressing failed: {0}")]
|
#[error("contract addressing failed: {0}")]
|
||||||
Address(String),
|
Address(String),
|
||||||
|
/// The op's own id could not be derived, so no receipt can name the
|
||||||
|
/// operation it is about.
|
||||||
|
#[error("op id could not be derived: {0}")]
|
||||||
|
OpId(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decide one declared obligation against what this binder actually holds.
|
||||||
|
///
|
||||||
|
/// Every arm either evaluates the obligation or reports it `Undischarged`
|
||||||
|
/// with the reason it was not evaluated. **There is no default `Closed`.**
|
||||||
|
/// `Closed` is JouleContract's positive "this obligation was discharged", and
|
||||||
|
/// it used to be the fall-through for every Pre, Post and Inv obligation —
|
||||||
|
/// signed, attributed to a named `Deterministic` tier, with nothing having
|
||||||
|
/// evaluated them. The worst case was the Post obligation below: it states
|
||||||
|
/// the same property as the frame obligation, so a single receipt could carry
|
||||||
|
/// `Frame = Undischarged { "out-of-frame edit: deploy/secrets.env" }` and
|
||||||
|
/// `Post = Closed` for "every edit applied within the declared frame" — one
|
||||||
|
/// signature asserting both that an edit escaped the frame and that every
|
||||||
|
/// edit stayed inside it.
|
||||||
|
fn discharge_for(
|
||||||
|
signed: &SignedOp,
|
||||||
|
prior: &ContentId,
|
||||||
|
frame_result: &DischargeResult,
|
||||||
|
ob: &joulecontract_schema::Obligation,
|
||||||
|
) -> DischargeResult {
|
||||||
|
use joulecontract_schema::ObligationKind as Kind;
|
||||||
|
match (&ob.kind, ob.text.as_str()) {
|
||||||
|
// Decided by `frame_check`.
|
||||||
|
(Kind::Frame, _) => frame_result.clone(),
|
||||||
|
|
||||||
|
// The same property as the frame obligation, so it carries the same
|
||||||
|
// evidence rather than a second, contradictory verdict.
|
||||||
|
(Kind::Post, "every edit applied within the declared frame") => frame_result.clone(),
|
||||||
|
|
||||||
|
// The contract names the tree the op must build on. `SignedOp::verify`
|
||||||
|
// below binds the op to `prior`; this checks the *contract* was
|
||||||
|
// written for that same prior, so the two cannot disagree silently.
|
||||||
|
(Kind::Pre, text) if text.starts_with("op builds on prior tree ") => {
|
||||||
|
let expected = format!("op builds on prior tree {}", prior.0);
|
||||||
|
if text == expected {
|
||||||
|
DischargeResult::Closed
|
||||||
|
} else {
|
||||||
|
DischargeResult::Undischarged {
|
||||||
|
reason: format!("contract declares `{text}`, this receipt states prior {}", prior.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly what `SignedOp::verify` decides: it recomputes `op_id`,
|
||||||
|
// rebuilds `op_id ‖ prior`, checks the signature against `op.author`,
|
||||||
|
// and rejects replay onto a different base.
|
||||||
|
(Kind::Inv, "op signature verifies over op_id ‖ prior") => match signed.verify(prior) {
|
||||||
|
Ok(()) => DischargeResult::Closed,
|
||||||
|
Err(e) => DischargeResult::Undischarged {
|
||||||
|
reason: format!("signature does not verify over op_id ‖ prior: {e}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exactly what `CodingOp::validate` decides (SPEC §2: known alg,
|
||||||
|
// non-empty edits, ascending `(path, start)`, no overlap).
|
||||||
|
(Kind::Inv, "edits are ordered and pairwise disjoint") => match signed.op.validate() {
|
||||||
|
Ok(()) => DischargeResult::Closed,
|
||||||
|
Err(e) => DischargeResult::Undischarged { reason: e.to_string() },
|
||||||
|
},
|
||||||
|
|
||||||
|
// An obligation this binder does not recognise. A caller may declare
|
||||||
|
// any prose it likes; none of it is discharged by being written down.
|
||||||
|
(_, text) => DischargeResult::Undischarged {
|
||||||
|
reason: format!("not evaluated by this binder: {text}"),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check every edit path against the contract's frame. A path is in-frame
|
/// Check every edit path against the contract's frame. A path is in-frame
|
||||||
|
|
@ -147,13 +218,33 @@ impl Binder {
|
||||||
/// set, not analyzed from the artifact. An out-of-frame op still gets a
|
/// set, not analyzed from the artifact. An out-of-frame op still gets a
|
||||||
/// receipt: the frame serializes `undischarged` with the offending path,
|
/// receipt: the frame serializes `undischarged` with the offending path,
|
||||||
/// because a breach recorded honestly beats a breach driven underground.
|
/// because a breach recorded honestly beats a breach driven underground.
|
||||||
|
/// Seal a receipt for one signed op against a contract.
|
||||||
|
///
|
||||||
|
/// Takes the **signed** op and the `prior` the verifier believes current,
|
||||||
|
/// because two of the contract's obligations are statements about exactly
|
||||||
|
/// those: the signature over `op_id ‖ prior`, and the tree the op builds
|
||||||
|
/// on. With an unsigned `CodingOp` and a caller-supplied `op_id` neither
|
||||||
|
/// could be decided — and both were nonetheless sealed as discharged.
|
||||||
|
///
|
||||||
|
/// The op id is **derived** from the op rather than supplied, so
|
||||||
|
/// `transition` always names the operation whose obligations were checked;
|
||||||
|
/// a caller-supplied id was trusted and never re-derived, so a receipt
|
||||||
|
/// could carry one op's frame verdict under another op's id.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// [`BindingError::OpId`] if the op's id cannot be derived (it is
|
||||||
|
/// structurally invalid), or [`BindingError::Address`] if the contract
|
||||||
|
/// cannot be addressed. A failing *obligation* is not an error: it is
|
||||||
|
/// recorded as `Undischarged` on the receipt, which is the point.
|
||||||
pub fn receipt_for_op(
|
pub fn receipt_for_op(
|
||||||
&self,
|
&self,
|
||||||
contract: &Contract,
|
contract: &Contract,
|
||||||
op: &CodingOp,
|
signed: &SignedOp,
|
||||||
op_id: &ContentId,
|
prior: &ContentId,
|
||||||
energy: &Energy,
|
energy: &Energy,
|
||||||
) -> Result<Receipt, BindingError> {
|
) -> Result<Receipt, BindingError> {
|
||||||
|
let op: &CodingOp = &signed.op;
|
||||||
|
let op_id = op.op_id().map_err(|e| BindingError::OpId(e.to_string()))?;
|
||||||
let frame_result = match frame_check(contract, op) {
|
let frame_result = match frame_check(contract, op) {
|
||||||
Ok(()) => DischargeResult::Closed,
|
Ok(()) => DischargeResult::Closed,
|
||||||
Err(BindingError::OutOfFrame { path }) => DischargeResult::Undischarged {
|
Err(BindingError::OutOfFrame { path }) => DischargeResult::Undischarged {
|
||||||
|
|
@ -172,16 +263,13 @@ impl Binder {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ob| {
|
.map(|ob| {
|
||||||
let is_frame = ob.kind == joulecontract_schema::ObligationKind::Frame;
|
let is_frame = ob.kind == joulecontract_schema::ObligationKind::Frame;
|
||||||
|
let result = discharge_for(signed, prior, &frame_result, &ob);
|
||||||
DischargeEntry {
|
DischargeEntry {
|
||||||
obligation: ob,
|
obligation: ob,
|
||||||
tier: binding_tier.clone(),
|
tier: binding_tier.clone(),
|
||||||
tier_class: TierClass::Deterministic,
|
tier_class: TierClass::Deterministic,
|
||||||
tier_operator_keys: self.tier_operator_keys.clone(),
|
tier_operator_keys: self.tier_operator_keys.clone(),
|
||||||
result: if is_frame {
|
result,
|
||||||
frame_result.clone()
|
|
||||||
} else {
|
|
||||||
DischargeResult::Closed
|
|
||||||
},
|
|
||||||
mode: is_frame.then_some(FrameMode::Observed),
|
mode: is_frame.then_some(FrameMode::Observed),
|
||||||
qualification: Some(self.qualification.clone()),
|
qualification: Some(self.qualification.clone()),
|
||||||
verifier_keys: self.verifier_keys.clone(),
|
verifier_keys: self.verifier_keys.clone(),
|
||||||
|
|
@ -197,7 +285,7 @@ impl Binder {
|
||||||
contract: contract
|
contract: contract
|
||||||
.content_address()
|
.content_address()
|
||||||
.map_err(|e| BindingError::Address(e.to_string()))?,
|
.map_err(|e| BindingError::Address(e.to_string()))?,
|
||||||
transition: bridge_id(op_id),
|
transition: bridge_id(&op_id),
|
||||||
generator: self.generator.clone(),
|
generator: self.generator.clone(),
|
||||||
discharge,
|
discharge,
|
||||||
execution: vec![ContentAddress::of_bytes(
|
execution: vec![ContentAddress::of_bytes(
|
||||||
|
|
@ -240,24 +328,44 @@ mod tests {
|
||||||
ContentAddress::of_bytes(label.as_bytes())
|
ContentAddress::of_bytes(label.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn op(paths: &[&str]) -> (CodingOp, ContentId) {
|
fn author_key() -> ed25519_dalek::SigningKey {
|
||||||
|
ed25519_dalek::SigningKey::from_bytes(&[42u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tree `contract()` declares its op must build on.
|
||||||
|
fn prior() -> ContentId {
|
||||||
|
ContentId::of_bytes(b"prior tree")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A real signed op. `author` is the signing key's own verifying key, and
|
||||||
|
/// the edits are sorted into canonical `(path, start)` order, because
|
||||||
|
/// `op_id` runs `validate` and `sign` runs `op_id` — an op that is not
|
||||||
|
/// canonical has no id and therefore no signature.
|
||||||
|
///
|
||||||
|
/// This replaces a fixture that set `author: [7u8; 32]` (not a key) and
|
||||||
|
/// returned `ContentId::of_bytes(b"the op canonical bytes")` as the id —
|
||||||
|
/// a constant unrelated to the op, which is what let the untrusted
|
||||||
|
/// `op_id` parameter go unnoticed.
|
||||||
|
fn op(paths: &[&str]) -> SignedOp {
|
||||||
|
let key = author_key();
|
||||||
|
let mut sorted: Vec<&&str> = paths.iter().collect();
|
||||||
|
sorted.sort();
|
||||||
let op = CodingOp {
|
let op = CodingOp {
|
||||||
alg: 0x01,
|
alg: 0x01,
|
||||||
prior: ContentId::of_bytes(b"prior tree"),
|
prior: prior(),
|
||||||
edits: paths
|
edits: sorted
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(|p| Edit {
|
.map(|p| Edit {
|
||||||
path: p.to_string(),
|
path: (*p).to_string(),
|
||||||
anchor: Anchor::ByteRange { start: 0, end: 4 },
|
anchor: Anchor::ByteRange { start: 0, end: 4 },
|
||||||
replacement: b"new!".to_vec(),
|
replacement: b"new!".to_vec(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
author: [7u8; 32],
|
author: key.verifying_key().to_bytes(),
|
||||||
intent: "rename the settlement index field".into(),
|
intent: "rename the settlement index field".into(),
|
||||||
tier: Tier::L1,
|
tier: Tier::L1,
|
||||||
};
|
};
|
||||||
let id = ContentId::of_bytes(b"the op canonical bytes");
|
op.sign(&key).expect("a canonical op signs")
|
||||||
(op, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn binder() -> Binder {
|
fn binder() -> Binder {
|
||||||
|
|
@ -323,13 +431,13 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn in_frame_op_discharges_and_validates_to_jr2() {
|
fn in_frame_op_discharges_and_validates_to_jr2() {
|
||||||
let c = contract(&["src/", "Cargo.toml"]);
|
let c = contract(&["src/", "Cargo.toml"]);
|
||||||
let (op, id) = op(&["src/lib.rs", "Cargo.toml"]);
|
let signed = op(&["src/lib.rs", "Cargo.toml"]);
|
||||||
let energy = Energy {
|
let energy = Energy {
|
||||||
measured_uj: Some(1_200),
|
measured_uj: Some(1_200),
|
||||||
estimated_uj: 1_000,
|
estimated_uj: 1_000,
|
||||||
provenance: Provenance::HwShunt,
|
provenance: Provenance::HwShunt,
|
||||||
};
|
};
|
||||||
let r = binder().receipt_for_op(&c, &op, &id, &energy).unwrap();
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
let v = validate(&c, &r).unwrap();
|
let v = validate(&c, &r).unwrap();
|
||||||
assert_eq!(v.max_profile.rigor, Rigor::Jr2);
|
assert_eq!(v.max_profile.rigor, Rigor::Jr2);
|
||||||
assert_eq!(v.max_profile.energy, EnergyClass::Metered);
|
assert_eq!(v.max_profile.energy, EnergyClass::Metered);
|
||||||
|
|
@ -341,9 +449,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn out_of_frame_edit_is_caught_at_the_boundary() {
|
fn out_of_frame_edit_is_caught_at_the_boundary() {
|
||||||
let c = contract(&["src/"]);
|
let c = contract(&["src/"]);
|
||||||
let (op, id) = op(&["src/lib.rs", "deploy/secrets.env"]);
|
let signed = op(&["src/lib.rs", "deploy/secrets.env"]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
frame_check(&c, &op),
|
frame_check(&c, &signed.op),
|
||||||
Err(BindingError::OutOfFrame {
|
Err(BindingError::OutOfFrame {
|
||||||
path: "deploy/secrets.env".into()
|
path: "deploy/secrets.env".into()
|
||||||
})
|
})
|
||||||
|
|
@ -355,7 +463,7 @@ mod tests {
|
||||||
estimated_uj: 900,
|
estimated_uj: 900,
|
||||||
provenance: Provenance::ModelBased,
|
provenance: Provenance::ModelBased,
|
||||||
};
|
};
|
||||||
let r = binder().receipt_for_op(&c, &op, &id, &energy).unwrap();
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
let v = validate(&c, &r).unwrap();
|
let v = validate(&c, &r).unwrap();
|
||||||
assert!(v.demotions.iter().any(|d| matches!(
|
assert!(v.demotions.iter().any(|d| matches!(
|
||||||
d,
|
d,
|
||||||
|
|
@ -364,12 +472,153 @@ mod tests {
|
||||||
assert_eq!(v.max_profile.rigor, Rigor::Jr1, "undischarged frame caps rigor");
|
assert_eq!(v.max_profile.rigor, Rigor::Jr1, "undischarged frame caps rigor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The contradiction this fix removes. The Post obligation states the same
|
||||||
|
/// property as the Frame obligation, so one receipt cannot carry
|
||||||
|
/// "an edit escaped the frame" and "every edit stayed inside it" at once.
|
||||||
|
#[test]
|
||||||
|
fn the_post_obligation_carries_the_frame_verdict() {
|
||||||
|
let c = contract(&["src/"]);
|
||||||
|
let signed = op(&["src/lib.rs", "deploy/secrets.env"]);
|
||||||
|
let energy = Energy {
|
||||||
|
measured_uj: Some(900),
|
||||||
|
estimated_uj: 900,
|
||||||
|
provenance: Provenance::HwShunt,
|
||||||
|
};
|
||||||
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
|
|
||||||
|
let post = r
|
||||||
|
.discharge
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.obligation.text == "every edit applied within the declared frame")
|
||||||
|
.expect("the contract declares it");
|
||||||
|
let frame = r
|
||||||
|
.discharge
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.obligation.kind == joulecontract_schema::ObligationKind::Frame)
|
||||||
|
.expect("the contract declares it");
|
||||||
|
|
||||||
|
assert_eq!(post.result, frame.result, "same property, same verdict");
|
||||||
|
assert!(
|
||||||
|
matches!(&post.result, DischargeResult::Undischarged { reason } if reason.contains("deploy/secrets.env")),
|
||||||
|
"got {:?}",
|
||||||
|
post.result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The signature obligation is decided, not assumed. A tampered signature
|
||||||
|
/// leaves it undischarged instead of sealing "verifies" over bytes that
|
||||||
|
/// do not.
|
||||||
|
#[test]
|
||||||
|
fn the_signature_obligation_is_decided_not_assumed() {
|
||||||
|
let c = contract(&["src/"]);
|
||||||
|
let mut signed = op(&["src/lib.rs"]);
|
||||||
|
let energy = Energy {
|
||||||
|
measured_uj: Some(900),
|
||||||
|
estimated_uj: 900,
|
||||||
|
provenance: Provenance::HwShunt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intact: the obligation closes on a real check.
|
||||||
|
let good = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
|
let sig_of = |r: &Receipt| {
|
||||||
|
r.discharge
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.obligation.text == "op signature verifies over op_id \u{2016} prior")
|
||||||
|
.map(|d| d.result.clone())
|
||||||
|
.expect("the contract declares it")
|
||||||
|
};
|
||||||
|
assert_eq!(sig_of(&good), DischargeResult::Closed);
|
||||||
|
|
||||||
|
// Tampered: undischarged, with the reason on the record.
|
||||||
|
*signed.sig.last_mut().expect("signature bytes") ^= 1;
|
||||||
|
let bad = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(sig_of(&bad), DischargeResult::Undischarged { .. }),
|
||||||
|
"got {:?}",
|
||||||
|
sig_of(&bad)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A caller may declare any prose it likes; none of it is discharged by
|
||||||
|
/// being written down. The fall-through used to be `Closed`.
|
||||||
|
#[test]
|
||||||
|
fn an_unrecognised_obligation_is_never_closed() {
|
||||||
|
let mut c = contract(&["src/"]);
|
||||||
|
c.post.push("the author was well rested".into());
|
||||||
|
let signed = op(&["src/lib.rs"]);
|
||||||
|
let energy = Energy {
|
||||||
|
measured_uj: Some(900),
|
||||||
|
estimated_uj: 900,
|
||||||
|
provenance: Provenance::HwShunt,
|
||||||
|
};
|
||||||
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
|
|
||||||
|
let entry = r
|
||||||
|
.discharge
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.obligation.text == "the author was well rested")
|
||||||
|
.expect("declared");
|
||||||
|
assert!(
|
||||||
|
matches!(&entry.result, DischargeResult::Undischarged { reason } if reason.contains("not evaluated")),
|
||||||
|
"got {:?}",
|
||||||
|
entry.result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The receipt names the operation whose obligations it checked: the id is
|
||||||
|
/// derived from the op, not supplied beside it.
|
||||||
|
#[test]
|
||||||
|
fn the_transition_names_the_op_it_checked() {
|
||||||
|
let c = contract(&["src/"]);
|
||||||
|
let signed = op(&["src/lib.rs"]);
|
||||||
|
let energy = Energy {
|
||||||
|
measured_uj: Some(900),
|
||||||
|
estimated_uj: 900,
|
||||||
|
provenance: Provenance::HwShunt,
|
||||||
|
};
|
||||||
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
|
assert_eq!(r.transition, bridge_id(&signed.op.op_id().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A contract written for a different tree does not have its precondition
|
||||||
|
/// discharged by a receipt that states another prior.
|
||||||
|
#[test]
|
||||||
|
fn a_contract_for_a_different_prior_leaves_the_precondition_open() {
|
||||||
|
let c = contract_for_op(
|
||||||
|
"rename the settlement index field",
|
||||||
|
&ContentId::of_bytes(b"some other tree"),
|
||||||
|
&["src/".to_string()],
|
||||||
|
0.01,
|
||||||
|
key("enb"),
|
||||||
|
None,
|
||||||
|
Rigor::Jr2,
|
||||||
|
);
|
||||||
|
let signed = op(&["src/lib.rs"]);
|
||||||
|
let energy = Energy {
|
||||||
|
measured_uj: Some(900),
|
||||||
|
estimated_uj: 900,
|
||||||
|
provenance: Provenance::HwShunt,
|
||||||
|
};
|
||||||
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
|
|
||||||
|
let pre = r
|
||||||
|
.discharge
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.obligation.kind == joulecontract_schema::ObligationKind::Pre)
|
||||||
|
.expect("declared");
|
||||||
|
assert!(
|
||||||
|
matches!(&pre.result, DischargeResult::Undischarged { .. }),
|
||||||
|
"got {:?}",
|
||||||
|
pre.result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn estimator_energy_is_unaccounted_never_promoted() {
|
fn estimator_energy_is_unaccounted_never_promoted() {
|
||||||
let c = contract(&["src/"]);
|
let c = contract(&["src/"]);
|
||||||
let (op, id) = op(&["src/lib.rs"]);
|
let signed = op(&["src/lib.rs"]);
|
||||||
let energy = Energy::from_tokens(100, 10);
|
let energy = Energy::from_tokens(100, 10);
|
||||||
let r = binder().receipt_for_op(&c, &op, &id, &energy).unwrap();
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
assert_eq!(r.provenance, EnergyClass::Unaccounted);
|
assert_eq!(r.provenance, EnergyClass::Unaccounted);
|
||||||
assert!(r.meter.is_none(), "an estimator figure never carries a meter binding");
|
assert!(r.meter.is_none(), "an estimator figure never carries a meter binding");
|
||||||
let v = validate(&c, &r).unwrap();
|
let v = validate(&c, &r).unwrap();
|
||||||
|
|
@ -381,13 +630,13 @@ mod tests {
|
||||||
// Receipt-time enforcement (JouleContract §5.3): the deviation is on
|
// Receipt-time enforcement (JouleContract §5.3): the deviation is on
|
||||||
// the record as a negative residual; amend-or-fix is the resolution.
|
// the record as a negative residual; amend-or-fix is the resolution.
|
||||||
let c = contract(&["src/"]);
|
let c = contract(&["src/"]);
|
||||||
let (op, id) = op(&["src/lib.rs"]);
|
let signed = op(&["src/lib.rs"]);
|
||||||
let energy = Energy {
|
let energy = Energy {
|
||||||
measured_uj: Some(50_000), // 0.05 J against a 0.01 J ceiling
|
measured_uj: Some(50_000), // 0.05 J against a 0.01 J ceiling
|
||||||
estimated_uj: 50_000,
|
estimated_uj: 50_000,
|
||||||
provenance: Provenance::HwShunt,
|
provenance: Provenance::HwShunt,
|
||||||
};
|
};
|
||||||
let r = binder().receipt_for_op(&c, &op, &id, &energy).unwrap();
|
let r = binder().receipt_for_op(&c, &signed, &prior(), &energy).unwrap();
|
||||||
assert!(r.residual() < 0.0);
|
assert!(r.residual() < 0.0);
|
||||||
assert!(validate(&c, &r).is_ok(), "the system keeps running; the record decides");
|
assert!(validate(&c, &r).is_ok(), "the system keeps running; the record decides");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue