feat: integrate meshservice crate into workspace
- Add meshservice to workspace members - Fix quicprochat-client: add MeshTrace/MeshStats slash commands - Add integration test: meshservice_tcp_transport - Document integration points in README and docs/status.md - Verify shared identity (IdentityKeypair → MeshAddress)
This commit is contained in:
@@ -19,6 +19,19 @@ A generic decentralized service layer for mesh networks. Build any peer-to-peer
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## QuicProChat / quicprochat-p2p
|
||||
|
||||
This crate lives in the **product.quicproquo** workspace. Integration with the mesh stack:
|
||||
|
||||
- **Ed25519 seed**: `MeshIdentity::seed_bytes()` matches `ServiceIdentity::from_secret(&seed)` (same `ed25519-dalek` derivation as `quicprochat_core::IdentityKeypair`); truncated mesh address is SHA-256(pubkey)[0..16] in both layers.
|
||||
- **Example transport**: integration test `crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs` sends `wire::encode(ServiceMessage)` over `TcpTransport` (length-prefixed framing). For iroh/production, embed the same bytes in `MeshEnvelope` on ALPN `quicprochat/mesh/1`.
|
||||
|
||||
Run the test from the repo root:
|
||||
|
||||
```bash
|
||||
cargo test -p quicprochat-p2p --test meshservice_tcp_transport
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Generic Protocol**: Any service can be built on top (therapy appointments, housing, repairs, tutoring...)
|
||||
|
||||
@@ -119,6 +119,8 @@ pub enum Command {
|
||||
MeshRoute,
|
||||
MeshIdentity,
|
||||
MeshStore,
|
||||
MeshTrace { address: String },
|
||||
MeshStats,
|
||||
|
||||
// Security / crypto
|
||||
Verify { username: String },
|
||||
@@ -187,6 +189,8 @@ impl Command {
|
||||
Command::MeshRoute => Some(SlashCommand::MeshRoute),
|
||||
Command::MeshIdentity => Some(SlashCommand::MeshIdentity),
|
||||
Command::MeshStore => Some(SlashCommand::MeshStore),
|
||||
Command::MeshTrace { address } => Some(SlashCommand::MeshTrace { address }),
|
||||
Command::MeshStats => Some(SlashCommand::MeshStats),
|
||||
Command::Verify { username } => Some(SlashCommand::Verify { username }),
|
||||
Command::UpdateKey => Some(SlashCommand::UpdateKey),
|
||||
Command::Typing => Some(SlashCommand::Typing),
|
||||
@@ -348,6 +352,8 @@ fn slash_to_command(sc: SlashCommand) -> Command {
|
||||
SlashCommand::MeshRoute => Command::MeshRoute,
|
||||
SlashCommand::MeshIdentity => Command::MeshIdentity,
|
||||
SlashCommand::MeshStore => Command::MeshStore,
|
||||
SlashCommand::MeshTrace { address } => Command::MeshTrace { address },
|
||||
SlashCommand::MeshStats => Command::MeshStats,
|
||||
SlashCommand::Verify { username } => Command::Verify { username },
|
||||
SlashCommand::UpdateKey => Command::UpdateKey,
|
||||
SlashCommand::Typing => Command::Typing,
|
||||
@@ -415,6 +421,8 @@ async fn execute_slash(
|
||||
SlashCommand::MeshRoute => cmd_mesh_route(session),
|
||||
SlashCommand::MeshIdentity => cmd_mesh_identity(session),
|
||||
SlashCommand::MeshStore => cmd_mesh_store(session),
|
||||
SlashCommand::MeshTrace { address } => cmd_mesh_trace(session, &address),
|
||||
SlashCommand::MeshStats => cmd_mesh_stats(session),
|
||||
SlashCommand::Verify { username } => cmd_verify(session, client, &username).await,
|
||||
SlashCommand::UpdateKey => cmd_update_key(session, client).await,
|
||||
SlashCommand::Typing => cmd_typing(session, client).await,
|
||||
|
||||
@@ -434,6 +434,10 @@ impl PlaybookRunner {
|
||||
"mesh-route" => Ok(Command::MeshRoute),
|
||||
"mesh-identity" | "mesh-id" => Ok(Command::MeshIdentity),
|
||||
"mesh-store" => Ok(Command::MeshStore),
|
||||
"mesh-trace" => Ok(Command::MeshTrace {
|
||||
address: self.resolve_str(&step.args, "address")?,
|
||||
}),
|
||||
"mesh-stats" => Ok(Command::MeshStats),
|
||||
|
||||
other => bail!("unknown command: {other}"),
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ humantime-serde = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
meshservice = { path = "../meshservice" }
|
||||
|
||||
[[example]]
|
||||
name = "fapp_demo"
|
||||
|
||||
73
crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs
Normal file
73
crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Integration: [`meshservice`] wire payloads over [`quicprochat_p2p::transport_tcp::TcpTransport`].
|
||||
//!
|
||||
//! Demonstrates that the same Ed25519 seed backs both [`MeshIdentity`] (P2P) and
|
||||
//! [`meshservice::identity::ServiceIdentity`], so service-layer signatures verify after
|
||||
//! hop-across-TCP. Production mesh would use [`MeshEnvelope`] / iroh; this test keeps
|
||||
//! the transport boundary explicit.
|
||||
|
||||
use meshservice::capabilities;
|
||||
use meshservice::identity::ServiceIdentity;
|
||||
use meshservice::router::ServiceRouter;
|
||||
use meshservice::services::fapp::{create_announce, FappService, Modality, SlotAnnounce, Specialism};
|
||||
use meshservice::wire;
|
||||
use quicprochat_p2p::address::MeshAddress;
|
||||
use quicprochat_p2p::identity::MeshIdentity;
|
||||
use quicprochat_p2p::transport::MeshTransport;
|
||||
use quicprochat_p2p::transport_tcp::TcpTransport;
|
||||
|
||||
#[tokio::test]
|
||||
async fn meshservice_fapp_over_tcp_roundtrip() {
|
||||
let seed = [0x5eu8; 32];
|
||||
let mesh = MeshIdentity::from_seed(seed);
|
||||
let service = ServiceIdentity::from_secret(&seed);
|
||||
|
||||
assert_eq!(mesh.public_key(), service.public_key());
|
||||
assert_eq!(
|
||||
*MeshAddress::from_public_key(&mesh.public_key()).as_bytes(),
|
||||
service.address()
|
||||
);
|
||||
|
||||
let announce = SlotAnnounce::new(
|
||||
&[Specialism::CognitiveBehavioral],
|
||||
Modality::VideoCall,
|
||||
"803",
|
||||
)
|
||||
.with_slots(2);
|
||||
|
||||
let msg = create_announce(&service, &announce, 1).expect("create_announce");
|
||||
let frame = wire::encode(&msg).expect("wire encode");
|
||||
|
||||
let transport = TcpTransport::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("bind tcp");
|
||||
let dest = transport.transport_addr();
|
||||
|
||||
let recv = tokio::spawn(async move { transport.recv().await.expect("recv") });
|
||||
let send_transport = TcpTransport::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("bind sender");
|
||||
send_transport
|
||||
.send(&dest, &frame)
|
||||
.await
|
||||
.expect("send");
|
||||
|
||||
let packet = recv.await.expect("join recv");
|
||||
let decoded = wire::decode(&packet.data).expect("wire decode");
|
||||
assert!(decoded.verify(&service.public_key()));
|
||||
assert_eq!(decoded.service_id, meshservice::service_ids::FAPP);
|
||||
|
||||
let mut router = ServiceRouter::new(capabilities::RELAY);
|
||||
router.register(Box::new(FappService::relay()));
|
||||
let action = router
|
||||
.handle(decoded, Some(service.public_key()))
|
||||
.expect("router handle");
|
||||
assert!(
|
||||
matches!(
|
||||
action,
|
||||
meshservice::router::ServiceAction::Store
|
||||
| meshservice::router::ServiceAction::StoreAndForward
|
||||
),
|
||||
"unexpected action: {action:?}"
|
||||
);
|
||||
assert!(!router.store().is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user