From f460aea143a1aa1ab11aea2cf13952dd414f8441 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:54:39 -0700 Subject: [PATCH] Bootstrap associations --- Cargo.lock | 3 + bindings_ffi/Cargo.lock | 6 -- xmtp_id/Cargo.toml | 3 + xmtp_id/src/associations/hashes.rs | 12 +++ xmtp_id/src/associations/member.rs | 122 +++++++++++++++++++++++++ xmtp_id/src/associations/mod.rs | 8 ++ xmtp_id/src/associations/state.rs | 109 ++++++++++++++++++++++ xmtp_id/src/associations/test_utils.rs | 21 +++++ xmtp_id/src/lib.rs | 1 + 9 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 xmtp_id/src/associations/hashes.rs create mode 100644 xmtp_id/src/associations/member.rs create mode 100644 xmtp_id/src/associations/mod.rs create mode 100644 xmtp_id/src/associations/state.rs create mode 100644 xmtp_id/src/associations/test_utils.rs diff --git a/Cargo.lock b/Cargo.lock index fe7d560ac..34e9beb42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5789,13 +5789,16 @@ dependencies = [ "async-trait", "chrono", "futures", + "hex", "log", "openmls", "openmls_basic_credential", "openmls_rust_crypto", "openmls_traits", "prost 0.12.3", + "rand", "serde", + "sha2 0.10.8", "thiserror", "tracing", "xmtp_cryptography", diff --git a/bindings_ffi/Cargo.lock b/bindings_ffi/Cargo.lock index ce3dade1a..f835d820b 100644 --- a/bindings_ffi/Cargo.lock +++ b/bindings_ffi/Cargo.lock @@ -5383,7 +5383,6 @@ dependencies = [ "pbjson-types 0.5.1", "prost 0.12.3", "serde", - "serde_json", "tokio", "tonic", "tower", @@ -5474,9 +5473,7 @@ name = "xmtp_v2" version = "0.1.0" dependencies = [ "aes-gcm", - "chrono", "ecdsa 0.15.1", - "ethers-core", "generic-array", "getrandom", "hex", @@ -5484,11 +5481,8 @@ dependencies = [ "k256 0.12.0", "rand", "rand_chacha", - "rlp", - "serde", "sha2", "sha3", - "thiserror", ] [[package]] diff --git a/xmtp_id/Cargo.toml b/xmtp_id/Cargo.toml index 76532705a..6191d0fc6 100644 --- a/xmtp_id/Cargo.toml +++ b/xmtp_id/Cargo.toml @@ -21,4 +21,7 @@ chrono.workspace = true serde.workspace = true async-trait.workspace = true futures.workspace = true +sha2 = "0.10.8" +rand.workspace = true +hex.workspace = true diff --git a/xmtp_id/src/associations/hashes.rs b/xmtp_id/src/associations/hashes.rs new file mode 100644 index 000000000..2434f67e8 --- /dev/null +++ b/xmtp_id/src/associations/hashes.rs @@ -0,0 +1,12 @@ +use sha2::{Digest, Sha256}; + +pub fn sha256_string(input: String) -> String { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + format!("{:x}", result) +} + +pub fn generate_xid(account_address: &String, nonce: &u64) -> String { + sha256_string(format!("{}{}", account_address, nonce)) +} diff --git a/xmtp_id/src/associations/member.rs b/xmtp_id/src/associations/member.rs new file mode 100644 index 000000000..8f65687de --- /dev/null +++ b/xmtp_id/src/associations/member.rs @@ -0,0 +1,122 @@ +#[derive(Clone, Debug, PartialEq)] +pub enum MemberKind { + Installation, + Address, +} + +impl std::fmt::Display for MemberKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MemberKind::Installation => write!(f, "installation"), + MemberKind::Address => write!(f, "address"), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum MemberIdentifier { + Address(String), + Installation(Vec), +} + +impl MemberIdentifier { + pub fn to_string(&self) -> String { + match self { + MemberIdentifier::Address(address) => address.to_string(), + MemberIdentifier::Installation(installation) => hex::encode(installation), + } + } + + pub fn kind(&self) -> MemberKind { + match self { + MemberIdentifier::Address(_) => MemberKind::Address, + MemberIdentifier::Installation(_) => MemberKind::Installation, + } + } +} + +impl std::fmt::Display for MemberIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +impl From for MemberIdentifier { + fn from(address: String) -> Self { + MemberIdentifier::Address(address) + } +} + +impl From> for MemberIdentifier { + fn from(installation: Vec) -> Self { + MemberIdentifier::Installation(installation) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Member { + pub identifier: MemberIdentifier, + pub added_by_entity: Option, +} + +impl Member { + pub fn new(identifier: MemberIdentifier, added_by_entity: Option) -> Self { + Self { + identifier, + added_by_entity, + } + } + + pub fn kind(&self) -> MemberKind { + self.identifier.kind() + } +} + +impl PartialEq for Member { + fn eq(&self, other: &MemberIdentifier) -> bool { + self.identifier.eq(other) + } +} + +#[cfg(test)] +mod tests { + use crate::associations::test_utils; + + use super::*; + + use test_utils::rand_string; + + impl Default for MemberIdentifier { + fn default() -> Self { + MemberIdentifier::Address(rand_string()) + } + } + + impl Default for Member { + fn default() -> Self { + Self { + identifier: MemberIdentifier::default(), + added_by_entity: None, + } + } + } + + #[test] + fn test_identifier_comparisons() { + let address_1 = MemberIdentifier::Address("0x123".to_string()); + let address_2 = MemberIdentifier::Address("0x456".to_string()); + let address_1_copy = MemberIdentifier::Address("0x123".to_string()); + + assert!(address_1 != address_2); + assert!(address_1.ne(&address_2)); + assert!(address_1 == address_1_copy); + + let installation_1 = MemberIdentifier::Installation(vec![1, 2, 3]); + let installation_2 = MemberIdentifier::Installation(vec![4, 5, 6]); + let installation_1_copy = MemberIdentifier::Installation(vec![1, 2, 3]); + + assert!(installation_1 != installation_2); + assert!(installation_1.ne(&installation_2)); + assert!(installation_1 == installation_1_copy); + } +} diff --git a/xmtp_id/src/associations/mod.rs b/xmtp_id/src/associations/mod.rs new file mode 100644 index 000000000..839409895 --- /dev/null +++ b/xmtp_id/src/associations/mod.rs @@ -0,0 +1,8 @@ +mod hashes; +mod member; +mod state; +#[cfg(test)] +mod test_utils; + +pub use self::member::{Member, MemberIdentifier, MemberKind}; +pub use self::state::AssociationState; diff --git a/xmtp_id/src/associations/state.rs b/xmtp_id/src/associations/state.rs new file mode 100644 index 000000000..005491af7 --- /dev/null +++ b/xmtp_id/src/associations/state.rs @@ -0,0 +1,109 @@ +use std::collections::{HashMap, HashSet}; + +use super::{hashes::generate_xid, member::Member, MemberIdentifier, MemberKind}; + +#[derive(Clone, Debug)] +pub struct AssociationState { + xid: String, + members: HashMap, + recovery_address: String, + seen_signatures: HashSet>, +} + +impl AssociationState { + pub fn add(&self, member: Member) -> Self { + let mut new_state = self.clone(); + let _ = new_state.members.insert(member.identifier.clone(), member); + + new_state + } + + pub fn remove(&self, identifier: &MemberIdentifier) -> Self { + let mut new_state = self.clone(); + let _ = new_state.members.remove(identifier); + + new_state + } + + pub fn set_recovery_address(&self, recovery_address: String) -> Self { + let mut new_state = self.clone(); + new_state.recovery_address = recovery_address; + + new_state + } + + pub fn get(&self, identifier: &MemberIdentifier) -> Option { + self.members.get(identifier).map(|e| e.clone()) + } + + pub fn add_seen_signatures(&self, signatures: Vec>) -> Self { + let mut new_state = self.clone(); + new_state.seen_signatures.extend(signatures); + + new_state + } + + pub fn has_seen(&self, signature: &Vec) -> bool { + self.seen_signatures.contains(signature) + } + + pub fn members(&self) -> Vec { + self.members.values().cloned().collect() + } + + pub fn xid(&self) -> &String { + &self.xid + } + + pub fn recovery_address(&self) -> &String { + &self.recovery_address + } + + pub fn members_by_parent(&self, parent_id: &MemberIdentifier) -> Vec { + self.members + .values() + .filter(|e| e.added_by_entity.eq(&Some(parent_id.clone()))) + .cloned() + .collect() + } + + pub fn members_by_kind(&self, kind: MemberKind) -> Vec { + self.members + .values() + .filter(|e| e.kind() == kind) + .cloned() + .collect() + } + + pub fn new(account_address: String, nonce: u64) -> Self { + let xid = generate_xid(&account_address, &nonce); + let identifier = MemberIdentifier::Address(account_address.clone()); + let new_member = Member::new(identifier.clone(), None); + Self { + members: { + let mut members = HashMap::new(); + members.insert(identifier, new_member); + members + }, + seen_signatures: HashSet::new(), + recovery_address: account_address, + xid, + } + } +} + +#[cfg(test)] +mod tests { + use crate::associations::test_utils::rand_string; + + use super::*; + + #[test] + fn can_add_remove() { + let starting_state = AssociationState::new(rand_string(), 0); + let new_entity = Member::default(); + let with_add = starting_state.add(new_entity.clone()); + assert!(with_add.get(&new_entity.identifier).is_some()); + assert!(starting_state.get(&new_entity.identifier).is_none()); + } +} diff --git a/xmtp_id/src/associations/test_utils.rs b/xmtp_id/src/associations/test_utils.rs new file mode 100644 index 000000000..ce8a08a4c --- /dev/null +++ b/xmtp_id/src/associations/test_utils.rs @@ -0,0 +1,21 @@ +use rand::{distributions::Alphanumeric, Rng}; + +pub fn rand_string() -> String { + let v: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + v +} + +pub fn rand_u64() -> u64 { + rand::thread_rng().gen() +} + +pub fn rand_vec() -> Vec { + let mut buf = [0u8; 32]; + rand::thread_rng().fill(&mut buf[..]); + buf.to_vec() +} diff --git a/xmtp_id/src/lib.rs b/xmtp_id/src/lib.rs index 5eee16f19..6d03c5270 100644 --- a/xmtp_id/src/lib.rs +++ b/xmtp_id/src/lib.rs @@ -1,3 +1,4 @@ +pub mod associations; pub mod credential_verifier; pub mod verified_key_package;