use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const IMPLYING_ROLES: [&str; 2] = ["depends_on", "refines"];
pub const CONFLICT_ROLE: &str = "contradicts";
pub const RETIRING_ROLE: &str = "supersedes";
pub const FIRED: &str = "fired";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct TypedEdge {
pub from: String,
pub role: String,
pub to: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClauseSet {
pub units: BTreeSet<String>,
pub implies: BTreeSet<(String, String)>,
pub conflicts: BTreeSet<(String, String)>,
}
impl ClauseSet {
#[must_use]
pub fn from_graph(ids: &BTreeSet<String>, edges: &BTreeSet<TypedEdge>) -> Self {
let retired: BTreeSet<&str> = edges
.iter()
.filter(|e| e.role == RETIRING_ROLE)
.map(|e| e.to.as_str())
.collect();
let mut cs = Self {
units: ids
.iter()
.filter(|id| !retired.contains(id.as_str()))
.cloned()
.collect(),
..Self::default()
};
for e in edges {
if IMPLYING_ROLES.contains(&e.role.as_str()) {
cs.implies.insert((e.from.clone(), e.to.clone()));
} else if e.role == CONFLICT_ROLE {
cs.conflicts.insert(ordered(&e.from, &e.to));
}
}
cs
}
#[must_use]
pub fn checkable_n(&self) -> usize {
self.implies.len() + self.conflicts.len()
}
}
#[must_use]
pub fn unencoded_edges(edges: &BTreeSet<TypedEdge>) -> usize {
edges
.iter()
.filter(|e| {
!IMPLYING_ROLES.contains(&e.role.as_str())
&& e.role != CONFLICT_ROLE
&& e.role != RETIRING_ROLE
})
.count()
}
fn ordered(a: &str, b: &str) -> (String, String) {
if a <= b {
(a.to_string(), b.to_string())
} else {
(b.to_string(), a.to_string())
}
}
fn sha256_lines<I: IntoIterator<Item = String>>(lines: I) -> String {
let mut h = Sha256::new();
for l in lines {
h.update(l.as_bytes());
h.update(b"\n");
}
format!("{:x}", h.finalize())
}
#[must_use]
pub fn census_id_set_sha256(ids: &BTreeSet<String>) -> String {
sha256_lines(ids.iter().cloned())
}
#[must_use]
pub fn relations_sha256(edges: &BTreeSet<TypedEdge>) -> String {
sha256_lines(
edges
.iter()
.map(|e| format!("{}\t{}\t{}", e.from, e.role, e.to)),
)
}
#[must_use]
pub fn witness_path(contract_dir: &Path, relations_sha: &str) -> PathBuf {
contract_dir
.join("witness")
.join(format!("{relations_sha}.json"))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Witness {
pub census_id_set_sha256: String,
pub relations_sha256: String,
pub reasoner_git_sha: Option<String>,
pub checkable_n: usize,
pub result: WitnessResult,
pub pc_reasoner: String,
pub cpu_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "payload", rename_all = "snake_case")]
pub enum WitnessResult<I = String> {
UnsatCore(Core<I>),
Model(Model<I>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Core<I = String> {
pub steps: Vec<Step<I>>,
pub conflict: (I, I),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Step<I = String> {
Unit(I),
Implies(I, I),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Model<I = String> {
#[serde(rename = "false")]
pub false_vars: Vec<I>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Checked<S = BTreeSet<String>> {
Sat,
Unsat { core: S },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckError<I = String> {
EmptyCore,
NotAUnit(I),
NoSuchImplication(I, I),
PremiseNotDerived { premise: I, step: usize },
NoSuchConflict(I, I),
ConflictSideNotDerived(I),
ModelFalsifiesUnit(I),
ModelFalsifiesImplication(I, I),
ModelFalsifiesConflict(I, I),
}
impl<I: fmt::Display> fmt::Display for CheckError<I> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyCore => write!(f, "the core derives nothing"),
Self::NotAUnit(v) => write!(
f,
"step asserts `{v}`, which is not a unit of the current graph"
),
Self::NoSuchImplication(a, b) => {
write!(
f,
"step uses `{a} ⇒ {b}`, which is not a clause of the current graph"
)
}
Self::PremiseNotDerived { premise, step } => {
write!(
f,
"step {step} uses `{premise}` before any earlier step derived it"
)
}
Self::NoSuchConflict(a, b) => {
write!(
f,
"the conflict `{a} ⊥ {b}` is not a clause of the current graph"
)
}
Self::ConflictSideNotDerived(v) => {
write!(f, "the conflict names `{v}`, which the core never derived")
}
Self::ModelFalsifiesUnit(v) => write!(f, "the model makes the unit `{v}` false"),
Self::ModelFalsifiesImplication(a, b) => {
write!(
f,
"the model makes `{a}` true and `{b}` false, against `{a} ⇒ {b}`"
)
}
Self::ModelFalsifiesConflict(a, b) => {
write!(
f,
"the model makes both `{a}` and `{b}` true, against `{a} ⊥ {b}`"
)
}
}
}
}
impl<I: fmt::Debug + fmt::Display> std::error::Error for CheckError<I> {}
pub trait IdSet<I>: Default {
fn has(&self, x: &I) -> bool;
fn put(&mut self, x: I);
}
impl<I: Ord> IdSet<I> for BTreeSet<I> {
fn has(&self, x: &I) -> bool {
self.contains(x)
}
fn put(&mut self, x: I) {
self.insert(x);
}
}
pub trait ClauseView<I> {
fn has_unit(&self, v: &I) -> bool;
fn has_implies(&self, a: &I, b: &I) -> bool;
fn has_conflict(&self, a: &I, b: &I) -> bool;
fn units<'s>(&'s self) -> impl Iterator<Item = &'s I>
where
I: 's;
fn implications<'s>(&'s self) -> impl Iterator<Item = (&'s I, &'s I)>
where
I: 's;
fn conflict_pairs<'s>(&'s self) -> impl Iterator<Item = (&'s I, &'s I)>
where
I: 's;
}
impl ClauseView<String> for ClauseSet {
fn has_unit(&self, v: &String) -> bool {
self.units.contains(v)
}
fn has_implies(&self, a: &String, b: &String) -> bool {
self.implies.contains(&(a.clone(), b.clone()))
}
fn has_conflict(&self, a: &String, b: &String) -> bool {
self.conflicts.contains(&ordered(a, b))
}
fn units<'s>(&'s self) -> impl Iterator<Item = &'s String>
where
String: 's,
{
self.units.iter()
}
fn implications<'s>(&'s self) -> impl Iterator<Item = (&'s String, &'s String)>
where
String: 's,
{
self.implies.iter().map(|(a, b)| (a, b))
}
fn conflict_pairs<'s>(&'s self) -> impl Iterator<Item = (&'s String, &'s String)>
where
String: 's,
{
self.conflicts.iter().map(|(a, b)| (a, b))
}
}
pub fn check(cs: &ClauseSet, result: &WitnessResult) -> Result<Checked, CheckError> {
check_with(cs, result)
}
pub fn check_with<I, S, C>(cs: &C, result: &WitnessResult<I>) -> Result<Checked<S>, CheckError<I>>
where
I: Ord + Clone,
S: IdSet<I>,
C: ClauseView<I>,
{
match result {
WitnessResult::UnsatCore(core) => check_core(cs, core),
WitnessResult::Model(model) => check_model::<I, S, C>(cs, model).map(|()| Checked::Sat),
}
}
fn check_core<I, S, C>(cs: &C, core: &Core<I>) -> Result<Checked<S>, CheckError<I>>
where
I: Ord + Clone,
S: IdSet<I>,
C: ClauseView<I>,
{
if core.steps.is_empty() {
return Err(CheckError::EmptyCore);
}
let mut derived = S::default();
for (i, step) in core.steps.iter().enumerate() {
let v = check_step(cs, &derived, i, step)?.clone();
derived.put(v);
}
let (a, b) = &core.conflict;
if !cs.has_conflict(a, b) {
return Err(CheckError::NoSuchConflict(a.clone(), b.clone()));
}
if let Some(side) = [a, b].into_iter().find(|s| !derived.has(s)) {
return Err(CheckError::ConflictSideNotDerived(side.clone()));
}
Ok(Checked::Unsat { core: derived })
}
fn check_step<'a, I, S, C>(
cs: &C,
derived: &S,
i: usize,
step: &'a Step<I>,
) -> Result<&'a I, CheckError<I>>
where
I: Ord + Clone,
S: IdSet<I>,
C: ClauseView<I>,
{
match step {
Step::Unit(v) if cs.has_unit(v) => Ok(v),
Step::Unit(v) => Err(CheckError::NotAUnit(v.clone())),
Step::Implies(a, b) if !cs.has_implies(a, b) => {
Err(CheckError::NoSuchImplication(a.clone(), b.clone()))
}
Step::Implies(a, _) if !derived.has(a) => Err(CheckError::PremiseNotDerived {
premise: a.clone(),
step: i,
}),
Step::Implies(_, b) => Ok(b),
}
}
fn check_model<I, S, C>(cs: &C, model: &Model<I>) -> Result<(), CheckError<I>>
where
I: Ord + Clone,
S: IdSet<I>,
C: ClauseView<I>,
{
let mut f = S::default();
for v in &model.false_vars {
f.put(v.clone());
}
if let Some(u) = cs.units().find(|u| f.has(u)) {
return Err(CheckError::ModelFalsifiesUnit(u.clone()));
}
if let Some((a, b)) = cs.implications().find(|(a, b)| !f.has(a) && f.has(b)) {
return Err(CheckError::ModelFalsifiesImplication(a.clone(), b.clone()));
}
if let Some((a, b)) = cs.conflict_pairs().find(|(a, b)| !f.has(a) && !f.has(b)) {
return Err(CheckError::ModelFalsifiesConflict(a.clone(), b.clone()));
}
Ok(())
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CertificateFixture {
pub clauses: ClauseSet,
pub result: WitnessResult,
}
pub const CORRUPT_CORE_FIXTURE: &str = include_str!("../../fixtures/unsat-core-corrupt.json");
pub fn pc_checker() -> Result<&'static str, String> {
let fx: CertificateFixture = serde_json::from_str(CORRUPT_CORE_FIXTURE)
.map_err(|e| format!("the corrupt-core fixture does not parse: {e}"))?;
match check(&fx.clauses, &fx.result) {
Err(_) => Ok(FIRED),
Ok(c) => Err(format!(
"the checker accepted the corrupt-core fixture as {c:?}"
)),
}
}
#[must_use]
pub fn variables(cs: &ClauseSet) -> BTreeSet<String> {
let mut v: BTreeSet<String> = cs.units.clone();
for (a, b) in cs.implies.iter().chain(cs.conflicts.iter()) {
v.insert(a.clone());
v.insert(b.clone());
}
v
}
#[must_use]
pub fn implication_adjacency(cs: &ClauseSet) -> BTreeMap<&str, Vec<&str>> {
let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (a, b) in &cs.implies {
adj.entry(a.as_str()).or_default().push(b.as_str());
}
adj
}
#[cfg(test)]
#[path = "witness_tests.rs"]
mod tests;
#[cfg(test)]
mod planted {
use super::super::witness_planted::{
adversarial_cores, adversarial_models, consistent, contradicted, satisfiable, Rng,
};
use super::{check, Checked};
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn ont_planted(seed in any::<u64>()) {
let mut rng = Rng::new(seed);
let sat = consistent(&mut rng);
prop_assert!(satisfiable(&sat.clauses, sat.n), "the generator built an unsatisfiable 'consistent' graph");
prop_assert_eq!(check(&sat.clauses, &sat.planted), Ok(Checked::Sat));
for c in adversarial_cores(&mut rng, &sat.clauses, sat.n) {
let v = check(&sat.clauses, &c);
prop_assert!(v.is_err(), "a consistent-by-construction graph got a core: {:?} for {:?} over {:?}", v, c, sat.clauses);
}
let unsat = contradicted(&mut rng);
prop_assert!(!satisfiable(&unsat.clauses, unsat.n), "the generator built a satisfiable 'contradiction'");
prop_assert!(
matches!(check(&unsat.clauses, &unsat.planted), Ok(Checked::Unsat { .. })),
"the planted derivation did not check: {:?}", check(&unsat.clauses, &unsat.planted)
);
for m in adversarial_models(&mut rng, unsat.n) {
let v = check(&unsat.clauses, &m);
prop_assert!(v.is_err(), "a planted contradiction got a model: {:?} for {:?} over {:?}", v, m, unsat.clauses);
}
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::super::witness_small::{
accepted_verdict_is_semantic_u8, check_u8, core_as, id_u8, model_as, SmallGraph,
CORE_BOUND, STEP_CODES,
};
fn cores_of_len<const LEN: usize>() {
let g = any_graph();
let mut codes = [0u8; LEN];
for c in &mut codes {
let k: u8 = kani::any();
kani::assume(k < STEP_CODES);
*c = k;
}
let conflict: usize = kani::any();
kani::assume(conflict < 3);
assert!(accepted_verdict_is_semantic_u8(
&g,
&check_u8(&g, &core_as(&codes, conflict, id_u8))
));
}
#[kani::proof]
#[kani::unwind(9)]
#[kani::solver(cadical)]
fn kani_ont_9_1() {
cores_of_len::<CORE_BOUND>();
}
#[kani::proof]
#[kani::unwind(9)]
#[kani::solver(cadical)]
fn kani_ont_9_1_len1() {
cores_of_len::<1>();
}
#[kani::proof]
#[kani::unwind(9)]
#[kani::solver(cadical)]
fn kani_ont_9_1_len2() {
cores_of_len::<2>();
}
#[kani::proof]
#[kani::unwind(9)]
#[kani::solver(cadical)]
fn kani_ont_9_1_model() {
let g = any_graph();
let false_bits: u8 = kani::any();
kani::assume(false_bits < 8);
assert!(accepted_verdict_is_semantic_u8(
&g,
&check_u8(&g, &model_as(false_bits, id_u8))
));
}
fn any_graph() -> SmallGraph {
SmallGraph {
units: kani::any(),
implies: kani::any(),
conflicts: kani::any(),
}
}
}