Skip to content

Commit ab4ad82

Browse files
committed
Use new association state model
1 parent 7fb0dcd commit ab4ad82

File tree

6 files changed

+520
-610
lines changed

6 files changed

+520
-610
lines changed
+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use super::entity::{Entity, EntityRole};
2+
use super::hashes::{generate_xid, sha256_string};
3+
use super::signature::{Signature, SignatureError, SignatureKind};
4+
use super::state::{AssociationState, StateError};
5+
6+
use thiserror::Error;
7+
8+
// const ALLOWED_CREATE_ENTITY_ROLES: [EntityRole; 2] = [EntityRole::LegacyKey, EntityRole::Address];
9+
10+
#[derive(Debug, Error, PartialEq)]
11+
pub enum AssociationError {
12+
#[error("Error creating association {0}")]
13+
Generic(String),
14+
#[error("Multiple create operations detected")]
15+
MultipleCreate,
16+
#[error("XID not yet created")]
17+
NotCreated,
18+
#[error("Signature validation failed {0}")]
19+
Signature(#[from] SignatureError),
20+
#[error("State update failed")]
21+
StateError(#[from] StateError),
22+
#[error("Missing existing member")]
23+
MissingExistingMember,
24+
#[error("Legacy key is only allowed to be associated using a legacy signature with nonce 0")]
25+
LegacySignatureReuse,
26+
#[error("Signature not allowed for role {0:?} {1:?}")]
27+
SignatureNotAllowed(EntityRole, SignatureKind),
28+
#[error("Replay detected")]
29+
Replay,
30+
}
31+
32+
pub trait LogEntry {
33+
fn update_state(
34+
&self,
35+
existing_state: Option<AssociationState>,
36+
) -> Result<AssociationState, AssociationError>;
37+
fn hash(&self) -> String;
38+
}
39+
40+
pub struct CreateXid {
41+
pub nonce: u32,
42+
pub account_address: String,
43+
pub initial_association: AddAssociation,
44+
}
45+
46+
impl LogEntry for CreateXid {
47+
fn update_state(
48+
&self,
49+
existing_state: Option<AssociationState>,
50+
) -> Result<AssociationState, AssociationError> {
51+
if existing_state.is_some() {
52+
return Err(AssociationError::MultipleCreate);
53+
}
54+
55+
let account_address = self.account_address.clone();
56+
57+
let initial_state = AssociationState::new(account_address, self.nonce);
58+
let new_state = self.initial_association.update_state(Some(initial_state))?;
59+
60+
Ok(new_state.mark_event_seen(self.hash()))
61+
}
62+
63+
fn hash(&self) -> String {
64+
// Once we have real signatures the nonce and the recovery address should become part of the text
65+
let inputs = format!(
66+
"{}{}{}",
67+
self.nonce,
68+
self.account_address,
69+
self.initial_association.hash()
70+
);
71+
72+
sha256_string(inputs)
73+
}
74+
}
75+
76+
pub struct AddAssociation {
77+
pub client_timestamp_ns: u32,
78+
pub new_member_role: EntityRole,
79+
pub new_member_signature: Box<dyn Signature>,
80+
pub existing_member_signature: Box<dyn Signature>,
81+
}
82+
83+
impl AddAssociation {
84+
pub fn new_member_address(&self) -> String {
85+
self.new_member_signature.recover_signer().unwrap()
86+
}
87+
}
88+
89+
impl LogEntry for AddAssociation {
90+
fn update_state(
91+
&self,
92+
maybe_existing_state: Option<AssociationState>,
93+
) -> Result<AssociationState, AssociationError> {
94+
let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
95+
96+
let association_hash = self.hash();
97+
if existing_state.has_seen(&association_hash) {
98+
return Err(AssociationError::Replay);
99+
}
100+
101+
let new_member_address = self.new_member_signature.recover_signer()?;
102+
let existing_member_address = self.existing_member_signature.recover_signer()?;
103+
if new_member_address == existing_member_address {
104+
return Err(AssociationError::Generic("tried to add self".to_string()));
105+
}
106+
107+
if self.new_member_role == EntityRole::LegacyKey {
108+
if existing_state.xid != generate_xid(&existing_member_address, &0) {
109+
return Err(AssociationError::LegacySignatureReuse);
110+
}
111+
}
112+
113+
// Get the current version of the entity that added this new entry. If it has been revoked and added back, it will now be unrevoked
114+
let existing_entity = existing_state
115+
.get(&existing_member_address)
116+
.ok_or(AssociationError::MissingExistingMember)?;
117+
118+
// Make sure that the signature type lines up with the role
119+
if !allowed_signature_for_role(
120+
&self.new_member_role,
121+
&self.new_member_signature.signature_kind(),
122+
) {
123+
return Err(AssociationError::SignatureNotAllowed(
124+
self.new_member_role.clone(),
125+
self.new_member_signature.signature_kind(),
126+
));
127+
}
128+
129+
let new_member = Entity::new(
130+
self.new_member_role.clone(),
131+
new_member_address,
132+
Some(existing_entity.id),
133+
);
134+
135+
println!(
136+
"Adding new entity to state {:?} with hash {}",
137+
&new_member, &association_hash
138+
);
139+
140+
Ok(existing_state.add(new_member).mark_event_seen(self.hash()))
141+
}
142+
143+
fn hash(&self) -> String {
144+
let inputs = format!(
145+
"{}{:?}{}{}",
146+
self.client_timestamp_ns,
147+
self.new_member_role,
148+
self.existing_member_signature.text(),
149+
self.new_member_signature.text()
150+
);
151+
sha256_string(inputs)
152+
}
153+
}
154+
155+
pub struct RevokeAssociation {
156+
pub client_timestamp_ns: u32,
157+
pub recovery_address_signature: Box<dyn Signature>,
158+
pub revoked_member: String,
159+
}
160+
161+
impl LogEntry for RevokeAssociation {
162+
fn update_state(
163+
&self,
164+
maybe_existing_state: Option<AssociationState>,
165+
) -> Result<AssociationState, AssociationError> {
166+
let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
167+
// Don't need to check for replay here since revocation is idempotent
168+
let recovery_signer = self.recovery_address_signature.recover_signer()?;
169+
// Make sure there is a recovery address set on the state
170+
let state_recovery_address = existing_state.recovery_address.clone();
171+
172+
// Ensure this message is signed by the recovery address
173+
if recovery_signer != state_recovery_address {
174+
return Err(AssociationError::MissingExistingMember);
175+
}
176+
177+
let installations_to_remove: Vec<Entity> = existing_state
178+
.entities_by_parent(&self.revoked_member)
179+
.into_iter()
180+
// Only remove children if they are installations
181+
.filter(|child| child.role == EntityRole::Installation)
182+
.collect();
183+
184+
// Actually apply the revocation to the parent
185+
let new_state = existing_state.remove(self.revoked_member.clone());
186+
187+
Ok(installations_to_remove
188+
.iter()
189+
.fold(new_state, |state, installation| {
190+
state.remove(installation.id.clone())
191+
})
192+
.mark_event_seen(self.hash()))
193+
}
194+
195+
fn hash(&self) -> String {
196+
let inputs = format!(
197+
"{}{}{}",
198+
self.client_timestamp_ns,
199+
self.recovery_address_signature.text(),
200+
self.revoked_member,
201+
);
202+
sha256_string(inputs)
203+
}
204+
}
205+
206+
pub enum AssociationEvent {
207+
CreateXid(CreateXid),
208+
AddAssociation(AddAssociation),
209+
RevokeAssociation(RevokeAssociation),
210+
}
211+
212+
impl LogEntry for AssociationEvent {
213+
fn update_state(
214+
&self,
215+
existing_state: Option<AssociationState>,
216+
) -> Result<AssociationState, AssociationError> {
217+
match self {
218+
AssociationEvent::CreateXid(event) => event.update_state(existing_state),
219+
AssociationEvent::AddAssociation(event) => event.update_state(existing_state),
220+
AssociationEvent::RevokeAssociation(event) => event.update_state(existing_state),
221+
}
222+
}
223+
224+
fn hash(&self) -> String {
225+
match self {
226+
AssociationEvent::CreateXid(event) => event.hash(),
227+
AssociationEvent::AddAssociation(event) => event.hash(),
228+
AssociationEvent::RevokeAssociation(event) => event.hash(),
229+
}
230+
}
231+
}
232+
233+
// Ensure that the type of signature matches the new entity's role.
234+
pub fn allowed_signature_for_role(role: &EntityRole, signature_kind: &SignatureKind) -> bool {
235+
match role {
236+
EntityRole::Address => match signature_kind {
237+
SignatureKind::Erc191 => true,
238+
SignatureKind::Erc1271 => true,
239+
SignatureKind::InstallationKey => false,
240+
SignatureKind::LegacyKey => false,
241+
},
242+
EntityRole::LegacyKey => match signature_kind {
243+
SignatureKind::Erc191 => false,
244+
SignatureKind::Erc1271 => false,
245+
SignatureKind::InstallationKey => false,
246+
SignatureKind::LegacyKey => true,
247+
},
248+
EntityRole::Installation => match signature_kind {
249+
SignatureKind::Erc191 => false,
250+
SignatureKind::Erc1271 => false,
251+
SignatureKind::InstallationKey => true,
252+
SignatureKind::LegacyKey => false,
253+
},
254+
}
255+
}

xmtp_id/src/associations/entity.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ pub enum EntityRole {
99
pub struct Entity {
1010
pub role: EntityRole,
1111
pub id: String,
12-
pub is_revoked: bool,
12+
pub added_by_entity: Option<String>,
1313
}
1414

1515
impl Entity {
16-
pub fn new(role: EntityRole, id: String, is_revoked: bool) -> Self {
16+
pub fn new(role: EntityRole, id: String, added_by_entity: Option<String>) -> Self {
1717
Self {
1818
role,
1919
id,
20-
is_revoked,
20+
added_by_entity,
2121
}
2222
}
2323
}
@@ -35,7 +35,7 @@ mod tests {
3535
Self {
3636
role: EntityRole::Address,
3737
id: rand_string(),
38-
is_revoked: false,
38+
added_by_entity: None,
3939
}
4040
}
4141
}

xmtp_id/src/associations/hashes.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use sha2::{Digest, Sha256};
2+
3+
pub fn sha256_string(input: String) -> String {
4+
let mut hasher = Sha256::new();
5+
hasher.update(input.as_bytes());
6+
let result = hasher.finalize();
7+
format!("{:x}", result)
8+
}
9+
10+
pub fn generate_xid(account_address: &String, nonce: &u32) -> String {
11+
sha256_string(format!("{}{}", account_address, nonce))
12+
}

0 commit comments

Comments
 (0)