diff --git a/capsule-core/src/circuit.zig b/capsule-core/src/circuit.zig new file mode 100644 index 0000000..438b16e --- /dev/null +++ b/capsule-core/src/circuit.zig @@ -0,0 +1,102 @@ +//! RFC-0018: Circuit Building Logic +//! +//! Orchestrates the selection of relays via QVL and the construction of onion packets. + +const std = @import("std"); +const relay = @import("relay"); +const dht = @import("dht"); // Needed for NodeId type +const QvlStore = @import("qvl_store.zig").QvlStore; +const PeerTable = @import("peer_table.zig").PeerTable; + +pub const CircuitError = error{ + NoRelaysAvailable, + TargetNotFound, + RelayNotFound, + PathConstructionFailed, +}; + +pub const CircuitBuilder = struct { + allocator: std.mem.Allocator, + qvl_store: *QvlStore, + peer_table: *PeerTable, + onion_builder: relay.OnionBuilder, + + pub fn init(allocator: std.mem.Allocator, qvl_store: *QvlStore, peer_table: *PeerTable) CircuitBuilder { + return .{ + .allocator = allocator, + .qvl_store = qvl_store, + .peer_table = peer_table, + .onion_builder = relay.OnionBuilder.init(allocator), + }; + } + + /// Builds a 1-hop circuit (MVP): Source -> Relay -> Target + /// Returns the fully wrapped packet ready to be sent to the Relay. + pub fn buildOneHopCircuit( + self: *CircuitBuilder, + target_did: []const u8, + payload: []const u8, + ) !relay.RelayPacket { + // 1. Resolve Target + // We need the Target's NodeID (for the inner routing header). + // For MVP, we assume DID ~= NodeID or we have a mapping. + // Let's assume we can lookup by DID in PeerTable to get public key/ID. + // (PeerTable currently uses did_short [8]u8, but let's assume we can map). + + // MVP: Fake resolution. + var target_id = [_]u8{0} ** 32; + if (target_did.len >= 32) @memcpy(&target_id, target_did[0..32]); + + // 2. Select a Relay + const trusted_dids = try self.qvl_store.getTrustedRelays(0.5, 10); + defer { + for (trusted_dids) |did| self.allocator.free(did); + self.allocator.free(trusted_dids); + } + + if (trusted_dids.len == 0) return error.NoRelaysAvailable; + + // Pick random relay + const rand_idx = std.crypto.random.intRangeAtMost(usize, 0, trusted_dids.len - 1); + const relay_did = trusted_dids[rand_idx]; + + // Resolve Relay NodeID + var relay_id = [_]u8{0} ** 32; + if (relay_did.len >= 32) { + @memcpy(&relay_id, relay_did[0..32]); + } else { + // If DID is short, maybe pad? MVP hack. + std.mem.copyForwards(u8, &relay_id, relay_did); + } + + // 3. Wrap Inner Layer (Target) + // The Payload is destined for Target. + // next_hop for Inner Layer is Target. + // But wait, the Relay receives the outer packet, unwraps it. + // It sees: Next Hop = Target. + // So the Relay forwards the *Inner Payload* to Target. + // Is the Inner Payload encrypted for Target? YES. + + // Mock Session secrets + const relay_secret = [_]u8{0xAA} ** 32; + + // Wrap: Relay Packet -> [ NextHop: Target | Payload ] + const packet = try self.onion_builder.wrapLayer(payload, target_id, relay_secret); + + // The `packet` returned is what we send to the Relay. + // The Relay will unwrap it, see `target_id`, and forward `packet.payload` to `target_id`. + // Note: `packet.payload` here is the original `payload` (if only 1 layer). + // If we want E2E encryption for Target, we must have encrypted `payload` beforehand. + // This function assumes `payload` is ALREADY E2E encrypted (e.g. LWF frame). + + return packet; + } +}; + +test "Circuit: Build 1-Hop" { + // Basic test + const allocator = std.testing.allocator; + // We would need mocks for QvlStore etc. + // For now, satisfy the compiler. + _ = allocator; +} diff --git a/capsule-core/src/node.zig b/capsule-core/src/node.zig index d666c91..fcc1521 100644 --- a/capsule-core/src/node.zig +++ b/capsule-core/src/node.zig @@ -21,6 +21,7 @@ const storage_mod = @import("storage.zig"); const qvl_store_mod = @import("qvl_store.zig"); const control_mod = @import("control.zig"); const quarantine_mod = @import("quarantine"); +const circuit_mod = @import("circuit.zig"); const NodeConfig = config_mod.NodeConfig; const UTCP = utcp_mod.UTCP; diff --git a/capsule-core/src/qvl_store.zig b/capsule-core/src/qvl_store.zig index c7a3b23..baabbf5 100644 --- a/capsule-core/src/qvl_store.zig +++ b/capsule-core/src/qvl_store.zig @@ -246,4 +246,38 @@ pub const QvlStore = struct { return events; } + + /// Retrieve a list of trusted relay DIDs based on QVL scores. + pub fn getTrustedRelays(self: *QvlStore, min_score: f64, limit: usize) ![][]u8 { + const sql_slice = try std.fmt.allocPrint(self.allocator, "SELECT did FROM qvl_vertices WHERE trust_score >= {d} ORDER BY trust_score DESC LIMIT {d};", .{ min_score, limit }); + defer self.allocator.free(sql_slice); + const sql = try self.allocator.dupeZ(u8, sql_slice); + defer self.allocator.free(sql); + + var res: c.duckdb_result = undefined; + if (c.duckdb_query(self.conn, sql.ptr, &res) != c.DuckDBSuccess) { + std.log.err("DuckDB Relay Query Error: {s}", .{c.duckdb_result_error(&res)}); + c.duckdb_destroy_result(&res); + return error.QueryFailed; + } + defer c.duckdb_destroy_result(&res); + + const row_count = c.duckdb_row_count(&res); + // If we found nothing, return empty slice + if (row_count == 0) return &[_][]u8{}; + + var relays = try self.allocator.alloc([]u8, row_count); + + for (0..row_count) |i| { + const val = c.duckdb_value_varchar(&res, i, 0); + defer c.duckdb_free(val); + if (val == null) { + // Should not happen if DB is correct, but handle safely + relays[i] = try self.allocator.dupe(u8, "UNKNOWN"); + } else { + relays[i] = try self.allocator.dupe(u8, std.mem.span(val)); + } + } + return relays; + } };