From c7a5d68114385044f6cd321c9a3730ea369cbfc8 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:53:12 -0700 Subject: [PATCH] Add publish to AccountDirectory --- Cargo.lock | 1 + core/account/src/account.rs | 58 +++++++- .../examples/message-exchange/main.rs | 16 +-- crates/generic-chat/src/builder.rs | 6 +- crates/generic-chat/src/client.rs | 68 +++++----- crates/generic-chat/src/delegate.rs | 55 ++++---- crates/generic-chat/src/event.rs | 6 +- crates/generic-chat/tests/group_v2.rs | 126 ++++++++++-------- crates/generic-chat/tests/saro_and_raya.rs | 85 +++++------- crates/logos-chat/src/logos.rs | 10 +- 10 files changed, 241 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e7467b..129e132 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,6 +1317,7 @@ dependencies = [ "clap", "crossbeam-channel", "crossterm 0.29.0", + "hex", "logos-account", "logos-chat", "ratatui", diff --git a/core/account/src/account.rs b/core/account/src/account.rs index 9e9fd78..c01408e 100644 --- a/core/account/src/account.rs +++ b/core/account/src/account.rs @@ -10,8 +10,9 @@ use std::{ use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; use crate::{ - AccountAddr, AccountEntry, AccountError, AccountLog, AccountRegistry, EntryData, - SignedAccountLog, verify_extension, verify_log, + AccountAddr, AccountDirectory, AccountEntry, AccountError, AccountLog, AccountRegistry, + EntryData, Lamport, SignedAccountLog, SignedDeviceBundle, encode_bundle_payload, + verify_extension, verify_log, }; /// Logs "published" by accounts, keyed by address. @@ -87,7 +88,7 @@ impl TestLogosAccount { TestAccountService::new().account() } - fn with_service(service: TestAccountService) -> Self { + pub fn with_service(service: TestAccountService) -> Self { let signing_key = Ed25519SigningKey::generate(); let addr = AccountAddr::from(&signing_key.verifying_key()); Self { @@ -112,7 +113,50 @@ impl TestLogosAccount { &self.addr } - pub fn endorse_ed25519_signer( + /// Endorse `key` on this account: append it to the log, sign the log whole, + /// and publish it. + /// + /// `directory` is transitional — clients still resolve an account through + /// [`AccountDirectory`] rather than reading endorsements from + /// [`AccountRegistry`], so every endorsement also mirrors the account's live + /// key set there as a signed device bundle. The parameter goes away once + /// they read the registry. + pub fn endorse_ed25519_signer( + &mut self, + directory: &mut D, + key: &Ed25519VerifyingKey, + ) -> Result<(), AccountError> { + self.append_ed25519_endorsement(key)?; + + // TODO: delete with the directory — the `directory` parameter, everything + // below, and the bundle imports; `append_ed25519_endorsement` is what's left. + let devices = self + .log + .live_entries() + .iter() + .map(|data| match data { + EntryData::Ed25519Key(bytes) => Ed25519VerifyingKey::from_bytes(bytes) + .map_err(|_| AccountError::Generic("endorsed key is invalid".into())), + }) + .collect::, _>>()?; + + // Every endorsement appends an entry, so the log's length is a version + // that only ever climbs — what the directory demands of a republish. + let payload = encode_bundle_payload(self.log.entries().len() as Lamport, &devices); + let bundle = SignedDeviceBundle { + account_pub: self.signing_key.verifying_key(), + signature: self.signing_key.sign(&payload), + payload, + }; + directory + .publish(&bundle) + .map_err(|e| AccountError::Generic(e.to_string())) + } + + /// The endorsement itself: extend the log, sign it, publish it to the + /// account service. What [`Self::endorse_ed25519_signer`] reduces to once + /// the directory mirror is gone. + fn append_ed25519_endorsement( &mut self, key: &Ed25519VerifyingKey, ) -> Result<(), AccountError> { @@ -148,7 +192,7 @@ mod tests { let mut account = srv.account(); let dev = device(); - account.endorse_ed25519_signer(&dev).unwrap(); + account.append_ed25519_endorsement(&dev).unwrap(); assert!(srv.is_ed25519_endorsed(&dev, account.address()).unwrap()); assert!( @@ -176,8 +220,8 @@ mod tests { let mut account = srv.account(); let (a, b) = (device(), device()); - account.endorse_ed25519_signer(&a).unwrap(); - account.endorse_ed25519_signer(&b).unwrap(); + account.append_ed25519_endorsement(&a).unwrap(); + account.append_ed25519_endorsement(&b).unwrap(); let keys = srv .endorsed_ed25519_keys(account.address()) diff --git a/crates/generic-chat/examples/message-exchange/main.rs b/crates/generic-chat/examples/message-exchange/main.rs index b3e3c23..2b4d7e3 100644 --- a/crates/generic-chat/examples/message-exchange/main.rs +++ b/crates/generic-chat/examples/message-exchange/main.rs @@ -9,21 +9,21 @@ fn main() { let bus = MessageBus::default(); let mut reg = EphemeralRegistry::new(); - // Mint two accounts, each with a delegate signer, and publish their device - // bundles so a peer can resolve an account address to its device. - let saro_account = TestLogosAccount::new(); + // Mint two accounts, each endorsing a delegate signer, so a peer can resolve + // an account address to its device. + let mut saro_account = TestLogosAccount::new(); let saro_delegate = DelegateSigner::random(); saro_account - .add_delegate_signer(&mut reg, saro_delegate.public_key()) + .endorse_ed25519_signer(&mut reg, saro_delegate.public_key()) .unwrap(); - let raya_account = TestLogosAccount::new(); + let mut raya_account = TestLogosAccount::new(); let raya_delegate = DelegateSigner::random(); raya_account - .add_delegate_signer(&mut reg, raya_delegate.public_key()) + .endorse_ed25519_signer(&mut reg, raya_delegate.public_key()) .unwrap(); - let (mut saro, saro_events) = ChatClientBuilder::new(saro_account.address()) + let (mut saro, saro_events) = ChatClientBuilder::new(saro_account.address().to_bytes()) .auth(LogosAuthVerifier::new()) .ident(saro_delegate) .transport(InProcessDelivery::new(bus.clone())) @@ -31,7 +31,7 @@ fn main() { .build() .unwrap(); - let (mut raya, raya_events) = ChatClientBuilder::new(raya_account.address()) + let (mut raya, raya_events) = ChatClientBuilder::new(raya_account.address().to_bytes()) .auth(LogosAuthVerifier::new()) .ident(raya_delegate) .transport(InProcessDelivery::new(bus)) diff --git a/crates/generic-chat/src/builder.rs b/crates/generic-chat/src/builder.rs index 79ffb32..bc5db77 100644 --- a/crates/generic-chat/src/builder.rs +++ b/crates/generic-chat/src/builder.rs @@ -19,7 +19,7 @@ pub struct Unset; pub struct ChatClientBuilder { ident: I, auth: AS, - account: String, + account: Vec, transport: T, registration: R, storage: S, @@ -28,14 +28,14 @@ pub struct ChatClientBuilder) -> Self { + pub fn new(account: impl Into>) -> Self { Self { ident: Unset, auth: Unset, diff --git a/crates/generic-chat/src/client.rs b/crates/generic-chat/src/client.rs index b31d5ca..6dcc7b5 100644 --- a/crates/generic-chat/src/client.rs +++ b/crates/generic-chat/src/client.rs @@ -34,7 +34,9 @@ impl AuthVerifyService for LogosAuthVerifier { } type ClientCore = Core<(DelegateIdentity, T, R, ThreadedWakeupService, S)>; -type AccountAddressRef<'a> = &'a str; +/// An account address as the client handles it: opaque bytes, interpreted only +/// where they meet the account layer. +type AccountAddressRef<'a> = &'a [u8]; type LocalSignerId = IdentId; /// A member of a group conversation's roster. @@ -52,7 +54,7 @@ type LocalSignerId = IdentId; /// never commits stays pending for the life of the conversation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GroupMember { - pub account: Option, + pub account: Option>, pub local_identity: IdentId, pub pending: bool, } @@ -91,8 +93,8 @@ impl MemberWithAuthResult { /// The account this member's credential claims, if any. Trustworthy only /// when `auth_result` is `Valid`; the credential asserts it, unverified. - pub fn account_claim(&self) -> Option { - self.credential()?.account_addr().map(str::to_owned) + pub fn account_claim(&self) -> Option> { + self.credential()?.account_addr().map(<[u8]>::to_vec) } } @@ -107,7 +109,7 @@ impl From for GroupMember { .map(|c| IdentId::new(hex::encode(c.delegate_id().as_ref()))) .unwrap_or_else(|| IdentId::new(hex::encode(member.signer_id.as_bytes()))); let account = (member.auth_result == AuthResult::Valid) - .then(|| cred.and_then(|c| c.account_addr().map(|a| IdentId::new(a.to_string())))) + .then(|| cred.and_then(|c| c.account_addr().map(<[u8]>::to_vec))) .flatten(); GroupMember { account, @@ -175,7 +177,7 @@ where /// Dropped on `Drop` to wake the worker's `select!` and shut it down. shutdown: Option>, worker: Option>, - address: String, + address: Vec, } // -- GenericChatClient @@ -189,7 +191,7 @@ where pub fn new( ident: DelegateSigner, auth: AS, - account: String, + account: Vec, mut transport: T, reg: R, storage: S, @@ -201,8 +203,7 @@ where let wakeup_service = ThreadedWakeupService::new(wakeup_tx); let directory = reg.clone(); let ident = DelegateIdentity::new(ident, &account); - let mut core = - Core::new_with_name(ident, transport, reg, wakeup_service, storage)?; + let mut core = Core::new_with_name(ident, transport, reg, wakeup_service, storage)?; if let Some(config) = group_v2 { core.set_group_v2_config(config); } @@ -215,7 +216,7 @@ where core: ClientCore, auth: AS, directory: R, - address: String, + address: Vec, inbound: Receiver>, wakeup_events: Receiver, ) -> (Self, Receiver) { @@ -252,7 +253,7 @@ where } /// The account address peers use to reach this client. - pub fn addr(&self) -> &str { + pub fn addr(&self) -> &[u8] { &self.address } @@ -385,7 +386,9 @@ where &self, account: AccountAddressRef, ) -> Result, ClientError> { - let account = IdentId::new(account.to_string()); + // The directory keys accounts by the hex of the account key, so the + // address bytes are encoded at that boundary. + let account = IdentId::new(hex::encode(account)); let device_ids = resolve_device_ids(&self.directory, &account) .map_err(|e| ClientError::AccountResolution(e.to_string()))?; Ok(device_ids.into_iter().map(IdentId::new).collect()) @@ -500,9 +503,9 @@ fn events_from_inbound(result: PayloadOutcome, directory: &impl AccountDirectory } } -/// Interpret a hex account address as an Ed25519 account verifying key. -fn account_key_from_hex(addr: &str) -> Option { - let bytes: [u8; 32] = hex::decode(addr).ok()?.try_into().ok()?; +/// Interpret account address bytes as an Ed25519 account verifying key. +fn account_key_from_bytes(addr: &[u8]) -> Option { + let bytes: [u8; 32] = addr.try_into().ok()?; Ed25519VerifyingKey::from_bytes(&bytes).ok() } @@ -516,7 +519,7 @@ enum SenderError { NotHex, /// Credential bytes did not decode to a delegate credential. Malformed, - /// The claimed account address is not an Ed25519 verifying key. + /// The claimed account address is not the bytes of an Ed25519 verifying key. AccountNotAKey, /// The account → device mapping is wrong or could not be confirmed: the /// device is not in the account's published set, the account published none, @@ -529,7 +532,7 @@ enum AccountClaim { /// The credential claimed no account. None, /// Confirmed: the directory lists this device under the claimed account. - Verified(IdentId), + Verified(Vec), /// An account was claimed but could not be confirmed (see [`SenderError`]). Unverified(SenderError), } @@ -561,8 +564,8 @@ fn parse_credential( let Some(account_addr) = cred.account_addr() else { return Ok((device, AccountClaim::None)); }; - let Some(account_key) = account_key_from_hex(account_addr) else { - tracing::warn!(account_addr, "account address is not a verifying key"); + let Some(account_key) = account_key_from_bytes(account_addr) else { + tracing::warn!(account_addr = %hex::encode(account_addr), "account address is not a verifying key"); return Ok(( device, AccountClaim::Unverified(SenderError::AccountNotAKey), @@ -570,10 +573,10 @@ fn parse_credential( }; let claim = match directory.fetch(&account_key) { Ok(Some(set)) if set.devices.iter().any(|d| d.as_str() == device.as_str()) => { - AccountClaim::Verified(IdentId::new(account_addr.to_string())) + AccountClaim::Verified(account_addr.to_vec()) } _ => { - tracing::warn!(account_addr, device = %device.as_str(), "account → device mapping is wrong or unconfirmable"); + tracing::warn!(account_addr = %hex::encode(account_addr), device = %device.as_str(), "account → device mapping is wrong or unconfirmable"); AccountClaim::Unverified(SenderError::Unverified) } }; @@ -739,6 +742,11 @@ mod sender_check_tests { IdentId::new(hex::encode(k.as_ref())) } + /// An account address as the client carries it: the raw key bytes. + fn account_addr(k: &Ed25519VerifyingKey) -> Vec { + k.as_ref().to_vec() + } + /// The account published a device set that includes the sending device — the /// claim checks out, so the message is delivered with a verified account. #[test] @@ -746,11 +754,11 @@ mod sender_check_tests { let account = key(); let device = key(); let dir = FakeDir::with_devices(&account, &[&device]); - let cred = DelegateCredential::associated(&device, &hex::encode(account.as_ref())); + let cred = DelegateCredential::associated(&device, account.as_ref()); assert_eq!( decode_sender(&dir, &encoded(cred)), Ok(MessageSender { - account: Some(local_id(&account)), + account: Some(account_addr(&account)), local_identity: local_id(&device), }) ); @@ -764,7 +772,7 @@ mod sender_check_tests { let endorsed = key(); let spoofer = key(); let dir = FakeDir::with_devices(&account, &[&endorsed]); - let cred = DelegateCredential::associated(&spoofer, &hex::encode(account.as_ref())); + let cred = DelegateCredential::associated(&spoofer, account.as_ref()); assert_eq!( decode_sender(&dir, &encoded(cred)), Err(SenderError::Unverified) @@ -793,7 +801,7 @@ mod sender_check_tests { let account = key(); let device = key(); let dir = FakeDir::default(); // nothing published - let cred = DelegateCredential::associated(&device, &hex::encode(account.as_ref())); + let cred = DelegateCredential::associated(&device, account.as_ref()); assert_eq!( decode_sender(&dir, &encoded(cred)), Err(SenderError::Unverified) @@ -810,7 +818,7 @@ mod sender_check_tests { fail: true, ..Default::default() }; - let cred = DelegateCredential::associated(&device, &hex::encode(account.as_ref())); + let cred = DelegateCredential::associated(&device, account.as_ref()); assert_eq!( decode_sender(&dir, &encoded(cred)), Err(SenderError::Unverified) @@ -841,7 +849,7 @@ mod sender_check_tests { #[test] fn non_key_account_address_is_dropped() { let dir = FakeDir::default(); - let cred = DelegateCredential::associated(&key(), "user@example.com"); + let cred = DelegateCredential::associated(&key(), b"user@example.com"); assert_eq!( decode_sender(&dir, &encoded(cred)), Err(SenderError::AccountNotAKey) @@ -870,11 +878,11 @@ mod sender_check_tests { fn resolves_verified_member_to_account_and_device() { let account = key(); let device = key(); - let cred = DelegateCredential::associated(&device, &hex::encode(account.as_ref())); + let cred = DelegateCredential::associated(&device, account.as_ref()); assert_eq!( GroupMember::from(member_entry(cred, AuthResult::Valid, false)), GroupMember { - account: Some(local_id(&account)), + account: Some(account_addr(&account)), local_identity: local_id(&device), pending: false, } @@ -887,7 +895,7 @@ mod sender_check_tests { fn resolves_unverified_member_to_device_without_account() { let account = key(); let device = key(); - let cred = DelegateCredential::associated(&device, &hex::encode(account.as_ref())); + let cred = DelegateCredential::associated(&device, account.as_ref()); assert_eq!( GroupMember::from(member_entry(cred, AuthResult::Mismatch, false)), GroupMember { diff --git a/crates/generic-chat/src/delegate.rs b/crates/generic-chat/src/delegate.rs index a8ea88c..6eed7a7 100644 --- a/crates/generic-chat/src/delegate.rs +++ b/crates/generic-chat/src/delegate.rs @@ -3,8 +3,6 @@ use libchat::{IdentId, IdentityProvider, trunc}; use crate::ClientError; -type AccountAddr = String; - /// A local signing identity that holds an Ed25519 keypair — the per-device /// (installation) signer. It knows nothing about accounts: the client composes /// the account association into the wire credential ([`DelegateIdentity`]). @@ -42,7 +40,7 @@ pub(crate) struct DelegateIdentity { } impl DelegateIdentity { - pub(crate) fn new(signer: DelegateSigner, account: &str) -> Self { + pub(crate) fn new(signer: DelegateSigner, account: &[u8]) -> Self { let credential = DelegateCredential::associated(signer.public_key(), account); Self { identifier: credential.into(), @@ -73,11 +71,12 @@ impl IdentityProvider for DelegateIdentity { /// /// Serialized as a TLV byte sequence prefixed with magic bytes `0x23 0x23`. /// A credential without an `account_addr` is *unassociated* — it identifies the -/// delegate key but has not yet been linked to an account. +/// delegate key but has not yet been linked to an account. The address is +/// carried as opaque bytes; only the account layer interprets them. #[derive(Debug)] pub struct DelegateCredential { delegate_id: Ed25519VerifyingKey, - account_addr: Option, + account_addr: Option>, } impl DelegateCredential { @@ -91,10 +90,10 @@ impl DelegateCredential { } } - pub fn associated(delegate: &Ed25519VerifyingKey, account: &str) -> Self { + pub fn associated(delegate: &Ed25519VerifyingKey, account: &[u8]) -> Self { Self { delegate_id: delegate.clone(), - account_addr: Some(account.to_string()), + account_addr: Some(account.to_vec()), } } @@ -105,7 +104,7 @@ impl DelegateCredential { /// The account this delegate claims to act for, if it is associated. The /// claim is unverified — confirm it against the account directory. - pub fn account_addr(&self) -> Option<&str> { + pub fn account_addr(&self) -> Option<&[u8]> { self.account_addr.as_deref() } @@ -120,13 +119,12 @@ impl DelegateCredential { data.extend_from_slice(&[Self::TAG_DELEGATE_ID, key_bytes.len() as u8]); data.extend_from_slice(key_bytes); if let Some(addr) = self.account_addr { - let addr_bytes = addr.as_bytes(); debug_assert!( - addr_bytes.len() <= 255, + addr.len() <= 255, "account_addr too large for 1-byte TLV length" ); - data.extend_from_slice(&[Self::TAG_ACCOUNT_ADDR, addr_bytes.len() as u8]); - data.extend_from_slice(addr_bytes); + data.extend_from_slice(&[Self::TAG_ACCOUNT_ADDR, addr.len() as u8]); + data.extend_from_slice(&addr); } data } @@ -167,10 +165,7 @@ impl TryFrom> for DelegateCredential { ); } DelegateCredential::TAG_ACCOUNT_ADDR => { - account_addr = Some( - String::from_utf8(v.to_vec()) - .map_err(|_| ClientError::BadlyFormedCredential)?, - ); + account_addr = Some(v.to_vec()); } _ => {} } @@ -218,7 +213,7 @@ mod tests { #[test] fn roundtrip_associated() { let key = test_key(); - let bytes = DelegateCredential::associated(&key, "user@example.com").serialize(); + let bytes = DelegateCredential::associated(&key, b"user@example.com").serialize(); let recovered: DelegateCredential = bytes.clone().try_into().unwrap(); assert_eq!(recovered.serialize(), bytes); } @@ -235,7 +230,7 @@ mod tests { #[test] fn ident_id_roundtrip_associated() { let key = test_key(); - let addr = "user@example.com"; + let addr = b"user@example.com"; let original = DelegateCredential::associated(&key, addr).serialize(); let ident_id: IdentId = DelegateCredential::associated(&key, addr).into(); let recovered: DelegateCredential = ident_id.try_into().unwrap(); @@ -245,12 +240,12 @@ mod tests { #[test] fn account_addr_preserved_across_roundtrip() { let key = test_key(); - let addr = "alice@libchat.example"; + let addr = b"alice@libchat.example"; let recovered: DelegateCredential = DelegateCredential::associated(&key, addr) .serialize() .try_into() .unwrap(); - assert_eq!(recovered.account_addr.as_deref(), Some(addr)); + assert_eq!(recovered.account_addr.as_deref(), Some(addr.as_slice())); } #[test] @@ -293,18 +288,16 @@ mod tests { )); } + /// An account address is opaque bytes — a raw key, not text — so bytes that + /// are not valid UTF-8 survive the round trip verbatim. #[test] - fn invalid_utf8_account_addr_rejected() { + fn non_utf8_account_addr_roundtrips() { let key = test_key(); - // Build a valid credential then corrupt the account_addr bytes - let mut bytes = DelegateCredential::unassociated(&key).serialize(); - // Append a TAG_ACCOUNT_ADDR field with invalid UTF-8 - bytes.push(DelegateCredential::TAG_ACCOUNT_ADDR); - bytes.push(3); // len - bytes.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // invalid UTF-8 - assert!(matches!( - DelegateCredential::try_from(bytes), - Err(ClientError::BadlyFormedCredential) - )); + let addr = [0xFFu8, 0xFE, 0xFD]; + let recovered: DelegateCredential = DelegateCredential::associated(&key, &addr) + .serialize() + .try_into() + .unwrap(); + assert_eq!(recovered.account_addr.as_deref(), Some(addr.as_slice())); } } diff --git a/crates/generic-chat/src/event.rs b/crates/generic-chat/src/event.rs index 4618e02..fd6a402 100644 --- a/crates/generic-chat/src/event.rs +++ b/crates/generic-chat/src/event.rs @@ -15,11 +15,11 @@ use libchat::{ConversationClass, IdentId}; /// `account` is present only when the sender associated an account *and* the /// account → device directory confirmed this device belongs to it — spoofed or /// unconfirmable claims never reach the application, so a `Some` account is -/// always verified. `local_identity` is the sending device (delegate key), -/// hex-encoded. +/// always verified. It carries the account address bytes. `local_identity` is +/// the sending device (delegate key), hex-encoded. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MessageSender { - pub account: Option, + pub account: Option>, pub local_identity: IdentId, } diff --git a/crates/generic-chat/tests/group_v2.rs b/crates/generic-chat/tests/group_v2.rs index aa5f6f7..9b5847d 100644 --- a/crates/generic-chat/tests/group_v2.rs +++ b/crates/generic-chat/tests/group_v2.rs @@ -36,13 +36,13 @@ fn fast_group_v2_config() -> GroupV2Config { type TestClient = ChatClient; -/// A client for a fresh account: mints the account and a delegate, publishes -/// the endorsing bundle, and builds the client on the shared bus/registry with -/// the fast GroupV2 timers. Returns the account address peers invite by. +/// A client for a fresh account: mints the account and a delegate, endorses the +/// delegate on the account, and builds the client on the shared bus/registry +/// with the fast GroupV2 timers. Returns the account address peers invite by. fn create_test_client( message_bus: MessageBus, reg: EphemeralRegistry, -) -> (TestClient, Receiver, String) { +) -> (TestClient, Receiver, Vec) { create_test_client_with(message_bus, reg, fast_group_v2_config()) } @@ -52,13 +52,13 @@ fn create_test_client_with( message_bus: MessageBus, mut reg: EphemeralRegistry, config: GroupV2Config, -) -> (TestClient, Receiver, String) { - let account = TestLogosAccount::new(); +) -> (TestClient, Receiver, Vec) { + let mut account = TestLogosAccount::new(); let delegate = DelegateSigner::random(); account - .add_delegate_signer(&mut reg, delegate.public_key()) + .endorse_ed25519_signer(&mut reg, delegate.public_key()) .unwrap(); - let (client, events) = ChatClientBuilder::new(account.address()) + let (client, events) = ChatClientBuilder::new(account.address().to_bytes()) .auth(LogosAuthVerifier::new()) .ident(delegate) .transport(InProcessDelivery::new(message_bus)) @@ -66,7 +66,7 @@ fn create_test_client_with( .group_v2_config(config) .build() .expect("client create"); - let addr = client.addr().to_string(); + let addr = client.addr().to_vec(); (client, events, addr) } @@ -108,16 +108,13 @@ fn wait_for_group_started(events: &Receiver, label: &str) -> String { /// roster settles asynchronously as each member applies the add commit, so it is /// polled rather than snapshotted; members still awaiting that commit are /// skipped so an invite alone never reads as convergence. -fn wait_for_members(client: &mut TestClient, convo_id: &str, expected: &[&str]) { +fn wait_for_members(client: &mut TestClient, convo_id: &str, expected: &[&[u8]]) { use std::collections::BTreeSet; - let want: BTreeSet = expected.iter().map(|s| s.to_string()).collect(); + let want: BTreeSet> = expected.iter().map(|a| a.to_vec()).collect(); let deadline = std::time::Instant::now() + Duration::from_secs(10); loop { let roster = client.group_members(convo_id).expect("group_members"); - let got: BTreeSet = roster - .iter() - .filter_map(|m| m.account.as_ref().map(|a| a.as_str().to_string())) - .collect(); + let got: BTreeSet> = roster.iter().filter_map(|m| m.account.clone()).collect(); if got == want { return; } @@ -129,14 +126,14 @@ fn wait_for_members(client: &mut TestClient, convo_id: &str, expected: &[&str]) } /// Wait for `content` to arrive and return the sender's verified account. -fn wait_for_message(events: &Receiver, content: &[u8]) -> Option { +fn wait_for_message(events: &Receiver, content: &[u8]) -> Option> { let label = format!("MessageReceived({})", String::from_utf8_lossy(content)); wait_for_event(events, &label, Duration::from_secs(10), |e| match e { Event::MessageReceived { content: got, sender, .. - } if got == content => Some(sender.account.as_ref().map(|a| a.as_str().to_string())), + } if got == content => Some(sender.account.clone()), _ => None, }) } @@ -154,7 +151,7 @@ fn group_v2_three_members() { let (mut pax, pax_events, pax_addr) = create_test_client(bus.clone(), reg.clone()); let convo_id = saro - .create_group_conversation(&[&raya_addr], unnamed_group()) + .create_group_conversation(&[raya_addr.as_slice()], unnamed_group()) .expect("saro create group"); // The invite lands once saro's steward commit finalizes (wakeup-driven); @@ -163,24 +160,32 @@ fn group_v2_three_members() { assert_eq!(raya_convo_id, convo_id); // Both sides see the two-account roster once the add commits. - wait_for_members(&mut saro, &convo_id, &[&saro_addr, &raya_addr]); - wait_for_members(&mut raya, &raya_convo_id, &[&saro_addr, &raya_addr]); + wait_for_members( + &mut saro, + &convo_id, + &[saro_addr.as_slice(), raya_addr.as_slice()], + ); + wait_for_members( + &mut raya, + &raya_convo_id, + &[saro_addr.as_slice(), raya_addr.as_slice()], + ); saro.send_message(&convo_id, b"hello raya").unwrap(); assert_eq!( wait_for_message(&raya_events, b"hello raya").as_deref(), - Some(saro_addr.as_str()) + Some(saro_addr.as_slice()) ); raya.send_message(&raya_convo_id, b"hi saro").unwrap(); assert_eq!( wait_for_message(&saro_events, b"hi saro").as_deref(), - Some(raya_addr.as_str()) + Some(raya_addr.as_slice()) ); // A non-creator grows the group: raya proposes pax, the steward commits, // and raya (who holds the pending invite) routes the welcome to pax. - raya.add_group_members(&raya_convo_id, &[&pax_addr]) + raya.add_group_members(&raya_convo_id, &[pax_addr.as_slice()]) .expect("raya add pax"); let pax_convo_id = wait_for_group_started(&pax_events, "pax ConversationStarted"); assert_eq!(pax_convo_id, convo_id); @@ -190,25 +195,29 @@ fn group_v2_three_members() { saro.send_message(&convo_id, b"all three?").unwrap(); assert_eq!( wait_for_message(&raya_events, b"all three?").as_deref(), - Some(saro_addr.as_str()) + Some(saro_addr.as_slice()) ); assert_eq!( wait_for_message(&pax_events, b"all three?").as_deref(), - Some(saro_addr.as_str()) + Some(saro_addr.as_slice()) ); pax.send_message(&pax_convo_id, b"pax is in").unwrap(); assert_eq!( wait_for_message(&saro_events, b"pax is in").as_deref(), - Some(pax_addr.as_str()) + Some(pax_addr.as_slice()) ); assert_eq!( wait_for_message(&raya_events, b"pax is in").as_deref(), - Some(pax_addr.as_str()) + Some(pax_addr.as_slice()) ); // All three rosters converge on the same three accounts. - let all = [saro_addr.as_str(), raya_addr.as_str(), pax_addr.as_str()]; + let all = [ + saro_addr.as_slice(), + raya_addr.as_slice(), + pax_addr.as_slice(), + ]; wait_for_members(&mut saro, &convo_id, &all); wait_for_members(&mut raya, &raya_convo_id, &all); wait_for_members(&mut pax, &pax_convo_id, &all); @@ -238,8 +247,11 @@ fn peers_invited_to_many_groups() { let mut convo_ids = Vec::new(); for _ in 0..GROUPS { convo_ids.push( - saro.create_group_conversation(&[&raya_addr, &pax_addr], unnamed_group()) - .expect("saro create group"), + saro.create_group_conversation( + &[raya_addr.as_slice(), pax_addr.as_slice()], + unnamed_group(), + ) + .expect("saro create group"), ); } @@ -256,11 +268,11 @@ fn peers_invited_to_many_groups() { saro.send_message(convo_id, &msg).unwrap(); assert_eq!( wait_for_message(&raya_events, &msg).as_deref(), - Some(saro_addr.as_str()) + Some(saro_addr.as_slice()) ); assert_eq!( wait_for_message(&pax_events, &msg).as_deref(), - Some(saro_addr.as_str()) + Some(saro_addr.as_slice()) ); } @@ -280,10 +292,7 @@ fn group_creator_is_in_own_roster() { .create_group_conversation(&[], unnamed_group()) .expect("empty group"); let roster = saro.group_members(&convo_id).expect("group_members"); - let accounts: Vec> = roster - .iter() - .map(|m| m.account.as_ref().map(|a| a.as_str().to_string())) - .collect(); + let accounts: Vec>> = roster.iter().map(|m| m.account.clone()).collect(); assert_eq!(accounts, vec![Some(saro_addr.clone())]); } @@ -307,19 +316,19 @@ fn invited_member_is_pending_until_the_group_commits() { let convo_id = saro .create_group_conversation(&[], unnamed_group()) .expect("empty group"); - saro.add_group_members(&convo_id, &[&raya_addr]) + saro.add_group_members(&convo_id, &[raya_addr.as_slice()]) .expect("saro invites raya"); let roster = saro.group_members(&convo_id).expect("group_members"); - let accounts = |pending: bool| -> Vec<&str> { + let accounts = |pending: bool| -> Vec<&[u8]> { roster .iter() .filter(|m| m.pending == pending) - .filter_map(|m| m.account.as_ref().map(|a| a.as_str())) + .filter_map(|m| m.account.as_deref()) .collect() }; - assert_eq!(accounts(false), vec![saro_addr.as_str()]); - assert_eq!(accounts(true), vec![raya_addr.as_str()]); + assert_eq!(accounts(false), vec![saro_addr.as_slice()]); + assert_eq!(accounts(true), vec![raya_addr.as_slice()]); } /// The pending flag is transient: once the group commits the add, the invitee @@ -337,11 +346,15 @@ fn pending_clears_once_the_add_commits() { let convo_id = saro .create_group_conversation(&[], unnamed_group()) .expect("empty group"); - saro.add_group_members(&convo_id, &[&raya_addr]) + saro.add_group_members(&convo_id, &[raya_addr.as_slice()]) .expect("saro invites raya"); let raya_convo_id = wait_for_group_started(&raya_events, "raya ConversationStarted"); - wait_for_members(&mut saro, &convo_id, &[&saro_addr, &raya_addr]); + wait_for_members( + &mut saro, + &convo_id, + &[saro_addr.as_slice(), raya_addr.as_slice()], + ); let roster = saro.group_members(&convo_id).expect("group_members"); assert!( @@ -371,21 +384,24 @@ fn add_batch_with_missing_key_package_invites_no_one() { let (_raya, raya_events, raya_addr) = create_test_client(bus.clone(), reg.clone()); let (_pax, pax_events, pax_addr) = create_test_client(bus.clone(), reg.clone()); - // Ghost: its account endorses a device in the directory, but that device - // never registered a key package (no client was built for it). - let ghost_account = TestLogosAccount::new(); + // Ghost: its account endorses a device, but that device never registered a + // key package (no client was built for it). + let mut ghost_account = TestLogosAccount::new(); let ghost_delegate = DelegateSigner::random(); ghost_account - .add_delegate_signer(&mut reg, ghost_delegate.public_key()) + .endorse_ed25519_signer(&mut reg, ghost_delegate.public_key()) .unwrap(); let convo_id = saro - .create_group_conversation(&[&raya_addr], unnamed_group()) + .create_group_conversation(&[raya_addr.as_slice()], unnamed_group()) .expect("saro create group"); wait_for_group_started(&raya_events, "raya ConversationStarted"); - saro.add_group_members(&convo_id, &[&ghost_account.address(), &pax_addr]) - .expect_err("ghost has no key package"); + saro.add_group_members( + &convo_id, + &[ghost_account.address().to_bytes(), pax_addr.as_slice()], + ) + .expect_err("ghost has no key package"); // Pax was in the failed batch and must not have been invited. assert!( @@ -410,8 +426,8 @@ fn group_invite_of_unpublished_account_is_an_error() { let unpublished = TestLogosAccount::new(); let err = saro - .create_group_conversation(&[&unpublished.address()], unnamed_group()) - .expect_err("no bundle published for the account"); + .create_group_conversation(&[unpublished.address().to_bytes()], unnamed_group()) + .expect_err("nothing published for the account"); assert!(matches!( err, logos_generic_chat::ClientError::AccountResolution(_) @@ -421,8 +437,8 @@ fn group_invite_of_unpublished_account_is_an_error() { .create_group_conversation(&[], unnamed_group()) .expect("empty group"); let err = saro - .add_group_members(&convo_id, &[&unpublished.address()]) - .expect_err("no bundle published for the account"); + .add_group_members(&convo_id, &[unpublished.address().to_bytes()]) + .expect_err("nothing published for the account"); assert!(matches!( err, logos_generic_chat::ClientError::AccountResolution(_) @@ -442,7 +458,7 @@ fn group_metadata_reaches_joiners() { let convo_id = saro .create_group_conversation( - &[&raya_addr], + &[raya_addr.as_slice()], GroupMetadata::new("Book Club", "Weekly reads"), ) .expect("saro create group"); diff --git a/crates/generic-chat/tests/saro_and_raya.rs b/crates/generic-chat/tests/saro_and_raya.rs index 16be174..7e89482 100644 --- a/crates/generic-chat/tests/saro_and_raya.rs +++ b/crates/generic-chat/tests/saro_and_raya.rs @@ -2,25 +2,14 @@ use std::time::Duration; use components::EphemeralRegistry; use crossbeam_channel::{Receiver, Sender}; -use crypto::Ed25519VerifyingKey; use logos_account::TestLogosAccount; use logos_generic_chat::{ AddressedEnvelope, ChatClient, ChatClientBuilder, ConversationClass, DelegateSigner, DeliveryService, Event, InProcessDelivery, LogosAuthVerifier, MessageBus, Transport, }; -/// Publish a signed device bundle endorsing `device` as a device of `account`, -/// so a receiver can verify the sender's account → device mapping. -fn publish_device_bundle( - reg: &mut EphemeralRegistry, - account: &TestLogosAccount, - device: &Ed25519VerifyingKey, -) { - account.add_delegate_signer(reg, device).unwrap(); -} - -/// A client for a fresh account: mints the account and a delegate, publishes -/// the endorsing bundle, and builds the client on the shared bus/registry. +/// A client for a fresh account: mints the account and a delegate, endorses the +/// delegate on the account, and builds the client on the shared bus/registry. #[allow(clippy::type_complexity)] fn create_test_client( message_bus: MessageBus, @@ -32,11 +21,13 @@ fn create_test_client( ), logos_generic_chat::ClientError, > { - let account = TestLogosAccount::new(); + let mut account = TestLogosAccount::new(); let delegate = DelegateSigner::random(); - publish_device_bundle(&mut reg, &account, delegate.public_key()); + account + .endorse_ed25519_signer(&mut reg, delegate.public_key()) + .unwrap(); let d = InProcessDelivery::new(message_bus); - ChatClientBuilder::new(account.address()) + ChatClientBuilder::new(account.address().to_bytes()) .auth(LogosAuthVerifier::new()) .ident(delegate) .transport(d) @@ -90,17 +81,19 @@ fn direct_v1_standalone_integration() { let mut reg_service = EphemeralRegistry::new(); - // Create accounts and delegates, and publish device bundles so the - // receiver can verify the account → device mapping carried in the + // Create accounts and delegates, and endorse each delegate on its account so + // the receiver can verify the account → device mapping carried in the // sender's credential. - let saro_account = TestLogosAccount::new(); - let saro_account_id = saro_account.address(); + let mut saro_account = TestLogosAccount::new(); + let saro_account_id = saro_account.address().to_bytes().to_vec(); let saro_delegate = DelegateSigner::random(); let saro_device_id = hex::encode(saro_delegate.public_key().as_ref()); - publish_device_bundle(&mut reg_service, &saro_account, saro_delegate.public_key()); + saro_account + .endorse_ed25519_signer(&mut reg_service, saro_delegate.public_key()) + .unwrap(); // Build saro's client with its account so its outbound messages carry a - // credential the receiver can verify against the published bundle. + // credential the receiver can verify against the endorsement. let (mut saro, _saro_events) = ChatClientBuilder::new(saro_account_id.clone()) .auth(LogosAuthVerifier::new()) .ident(saro_delegate) @@ -127,12 +120,9 @@ fn direct_v1_standalone_integration() { content, sender, .. } => { assert_eq!(content.as_slice(), b"Hey from saro"); - // saro associated an account and published a matching bundle, so the + // saro associated an account that endorses its delegate, so the // sender surfaces with a verified account and its device. - assert_eq!( - sender.account.as_ref().map(|a| a.as_str()), - Some(saro_account_id.as_str()) - ); + assert_eq!(sender.account.as_deref(), Some(saro_account_id.as_slice())); assert_eq!(sender.local_identity.as_str(), saro_device_id.as_str()); Ok(()) } @@ -150,10 +140,12 @@ fn direct_v1_by_account_address() { let bus = MessageBus::default(); let mut reg_service = EphemeralRegistry::new(); - let raya_account = TestLogosAccount::new(); - let raya_account_addr = raya_account.address(); + let mut raya_account = TestLogosAccount::new(); + let raya_account_addr = raya_account.address().to_bytes().to_vec(); let raya_delegate = DelegateSigner::random(); - publish_device_bundle(&mut reg_service, &raya_account, raya_delegate.public_key()); + raya_account + .endorse_ed25519_signer(&mut reg_service, raya_delegate.public_key()) + .unwrap(); let (mut raya, raya_events) = ChatClientBuilder::new(raya_account_addr.clone()) .auth(LogosAuthVerifier::new()) @@ -166,7 +158,7 @@ fn direct_v1_by_account_address() { create_test_client(bus.clone(), reg_service.clone()).expect("client create"); // Raya's shared address is her account address, not her signer id. - assert_eq!(raya.addr(), raya_account_addr.as_str()); + assert_eq!(raya.addr(), raya_account_addr); let convo_id = saro.create_direct_conversation(&raya_account_addr).unwrap(); // DirectV1 is the pairwise shape, so the joiner sees it classed Private even @@ -194,11 +186,11 @@ fn direct_v1_by_account_address() { content, sender, .. } => { assert_eq!(content.as_slice(), b"hi saro"); - // raya's bundle endorses her delegate, so her sender surfaces with + // raya's account endorses her delegate, so her sender surfaces with // the verified account. assert_eq!( - sender.account.as_ref().map(|a| a.as_str()), - Some(raya_account_addr.as_str()) + sender.account.as_deref(), + Some(raya_account_addr.as_slice()) ); Ok(()) } @@ -237,8 +229,8 @@ fn saro_raya_message_exchange() { } => { assert_eq!(convo_id, raya_convo_id); assert_eq!(content.as_slice(), b"hello raya"); - // saro's account published a bundle endorsing its delegate, so the - // sender surfaces a verified account. + // saro's account endorses its delegate, so the sender surfaces a + // verified account. assert!(sender.account.is_some()); assert!(!sender.local_identity.as_str().is_empty()); Ok(()) @@ -319,19 +311,16 @@ fn direct_conversation_lists_its_participants() { create_test_client(bus.clone(), reg.clone()).expect("client create"); let (raya, _raya_events) = create_test_client(bus.clone(), reg.clone()).expect("client create"); - let saro_addr = saro.addr().to_string(); - let raya_addr = raya.addr().to_string(); + let saro_addr = saro.addr().to_vec(); + let raya_addr = raya.addr().to_vec(); let convo_id = saro .create_direct_conversation(&raya_addr) .expect("convo create"); let roster = saro.group_members(&convo_id).expect("group_members"); - let mut accounts: Vec> = roster - .iter() - .map(|m| m.account.as_ref().map(|a| a.as_str())) - .collect(); + let mut accounts: Vec> = roster.iter().map(|m| m.account.as_deref()).collect(); accounts.sort(); - let mut expected = vec![Some(saro_addr.as_str()), Some(raya_addr.as_str())]; + let mut expected = vec![Some(saro_addr.as_slice()), Some(raya_addr.as_slice())]; expected.sort(); assert_eq!(accounts, expected); @@ -408,7 +397,7 @@ fn malformed_inbound_surfaces_as_error_event() { let delivery = FailingDelivery::new(); let inbound_tx = delivery.inbound_sender(); - let (_client, events) = ChatClientBuilder::new(TestLogosAccount::new().address()) + let (_client, events) = ChatClientBuilder::new(TestLogosAccount::new().address().to_bytes()) .auth(LogosAuthVerifier::new()) .transport(delivery) .build() @@ -426,7 +415,7 @@ fn malformed_inbound_surfaces_as_error_event() { } /// Opening a conversation by an address whose account never published a -/// device bundle fails at resolution, not with a late key-package miss. +/// device list fails at resolution, not with a late key-package miss. #[test] fn unpublished_account_address_is_an_error() { let bus = MessageBus::default(); @@ -437,15 +426,15 @@ fn unpublished_account_address_is_an_error() { let unpublished = TestLogosAccount::new(); let err = saro - .create_direct_conversation(&unpublished.address()) - .expect_err("no bundle published for the account"); + .create_direct_conversation(unpublished.address().to_bytes()) + .expect_err("nothing published for the account"); assert!(matches!( err, logos_generic_chat::ClientError::AccountResolution(_) )); let err = saro - .create_direct_conversation("not-an-account-address") + .create_direct_conversation(b"not-an-account-address") .expect_err("not an account key"); assert!(matches!( err, diff --git a/crates/logos-chat/src/logos.rs b/crates/logos-chat/src/logos.rs index 7a5dc14..e2e7ca1 100644 --- a/crates/logos-chat/src/logos.rs +++ b/crates/logos-chat/src/logos.rs @@ -134,10 +134,10 @@ pub fn open_with_transport( ClientError, > { // A fresh account endorsing a fresh delegate each open: the account - // key is dropped after publishing the bundle, so devices cannot be - // added later. A caller-supplied, custody-holding account replaces + // key is dropped after the endorsement, so devices cannot be added + // later. A caller-supplied, custody-holding account replaces // this once the platform provides one. - let account = TestLogosAccount::new(); + let mut account = TestLogosAccount::new(); let delegate = DelegateSigner::random(); let mut registry = ContactRegistry::new( transport.clone(), @@ -145,9 +145,9 @@ pub fn open_with_transport( config.registry_publish_mode, ); account - .add_delegate_signer(&mut registry, delegate.public_key()) + .endorse_ed25519_signer(&mut registry, delegate.public_key()) .map_err(|e| ClientError::BundlePublish(e.to_string()))?; - let mut builder = ChatClientBuilder::new(account.address()) + let mut builder = ChatClientBuilder::new(account.address().to_bytes()) .auth(LogosAuthVerifier::new()) .ident(delegate) .transport(transport)