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:
parent
a60fd16e45
commit
cbb73d16b8
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue