Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Association History Processing (Part 1) #617

Merged
merged 1 commit into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions bindings_ffi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions xmtp_id/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

12 changes: 12 additions & 0 deletions xmtp_id/src/associations/hashes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use sha2::{Digest, Sha256};

pub fn sha256_string(input: String) -> String {
insipx marked this conversation as resolved.
Show resolved Hide resolved
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))
}
120 changes: 120 additions & 0 deletions xmtp_id/src/associations/member.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#[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<u8>),
}

impl MemberIdentifier {
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 {
let as_string = match self {
MemberIdentifier::Address(address) => address.to_string(),
MemberIdentifier::Installation(installation) => hex::encode(installation),
};

write!(f, "{}", as_string)
}
}

impl From<String> for MemberIdentifier {
fn from(address: String) -> Self {
MemberIdentifier::Address(address)
}
}

impl From<Vec<u8>> for MemberIdentifier {
fn from(installation: Vec<u8>) -> Self {
MemberIdentifier::Installation(installation)
}
}

#[derive(Clone, Debug, PartialEq)]
pub struct Member {
pub identifier: MemberIdentifier,
pub added_by_entity: Option<MemberIdentifier>,
}

impl Member {
pub fn new(identifier: MemberIdentifier, added_by_entity: Option<MemberIdentifier>) -> Self {
Self {
identifier,
added_by_entity,
}
}

pub fn kind(&self) -> MemberKind {
self.identifier.kind()
}
}

impl PartialEq<MemberIdentifier> 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);
}
}
8 changes: 8 additions & 0 deletions xmtp_id/src/associations/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
109 changes: 109 additions & 0 deletions xmtp_id/src/associations/state.rs
Original file line number Diff line number Diff line change
@@ -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<MemberIdentifier, Member>,
recovery_address: String,
seen_signatures: HashSet<Vec<u8>>,
}

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<Member> {
self.members.get(identifier).cloned()
}

pub fn add_seen_signatures(&self, signatures: Vec<Vec<u8>>) -> Self {
let mut new_state = self.clone();
new_state.seen_signatures.extend(signatures);

new_state
}

pub fn has_seen(&self, signature: &Vec<u8>) -> bool {
self.seen_signatures.contains(signature)
}

pub fn members(&self) -> Vec<Member> {
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<Member> {
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<Member> {
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());
}
}
21 changes: 21 additions & 0 deletions xmtp_id/src/associations/test_utils.rs
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 13 in xmtp_id/src/associations/test_utils.rs

View workflow job for this annotation

GitHub Actions / Test

function `rand_u64` is never used

Check warning on line 13 in xmtp_id/src/associations/test_utils.rs

View workflow job for this annotation

GitHub Actions / Test

function `rand_u64` is never used
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unused for now, but will be once the second PR in the stack goes in

rand::thread_rng().gen()
}

pub fn rand_vec() -> Vec<u8> {

Check warning on line 17 in xmtp_id/src/associations/test_utils.rs

View workflow job for this annotation

GitHub Actions / Test

function `rand_vec` is never used

Check warning on line 17 in xmtp_id/src/associations/test_utils.rs

View workflow job for this annotation

GitHub Actions / Test

function `rand_vec` is never used
let mut buf = [0u8; 32];
rand::thread_rng().fill(&mut buf[..]);
buf.to_vec()
}
1 change: 1 addition & 0 deletions xmtp_id/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod associations;
pub mod credential_verifier;
pub mod verified_key_package;

Expand Down
Loading