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.
This commit is contained in:
Markus Maiwald 2026-01-31 03:54:36 +01:00
parent a60fd16e45
commit cbb73d16b8
3 changed files with 79 additions and 0 deletions

View File

@ -303,6 +303,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
l1_qvl_ffi_mod.addImport("qvl", l1_qvl_mod); 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); l1_qvl_ffi_mod.addImport("time", time_mod);
// QVL FFI static library (for Rust L2 Membrane Agent) // QVL FFI static library (for Rust L2 Membrane Agent)

View File

@ -13,6 +13,7 @@ const qvl = @import("qvl.zig");
const pop_mod = @import("proof_of_path.zig"); const pop_mod = @import("proof_of_path.zig");
const trust_graph = @import("trust_graph.zig"); const trust_graph = @import("trust_graph.zig");
const time = @import("time"); const time = @import("time");
const slash = @import("slash");
const RiskGraph = qvl.types.RiskGraph; const RiskGraph = qvl.types.RiskGraph;
const RiskEdge = qvl.types.RiskEdge; const RiskEdge = qvl.types.RiskEdge;
@ -260,6 +261,38 @@ export fn qvl_revoke_trust_edge(
return -2; // Not found 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) // TESTS (C ABI validation)
// ============================================================================ // ============================================================================

View File

@ -83,6 +83,13 @@ extern "C" {
from: u32, from: u32,
to: u32, to: u32,
) -> c_int; ) -> 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) 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 { impl Drop for QvlClient {
@ -321,4 +351,19 @@ mod tests {
assert_eq!(anomaly.score, 0.0); assert_eq!(anomaly.score, 0.0);
assert_eq!(anomaly.reason, AnomalyReason::None); 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);
}
} }