Skip to content

Commit 2fa200e

Browse files
committed
fix(grpc)+security+test: resuscitate echidna-grpc, bump tonic, add live-prover roundtrip suite
Three coupled changes that leave the platform at 0 cargo-audit findings with a test suite that actually exercises the Phase-3 backend fix. ### echidna-grpc Eight pre-existing compile errors (unrelated to the tonic bump but blocking it) are now fixed: - `main.rs` was missing the `mod ffi_wrapper;` declaration even though `ffi_wrapper.rs` sat next to it. - Three `echidna::core::ProofState::new(...)` callers were passing `String` / `&str`; the signature is now `new(goal: Term)`. Wrap the raw content in `Term::Hole(...)` at every call site. - `FfiProverBackend` was missing three required trait methods (`search_theorems`, `config`, `set_config`); added along with a `ProverConfig` field to satisfy the getter/setter pair. - `TacticResult::Success` takes `ProofState` directly, not `Box<ProofState>` — unboxed. - The local `CoreTacticResult` alias was unused; replaced the remaining three call sites with the re-exported `TacticResult`. - `parse_file` now stashes `source_path` + `ffi_source` in metadata, matching the pattern introduced in the rest of `src/rust/provers/`. - `ffi_wrapper.rs` `CStr::from_ptr(*const u8)` and `CString::as_ptr() → extern *const u8` calls were platform-dependent type mismatches; added `.cast()` at each boundary. Dropped the `c_void` and `FfiStatus/FfiStringSlice/FfiOwnedString/FfiProverConfig` imports that weren't used. ### tonic 0.12 → 0.14 (clears RUSTSEC-2025-0134) Once echidna-grpc compiles, the tonic bump is mechanical: - Cargo.toml: `tonic` / `prost` pinned to `0.14`, `tonic-prost` added for the new split-out generated-code helper crate. - Cargo.toml dev deps: `tonic` features `tls` → `tls-ring` (the default TLS provider flag rename in 0.14). - build-dependencies: `tonic-build` → `tonic-prost-build`. - build.rs: `tonic_build::compile_protos` → `tonic_prost_build::`. `cargo audit` now reports "0 findings", down from: - RUSTSEC-2025-0143 (capnp unsound API) — cleared earlier by commit c497661. - RUSTSEC-2025-0134 (rustls-pemfile unmaintained, pulled transitively via tonic 0.12.3 → rustls-pemfile 2.2.0) — cleared by this bump. ### Live-prover verify roundtrip suite `tests/live_prover_verify.rs` runs the real `ProverBackend::parse_file` → `verify_proof` pipeline against per-prover fixtures in `tests/live_goals/`. The 625-test unit suite stubs solver binaries, so a regression in the Phase-3 "prefer source_path over lossy IR" fix would not be caught there — this suite closes that gap by invoking the actual binaries and asserting both the positive verdict on a valid proof AND the negative verdict on a deliberately broken one. Covered: Z3, CVC5, Coq, Agda, Metamath, SPASS (the six apt-installable MVP provers). Auto-skip when a binary is absent on PATH (same convention as `live_prover_suite.rs`) so developers without every prover installed don't see spurious failures; CI enables the `live-provers` feature per-tier with the single binary provisioned. Fixtures: - `tautology.smt2` / `contradiction.smt2` (shared Z3/CVC5 pair). - `identity.v` / `broken.v` (Coq). - `Identity.agda` / `Broken.agda` (Agda). - `trivial.mm`, `trivial.dfg` (Metamath, SPASS — positive-only). `.gitignore` in `tests/live_goals/` drops Coq/Agda compilation artifacts (`*.vo`, `*.vok`, `*.vos`, `*.glob`, `.<base>.aux`, `*.agdai`). The 625 unit tests remain green. The live-prover test binary compiles cleanly; full end-to-end run confirmed via manual CLI invocation earlier in the session (all six MVP provers return verified=true via MCP tool-call). https://claude.ai/code/session_01NaC5RXjTyr5xg9XXUa2FCC
1 parent 940e940 commit 2fa200e

15 files changed

Lines changed: 391 additions & 186 deletions

