diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index c40c46df..a60b2c04 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -235,6 +235,33 @@ fn get_public_key(text: &str) -> Option { PublicKey::from_str(text).ok() } +fn get_dht_key( + routing_table: RoutingTable, +) -> impl FnOnce(&str) -> Option<(TypedKey, Option)> { + move |text| { + // Safety selection + let (text, ss) = if let Some((first, second)) = text.split_once('+') { + let ss = get_safety_selection(second, routing_table.clone())?; + (first, Some(ss)) + } else { + (text, None) + }; + if text.len() == 0 { + return None; + } + + let key = if let Some(key) = get_public_key(text) { + TypedKey::new(best_crypto_kind(), key) + } else if let Some(key) = get_typed_key(text) { + key + } else { + return None; + }; + + Some((key, ss)) + } +} + fn get_node_ref(routing_table: RoutingTable) -> impl FnOnce(&str) -> Option { move |text| { let (text, mods) = text @@ -923,14 +950,31 @@ impl VeilidAPI { return Ok(out); } async fn debug_record_get(&self, args: Vec) -> VeilidAPIResult { - let storage_manager = self.storage_manager()?; + let netman = self.network_manager()?; + let routing_table = netman.routing_table(); - let key = get_debug_argument_at(&args, 1, "debug_record_get", "key", get_typed_key)?; - let subkeys = - get_debug_argument_at(&args, 2, "debug_record_subkeys", "subkeys", get_string)?; + let (key, ss) = get_debug_argument_at( + &args, + 1, + "debug_record_get", + "key", + get_dht_key(routing_table), + )?; + let subkeys = get_debug_argument_at(&args, 2, "debug_record_get", "subkeys", get_string)?; - // let rc = self.routing_context(); + // Get routing context with optional privacy + let rc = self.routing_context(); + let rc = if let Some(ss) = ss { + let rcp = match rc.with_custom_privacy(ss) { + Err(e) => return Ok(format!("Can't use safety selection: {}", e)), + Ok(v) => v, + }; + rcp + } else { + rc + }; + // Do a record get return Ok("TODO".to_owned()); } @@ -958,7 +1002,8 @@ impl VeilidAPI { entries [dead|reliable] entry nodeinfo - config [key [new value]] + config [configkey [new value]] + txtrecord purge attach detach @@ -975,8 +1020,9 @@ impl VeilidAPI { test record list purge [bytes] - get + get + is: dot path like network.protocol.udp.enabled is: * direct: [+][] * relay: @[+][] @@ -988,6 +1034,7 @@ impl VeilidAPI { is: udp|tcp|ws|wss is: ipv4|ipv6 is: public|local + is: [+] is: * a number: 2 * a comma-separated inclusive range list: 1..=3,5..=8 diff --git a/veilid-core/src/veilid_api/json_api/process.rs b/veilid-core/src/veilid_api/json_api/process.rs index f27a7c9a..018dc276 100644 --- a/veilid-core/src/veilid_api/json_api/process.rs +++ b/veilid-core/src/veilid_api/json_api/process.rs @@ -243,12 +243,12 @@ impl JsonRequestProcessor { .map(|new_rc| self.add_routing_context(new_rc)), ), }, - RoutingContextRequestOp::WithCustomPrivacy { stability } => { + RoutingContextRequestOp::WithCustomPrivacy { safety_selection } => { RoutingContextResponseOp::WithCustomPrivacy { result: to_json_api_result( routing_context .clone() - .with_custom_privacy(stability) + .with_custom_privacy(safety_selection) .map(|new_rc| self.add_routing_context(new_rc)), ), } diff --git a/veilid-core/src/veilid_api/json_api/routing_context.rs b/veilid-core/src/veilid_api/json_api/routing_context.rs index 4ec8a881..8ecfff32 100644 --- a/veilid-core/src/veilid_api/json_api/routing_context.rs +++ b/veilid-core/src/veilid_api/json_api/routing_context.rs @@ -20,7 +20,7 @@ pub enum RoutingContextRequestOp { Release, WithPrivacy, WithCustomPrivacy { - stability: Stability, + safety_selection: SafetySelection, }, WithSequencing { sequencing: Sequencing, diff --git a/veilid-core/src/veilid_api/routing_context.rs b/veilid-core/src/veilid_api/routing_context.rs index 37989be1..47b30271 100644 --- a/veilid-core/src/veilid_api/routing_context.rs +++ b/veilid-core/src/veilid_api/routing_context.rs @@ -46,24 +46,22 @@ impl RoutingContext { } pub fn with_privacy(self) -> VeilidAPIResult { - self.with_custom_privacy(Stability::default()) - } - - pub fn with_custom_privacy(self, stability: Stability) -> VeilidAPIResult { let config = self.api.config()?; let c = config.get(); + self.with_custom_privacy(SafetySelection::Safe(SafetySpec { + preferred_route: None, + hop_count: c.network.rpc.default_route_hop_count as usize, + stability: Stability::default(), + sequencing: Sequencing::default(), + })) + } + + pub fn with_custom_privacy(self, safety_selection: SafetySelection) -> VeilidAPIResult { Ok(Self { api: self.api.clone(), inner: Arc::new(Mutex::new(RoutingContextInner {})), - unlocked_inner: Arc::new(RoutingContextUnlockedInner { - safety_selection: SafetySelection::Safe(SafetySpec { - preferred_route: None, - hop_count: c.network.rpc.default_route_hop_count as usize, - stability, - sequencing: self.sequencing(), - }), - }), + unlocked_inner: Arc::new(RoutingContextUnlockedInner { safety_selection }), }) } diff --git a/veilid-flutter/lib/routing_context.dart b/veilid-flutter/lib/routing_context.dart index ec547f56..42f5125c 100644 --- a/veilid-flutter/lib/routing_context.dart +++ b/veilid-flutter/lib/routing_context.dart @@ -28,7 +28,7 @@ abstract class DHTSchema { default: { throw VeilidAPIExceptionInternal( - "Invalid VeilidAPIException type: ${json['kind']}"); + "Invalid DHTSchema type: ${json['kind']}"); } } } @@ -196,6 +196,7 @@ class ValueData { } } +////////////////////////////////////// /// Stability enum Stability { @@ -228,6 +229,80 @@ enum Sequencing { } } +////////////////////////////////////// +/// SafetySelection + +abstract class SafetySelection { + factory SafetySelection.fromJson(dynamic json) { + var m = json as Map; + if (m.containsKey("Unsafe")) { + return SafetySelectionUnsafe( + sequencing: Sequencing.fromJson(m["Unsafe"])); + } else if (m.containsKey("Safe")) { + return SafetySelectionSafe(safetySpec: SafetySpec.fromJson(m["Safe"])); + } else { + throw VeilidAPIExceptionInternal("Invalid SafetySelection"); + } + } + Map toJson(); +} + +class SafetySelectionUnsafe implements SafetySelection { + final Sequencing sequencing; + // + SafetySelectionUnsafe({ + required this.sequencing, + }); + + @override + Map toJson() { + return {'Unsafe': sequencing.toJson()}; + } +} + +class SafetySelectionSafe implements SafetySelection { + final SafetySpec safetySpec; + // + SafetySelectionSafe({ + required this.safetySpec, + }); + + @override + Map toJson() { + return {'Safe': safetySpec.toJson()}; + } +} + +/// Options for safety routes (sender privacy) +class SafetySpec { + final String? preferredRoute; + final int hopCount; + final Stability stability; + final Sequencing sequencing; + // + SafetySpec({ + this.preferredRoute, + required this.hopCount, + required this.stability, + required this.sequencing, + }); + + SafetySpec.fromJson(dynamic json) + : preferredRoute = json['preferred_route'], + hopCount = json['hop_count'], + stability = Stability.fromJson(json['stability']), + sequencing = Sequencing.fromJson(json['sequencing']); + + Map toJson() { + return { + 'preferred_route': preferredRoute, + 'hop_count': hopCount, + 'stability': stability.toJson(), + 'sequencing': sequencing.toJson() + }; + } +} + ////////////////////////////////////// /// RouteBlob class RouteBlob { @@ -251,7 +326,7 @@ class RouteBlob { abstract class VeilidRoutingContext { // Modifiers VeilidRoutingContext withPrivacy(); - VeilidRoutingContext withCustomPrivacy(Stability stability); + VeilidRoutingContext withCustomPrivacy(SafetySelection safetySelection); VeilidRoutingContext withSequencing(Sequencing sequencing); // App call/message diff --git a/veilid-flutter/lib/veilid_ffi.dart b/veilid-flutter/lib/veilid_ffi.dart index 1bf46e84..72b3040e 100644 --- a/veilid-flutter/lib/veilid_ffi.dart +++ b/veilid-flutter/lib/veilid_ffi.dart @@ -595,9 +595,9 @@ class VeilidRoutingContextFFI implements VeilidRoutingContext { } @override - VeilidRoutingContextFFI withCustomPrivacy(Stability stability) { + VeilidRoutingContextFFI withCustomPrivacy(SafetySelection safetySelection) { final newId = _ctx.ffi._routingContextWithCustomPrivacy( - _ctx.id, jsonEncode(stability).toNativeUtf8()); + _ctx.id, jsonEncode(safetySelection).toNativeUtf8()); return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi)); } diff --git a/veilid-flutter/lib/veilid_js.dart b/veilid-flutter/lib/veilid_js.dart index 08a88140..32bde035 100644 --- a/veilid-flutter/lib/veilid_js.dart +++ b/veilid-flutter/lib/veilid_js.dart @@ -45,11 +45,11 @@ class VeilidRoutingContextJS implements VeilidRoutingContext { } @override - VeilidRoutingContextJS withCustomPrivacy(Stability stability) { + VeilidRoutingContextJS withCustomPrivacy(SafetySelection safetySelection) { final newId = js_util.callMethod( wasm, "routing_context_with_custom_privacy", - [_ctx.id, jsonEncode(stability)]); + [_ctx.id, jsonEncode(safetySelection)]); return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js)); } diff --git a/veilid-flutter/rust/src/dart_ffi.rs b/veilid-flutter/rust/src/dart_ffi.rs index 9aae4eeb..79419af3 100644 --- a/veilid-flutter/rust/src/dart_ffi.rs +++ b/veilid-flutter/rust/src/dart_ffi.rs @@ -410,15 +410,15 @@ pub extern "C" fn routing_context_with_privacy(id: u32) -> u32 { } #[no_mangle] -pub extern "C" fn routing_context_with_custom_privacy(id: u32, stability: FfiStr) -> u32 { - let stability: veilid_core::Stability = - veilid_core::deserialize_opt_json(stability.into_opt_string()).unwrap(); +pub extern "C" fn routing_context_with_custom_privacy(id: u32, safety_selection: FfiStr) -> u32 { + let safety_selection: veilid_core::SafetySelection = + veilid_core::deserialize_opt_json(safety_selection.into_opt_string()).unwrap(); let rc = ROUTING_CONTEXTS.lock(); let Some(routing_context) = rc.get(&id) else { return 0; }; - let Ok(routing_context) = routing_context.clone().with_custom_privacy(stability) else { + let Ok(routing_context) = routing_context.clone().with_custom_privacy(safety_selection) else { return 0; }; let new_id = add_routing_context(routing_context); diff --git a/veilid-python/tests/test_routing_context.py b/veilid-python/tests/test_routing_context.py index d92b133b..ab6c9dcd 100644 --- a/veilid-python/tests/test_routing_context.py +++ b/veilid-python/tests/test_routing_context.py @@ -16,14 +16,28 @@ from .conftest import server_info @pytest.mark.asyncio async def test_routing_contexts(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + pass + rc = await api_connection.new_routing_context() async with rc: rcp = await rc.with_privacy(release=False) async with rcp: - rcps = await rcp.with_sequencing(veilid.Sequencing.ENSURE_ORDERED, release=False) - async with rcps: - rcpscp = await rcps.with_custom_privacy(veilid.Stability.RELIABLE, release=False) - await rcpscp.release() + pass + + rc = await (await api_connection.new_routing_context()).with_sequencing(veilid.Sequencing.ENSURE_ORDERED) + async with rc: + pass + + rc = await (await api_connection.new_routing_context()).with_custom_privacy( + veilid.SafetySelection.safe( + veilid.SafetySpec(None, 2, veilid.Stability.RELIABLE, veilid.Sequencing.ENSURE_ORDERED) + )) + await rc.release() + + rc = await (await api_connection.new_routing_context()).with_custom_privacy(veilid.SafetySelection.unsafe(veilid.Sequencing.ENSURE_ORDERED)) + await rc.release() @pytest.mark.asyncio diff --git a/veilid-python/veilid/api.py b/veilid-python/veilid/api.py index 9008bbf4..4a17804f 100644 --- a/veilid-python/veilid/api.py +++ b/veilid-python/veilid/api.py @@ -27,7 +27,7 @@ class RoutingContext(ABC): pass @abstractmethod - async def with_custom_privacy(self, stability: types.Stability, release = True) -> Self: + async def with_custom_privacy(self, safety_selection: types.SafetySelection, release = True) -> Self: pass @abstractmethod diff --git a/veilid-python/veilid/json_api.py b/veilid-python/veilid/json_api.py index 4725246e..8138cf57 100644 --- a/veilid-python/veilid/json_api.py +++ b/veilid-python/veilid/json_api.py @@ -16,7 +16,7 @@ from .state import VeilidState, VeilidUpdate from .types import (CryptoKey, CryptoKeyDistance, CryptoKind, DHTRecordDescriptor, DHTSchema, HashDigest, KeyPair, NewPrivateRouteResult, Nonce, OperationId, PublicKey, - RouteId, SecretKey, Sequencing, SharedSecret, Signature, + RouteId, SafetySelection, SecretKey, Sequencing, SharedSecret, Signature, Stability, Timestamp, TypedKey, TypedKeyPair, TypedSignature, ValueData, ValueSubkey, VeilidJSONEncoder, VeilidVersion, urlsafe_b64decode_no_pad) @@ -459,14 +459,14 @@ class _JsonRoutingContext(RoutingContext): await self.release() return self.__class__(self.api, new_rc_id) - async def with_custom_privacy(self, stability: Stability, release = True) -> Self: + async def with_custom_privacy(self, safety_selection: SafetySelection, release = True) -> Self: new_rc_id = raise_api_result( await self.api.send_ndjson_request( Operation.ROUTING_CONTEXT, validate=validate_rc_op, rc_id=self.rc_id, rc_op=RoutingContextOperation.WITH_CUSTOM_PRIVACY, - stability=stability, + safety_selection=safety_selection, ) ) if release: diff --git a/veilid-python/veilid/schema/Request.json b/veilid-python/veilid/schema/Request.json index 83ae07e8..d087d1cc 100644 --- a/veilid-python/veilid/schema/Request.json +++ b/veilid-python/veilid/schema/Request.json @@ -224,7 +224,7 @@ "type": "object", "required": [ "rc_op", - "stability" + "safety_selection" ], "properties": { "rc_op": { @@ -233,8 +233,8 @@ "WithCustomPrivacy" ] }, - "stability": { - "$ref": "#/definitions/Stability" + "safety_selection": { + "$ref": "#/definitions/SafetySelection" } } }, @@ -1566,6 +1566,77 @@ } } }, + "SafetySelection": { + "description": "The choice of safety route to include in compiled routes", + "oneOf": [ + { + "description": "Don't use a safety route, only specify the sequencing preference", + "type": "object", + "required": [ + "Unsafe" + ], + "properties": { + "Unsafe": { + "$ref": "#/definitions/Sequencing" + } + }, + "additionalProperties": false + }, + { + "description": "Use a safety route and parameters specified by a SafetySpec", + "type": "object", + "required": [ + "Safe" + ], + "properties": { + "Safe": { + "$ref": "#/definitions/SafetySpec" + } + }, + "additionalProperties": false + } + ] + }, + "SafetySpec": { + "description": "Options for safety routes (sender privacy)", + "type": "object", + "required": [ + "hop_count", + "sequencing", + "stability" + ], + "properties": { + "hop_count": { + "description": "must be greater than 0", + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "preferred_route": { + "description": "preferred safety route set id if it still exists", + "type": [ + "string", + "null" + ] + }, + "sequencing": { + "description": "prefer connection-oriented sequenced protocols", + "allOf": [ + { + "$ref": "#/definitions/Sequencing" + } + ] + }, + "stability": { + "description": "prefer reliability over speed", + "allOf": [ + { + "$ref": "#/definitions/Stability" + } + ] + } + } + }, "Sequencing": { "type": "string", "enum": [ diff --git a/veilid-python/veilid/types.py b/veilid-python/veilid/types.py index 7faa579a..4f1ea344 100644 --- a/veilid-python/veilid/types.py +++ b/veilid-python/veilid/types.py @@ -67,6 +67,9 @@ class DHTSchemaKind(StrEnum): DFLT = "DFLT" SMPL = "SMPL" +class SafetySelectionKind(StrEnum): + UNSAFE = "Unsafe" + SAFE = "Safe" #################################################################### @@ -357,7 +360,7 @@ class ValueData: def __lt__(self, other): if other is None: - return true + return True if self.data < other.data: return True if self.data > other.data: @@ -383,3 +386,61 @@ class ValueData: def to_json(self) -> dict: return self.__dict__ + + +#################################################################### + +class SafetySpec: + preferred_route: Optional[RouteId] + hop_count: int + stability: Stability + sequencing: Sequencing + + def __init__(self, preferred_route: Optional[RouteId], hop_count: int, stability: Stability, sequencing: Sequencing): + self.preferred_route = preferred_route + self.hop_count = hop_count + self.stability = stability + self.sequencing = sequencing + + @classmethod + def from_json(cls, j: dict) -> Self: + return cls(RouteId(j["preferred_route"]) if "preferred_route" in j else None, + j["hop_count"], + Stability(j["stability"]), + Sequencing(j["sequencing"])) + + def to_json(self) -> dict: + return self.__dict__ + +class SafetySelection: + kind: SafetySelectionKind + + def __init__(self, kind: SafetySelectionKind, **kwargs): + self.kind = kind + for k, v in kwargs.items(): + setattr(self, k, v) + + @classmethod + def unsafe(cls, sequencing: Sequencing) -> Self: + return cls(SafetySelectionKind.UNSAFE, sequencing=sequencing) + + @classmethod + def safe(cls, safety_spec: SafetySpec) -> Self: + return cls(SafetySelectionKind.SAFE, safety_spec=safety_spec) + + @classmethod + def from_json(cls, j: dict) -> Self: + if "Safe" in j: + return cls.safe(SafetySpec.from_json(j["Safe"])) + elif "Unsafe" in j: + return cls.unsafe(Sequencing(j["Unsafe"])) + raise Exception("Invalid SafetySelection") + + def to_json(self) -> dict: + if self.kind == SafetySelectionKind.UNSAFE: + return {"Unsafe": self.sequencing } + elif self.kind == SafetySelectionKind.SAFE: + return {"Safe": self.safety_spec.to_json() } + else: + raise Exception("Invalid SafetySelection") +