From cbb73d16b8a951f22919f5b8b64c1c83c77cdd0a Mon Sep 17 00:00:00 2001 From: Markus Maiwald Date: Sat, 31 Jan 2026 03:54:36 +0100 Subject: [PATCH] Phase 8 Sprint 1: FFI Export for Slash Protocol - Zig L1: Implemented qvl_issue_slash_signal (constructs SlashSignal) - Rust L2: Added FFI binding and safe wrapper issue_slash_signal - Config: Wired l1_slash_mod into qvl_ffi build - Verified: Unit test for signal creation passing The active defense loop is closed. L2 can now pull the trigger. --- build.zig | 1 + l1-identity/qvl_ffi.zig | 33 +++++++++++++++++++++++++ membrane-agent/src/qvl_ffi.rs | 45 +++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/build.zig b/build.zig index 14a455a..2b6be86 100644 --- a/build.zig +++ b/build.zig @@ -303,6 +303,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); l1_qvl_ffi_mod.addImport("qvl", l1_qvl_mod); + l1_qvl_ffi_mod.addImport("slash", l1_slash_mod); l1_qvl_ffi_mod.addImport("time", time_mod); // QVL FFI static library (for Rust L2 Membrane Agent) diff --git a/l1-identity/qvl_ffi.zig b/l1-identity/qvl_ffi.zig index d7f5b02..fc25abb 100644 --- a/l1-identity/qvl_ffi.zig +++ b/l1-identity/qvl_ffi.zig @@ -13,6 +13,7 @@ const qvl = @import("qvl.zig"); const pop_mod = @import("proof_of_path.zig"); const trust_graph = @import("trust_graph.zig"); const time = @import("time"); +const slash = @import("slash"); const RiskGraph = qvl.types.RiskGraph; const RiskEdge = qvl.types.RiskEdge; @@ -260,6 +261,38 @@ export fn qvl_revoke_trust_edge( return -2; // Not found } +/// Issue a SlashSignal for a detected betrayal +/// Returns 0 on success, < 0 on error +/// If 'out_signal' is non-null, writes serialized signal (82 bytes) +export fn qvl_issue_slash_signal( + ctx: ?*QvlContext, + target_did: [*c]const u8, + reason: u8, + out_signal: [*c]u8, +) callconv(.c) c_int { + _ = ctx; // Context not strictly needed for constructing signal, but good for future validation + if (target_did == null) return -2; + + var did: [32]u8 = undefined; + @memcpy(&did, target_did[0..32]); + + const signal = slash.SlashSignal{ + .target_did = did, + .reason = @enumFromInt(reason), + .punishment = .Quarantine, // Default to Quarantine + .evidence_hash = [_]u8{0} ** 32, // TODO: Hash actual evidence + .timestamp = std.time.timestamp(), + .nonce = 0, + }; + + if (out_signal != null) { + const bytes = signal.serializeForSigning(); + @memcpy(out_signal[0..82], &bytes); + } + + return 0; +} + // ============================================================================ // TESTS (C ABI validation) // ============================================================================ diff --git a/membrane-agent/src/qvl_ffi.rs b/membrane-agent/src/qvl_ffi.rs index b09c81d..f858eba 100644 --- a/membrane-agent/src/qvl_ffi.rs +++ b/membrane-agent/src/qvl_ffi.rs @@ -83,6 +83,13 @@ extern "C" { from: u32, to: u32, ) -> c_int; + + fn qvl_issue_slash_signal( + ctx: *mut QvlContext, + target_did: *const u8, + reason: u8, + out_signal: *mut u8, + ) -> c_int; } // ============================================================================ @@ -261,6 +268,29 @@ impl QvlClient { Err(QvlError::MutationFailed) } } + + /// Issue a SlashSignal (returns 82-byte serialized signal for signing/broadcast) + pub fn issue_slash_signal(&self, target_did: &[u8; 32], reason: u8) -> Result<[u8; 82], QvlError> { + if self.ctx.is_null() { + return Err(QvlError::NullContext); + } + + let mut out = [0u8; 82]; + let result = unsafe { + qvl_issue_slash_signal( + self.ctx, + target_did.as_ptr(), + reason, + out.as_mut_ptr(), + ) + }; + + if result == 0 { + Ok(out) + } else { + Err(QvlError::MutationFailed) + } + } } impl Drop for QvlClient { @@ -321,4 +351,19 @@ mod tests { assert_eq!(anomaly.score, 0.0); assert_eq!(anomaly.reason, AnomalyReason::None); } + + #[test] + fn test_issue_slash_signal() { + let client = QvlClient::new().unwrap(); + let target = [1u8; 32]; + let reason = 1; // BetrayalNegativeCycle + + let signal = client.issue_slash_signal(&target, reason).unwrap(); + // Verify first byte (target DID[0] = 1) + assert_eq!(signal[0], 1); + // Verify reason (offset 32 = 1) + assert_eq!(signal[32], 1); + // Verify punishment (offset 33 = 1 Quarantine) + assert_eq!(signal[33], 1); + } }