Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ jobs:
cargo clippy -p warp-core --features host_test --test causal_wal_hardening_tests -- -D warnings -D missing_docs

- name: cargo clippy (compiler-produced pure evaluation)
run: cargo clippy -p warp-core --features trusted_runtime --lib --test edict_pure_evaluation_tests --test edict_pure_unsigned_subtraction_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests -- -D warnings -D missing_docs
run: cargo clippy -p warp-core --features trusted_runtime --lib --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests -- -D warnings -D missing_docs

clippy-det-fixed:
name: Clippy (det_fixed)
Expand Down Expand Up @@ -382,7 +382,7 @@ jobs:
cargo test -p warp-core --features host_test --test causal_wal_hardening_tests
- name: cargo test (compiler-produced pure evaluation)
run: |
cargo test -p warp-core --features trusted_runtime --test edict_pure_evaluation_tests --test edict_pure_unsigned_subtraction_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests
cargo test -p warp-core --features trusted_runtime --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests
cargo test -p warp-core --features trusted_runtime --lib edict_pure::
- name: cargo test (delta validation and the canonical delta merge)
run: |
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

## Unreleased

### Fixed

- Pure evaluation accepts compiler-produced conjunctive input guards instead of
rejecting their `all` predicate shape as an invalid artifact. Conjunctions
retain deterministic short-circuit metering and combined traversal limits.

### Removed

- The `echo-trace` and `echo-graph` crates. Nothing in the workspace used
Expand Down
4 changes: 4 additions & 0 deletions crates/warp-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ required-features = ["native_rule_bootstrap", "trusted_runtime"]
name = "edict_pure_evaluation_tests"
required-features = ["trusted_runtime"]

[[test]]
name = "edict_pure_predicates_tests"
required-features = ["trusted_runtime"]

[[test]]
name = "edict_byte_literal_tests"
required-features = ["trusted_runtime"]
Expand Down
3 changes: 3 additions & 0 deletions crates/warp-core/src/edict_pure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ pub fn evaluate(
let input = decode_canonical_cbor_v1(input_bytes).map_err(|_| EvaluationError::InvalidInput)?;
evaluate::run(&program, input)
}