Cargo.lock

Lines changed: 99 additions & 157 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/interfaces/grpc/Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ path = "main.rs"
1818

1919
[dependencies]
2020
# gRPC
21-
tonic = "0.12"
22-
prost = "0.13"
21+
tonic = "0.14"
22+
tonic-prost = "0.14"
23+
prost = "0.14"
2324
tokio = { version = "1.41", features = ["full"] }
2425
tokio-stream = "0.1"
2526

@@ -45,7 +46,7 @@ uuid = { version = "1.11", features = ["v4", "serde"] }
4546
async-trait = "0.1"
4647

4748
[build-dependencies]
48-
tonic-build = "0.12"
49+
tonic-prost-build = "0.14"
4950

5051
[dev-dependencies]
51-
tonic = { version = "0.12", features = ["tls"] }
52+
tonic = { version = "0.14", features = ["tls-ring"] }

src/interfaces/grpc/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
22

33
fn main() -> Result<(), Box<dyn std::error::Error>> {
4-
tonic_build::compile_protos("proto/echidna.proto")?;
4+
tonic_prost_build::compile_protos("proto/echidna.proto")?;
55
Ok(())
66
}

src/interfaces/grpc/ffi_wrapper.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
// gRPC FFI Wrapper - Connects gRPC interface to ECHIDNA Rust core via Zig FFI
33

44
use std::ffi::{CString, CStr};
5-
use std::os::raw::{c_int, c_void};
5+
use std::os::raw::c_int;
66
use anyhow::{Result, anyhow};
77

