Skip to content

Commit

Permalink
Static association state checking (#1350)
Browse files Browse the repository at this point in the history
* This is it on a high level

* move the logic to a better spot

* use the remote service

* napi, bump version

* lint

* test cleanup

* cleanup

* comments

* do it faster

* add the option to provide your own scw verifier
  • Loading branch information
codabrink authored Nov 27, 2024
1 parent 6ff8ae4 commit de9757e
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 22 deletions.
9 changes: 7 additions & 2 deletions bindings_node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ crate-type = ["cdylib"]

[dependencies]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
futures.workspace = true
hex.workspace = true
napi = { version = "2.12.2", default-features = false, features = [
"napi4",
Expand All @@ -17,14 +18,18 @@ napi = { version = "2.12.2", default-features = false, features = [
napi-derive = "2.12.2"
prost.workspace = true
tokio = { workspace = true, features = ["sync"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json", "chrono"] }
tracing-subscriber = { workspace = true, features = [
"env-filter",
"fmt",
"json",
"chrono",
] }
tracing.workspace = true
xmtp_api_grpc = { path = "../xmtp_api_grpc" }
xmtp_cryptography = { path = "../xmtp_cryptography" }
xmtp_id = { path = "../xmtp_id" }
xmtp_mls = { path = "../xmtp_mls" }
xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] }
futures.workspace = true

[build-dependencies]
napi-build = "2.0.1"
Expand Down
2 changes: 1 addition & 1 deletion bindings_node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@xmtp/node-bindings",
"version": "0.0.25",
"version": "0.0.26",
"repository": {
"type": "git",
"url": "git+https://[email protected]/xmtp/libxmtp.git",
Expand Down
49 changes: 48 additions & 1 deletion bindings_node/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ pub use xmtp_api_grpc::grpc_api_helper::Client as TonicApiClient;
use xmtp_cryptography::signature::ed25519_public_key_to_address;
use xmtp_id::associations::builder::SignatureRequest;
use xmtp_id::associations::MemberIdentifier;
use xmtp_mls::api::ApiClientWrapper;
use xmtp_mls::builder::ClientBuilder;
use xmtp_mls::groups::scoped_client::LocalScopedGroupClient;
use xmtp_mls::identity::IdentityStrategy;
use xmtp_mls::retry::Retry;
use xmtp_mls::storage::{EncryptedMessageStore, EncryptionKey, StorageOption};
use xmtp_mls::Client as MlsClient;
use xmtp_proto::xmtp::mls::message_contents::DeviceSyncKind;
Expand Down Expand Up @@ -131,7 +133,7 @@ pub async fn create_client(
log_options: Option<LogOptions>,
) -> Result<Client> {
init_logging(log_options.unwrap_or_default())?;
let api_client = TonicApiClient::create(host.clone(), is_secure)
let api_client = TonicApiClient::create(&host, is_secure)
.await
.map_err(|_| Error::from_reason("Error creating Tonic API client"))?;

Expand Down Expand Up @@ -343,3 +345,48 @@ impl Client {
Ok(association_state.get(identifier).is_some())
}
}

#[napi]
pub async fn is_installation_authorized(
host: String,
inbox_id: String,
installation_id: Uint8Array,
) -> Result<bool> {
is_member_of_association_state(
&host,
&inbox_id,
&MemberIdentifier::Installation(installation_id.to_vec()),
)
.await
}

#[napi]
pub async fn is_address_authorized(
host: String,
inbox_id: String,
address: String,
) -> Result<bool> {
is_member_of_association_state(&host, &inbox_id, &MemberIdentifier::Address(address)).await
}

async fn is_member_of_association_state(
host: &str,
inbox_id: &str,
identifier: &MemberIdentifier,
) -> Result<bool> {
let api_client = TonicApiClient::create(host, true)
.await
.map_err(ErrorWrapper::from)?;
let api_client = ApiClientWrapper::new(Arc::new(api_client), Retry::default());

let is_member = xmtp_mls::identity_updates::is_member_of_association_state(
&api_client,
inbox_id,
identifier,
None,
)
.await
.map_err(ErrorWrapper::from)?;

Ok(is_member)
}
2 changes: 1 addition & 1 deletion bindings_node/src/inbox_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub async fn get_inbox_id_for_address(
) -> Result<Option<String>> {
let account_address = account_address.to_lowercase();
let api_client = ApiClientWrapper::new(
TonicApiClient::create(host.clone(), is_secure)
TonicApiClient::create(host, is_secure)
.await
.map_err(ErrorWrapper::from)?
.into(),
Expand Down
12 changes: 5 additions & 7 deletions examples/cli/cli-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,13 @@ async fn main() -> color_eyre::eyre::Result<()> {
)
.await?,
),
(false, Env::Local) => {
Box::new(ClientV3::create("http://localhost:5556".into(), false).await?)
}
(false, Env::Local) => Box::new(ClientV3::create("http://localhost:5556", false).await?),
(false, Env::Dev) => {
Box::new(ClientV3::create("https://grpc.dev.xmtp.network:443".into(), true).await?)
Box::new(ClientV3::create("https://grpc.dev.xmtp.network:443", true).await?)
}
(false, Env::Production) => {
Box::new(ClientV3::create("https://grpc.production.xmtp.network:443", true).await?)
}
(false, Env::Production) => Box::new(
ClientV3::create("https://grpc.production.xmtp.network:443".into(), true).await?,
),
(true, Env::Production) => todo!("not supported"),
};

Expand Down
2 changes: 1 addition & 1 deletion xmtp_api_grpc/src/grpc_api_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub struct Client {
}

impl Client {
pub async fn create(host: String, is_secure: bool) -> Result<Self, Error> {
pub async fn create(host: impl ToString, is_secure: bool) -> Result<Self, Error> {
let host = host.to_string();
let app_version = MetadataValue::try_from(&String::from("0.0.0"))
.map_err(|e| Error::new(ErrorKind::MetadataError).with(e))?;
Expand Down
4 changes: 2 additions & 2 deletions xmtp_api_grpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ mod utils {
#[async_trait::async_trait]
impl XmtpTestClient for crate::Client {
async fn create_local() -> Self {
crate::Client::create("http://localhost:5556".into(), false)
crate::Client::create("http://localhost:5556", false)
.await
.unwrap()
}

async fn create_dev() -> Self {
crate::Client::create("https://grpc.dev.xmtp.network:443".into(), false)
crate::Client::create("https://grpc.dev.xmtp.network:443", false)
.await
.unwrap()
}
Expand Down
98 changes: 92 additions & 6 deletions xmtp_mls/src/identity_updates.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use std::collections::{HashMap, HashSet};

use crate::{
retry::{Retry, RetryableError},
retry_async, retryable,
storage::association_state::StoredAssociationState,
};
use futures::future::try_join_all;
use std::collections::{HashMap, HashSet};
use thiserror::Error;
use xmtp_cryptography::CredentialSign;
use xmtp_id::{
Expand All @@ -16,12 +15,13 @@ use xmtp_id::{
unverified::{
UnverifiedIdentityUpdate, UnverifiedInstallationKeySignature, UnverifiedSignature,
},
AssociationError, AssociationState, AssociationStateDiff, IdentityUpdate,
AssociationError, AssociationState, AssociationStateDiff, IdentityAction, IdentityUpdate,
InstallationKeyContext, MemberIdentifier, SignatureError,
},
scw_verifier::SmartContractSignatureVerifier,
scw_verifier::{RemoteSignatureVerifier, SmartContractSignatureVerifier},
InboxIdRef,
};
use xmtp_proto::api_client::{ClientWithMetadata, XmtpIdentityClient, XmtpMlsClient};

use crate::{
api::{ApiClientWrapper, GetIdentityUpdatesV2Filter, InboxUpdate},
Expand Down Expand Up @@ -284,7 +284,7 @@ where
let inbox_id = self.inbox_id();
let builder = SignatureRequestBuilder::new(inbox_id);
let installation_public_key = self.identity().installation_keys.verifying_key();
let new_member_identifier: MemberIdentifier = new_wallet_address.into();
let new_member_identifier = MemberIdentifier::Address(new_wallet_address);

let mut signature_request = builder
.add_association(new_member_identifier, installation_public_key.into())
Expand Down Expand Up @@ -528,6 +528,53 @@ async fn verify_updates(
.await
}

/// A static lookup method to verify if an identity is a member of an inbox
pub async fn is_member_of_association_state<Client>(
api_client: &ApiClientWrapper<Client>,
inbox_id: &str,
identifier: &MemberIdentifier,
scw_verifier: Option<Box<dyn SmartContractSignatureVerifier>>,
) -> Result<bool, ClientError>
where
Client: XmtpMlsClient + XmtpIdentityClient + ClientWithMetadata + Send + Sync,
{
let filters = vec![GetIdentityUpdatesV2Filter {
inbox_id: inbox_id.to_string(),
sequence_id: None,
}];
let mut updates = api_client.get_identity_updates_v2(filters).await?;

let Some(updates) = updates.remove(inbox_id) else {
return Err(ClientError::Generic(
"Unable to find provided inbox_id".to_string(),
));
};
let updates: Vec<_> = updates.into_iter().map(|u| u.update).collect();

let mut association_state = None;

let scw_verifier = scw_verifier.unwrap_or_else(|| {
Box::new(RemoteSignatureVerifier::new(api_client.api_client.clone()))
as Box<dyn SmartContractSignatureVerifier>
});

let updates: Vec<_> = updates
.iter()
.map(|u| u.to_verified(&scw_verifier))
.collect();
let updates = try_join_all(updates).await?;

for update in updates {
association_state =
Some(update.update_state(association_state, update.client_timestamp_ns)?);
}
let association_state = association_state.ok_or(ClientError::Generic(
"Unable to create association state".to_string(),
))?;

Ok(association_state.get(identifier).is_some())
}

#[cfg(test)]
pub(crate) mod tests {
#[cfg(target_arch = "wasm32")]
Expand All @@ -550,7 +597,7 @@ pub(crate) mod tests {
Client, XmtpApi,
};

use super::load_identity_updates;
use super::{is_member_of_association_state, load_identity_updates};

async fn get_association_state<ApiClient, Verifier>(
client: &Client<ApiClient, Verifier>,
Expand Down Expand Up @@ -579,6 +626,45 @@ pub(crate) mod tests {
.expect("insert should succeed");
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_is_member_of_association_state() {
let wallet = generate_local_wallet();
let client = ClientBuilder::new_test_client(&wallet).await;

let wallet2 = generate_local_wallet();

let mut request = client
.associate_wallet(wallet2.get_address())
.await
.unwrap();
add_wallet_signature(&mut request, &wallet2).await;
client.apply_signature_request(request).await.unwrap();

let conn = client.store().conn().unwrap();
let state = client
.get_latest_association_state(&conn, client.inbox_id())
.await
.unwrap();

// The installation, wallet1 address, and the newly associated wallet2 address
assert_eq!(state.members().len(), 3);

let api_client = &client.api_client;

// Check that the second wallet is associated with our new static helper
let is_member = is_member_of_association_state(
api_client,
client.inbox_id(),
&MemberIdentifier::Address(wallet2.get_address()),
None,
)
.await
.unwrap();

assert!(is_member);
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn create_inbox_round_trip() {
Expand Down
2 changes: 1 addition & 1 deletion xmtp_mls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub mod configuration;
pub mod groups;
mod hpke;
pub mod identity;
mod identity_updates;
pub mod identity_updates;
mod intents;
mod mutex_registry;
pub mod retry;
Expand Down

0 comments on commit de9757e

Please sign in to comment.