#[cfg(test)]
mod predicate_tests;
17 changes: 14 additions & 3 deletions crates/warp-core/src/edict_pure/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,20 @@ pub(crate) fn predicate(
depth: usize,
) -> Result<bool, Error> {
meter.step(depth)?;
let left = expression(&predicate.left, locals, helpers, meter, depth + 1)?;
let right = expression(&predicate.right, locals, helpers, meter, depth + 1)?;
match (&predicate.op, left, right) {
let (op, left, right) = match predicate {
Predicate::Compare { op, left, right } => (op, left, right),
Predicate::All(values) => {
for value in values {
if !self::predicate(value, locals, helpers, meter, depth + 1)? {
return Ok(false);
}
}
return Ok(true);
}
};
let left = expression(left, locals, helpers, meter, depth + 1)?;
let right = expression(right, locals, helpers, meter, depth + 1)?;
match (op, left, right) {
(Comparison::Equal, Value::Integer(left), Value::Integer(right)) => Ok(left == right),
(Comparison::LessOrEqual, Value::Integer(left), Value::Integer(right)) => Ok(left <= right),
(Comparison::Equal, Value::Bytes(left), Value::Bytes(right)) => {
Expand Down
18 changes: 14 additions & 4 deletions crates/warp-core/src/edict_pure/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,10 +601,20 @@ impl Heights<'_> {
}
fn predicate(&mut self, predicate: &Predicate, depth: usize) -> Result<usize, Error> {
self.enter(depth)?;
let height = self
.expr(&predicate.left, depth + 1)?
.max(self.expr(&predicate.right, depth + 1)?)
+ 1;
let height = match predicate {
Predicate::Compare { left, right, .. } => {
self.expr(left, depth + 1)?
.max(self.expr(right, depth + 1)?)
+ 1
}
Predicate::All(values) => {
let mut maximum = 0;
for value in values {
maximum = maximum.max(self.predicate(value, depth + 1)?);
}
maximum + 1
}
};
if depth + height > MAX_DEPTH {
return Err(Error::UnsupportedProgram);
}
Expand Down
11 changes: 7 additions & 4 deletions crates/warp-core/src/edict_pure/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ pub(crate) enum Expr {
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Predicate {
pub op: Comparison,
pub left: Expr,
pub right: Expr,
pub(crate) enum Predicate {
Compare {
op: Comparison,
left: Expr,
right: Expr,
},
All(Vec<Predicate>),
}

#[derive(Debug, PartialEq, Eq)]
Expand Down
158 changes: 158 additions & 0 deletions crates/warp-core/src/edict_pure/predicate_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: Apache-2.0
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
//! Closed predicate decoding, metering and combined traversal boundaries.
use super::{
evaluate::{predicate, Meter},
functions::check_roots,
model::{Comparison, Expr, Predicate, MAX_PROGRAM_NODES},
syntax::Parser,
EvaluationError as Error, EvaluationLimits,
};
use echo_edict_canonical::CanonicalValueV1 as Value;
use std::collections::BTreeMap;
fn limits(steps: u64) -> EvaluationLimits {
EvaluationLimits {
max_package_bytes: 32768,
max_input_bytes: 4194304,
max_steps: steps,
max_allocated_bytes: 16777216,
max_output_bytes: 4194304,
}
}
fn compare(left: i128, right: i128) -> Predicate {
Predicate::Compare {
op: Comparison::Equal,
left: Expr::Constant(Value::Integer(left)),
right: Expr::Constant(Value::Integer(right)),
}
}
fn map(fields: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
Value::Map(
fields
.into_iter()
.map(|(key, value)| (Value::Text(key.into()), value))
.collect(),
)
}
fn all(values: Vec<Value>) -> Value {
map([
("kind", Value::Text("all".into())),
("values", Value::Array(values)),
])
}
fn wire_compare(left: i128, right: i128) -> Value {
let constant = |value| {
map([
("kind", Value::Text("const".into())),
(
"value",
map([
("kind", Value::Text("int".into())),
("width", Value::Text("U64".into())),
("value", Value::Integer(value)),
]),
),
])
};
map([
("kind", Value::Text("compare".into())),
("op", Value::Text("==".into())),
("left", constant(left)),
("right", constant(right)),
])
}
fn parse(value: &Value, remaining: usize) -> Result<Predicate, Error> {
Parser {
types: &Value::Map(vec![]),
coordinate: "generic.predicates@1",
remaining,
}
.predicate(value, 0)
}
#[test]
fn conjunction_truth_and_exact_short_circuit_metering() -> Result<(), Error> {
// Compare charges one predicate step plus expression/copy steps for each constant.
for (values, expected, steps) in [
(vec![compare(1, 1)], true, 6),
(vec![compare(1, 1), compare(2, 2)], true, 11),
(vec![compare(1, 2), compare(2, 2)], false, 6),
(vec![compare(1, 1), compare(2, 3)], false, 11),
(
vec![Predicate::All(vec![compare(1, 1)]), compare(1, 1)],
true,
12,
),
] {
let value = Predicate::All(values);
let mut meter = Meter::new(limits(steps));
assert_eq!(
predicate(&value, &BTreeMap::new(), &BTreeMap::new(), &mut meter, 0)?,
expected
);
assert_eq!(meter.usage().0, steps);
let mut too_small = Meter::new(limits(steps - 1));
assert_eq!(
predicate(
&value,
&BTreeMap::new(),
&BTreeMap::new(),
&mut too_small,
0
),
Err(Error::StepBudgetExceeded)
);
}
Ok(())
}
#[test]
fn conjunction_decoder_checks_closed_shape_and_all_children() {
let empty = all(vec![]);
assert_eq!(
parse(&empty, MAX_PROGRAM_NODES),
Err(Error::InvalidArtifact)
);
for value in [
map([("kind", Value::Text("all".into()))]),
map([
("kind", Value::Text("all".into())),
("values", Value::Integer(0)),
]),
map([
("kind", Value::Text("all".into())),
("values", Value::Array(vec![])),
("extra", Value::Integer(0)),
]),
] {
assert_eq!(
parse(&value, MAX_PROGRAM_NODES),
Err(Error::InvalidArtifact)
);
}
let unknown = map([("kind", Value::Text("unsupported".into()))]);
assert_eq!(
parse(&all(vec![wire_compare(1, 2), unknown]), MAX_PROGRAM_NODES),
Err(Error::InvalidArtifact)
);
}
#[test]
fn conjunction_decoder_and_combined_depth_refuse_exact_boundary_overruns() {
assert_eq!(
parse(&all(vec![wire_compare(1, 1)]), 1),
Err(Error::UnsupportedProgram)
);
for (wrappers, accepted) in [(63, true), (64, false), (65, false)] {
let mut value = wire_compare(1, 1);
for _ in 0..wrappers {
value = all(vec![value]);
}
let result = parse(&value, MAX_PROGRAM_NODES).and_then(|value| {
check_roots(&BTreeMap::new(), [], [&value])?;
Ok(value)
});
if accepted {
assert!(result.is_ok());
} else {
assert_eq!(result, Err(Error::UnsupportedProgram));
}
}
}
18 changes: 17 additions & 1 deletion crates/warp-core/src/edict_pure/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,22 @@ impl Parser<'_> {

pub fn predicate(&mut self, value: &Value, depth: usize) -> Result<Predicate, Error> {
self.enter(depth)?;
if text_field(value, "kind")? == "all" {
exact_fields(value, &["kind", "values"])?;
let values = array(field(value, "values")?)?;
if values.is_empty() {
return Err(Error::InvalidArtifact);
}
if values.len() > self.remaining {
return Err(Error::UnsupportedProgram);
}
return Ok(Predicate::All(
values
.iter()
.map(|value| self.predicate(value, depth + 1))
.collect::<Result<Vec<_>, _>>()?,
));
}
exact_fields(value, &["kind", "op", "left", "right"])?;
if text_field(value, "kind")? != "compare" {
return Err(Error::UnsupportedProgram);
Expand All @@ -207,7 +223,7 @@ impl Parser<'_> {
"<=" => Comparison::LessOrEqual,
_ => return Err(Error::UnsupportedProgram),
};
Ok(Predicate {
Ok(Predicate::Compare {
op,
left: self.expr(field(value, "left")?, depth + 1)?,
right: self.expr(field(value, "right")?, depth + 1)?,
Expand Down
85 changes: 85 additions & 0 deletions crates/warp-core/tests/edict_pure_predicates_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
//! A real public compiler/verifier package with nested conjunctive guards.
#![cfg(feature = "trusted_runtime")]
#![allow(clippy::unwrap_used, clippy::panic)]
use echo_edict_canonical::{
decode_canonical_cbor_v1 as decode, digest_canonical_value_bytes_v1 as digest,
encode_canonical_cbor_v1 as encode, CanonicalValueV1 as Value,
};
use warp_core::edict_pure::{evaluate, EvaluationError, EvaluationLimits};
const PACKAGE: &str =
include_str!("fixtures/edict-pure-predicates/executable-operation-package.cbor.hex");
const REPORT: &str = include_str!("fixtures/edict-pure-predicates/verification-report.cbor.hex");
const PIN: &str = "8dfbc0e76c2bce33b604832a6b8f4d94d6791c2863e4b1235c24d836baba72d0";
fn field<'a>(value: &'a Value, name: &str) -> &'a Value {
let Value::Map(fields) = value else {
panic!("record")
};
&fields
.iter()
.find(|(key, _)| *key == Value::Text(name.into()))
.unwrap()
.1
}
fn input(start: u64, end: u64) -> Vec<u8> {
encode(&Value::Map(
[
("bufferId", Value::Bytes(vec![1; 32])),
("basisHeadId", Value::Bytes(vec![2; 32])),
("text", Value::Bytes(b"0123456789abcdef".to_vec())),
("replacement", Value::Bytes("λ\r\n".as_bytes().to_vec())),
("startByte", Value::Integer(start.into())),
("endByte", Value::Integer(end.into())),
]
.into_iter()
.map(|(key, value)| (Value::Text(key.into()), value))
.collect(),
))
.unwrap()
}
fn limits() -> EvaluationLimits {
EvaluationLimits {
max_package_bytes: 32768,
max_input_bytes: 4194304,
max_steps: 10000,
max_allocated_bytes: 16777216,
max_output_bytes: 4194304,
}
}
#[test]
fn public_compiler_conjunction_executes_and_rejects_each_failed_range_guard() {
let bytes = hex::decode(PACKAGE.trim()).unwrap();
let pin: [u8; 32] = hex::decode(PIN).unwrap().try_into().unwrap();
assert_eq!(
digest("echo.operation-package/v1", &decode(&bytes).unwrap()).unwrap(),
pin
);
let report = decode(&hex::decode(REPORT.trim()).unwrap()).unwrap();
assert_eq!(field(&report, "outcome"), &Value::Text("accepted".into()));
assert_eq!(
field(field(&report, "package"), "digest"),
&Value::Array(vec![
Value::Text("sha256".into()),
Value::Bytes(pin.to_vec())
])
);
for (start, end, expected) in [
(7, 7, "0123456λ\r\n789abcdef"),
(7, 11, "0123456λ\r\nbcdef"),
] {
let supplied = input(start, end);
let result = evaluate(&bytes, pin, &supplied, limits()).unwrap();
assert_eq!(
field(&decode(&result.output).unwrap(), "updatedText"),
&Value::Bytes(expected.as_bytes().to_vec())
);
assert_eq!(result, evaluate(&bytes, pin, &supplied, limits()).unwrap());
}
for (start, end) in [(11, 7), (7, 17), (17, 17), (0, u64::MAX)] {
assert_eq!(
evaluate(&bytes, pin, &input(start, end), limits()),
Err(EvaluationError::InputConstraintFailed("where.0".into()))
);
}
}
Loading
Loading