8-
// Re-export FFI types from core
9-
pub use echidna::ffi::{FfiStatus, FfiStringSlice, FfiOwnedString, FfiProverConfig};
10-
118
// External Zig FFI functions (from libechidna_ffi.so)
129
extern "C" {
1310
pub fn echidna_init() -> c_int;
@@ -32,7 +29,7 @@ pub fn init_ffi() -> Result<()> {
3229
let err_msg = if err_ptr.is_null() {
3330
"Unknown FFI initialization error".to_string()
3431
} else {
35-
CStr::from_ptr(err_ptr).to_string_lossy().into_owned()
32+
CStr::from_ptr(err_ptr.cast()).to_string_lossy().into_owned()
3633
};
3734
return Err(anyhow!("FFI initialization failed: {}", err_msg));
3835
}
@@ -47,7 +44,7 @@ pub fn get_version() -> Result<String> {
4744
if ptr.is_null() {
4845
return Err(anyhow!("FFI version pointer is null"));
4946
}
50-
let version = CStr::from_ptr(ptr).to_string_lossy().into_owned();
47+
let version = CStr::from_ptr(ptr.cast()).to_string_lossy().into_owned();
5148
Ok(version)
5249
}
5350
}
@@ -59,7 +56,7 @@ pub fn get_last_error() -> Result<String> {
5956
if ptr.is_null() {
6057
return Err(anyhow!("No error message available"));
6158
}
62-
let error = CStr::from_ptr(ptr).to_string_lossy().into_owned();
59+
let error = CStr::from_ptr(ptr.cast()).to_string_lossy().into_owned();
6360
Ok(error)
6461
}
6562
}
@@ -88,7 +85,7 @@ pub fn destroy_prover(handle: i32) -> Result<()> {
8885
pub fn parse_string(handle: i32, content: &str) -> Result<()> {
8986
unsafe {
9087
let c_content = CString::new(content)?;
91-
let rc = echidna_parse_string(handle, c_content.as_ptr(), content.len());
88+
let rc = echidna_parse_string(handle, c_content.as_ptr().cast(), content.len());
9289
if rc != 0 {
9390
let error = get_last_error()?;
9491
return Err(anyhow!("Parse failed: {}", error));
@@ -109,7 +106,7 @@ pub fn verify_proof(handle: i32) -> Result<bool> {
109106
pub fn apply_tactic(handle: i32, tactic: &str) -> Result<bool> {
110107
unsafe {
111108
let c_tactic = CString::new(tactic)?;
112-
let rc = echidna_apply_tactic(handle, c_tactic.as_ptr(), tactic.len());
109+
let rc = echidna_apply_tactic(handle, c_tactic.as_ptr().cast(), tactic.len());
113110
Ok(rc == 0)
114111
}
115112
}

src/interfaces/grpc/main.rs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
22
// ECHIDNA gRPC Server - wired to core
33

4-
use echidna::core::{Tactic as CoreTactic, TacticResult as CoreTacticResult, Term};
4+
use echidna::core::{Tactic as CoreTactic, TacticResult, Term};
55
use echidna::provers::{ProverBackend, ProverConfig, ProverFactory, ProverKind as CoreProverKind};
66
use echidna_proto::v1::proof_service_server::{ProofService, ProofServiceServer};
77
use echidna_proto::v1::*;
@@ -10,8 +10,8 @@ use std::sync::Arc;
1010
use tokio::sync::{Mutex, RwLock};
1111
use tonic::{transport::Server, Request, Response, Status};
1212

13-
// Import FFI wrapper
14-
use crate::ffi_wrapper;
13+
// FFI wrapper module — file lives next to main.rs.
14+
mod ffi_wrapper;
1515

1616
pub mod echidna_proto {
1717
pub mod v1 {
@@ -22,11 +22,15 @@ pub mod echidna_proto {
2222
/// Wrapper for FFI-based prover backend
2323
struct FfiProverBackend {
2424
handle: i32,
25+
config: ProverConfig,
2526
}
2627

2728
impl FfiProverBackend {
2829
pub fn new(handle: i32) -> Self {
29-
FfiProverBackend { handle }
30+
FfiProverBackend {
31+
handle,
32+
config: ProverConfig::default(),
33+
}
3034
}
3135
}
3236

@@ -38,15 +42,29 @@ impl ProverBackend for FfiProverBackend {
3842
) -> anyhow::Result<echidna::core::ProofState> {
3943
let content = std::fs::read_to_string(&path)?;
4044
ffi_wrapper::parse_string(self.handle, &content)?;
41-
Ok(echidna::core::ProofState::new(content))
45+
let mut state = echidna::core::ProofState::new(Term::Hole(content.clone()));
46+
state.metadata.insert(
47+
"source_path".to_string(),
48+
serde_json::Value::String(path.to_string_lossy().into_owned()),
49+
);
50+
state.metadata.insert(
51+
"ffi_source".to_string(),
52+
serde_json::Value::String(content),
53+
);
54+
Ok(state)
4255
}
4356

4457
async fn parse_string(&self, content: &str) -> anyhow::Result<echidna::core::ProofState> {
4558
ffi_wrapper::parse_string(self.handle, content)?;
46-
Ok(echidna::core::ProofState::new(content.to_string()))
59+
let mut state = echidna::core::ProofState::new(Term::Hole(content.to_string()));
60+
state.metadata.insert(
61+
"ffi_source".to_string(),
62+
serde_json::Value::String(content.to_string()),
63+
);
64+
Ok(state)
4765
}
4866

49-
async fn verify_proof(&self, state: &echidna::core::ProofState) -> anyhow::Result<bool> {
67+
async fn verify_proof(&self, _state: &echidna::core::ProofState) -> anyhow::Result<bool> {
5068
ffi_wrapper::verify_proof(self.handle)
5169
}
5270

@@ -57,15 +75,15 @@ impl ProverBackend for FfiProverBackend {
5775
) -> anyhow::Result<TacticResult> {
5876
let tactic_str = format!("{:?}", tactic);
5977
if ffi_wrapper::apply_tactic(self.handle, &tactic_str)? {
60-
Ok(TacticResult::Success(Box::new(state.clone())))
78+
Ok(TacticResult::Success(state.clone()))
6179
} else {
6280
Ok(TacticResult::Error("Tactic failed".to_string()))
6381
}
6482
}
6583

6684
async fn suggest_tactics(
6785
&self,
68-
state: &echidna::core::ProofState,
86+
_state: &echidna::core::ProofState,
6987
limit: usize,
7088
) -> anyhow::Result<Vec<CoreTactic>> {
7189
let tactic_names = ffi_wrapper::suggest_tactics(self.handle, limit)?;
@@ -80,7 +98,13 @@ impl ProverBackend for FfiProverBackend {
8098
Ok(tactics)
8199
}
82100

83-
async fn export(&self, state: &echidna::core::ProofState) -> anyhow::Result<String> {
101+
async fn search_theorems(&self, _pattern: &str) -> anyhow::Result<Vec<String>> {
102+
// FFI layer doesn't expose theorem search yet; return empty so the
103+
// gRPC service can still satisfy the trait contract.
104+
Ok(Vec::new())
105+
}
106+
107+
async fn export(&self, _state: &echidna::core::ProofState) -> anyhow::Result<String> {
84108
ffi_wrapper::export_proof(self.handle)
85109
}
86110

@@ -92,6 +116,14 @@ impl ProverBackend for FfiProverBackend {
92116
// This is a bit simplified - in a real implementation we'd track the kind
93117
CoreProverKind::Lean // Default, would need to be set during creation
94118
}
119+
120+
fn config(&self) -> &ProverConfig {
121+
&self.config
122+
}
123+
124+
fn set_config(&mut self, config: ProverConfig) {
125+
self.config = config;
126+
}
95127
}
96128

97129
struct ProofSession {
@@ -167,7 +199,9 @@ impl ProofService for ProofServiceImpl {
167199
let session = ProofSession {
168200
prover_kind: core_kind,
169201
prover: Box::new(FfiProverBackend::new(handle)),
170-
state: Some(echidna::core::ProofState::new(req.goal.clone())),
202+
state: Some(echidna::core::ProofState::new(Term::Hole(
203+
req.goal.clone(),
204+
))),
171205
goal: req.goal.clone(),
172206
status,
173207
history: vec![],
@@ -377,7 +411,7 @@ impl ProofService for ProofServiceImpl {
377411
.map_err(|e| Status::internal(e.to_string()))?;
378412

379413
let success = match &result {
380-
CoreTacticResult::Success(new_state) => {
414+
TacticResult::Success(new_state) => {
381415
let complete = new_state.is_complete();
382416
session.state = Some(new_state.clone());
383417
session.history.push(format!("{:?}", tactic));
@@ -386,14 +420,14 @@ impl ProofService for ProofServiceImpl {
386420
}
387421
true
388422
},
389-
CoreTacticResult::QED => {
423+
TacticResult::QED => {
390424
session.status = ProofStatus::Success as i32;
391425
if let Some(s) = session.state.as_mut() {
392426
s.goals.clear();
393427
}
394428
true
395429
},
396-
CoreTacticResult::Error(_) => false,
430+
TacticResult::Error(_) => false,
397431
};
398432

399433
let elapsed = session.start_time.elapsed().as_secs_f64();
@@ -413,7 +447,7 @@ impl ProofService for ProofServiceImpl {
413447
proof_script: script,
414448
time_elapsed: Some(elapsed),
415449
error_message: match &result {
416-
CoreTacticResult::Error(msg) => Some(msg.clone()),
450+
TacticResult::Error(msg) => Some(msg.clone()),
417451
_ => None,
418452
},
419453
}),

tests/live_goals/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Coq compilation artifacts — generated when coqc runs on .v fixtures.
2+
*.vo
3+
*.vok
4+
*.vos
5+
*.glob
6+
.*.aux
7+
# Agda compilation artifacts
8+
*.agdai

tests/live_goals/Broken.agda

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Type error: claims A → B from no premise.
2+
module Broken where
3+
4+
bogus : (A : Set) -> (B : Set) -> A -> B
5+
bogus A B x = x

tests/live_goals/Identity.agda

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module Identity where
2+
3+
id : (A : Set) -> A -> A
4+
id A x = x

tests/live_goals/broken.v

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(* Type error: claims `True -> False` and proves with `exact I`. *)
2+
Theorem broken : True -> False.
3+
Proof. intro H. exact I. Qed.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(set-logic QF_LIA)
2+
(declare-const x Int)
3+
; Satisfiable assertion — SAT, so verify_proof reports "not verified".
4+
(assert (= x 1))
5+
(check-sat)

0 commit comments

Comments
 (0)