[Fix] Connection Management

This commit is contained in:
Alex Emmet 2026-09-13 20:58:41 +02:00
commit 3f2ac18333
No known key found for this signature in database
122 changed files with 19970 additions and 5263 deletions

View file

@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
async-trait = "0.1.89"
iota-identity = { path = "../iota-identity" }
iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" }
iota-paths = { path = "../iota-paths" }
@ -13,8 +15,13 @@ arc-swap = "1"
once_cell = "1.21.3"
r2d2 = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
thiserror = "2"
rand = "0.8"
rusqlite = "0.40.0"
tokio = { version = "1.50.0", features = ["full"] }
[dev-dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "1f19a0d897c265d1e3f590a876f95e766ff99318", features = ["crypto"] }
tempfile = "3"

View file

@ -0,0 +1,877 @@
use iota_identity::{
AuthorityId, AuthorityKind, IdentityError, LocalDescriptorPublisher, LocalUserDescriptor,
LocalUserId, LocalUserStore, PrincipalDescriptor, PrincipalHandle, PrincipalHome, PrincipalId,
PrincipalStore, PublicKeyBundle, ResolvedPrincipal, SignedPrincipalDescriptor,
};
use iota_util::crypto_helper::{public_key_bundle_from_base64, public_key_bundle_to_base64};
use rusqlite::{OptionalExtension, params};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::users::user_manager;
use crate::util::db;
const LOCAL_DESCRIPTOR_LIFETIME_MILLIS: i64 = 30 * 24 * 60 * 60 * 1_000;
const LOCAL_DESCRIPTOR_RENEWAL_MILLIS: i64 = 7 * 24 * 60 * 60 * 1_000;
#[derive(Default)]
pub struct SqliteLocalUserStore;
impl LocalUserStore for SqliteLocalUserStore {
fn get_local_user(
&self,
id: LocalUserId,
) -> Result<Option<LocalUserDescriptor>, IdentityError> {
user_manager::get_user(id.0)
.map_err(storage_error)
.map(|user| user.map(local_descriptor))
}
fn get_local_user_by_username(
&self,
username: &str,
) -> Result<Option<LocalUserDescriptor>, IdentityError> {
user_manager::get_user_by_username(username)
.map_err(storage_error)
.map(|user| user.map(local_descriptor))
}
fn is_hosted_here(&self, id: LocalUserId) -> Result<bool, IdentityError> {
self.get_local_user(id).map(|user| user.is_some())
}
fn local_user_for_principal(
&self,
principal: PrincipalHandle,
) -> Result<Option<LocalUserId>, IdentityError> {
db::with_db(|connection| {
connection
.query_row(
"SELECT local_user_id FROM hosted_principals WHERE principal_handle = ?1",
[principal.0],
|row| row.get::<_, i64>(0).map(LocalUserId),
)
.optional()
.map_err(Into::into)
})
.map_err(storage_error)
}
fn principal_for_local_user(
&self,
user: LocalUserId,
) -> Result<Option<PrincipalHandle>, IdentityError> {
SqlitePrincipalStore.principal_for_local_user(user)
}
}
fn local_descriptor(user: crate::users::user_profile::UserProfile) -> LocalUserDescriptor {
LocalUserDescriptor {
id: LocalUserId(user.user_id),
username: user.username,
display_name: user.display_name,
public_key: user.public_key,
}
}
#[derive(Default)]
pub struct SqlitePrincipalStore;
impl SqlitePrincipalStore {
pub fn principal_for_local_user(
&self,
user: LocalUserId,
) -> Result<Option<PrincipalHandle>, IdentityError> {
db::with_db(|connection| {
connection
.query_row(
"SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1",
[user.0],
|row| row.get::<_, i64>(0).map(PrincipalHandle),
)
.optional()
.map_err(Into::into)
})
.map_err(storage_error)
}
pub fn migrate_legacy_omega_authority(
&self,
authority: &AuthorityId,
) -> Result<(), IdentityError> {
db::with_immediate_transaction(|transaction| {
transaction.execute(
"INSERT INTO identity_configuration (singleton, omega_authority_id) VALUES (1, ?1) ON CONFLICT(singleton) DO UPDATE SET omega_authority_id = excluded.omega_authority_id",
[authority.as_str()],
)?;
let mut statement = transaction.prepare(
"SELECT principal_pk, remote_user_id FROM principals WHERE authority_id = 'omega:central'",
)?;
let legacy = statement
.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?
.collect::<Result<Vec<_>, _>>()?;
drop(statement);
for (legacy_handle, user_id) in legacy {
let current_handle = transaction
.query_row(
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
params![authority.as_str(), user_id],
|row| row.get::<_, i64>(0),
)
.optional()?;
if let Some(current_handle) = current_handle {
transaction.execute(
"INSERT OR IGNORE INTO principal_keys (principal_pk, public_key, valid_from, valid_until, source_revision) SELECT ?1, public_key, valid_from, valid_until, source_revision FROM principal_keys WHERE principal_pk = ?2",
params![current_handle, legacy_handle],
)?;
for (table, column) in [
("contacts", "principal_handle"),
("messages", "external_principal"),
("relay_replay", "signer_principal"),
("relay_inbox", "signer_principal"),
("pending_relays", "destination_principal"),
("blocked_users", "blocked_principal"),
] {
transaction.execute(
&format!("UPDATE {table} SET {column} = ?1 WHERE {column} = ?2"),
params![current_handle, legacy_handle],
)?;
}
transaction.execute(
"DELETE FROM principals WHERE principal_pk = ?1",
[legacy_handle],
)?;
} else {
transaction.execute(
"UPDATE principals SET authority_id = ?1 WHERE principal_pk = ?2",
params![authority.as_str(), legacy_handle],
)?;
}
}
transaction.execute_batch(
"CREATE TEMP TABLE IF NOT EXISTS observed_omega_users (user_id INTEGER PRIMARY KEY); DELETE FROM observed_omega_users; INSERT OR IGNORE INTO observed_omega_users SELECT user_id FROM users WHERE user_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT user_id FROM contacts WHERE user_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT external_user FROM messages WHERE external_user > 0; INSERT OR IGNORE INTO observed_omega_users SELECT signer_id FROM relay_replay WHERE signer_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT signer_id FROM relay_inbox WHERE signer_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT destination_id FROM relay_inbox WHERE destination_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT relay_signer_id FROM pending_relays WHERE relay_signer_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT relay_destination_user_id FROM pending_relays WHERE relay_destination_user_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT blocked_user_id FROM blocked_users WHERE blocked_user_id > 0;"
)?;
transaction.execute(
"INSERT OR IGNORE INTO principals (authority_kind, authority_id, remote_user_id, descriptor_revision, last_resolved_at) SELECT 'omega', ?1, user_id, 0, 0 FROM observed_omega_users",
[authority.as_str()],
)?;
for (table, principal_column, user_column) in [
("contacts", "principal_handle", "user_id"),
("messages", "external_principal", "external_user"),
("relay_replay", "signer_principal", "signer_id"),
("relay_inbox", "signer_principal", "signer_id"),
("pending_relays", "destination_principal", "relay_destination_user_id"),
("blocked_users", "blocked_principal", "blocked_user_id"),
] {
transaction.execute(
&format!("UPDATE {table} SET {principal_column} = (SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = {table}.{user_column}) WHERE {principal_column} IS NULL AND {user_column} IS NOT NULL"),
[authority.as_str()],
)?;
}
Ok(())
})
.map_err(storage_error)
}
pub fn ensure_local_principal(
&self,
authority: &AuthorityId,
authority_kind: AuthorityKind,
user: &LocalUserDescriptor,
home: PrincipalHome,
resolved_at: i64,
) -> Result<PrincipalHandle, IdentityError> {
let user_id = u64::try_from(user.id.0)
.map_err(|_| IdentityError::InvalidDescriptor("negative local user ID".into()))?;
let key = public_key_bundle_from_base64(&user.public_key)
.ok_or_else(|| IdentityError::InvalidDescriptor("stored user key is invalid".into()))?;
let encoded_home = encode_home(&home);
let current = db::with_db(|connection| {
connection
.query_row(
r#"SELECT p.authority_kind, p.username, p.display_name, p.home,
p.descriptor_revision, p.descriptor_valid_until,
COALESCE(p.descriptor_issued_at, p.last_resolved_at), p.retired_at,
(SELECT public_key FROM principal_keys
WHERE principal_pk = p.principal_pk
AND (valid_until IS NULL OR valid_until > ?3)
ORDER BY source_revision DESC LIMIT 1)
FROM principals p
WHERE p.authority_id = ?1 AND p.remote_user_id = ?2"#,
params![authority.as_str(), user.id.0, resolved_at],
|row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, Option<String>>(2)?,
row.get::<_, Option<String>>(3)?,
row.get::<_, i64>(4)?,
row.get::<_, Option<i64>>(5)?,
row.get::<_, i64>(6)?,
row.get::<_, Option<i64>>(7)?,
row.get::<_, Option<String>>(8)?,
))
},
)
.optional()
.map_err(Into::into)
})
.map_err(storage_error)?;
if current.as_ref().is_some_and(|current| current.7.is_some()) {
return Err(IdentityError::InvalidDescriptor(
"retired local principal cannot be published".into(),
));
}
let signed_fields_match = current.as_ref().is_some_and(|current| {
current.0 == authority_kind.as_str()
&& current.1.as_deref() == Some(user.username.as_str())
&& current.2.as_ref() == user.display_name.as_ref()
&& current.3.as_ref() == encoded_home.as_ref()
&& current.8.as_deref() == Some(user.public_key.as_str())
});
let current_valid_until = current.as_ref().and_then(|current| current.5);
let renew_validity = current_valid_until
.is_none_or(|valid_until| valid_until <= resolved_at + LOCAL_DESCRIPTOR_RENEWAL_MILLIS);
let revision = current.as_ref().map_or(0, |current| {
if signed_fields_match && !renew_validity {
current.4
} else {
current.4.saturating_add(1)
}
});
let issued_at = current.as_ref().map_or(resolved_at, |current| {
if signed_fields_match && !renew_validity {
current.6
} else {
resolved_at
}
});
let valid_until = if signed_fields_match && !renew_validity {
current_valid_until
} else {
Some(resolved_at.saturating_add(LOCAL_DESCRIPTOR_LIFETIME_MILLIS))
};
let descriptor = iota_identity::VerifiedPrincipalDescriptor::from_trusted_authority(
PrincipalDescriptor {
principal: PrincipalId {
authority: authority.clone(),
user_id,
},
authority_kind,
username: Some(user.username.clone()),
display_name: user.display_name.clone(),
public_keys: vec![key],
home,
revision,
valid_until,
issued_at,
},
)?;
let handle = self.upsert_remote_descriptor(&descriptor)?;
db::with_db(|connection| {
connection.execute(
"INSERT INTO hosted_principals (local_user_id, principal_handle) VALUES (?1, ?2) ON CONFLICT(local_user_id) DO UPDATE SET principal_handle = excluded.principal_handle",
params![user.id.0, handle.0],
)?;
Ok(())
})
.map_err(storage_error)?;
Ok(handle)
}
pub fn local_principal_descriptor(
&self,
user: LocalUserId,
) -> Result<PrincipalDescriptor, IdentityError> {
db::with_db(|connection| {
let row = connection
.query_row(
r#"SELECT p.authority_kind, p.authority_id, p.remote_user_id,
p.username, p.display_name, p.home,
p.descriptor_revision, p.descriptor_valid_until,
COALESCE(p.descriptor_issued_at, p.last_resolved_at)
FROM hosted_principals h
JOIN principals p ON p.principal_pk = h.principal_handle
WHERE h.local_user_id = ?1 AND p.retired_at IS NULL"#,
[user.0],
|row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, Option<String>>(3)?,
row.get::<_, Option<String>>(4)?,
row.get::<_, Option<String>>(5)?,
row.get::<_, i64>(6)?,
row.get::<_, Option<i64>>(7)?,
row.get::<_, i64>(8)?,
))
},
)
.optional()?;
let Some((kind, authority, remote_user_id, username, display_name, home, revision, valid_until, issued_at)) = row else {
return Ok(None);
};
let principal_pk = connection.query_row(
"SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1",
[user.0],
|row| row.get::<_, i64>(0),
)?;
let mut statement = connection.prepare(
"SELECT public_key FROM principal_keys WHERE principal_pk = ?1 AND valid_from <= ?2 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC",
)?;
let encoded = statement
.query_map(params![principal_pk, issued_at], |row| row.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()?;
Ok(Some((kind, authority, remote_user_id, username, display_name, home, revision, valid_until, issued_at, encoded)))
})
.map_err(storage_error)?
.ok_or(IdentityError::NotFound)
.and_then(|(kind, authority, remote_user_id, username, display_name, home, revision, valid_until, issued_at, encoded)| {
let authority_kind = match kind.as_str() {
"iota" => AuthorityKind::Iota,
"omega" => AuthorityKind::Omega,
_ => return Err(IdentityError::InvalidDescriptor("stored authority kind is invalid".into())),
};
let public_keys = encoded
.into_iter()
.map(|key| public_key_bundle_from_base64(&key).ok_or_else(|| IdentityError::InvalidDescriptor("stored principal key is invalid".into())))
.collect::<Result<Vec<_>, _>>()?;
Ok(PrincipalDescriptor {
principal: PrincipalId {
authority: AuthorityId::new(authority)?,
user_id: u64::try_from(remote_user_id).map_err(|_| IdentityError::InvalidDescriptor("stored principal user ID is negative".into()))?,
},
authority_kind,
username,
display_name,
public_keys,
home: decode_home(home.as_deref()).map_err(storage_error)?,
revision,
valid_until,
issued_at,
})
})
}
/* Retire hosted identity after its data is purged. Keeping descriptor
* revision state rejects descriptors issued before account removal. */
pub fn retire_local_principal(
&self,
user: LocalUserId,
retired_at: i64,
) -> Result<(), IdentityError> {
db::with_immediate_transaction(|transaction| {
let principal = transaction
.query_row(
"SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1",
[user.0],
|row| row.get::<_, i64>(0),
)
.optional()?
.ok_or_else(|| {
crate::storage_error::StorageError::Other(
"hosted principal was not found".into(),
)
})?;
transaction.execute(
"UPDATE principals SET descriptor_revision = descriptor_revision + 1, descriptor_valid_until = ?2, last_resolved_at = ?2, retired_at = ?2 WHERE principal_pk = ?1",
params![principal, retired_at],
)?;
transaction.execute(
"UPDATE principal_keys SET valid_until = MIN(COALESCE(valid_until, ?2), ?2) WHERE principal_pk = ?1",
params![principal, retired_at],
)?;
transaction.execute(
"DELETE FROM hosted_principals WHERE local_user_id = ?1",
[user.0],
)?;
Ok(())
})
.map_err(storage_error)
}
}
pub struct SqliteLocalDescriptorPublisher {
identity: iota_identity::LocalNodeIdentity,
}
impl SqliteLocalDescriptorPublisher {
pub fn new(identity: iota_identity::LocalNodeIdentity) -> Self {
Self { identity }
}
}
impl LocalDescriptorPublisher for SqliteLocalDescriptorPublisher {
fn principal_descriptor(
&self,
user: LocalUserId,
) -> Result<SignedPrincipalDescriptor, IdentityError> {
let local_user = SqliteLocalUserStore
.get_local_user(user)?
.ok_or(IdentityError::NotFound)?;
SqlitePrincipalStore.ensure_local_principal(
self.identity.authority_id(),
AuthorityKind::Iota,
&local_user,
PrincipalHome::Iota(self.identity.node_id().clone()),
now_millis(),
)?;
SignedPrincipalDescriptor::sign(
SqlitePrincipalStore.local_principal_descriptor(user)?,
&self.identity.keyring(),
)
}
}
pub struct LocalIdentityResolver {
authority: AuthorityId,
authority_kind: AuthorityKind,
home: PrincipalHome,
local_users: Arc<dyn LocalUserStore>,
principals: Arc<SqlitePrincipalStore>,
}
impl LocalIdentityResolver {
pub fn new(
authority: AuthorityId,
authority_kind: AuthorityKind,
home: PrincipalHome,
local_users: Arc<dyn LocalUserStore>,
principals: Arc<SqlitePrincipalStore>,
) -> Self {
Self {
authority,
authority_kind,
home,
local_users,
principals,
}
}
fn resolve_local(&self, user: LocalUserDescriptor) -> Result<ResolvedPrincipal, IdentityError> {
let handle = self.principals.ensure_local_principal(
&self.authority,
self.authority_kind,
&user,
self.home.clone(),
now_millis(),
)?;
self.principals
.get_principal(handle)?
.ok_or(IdentityError::NotFound)
}
}
#[async_trait::async_trait]
impl iota_identity::IdentityResolver for LocalIdentityResolver {
async fn resolve_address(
&self,
address: &iota_identity::UserAddress,
_: &iota_identity::ResolutionContext,
) -> Result<ResolvedPrincipal, IdentityError> {
if let Some(address_authority) = &address.authority {
let matches_home = matches!(
&self.home,
PrincipalHome::Omega(locator) if locator == address_authority
);
if address_authority.as_str() != self.authority.as_str() && !matches_home {
return Err(IdentityError::NotFound);
}
}
let user = match &address.selector {
iota_identity::UserSelector::UserId(user_id) => {
let user_id = i64::try_from(*user_id).map_err(|_| {
IdentityError::InvalidIdentifier("user ID exceeds local storage range".into())
})?;
self.local_users.get_local_user(LocalUserId(user_id))?
}
iota_identity::UserSelector::Username(username) => {
self.local_users.get_local_user_by_username(username)?
}
}
.ok_or(IdentityError::NotFound)?;
let resolved = self.resolve_local(user)?;
verify_pin(address.public_key_pin.as_ref(), &resolved.public_keys)?;
Ok(resolved)
}
async fn resolve_principal(
&self,
principal: &PrincipalId,
) -> Result<ResolvedPrincipal, IdentityError> {
if principal.authority != self.authority {
return Err(IdentityError::NotFound);
}
let user_id = i64::try_from(principal.user_id).map_err(|_| {
IdentityError::InvalidIdentifier("user ID exceeds local storage range".into())
})?;
let user = self
.local_users
.get_local_user(LocalUserId(user_id))?
.ok_or(IdentityError::NotFound)?;
self.resolve_local(user)
}
async fn signing_keys(
&self,
principal: &PrincipalId,
_: &iota_identity::ResolutionContext,
) -> Result<Vec<PublicKeyBundle>, IdentityError> {
self.resolve_principal(principal)
.await
.map(|resolved| resolved.public_keys)
}
}
fn verify_pin(
pin: Option<&PublicKeyBundle>,
keys: &[PublicKeyBundle],
) -> Result<(), IdentityError> {
let Some(pin) = pin else {
return Ok(());
};
let pin = pin
.try_as_bytes()
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
let matches = keys.iter().any(|key| {
key.try_as_bytes()
.map(|candidate| candidate == pin)
.unwrap_or(false)
});
if matches {
Ok(())
} else {
Err(IdentityError::KeyPinMismatch)
}
}
fn now_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(i64::MAX)
}
impl PrincipalStore for SqlitePrincipalStore {
fn get_principal(
&self,
handle: PrincipalHandle,
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
db::with_db(|connection| load_principal(connection, "p.principal_pk = ?1", handle.0))
.map_err(storage_error)
}
fn get_by_canonical_id(
&self,
principal: &PrincipalId,
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
let remote_user_id = i64::try_from(principal.user_id).map_err(|_| {
IdentityError::InvalidIdentifier("principal user ID exceeds storage range".into())
})?;
db::with_db(|connection| {
let handle = connection
.query_row(
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
params![principal.authority.as_str(), remote_user_id],
|row| row.get::<_, i64>(0),
)
.optional()?;
match handle {
Some(handle) => load_principal(connection, "p.principal_pk = ?1", handle),
None => Ok(None),
}
})
.map_err(storage_error)
}
fn get_by_username(
&self,
authority: &AuthorityId,
username: &str,
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
db::with_db(|connection| {
let handle = connection
.query_row(
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND username = ?2 AND retired_at IS NULL",
params![authority.as_str(), username],
|row| row.get::<_, i64>(0),
)
.optional()?;
match handle {
Some(handle) => load_principal(connection, "p.principal_pk = ?1", handle),
None => Ok(None),
}
})
.map_err(storage_error)
}
fn upsert_remote_descriptor(
&self,
descriptor: &iota_identity::VerifiedPrincipalDescriptor,
) -> Result<PrincipalHandle, IdentityError> {
let resolved_at = descriptor.resolved_at();
let descriptor = descriptor.descriptor();
if descriptor.public_keys.is_empty() {
return Err(IdentityError::InvalidDescriptor(
"principal descriptor contains no signing keys".into(),
));
}
if descriptor.revision < 0 {
return Err(IdentityError::InvalidDescriptor(
"principal descriptor revision is negative".into(),
));
}
if descriptor
.valid_until
.is_some_and(|valid_until| valid_until <= resolved_at)
{
return Err(IdentityError::InvalidDescriptor(
"principal descriptor is already expired".into(),
));
}
let remote_user_id = i64::try_from(descriptor.principal.user_id).map_err(|_| {
IdentityError::InvalidIdentifier("principal user ID exceeds storage range".into())
})?;
let home = encode_home(&descriptor.home);
db::with_immediate_transaction(|transaction| {
let current = transaction
.query_row(
"SELECT principal_pk, descriptor_revision, last_resolved_at, retired_at, authority_kind, username, display_name, home, descriptor_valid_until, descriptor_issued_at FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
params![descriptor.principal.authority.as_str(), remote_user_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, i64>(2)?, row.get::<_, Option<i64>>(3)?, row.get::<_, String>(4)?, row.get::<_, Option<String>>(5)?, row.get::<_, Option<String>>(6)?, row.get::<_, Option<String>>(7)?, row.get::<_, Option<i64>>(8)?, row.get::<_, Option<i64>>(9)?)),
)
.optional()?;
if let Some((handle, revision, current_resolved_at, retired_at, authority_kind, username, display_name, current_home, valid_until, issued_at)) = &current {
if retired_at.is_some()
|| *revision > descriptor.revision
|| (*revision == descriptor.revision && *current_resolved_at > resolved_at)
{
return Err(crate::storage_error::StorageError::Other(
"stale principal descriptor update was rejected".into(),
));
}
if *revision == descriptor.revision {
let mut stored_keys = transaction
.prepare("SELECT public_key FROM principal_keys WHERE principal_pk = ?1 AND source_revision = ?2 ORDER BY public_key")?
.query_map(params![handle, revision], |row| row.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()?;
let mut descriptor_keys = descriptor
.public_keys
.iter()
.map(public_key_bundle_to_base64)
.collect::<Vec<_>>();
stored_keys.sort();
descriptor_keys.sort();
if authority_kind != descriptor.authority_kind.as_str()
|| username != &descriptor.username
|| display_name != &descriptor.display_name
|| current_home != &home
|| valid_until != &descriptor.valid_until
|| issued_at.is_some_and(|issued_at| issued_at != descriptor.issued_at)
|| stored_keys != descriptor_keys
{
return Err(crate::storage_error::StorageError::Other(
"conflicting principal descriptor revision was rejected".into(),
));
}
}
}
transaction.execute(
r#"INSERT INTO principals (
authority_kind, authority_id, remote_user_id, username, display_name,
home, descriptor_revision, descriptor_valid_until, descriptor_issued_at,
last_resolved_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
ON CONFLICT(authority_id, remote_user_id) DO UPDATE SET
authority_kind = excluded.authority_kind,
username = excluded.username,
display_name = excluded.display_name,
home = excluded.home,
descriptor_revision = excluded.descriptor_revision,
descriptor_valid_until = excluded.descriptor_valid_until,
descriptor_issued_at = excluded.descriptor_issued_at,
last_resolved_at = excluded.last_resolved_at"#,
params![
descriptor.authority_kind.as_str(),
descriptor.principal.authority.as_str(),
remote_user_id,
descriptor.username,
descriptor.display_name,
home,
descriptor.revision,
descriptor.valid_until,
descriptor.issued_at,
resolved_at,
],
)?;
let handle = transaction.query_row(
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
params![descriptor.principal.authority.as_str(), remote_user_id],
|row| row.get::<_, i64>(0),
)?;
transaction.execute(
"UPDATE principal_keys SET valid_until = MIN(COALESCE(valid_until, ?2), ?2) WHERE principal_pk = ?1",
params![handle, resolved_at],
)?;
for key in &descriptor.public_keys {
let encoded = public_key_bundle_to_base64(key);
if encoded.is_empty() {
return Err(crate::storage_error::StorageError::Other(
"principal signing key could not be encoded".into(),
));
}
transaction.execute(
r#"INSERT INTO principal_keys (
principal_pk, public_key, valid_from, valid_until, source_revision
) VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(principal_pk, public_key) DO UPDATE SET
valid_until = excluded.valid_until,
source_revision = excluded.source_revision"#,
params![
handle,
encoded,
resolved_at,
descriptor.valid_until,
descriptor.revision,
],
)?;
}
Ok(PrincipalHandle(handle))
})
.map_err(storage_error)
}
fn signing_keys(&self, principal: &PrincipalId) -> Result<Vec<PublicKeyBundle>, IdentityError> {
let resolved = self
.get_by_canonical_id(principal)?
.ok_or(IdentityError::NotFound)?;
if !resolved.is_valid_at(now_millis()) {
return Err(IdentityError::Unavailable(
"cached principal descriptor is expired".into(),
));
}
if resolved.public_keys.is_empty() {
return Err(IdentityError::InvalidDescriptor(
"principal has no current signing keys".into(),
));
}
Ok(resolved.public_keys)
}
}
fn load_principal(
connection: &rusqlite::Connection,
predicate: &str,
value: i64,
) -> Result<Option<ResolvedPrincipal>, crate::storage_error::StorageError> {
let query = format!(
"SELECT p.principal_pk, p.authority_id, p.remote_user_id, p.username, p.home, p.descriptor_revision, p.descriptor_valid_until, p.last_resolved_at FROM principals p WHERE {predicate}"
);
let row = connection
.query_row(&query, [value], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, Option<String>>(3)?,
row.get::<_, Option<String>>(4)?,
row.get::<_, i64>(5)?,
row.get::<_, Option<i64>>(6)?,
row.get::<_, i64>(7)?,
))
})
.optional()?;
let Some((
handle,
authority,
remote_user_id,
username,
home,
descriptor_revision,
valid_until,
resolved_at,
)) = row
else {
return Ok(None);
};
let mut statement = connection.prepare(
"SELECT public_key FROM principal_keys WHERE principal_pk = ?1 AND valid_from <= ?2 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC, valid_from DESC",
)?;
let encoded = statement
.query_map(params![handle, now_millis()], |row| row.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()?;
let mut public_keys = Vec::with_capacity(encoded.len());
for value in encoded {
let key = public_key_bundle_from_base64(&value).ok_or_else(|| {
crate::storage_error::StorageError::Other("stored principal key is invalid".into())
})?;
public_keys.push(key);
}
let authority = AuthorityId::new(authority)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))?;
let user_id = u64::try_from(remote_user_id).map_err(|_| {
crate::storage_error::StorageError::Other("stored principal user ID is negative".into())
})?;
Ok(Some(ResolvedPrincipal {
principal: PrincipalId { authority, user_id },
handle: PrincipalHandle(handle),
username,
public_keys,
home: decode_home(home.as_deref())?,
descriptor_revision,
valid_until,
resolved_at,
}))
}
fn encode_home(home: &PrincipalHome) -> Option<String> {
match home {
PrincipalHome::Iota(id) => Some(format!("iota:{}", id.as_str())),
PrincipalHome::LegacyOmegaIota { omega, iota_id } => {
Some(format!("legacy_omega_iota:{}:{iota_id}", omega.as_str()))
}
PrincipalHome::Omega(locator) => Some(format!("omega:{}", locator.as_str())),
PrincipalHome::Unknown => None,
}
}
fn decode_home(home: Option<&str>) -> Result<PrincipalHome, crate::storage_error::StorageError> {
let Some(home) = home else {
return Ok(PrincipalHome::Unknown);
};
if let Some(id) = home.strip_prefix("iota:") {
return iota_identity::IotaNodeId::new(id)
.map(PrincipalHome::Iota)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()));
}
if let Some(value) = home.strip_prefix("legacy_omega_iota:") {
let (omega, iota_id) = value.rsplit_once(':').ok_or_else(|| {
crate::storage_error::StorageError::Other(
"stored legacy Omega Iota home is invalid".into(),
)
})?;
let omega = AuthorityId::new(omega)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))?;
let iota_id = iota_id.parse::<u64>().map_err(|_| {
crate::storage_error::StorageError::Other(
"stored legacy Omega Iota ID is invalid".into(),
)
})?;
return Ok(PrincipalHome::LegacyOmegaIota { omega, iota_id });
}
if let Some(locator) = home.strip_prefix("omega:") {
return iota_identity::AuthorityLocator::new(locator)
.map(PrincipalHome::Omega)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()));
}
Err(crate::storage_error::StorageError::Other(
"stored principal home is invalid".into(),
))
}
fn storage_error(error: impl std::fmt::Display) -> IdentityError {
IdentityError::Storage(error.to_string())
}

View file

@ -1,3 +1,5 @@
pub mod identity;
pub mod node_directory;
pub mod storage_error;
pub mod users;
pub mod util;

View file

@ -0,0 +1,420 @@
use async_trait::async_trait;
use iota_identity::{
AuthorityLocator, IdentityError, IotaNodeId, LocalNodeIdentity, NodeDescriptor,
NodeIdentityResolver, PublicKeyBundle, ResolvedNodeIdentity, SignedNodeDescriptor,
VerifiedNodeDescriptor,
};
use iota_util::crypto_helper::{public_key_bundle_from_base64, public_key_bundle_to_base64};
use rusqlite::{OptionalExtension, params};
use crate::util::db;
const DESCRIPTOR_LIFETIME_MILLIS: i64 = 30 * 24 * 60 * 60 * 1_000;
const DESCRIPTOR_RENEWAL_MILLIS: i64 = 7 * 24 * 60 * 60 * 1_000;
#[derive(Default)]
pub struct SqliteNodeDirectory;
impl SqliteNodeDirectory {
pub fn key_for_authentication_hint(
&self,
hint: u64,
) -> Result<Option<PublicKeyBundle>, IdentityError> {
db::with_db(|connection| {
let mut statement = connection.prepare("SELECT node_id FROM node_directory")?;
let nodes = statement
.query_map([], |row| row.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()?;
for node in nodes {
let node = IotaNodeId::new(node).map_err(|error| {
crate::storage_error::StorageError::Other(error.to_string())
})?;
if node.mtp_authentication_hint() != hint {
continue;
}
let key = connection
.query_row(
"SELECT public_key FROM node_keys WHERE node_id = ?1 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC LIMIT 1",
params![node.as_str(), now_millis()],
|row| row.get::<_, String>(0),
)
.optional()?;
return Ok(key);
}
Ok(None)
})
.map_err(storage_error)?
.map(|key| {
public_key_bundle_from_base64(&key).ok_or_else(|| {
IdentityError::InvalidDescriptor("stored node key is invalid".into())
})
})
.transpose()
}
pub fn ensure_local_descriptor(
&self,
identity: &LocalNodeIdentity,
direct_endpoints: Vec<AuthorityLocator>,
relay_hints: Vec<AuthorityLocator>,
now: i64,
) -> Result<SignedNodeDescriptor, IdentityError> {
let current = self.load(identity.node_id())?;
let fields_match = current.as_ref().is_some_and(|current| {
current.direct_endpoints == direct_endpoints && current.relay_hints == relay_hints
});
let renew = current
.as_ref()
.and_then(|current| current.valid_until)
.is_none_or(|valid_until| valid_until <= now + DESCRIPTOR_RENEWAL_MILLIS);
let revision = current.as_ref().map_or(0, |current| {
if fields_match && !renew {
current.descriptor_revision
} else {
current.descriptor_revision.saturating_add(1)
}
});
let valid_until = if fields_match && !renew {
current.and_then(|current| current.valid_until)
} else {
Some(now.saturating_add(DESCRIPTOR_LIFETIME_MILLIS))
};
let descriptor = NodeDescriptor {
node: identity.node_id().clone(),
public_keys: identity.public_keys(),
direct_endpoints,
relay_hints,
revision,
valid_until,
};
let signed = SignedNodeDescriptor::sign(descriptor, identity)?;
let verified = signed
.clone()
.verify(current_revision(&self.load(identity.node_id())?), now)?;
self.upsert(&verified)?;
Ok(signed)
}
pub fn upsert(&self, verified: &VerifiedNodeDescriptor) -> Result<(), IdentityError> {
let descriptor = verified.descriptor();
let direct_endpoints = serde_json::to_string(&descriptor.direct_endpoints)
.map_err(|error| IdentityError::Storage(error.to_string()))?;
let relay_hints = serde_json::to_string(&descriptor.relay_hints)
.map_err(|error| IdentityError::Storage(error.to_string()))?;
let public_key = public_key_bundle_to_base64(&descriptor.public_keys);
if public_key.is_empty() {
return Err(IdentityError::InvalidDescriptor(
"node public key could not be encoded".into(),
));
}
db::with_immediate_transaction(|transaction| {
let current = transaction
.query_row(
"SELECT n.descriptor_revision, n.last_resolved_at, n.descriptor_valid_until, n.direct_endpoints, n.relay_hints, (SELECT public_key FROM node_keys WHERE node_id = n.node_id AND source_revision = n.descriptor_revision LIMIT 1) FROM node_directory n WHERE n.node_id = ?1",
[descriptor.node.as_str()],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, Option<i64>>(2)?, row.get::<_, String>(3)?, row.get::<_, String>(4)?, row.get::<_, Option<String>>(5)?)),
)
.optional()?;
if let Some((revision, resolved_at, valid_until, current_endpoints, current_relays, current_key)) = current {
if revision > descriptor.revision
|| (revision == descriptor.revision && resolved_at > verified.resolved_at())
{
return Err(crate::storage_error::StorageError::Other(
"stale node descriptor update was rejected".into(),
));
}
if revision == descriptor.revision
&& (valid_until != descriptor.valid_until
|| current_endpoints != direct_endpoints
|| current_relays != relay_hints
|| current_key.as_deref() != Some(public_key.as_str()))
{
return Err(crate::storage_error::StorageError::Other(
"conflicting node descriptor revision was rejected".into(),
));
}
}
transaction.execute(
r#"INSERT INTO node_directory (
node_id, descriptor_revision, descriptor_valid_until,
direct_endpoints, relay_hints, last_resolved_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(node_id) DO UPDATE SET
descriptor_revision = excluded.descriptor_revision,
descriptor_valid_until = excluded.descriptor_valid_until,
direct_endpoints = excluded.direct_endpoints,
relay_hints = excluded.relay_hints,
last_resolved_at = excluded.last_resolved_at"#,
params![
descriptor.node.as_str(),
descriptor.revision,
descriptor.valid_until,
direct_endpoints,
relay_hints,
verified.resolved_at(),
],
)?;
transaction.execute(
"UPDATE node_keys SET valid_until = MIN(COALESCE(valid_until, ?2), ?2) WHERE node_id = ?1",
params![descriptor.node.as_str(), verified.resolved_at()],
)?;
transaction.execute(
r#"INSERT INTO node_keys (
node_id, public_key, valid_from, valid_until, source_revision
) VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(node_id, public_key) DO UPDATE SET
valid_until = excluded.valid_until,
source_revision = excluded.source_revision"#,
params![
descriptor.node.as_str(),
public_key,
verified.resolved_at(),
descriptor.valid_until,
descriptor.revision,
],
)?;
Ok(())
})
.map_err(storage_error)
}
pub fn pin_endpoint_record(
&self,
endpoint: &AuthorityLocator,
node: &IotaNodeId,
now: i64,
) -> Result<(), IdentityError> {
db::with_db(|connection| {
let current = connection
.query_row(
"SELECT node_id FROM node_endpoint_pins WHERE endpoint = ?1",
[endpoint.as_str()],
|row| row.get::<_, String>(0),
)
.optional()?;
if current.as_deref().is_some_and(|current| current != node.as_str()) {
return Err(crate::storage_error::StorageError::Other(
"endpoint node identity changed from its TOFU pin".into(),
));
}
connection.execute(
"INSERT OR IGNORE INTO node_endpoint_pins (endpoint, node_id, pinned_at) VALUES (?1, ?2, ?3)",
params![endpoint.as_str(), node.as_str(), now],
)?;
Ok(())
})
.map_err(storage_error)
}
pub fn pinned_node_record(
&self,
endpoint: &AuthorityLocator,
) -> Result<Option<IotaNodeId>, IdentityError> {
db::with_db(|connection| {
connection
.query_row(
"SELECT node_id FROM node_endpoint_pins WHERE endpoint = ?1",
[endpoint.as_str()],
|row| row.get::<_, String>(0),
)
.optional()
.map_err(Into::into)
})
.map_err(storage_error)?
.map(IotaNodeId::new)
.transpose()
}
pub fn pin_endpoint_certificate_record(
&self,
endpoint: &AuthorityLocator,
certificate: &[u8],
) -> Result<(), IdentityError> {
if certificate.is_empty() {
return Err(IdentityError::InvalidDescriptor(
"endpoint TLS certificate is empty".into(),
));
}
db::with_db(|connection| {
let current = connection
.query_row(
"SELECT tls_certificate FROM node_endpoint_pins WHERE endpoint = ?1",
[endpoint.as_str()],
|row| row.get::<_, Option<Vec<u8>>>(0),
)
.optional()?
.ok_or_else(|| {
crate::storage_error::StorageError::Other(
"endpoint must be pinned before its TLS certificate".into(),
)
})?;
if current
.as_deref()
.is_some_and(|current| current != certificate)
{
return Err(crate::storage_error::StorageError::Other(
"endpoint TLS certificate changed from its TOFU pin".into(),
));
}
connection.execute(
"UPDATE node_endpoint_pins SET tls_certificate = COALESCE(tls_certificate, ?2) WHERE endpoint = ?1",
params![endpoint.as_str(), certificate],
)?;
Ok(())
})
.map_err(storage_error)
}
pub fn pinned_endpoint_certificate_record(
&self,
endpoint: &AuthorityLocator,
) -> Result<Option<Vec<u8>>, IdentityError> {
db::with_db(|connection| {
connection
.query_row(
"SELECT tls_certificate FROM node_endpoint_pins WHERE endpoint = ?1",
[endpoint.as_str()],
|row| row.get::<_, Option<Vec<u8>>>(0),
)
.optional()
.map(|certificate| certificate.flatten())
.map_err(Into::into)
})
.map_err(storage_error)
}
pub fn record_successful_endpoint(
&self,
node: &IotaNodeId,
endpoint: &AuthorityLocator,
) -> Result<(), IdentityError> {
db::with_db(|connection| {
connection.execute(
"UPDATE node_directory SET last_successful_endpoint = ?2 WHERE node_id = ?1",
params![node.as_str(), endpoint.as_str()],
)?;
Ok(())
})
.map_err(storage_error)
}
fn load(&self, node: &IotaNodeId) -> Result<Option<ResolvedNodeIdentity>, IdentityError> {
db::with_db(|connection| {
let row = connection
.query_row(
"SELECT descriptor_revision, descriptor_valid_until, direct_endpoints, relay_hints, last_successful_endpoint, last_resolved_at FROM node_directory WHERE node_id = ?1",
[node.as_str()],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, Option<String>>(4)?, row.get::<_, i64>(5)?)),
)
.optional()?;
let Some((descriptor_revision, valid_until, direct_endpoints, relay_hints, last_successful_endpoint, resolved_at)) = row else {
return Ok(None);
};
let mut statement = connection.prepare(
"SELECT public_key FROM node_keys WHERE node_id = ?1 AND valid_from <= ?2 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC",
)?;
let encoded = statement
.query_map(params![node.as_str(), now_millis()], |row| row.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()?;
Ok(Some((descriptor_revision, valid_until, direct_endpoints, relay_hints, last_successful_endpoint, resolved_at, encoded)))
})
.map_err(storage_error)?
.map(|(descriptor_revision, valid_until, direct_endpoints, relay_hints, last_successful_endpoint, resolved_at, encoded)| {
Ok(ResolvedNodeIdentity {
node: node.clone(),
public_keys: encoded
.into_iter()
.map(|key| public_key_bundle_from_base64(&key).ok_or_else(|| IdentityError::InvalidDescriptor("stored node key is invalid".into())))
.collect::<Result<Vec<PublicKeyBundle>, _>>()?,
descriptor_revision,
valid_until,
direct_endpoints: serde_json::from_str(&direct_endpoints).map_err(|error| IdentityError::Storage(error.to_string()))?,
relay_hints: serde_json::from_str(&relay_hints).map_err(|error| IdentityError::Storage(error.to_string()))?,
last_successful_endpoint: last_successful_endpoint.map(AuthorityLocator::new).transpose()?,
resolved_at,
})
})
.transpose()
}
}
#[async_trait]
impl NodeIdentityResolver for SqliteNodeDirectory {
async fn resolve_node(&self, node: &IotaNodeId) -> Result<ResolvedNodeIdentity, IdentityError> {
let resolved = self.load(node)?.ok_or(IdentityError::NotFound)?;
if resolved
.valid_until
.is_some_and(|valid_until| valid_until <= now_millis())
{
return Err(IdentityError::Unavailable(
"cached node descriptor is expired".into(),
));
}
Ok(resolved)
}
}
impl iota_identity::NodeDirectory for SqliteNodeDirectory {
fn upsert_node_descriptor(
&self,
descriptor: &VerifiedNodeDescriptor,
) -> Result<(), IdentityError> {
self.upsert(descriptor)
}
fn pin_endpoint(
&self,
endpoint: &AuthorityLocator,
node: &IotaNodeId,
now: i64,
) -> Result<(), IdentityError> {
self.pin_endpoint_record(endpoint, node, now)
}
fn pinned_node(
&self,
endpoint: &AuthorityLocator,
) -> Result<Option<IotaNodeId>, IdentityError> {
self.pinned_node_record(endpoint)
}
fn pin_endpoint_certificate(
&self,
endpoint: &AuthorityLocator,
certificate: &[u8],
) -> Result<(), IdentityError> {
self.pin_endpoint_certificate_record(endpoint, certificate)
}
fn pinned_endpoint_certificate(
&self,
endpoint: &AuthorityLocator,
) -> Result<Option<Vec<u8>>, IdentityError> {
self.pinned_endpoint_certificate_record(endpoint)
}
fn record_successful_endpoint(
&self,
node: &IotaNodeId,
endpoint: &AuthorityLocator,
) -> Result<(), IdentityError> {
SqliteNodeDirectory::record_successful_endpoint(self, node, endpoint)
}
}
fn current_revision(node: &Option<ResolvedNodeIdentity>) -> Option<i64> {
node.as_ref().map(|node| node.descriptor_revision)
}
fn now_millis() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(i64::MAX)
}
fn storage_error(error: impl std::fmt::Display) -> IdentityError {
IdentityError::Storage(error.to_string())
}

View file

@ -1,6 +1,7 @@
#[derive(Debug, Clone)]
pub struct Contact {
pub user_id: i64,
pub principal: Option<iota_identity::PrincipalHandle>,
pub user_name: Option<String>,
pub created_at: i64,
pub last_message_at: Option<i64>,
@ -12,6 +13,7 @@ impl Default for Contact {
fn default() -> Self {
Contact {
user_id: 0,
principal: None,
user_name: None,
created_at: 0,
last_message_at: None,
@ -28,7 +30,8 @@ impl Contact {
.unwrap_or_default()
.as_millis() as i64;
Contact {
user_id: user_id,
user_id,
principal: None,
user_name: None,
created_at,
last_message_at: None,
@ -36,6 +39,13 @@ impl Contact {
notifications_read_at: 0,
}
}
pub fn for_principal(user_id: i64, principal: iota_identity::PrincipalHandle) -> Self {
Self {
principal: Some(principal),
..Self::new(user_id)
}
}
pub fn set_last_message_at(&mut self, p0: i64) {
self.last_message_at = Option::from(p0);
}

View file

@ -41,6 +41,31 @@ pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::Stora
try_add_user_with_credential_origin(user, CredentialOrigin::Local)
}
/* Reserve a local identity before account creation. The counter never moves
* backward, so a removed account cannot be assigned its old canonical ID. */
pub fn allocate_local_user_id(maximum: i64) -> Result<i64, crate::storage_error::StorageError> {
db::with_immediate_transaction(|tx| {
let next = tx.query_row(
"SELECT next_user_id FROM local_identity_allocator WHERE singleton = 1",
[],
|row| row.get::<_, i64>(0),
)?;
if next < 1 || next > maximum {
return Err(crate::storage_error::StorageError::Other(
"local user ID space is exhausted".into(),
));
}
let following = next.checked_add(1).ok_or_else(|| {
crate::storage_error::StorageError::Other("local user ID space is exhausted".into())
})?;
tx.execute(
"UPDATE local_identity_allocator SET next_user_id = ?1 WHERE singleton = 1",
params![following],
)?;
Ok(next)
})
}
pub fn try_add_user_with_credential_origin(
user: UserProfile,
credential_origin: CredentialOrigin,
@ -457,15 +482,15 @@ fn purge_database_rows(user_id: i64) -> Result<(), crate::storage_error::Storage
params![user_id],
)?;
tx.execute(
"DELETE FROM pending_relays WHERE relay_signer_id = ?1 OR relay_destination_user_id = ?1",
"DELETE FROM pending_relays WHERE relay_signer_id = ?1 OR relay_destination_user_id = ?1 OR signer_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1) OR destination_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1)",
params![user_id],
)?;
tx.execute(
"DELETE FROM relay_replay WHERE EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_id = relay_replay.signer_id AND relay_inbox.message_id = relay_replay.message_id AND (relay_inbox.signer_id = ?1 OR relay_inbox.destination_id = ?1))",
"DELETE FROM relay_replay WHERE EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_principal = relay_replay.signer_principal AND relay_inbox.message_id = relay_replay.message_id AND (relay_inbox.signer_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1) OR relay_inbox.destination_id = ?1))",
params![user_id],
)?;
tx.execute(
"DELETE FROM relay_inbox WHERE signer_id = ?1 OR destination_id = ?1",
"DELETE FROM relay_inbox WHERE signer_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1) OR destination_id = ?1",
params![user_id],
)?;
tx.commit()?;

View file

@ -6,35 +6,48 @@ use rusqlite::{OptionalExtension, params};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockMutation {
pub blocked_user_id: i64,
pub blocked_principal: iota_identity::PrincipalHandle,
pub revision: i64,
pub deleted: bool,
}
fn validate(user_id: i64, blocked_user_id: i64) -> Result<(), StorageError> {
if user_id <= 0 || blocked_user_id <= 0 || user_id == blocked_user_id {
fn validate(
user_id: i64,
blocked_principal: iota_identity::PrincipalHandle,
) -> Result<(), StorageError> {
if user_id <= 0 || blocked_principal.0 <= 0 {
Err(StorageError::Other("invalid block relationship".into()))
} else {
Ok(())
}
}
pub fn block(user_id: i64, blocked_user_id: i64) -> Result<BlockMutation, StorageError> {
validate(user_id, blocked_user_id)?;
pub fn block(
user_id: i64,
blocked_principal: iota_identity::PrincipalHandle,
) -> Result<BlockMutation, StorageError> {
validate(user_id, blocked_principal)?;
db::with_immediate_transaction(|tx| {
let prior = tx.query_row("SELECT id, revision FROM blocked_users WHERE user_id = ?1 AND blocked_user_id = ?2", params![user_id, blocked_user_id], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))).optional()?;
let blocked_user_id = tx.query_row(
"SELECT remote_user_id FROM principals WHERE principal_pk = ?1",
[blocked_principal.0],
|row| row.get::<_, i64>(0),
)?;
let prior = tx.query_row("SELECT id, revision FROM blocked_users WHERE user_id = ?1 AND blocked_principal = ?2", params![user_id, blocked_principal.0], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))).optional()?;
if let Some((_, revision)) = prior {
return Ok(BlockMutation {
blocked_user_id,
blocked_principal,
revision,
deleted: false,
});
}
let created_at = sync::now_millis();
tx.execute("INSERT INTO blocked_users (user_id, blocked_user_id, revision, created_at) VALUES (?1, ?2, 0, ?3)", params![user_id, blocked_user_id, created_at])?;
tx.execute("INSERT INTO blocked_users (user_id, blocked_user_id, blocked_principal, revision, created_at) VALUES (?1, ?2, ?3, 0, ?4)", params![user_id, blocked_user_id, blocked_principal.0, created_at])?;
let id = tx.last_insert_rowid();
let revision = sync::record_event(
tx,
user_id,
sync::EntityType::BlockedUser,
id,
blocked_principal.0,
sync::Operation::Upsert,
)?;
tx.execute(
@ -43,41 +56,48 @@ pub fn block(user_id: i64, blocked_user_id: i64) -> Result<BlockMutation, Storag
)?;
Ok(BlockMutation {
blocked_user_id,
blocked_principal,
revision,
deleted: false,
})
})
}
pub fn unblock(user_id: i64, blocked_user_id: i64) -> Result<Option<BlockMutation>, StorageError> {
validate(user_id, blocked_user_id)?;
pub fn unblock(
user_id: i64,
blocked_principal: iota_identity::PrincipalHandle,
) -> Result<Option<BlockMutation>, StorageError> {
validate(user_id, blocked_principal)?;
db::with_immediate_transaction(|tx| {
let id = tx
let row = tx
.query_row(
"SELECT id FROM blocked_users WHERE user_id = ?1 AND blocked_user_id = ?2",
params![user_id, blocked_user_id],
|r| r.get(0),
"SELECT id, blocked_user_id FROM blocked_users WHERE user_id = ?1 AND blocked_principal = ?2",
params![user_id, blocked_principal.0],
|r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
)
.optional()?;
let Some(id) = id else {
let Some((id, blocked_user_id)) = row else {
return Ok(None);
};
let revision = sync::record_event(
tx,
user_id,
sync::EntityType::BlockedUser,
id,
blocked_principal.0,
sync::Operation::Delete,
)?;
tx.execute("DELETE FROM blocked_users WHERE id = ?1", [id])?;
Ok(Some(BlockMutation {
blocked_user_id,
blocked_principal,
revision,
deleted: true,
}))
})
}
pub fn is_blocked(user_id: i64, candidate_sender_id: i64) -> Result<bool, StorageError> {
validate(user_id, candidate_sender_id)?;
if user_id <= 0 || candidate_sender_id <= 0 || user_id == candidate_sender_id {
return Err(StorageError::Other("invalid block relationship".into()));
}
db::with_db(|conn| {
Ok(conn
.query_row(
@ -89,16 +109,42 @@ pub fn is_blocked(user_id: i64, candidate_sender_id: i64) -> Result<bool, Storag
.is_some())
})
}
pub fn list(user_id: i64) -> Result<Vec<i64>, StorageError> {
pub fn is_principal_blocked(
user_id: i64,
candidate: iota_identity::PrincipalHandle,
) -> Result<bool, StorageError> {
if user_id <= 0 || candidate.0 <= 0 {
return Err(StorageError::Other("invalid block relationship".into()));
}
db::with_db(|conn| {
Ok(conn
.query_row(
"SELECT 1 FROM blocked_users WHERE user_id = ?1 AND blocked_principal = ?2",
params![user_id, candidate.0],
|_| Ok(()),
)
.optional()?
.is_some())
})
}
pub fn list(user_id: i64) -> Result<Vec<BlockMutation>, StorageError> {
if user_id <= 0 {
return Err(StorageError::Other("invalid block owner".into()));
}
db::with_db(|conn| {
let mut stmt = conn.prepare(
"SELECT blocked_user_id FROM blocked_users WHERE user_id = ?1 ORDER BY blocked_user_id",
"SELECT blocked_user_id, blocked_principal, revision FROM blocked_users WHERE user_id = ?1 ORDER BY blocked_principal",
)?;
stmt.query_map([user_id], |r| r.get(0))?
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)
stmt.query_map([user_id], |r| {
Ok(BlockMutation {
blocked_user_id: r.get(0)?,
blocked_principal: iota_identity::PrincipalHandle(r.get(1)?),
revision: r.get(2)?,
deleted: false,
})
})?
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)
})
}

View file

@ -50,7 +50,9 @@ impl MessageState {
pub struct StoredMessage {
pub id: i64,
pub external_user: i64,
pub external_principal: Option<iota_identity::PrincipalHandle>,
pub relay_signer_id: Option<i64>,
pub relay_signer_principal: Option<iota_identity::PrincipalHandle>,
pub relay_message_id: Option<String>,
pub message_time: i64,
pub authored_at: Option<i64>,
@ -74,11 +76,13 @@ pub struct StoredMessage {
pub struct NewMessage<'a> {
pub relay_signer_id: i64,
pub relay_signer_principal: iota_identity::PrincipalHandle,
pub relay_message_id: &'a str,
pub authored_at: i64,
pub send_time: i64,
pub storage_owner: i64,
pub external_user: i64,
pub external_principal: iota_identity::PrincipalHandle,
pub sent_by_self: bool,
pub content: &'a str,
pub height: i64,
@ -109,6 +113,26 @@ pub fn edit_message(
update_message_content(
storage_owner,
external_user,
None,
message_time,
editor_id,
new_content,
true,
)
}
pub fn edit_message_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
editor_id: i64,
new_content: &str,
) -> Result<(), StorageError> {
update_message_content(
storage_owner,
external_user,
Some(external_principal),
message_time,
editor_id,
new_content,
@ -133,6 +157,31 @@ pub fn apply_remote_edit(
update_message_content(
storage_owner,
external_user,
None,
message_time,
editor_id,
new_content,
false,
)
}
pub fn apply_remote_edit_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
editor_id: i64,
new_content: &str,
) -> Result<(), StorageError> {
if editor_id != external_user {
return Err(StorageError::Other(
"Remote editor does not match chat partner".into(),
));
}
update_message_content(
storage_owner,
external_user,
Some(external_principal),
message_time,
editor_id,
new_content,
@ -143,6 +192,7 @@ pub fn apply_remote_edit(
fn update_message_content(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
editor_id: i64,
new_content: &str,
@ -153,10 +203,20 @@ fn update_message_content(
r#"
SELECT id, content, sent_by_self, history_deleted
FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| {
Ok((
row.get::<_, i64>(0)?,
@ -221,16 +281,35 @@ pub fn hard_delete_message(
storage_owner: i64,
external_user: i64,
message_time: i64,
) -> Result<(), StorageError> {
hard_delete_message_with_principal(storage_owner, external_user, None, message_time)
}
fn hard_delete_message_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
) -> Result<(), StorageError> {
db::with_db(|conn| {
let msg_id = conn
.query_row(
r#"
SELECT id, history_deleted FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| row.get::<_, i64>(0),
)
.optional()?;
@ -277,14 +356,14 @@ pub fn remove_message_history_in_tx(
storage_owner: i64,
message_id: i64,
) -> Result<(), StorageError> {
let message: Option<(i64, i64)> = tx
let message: Option<(i64, Option<i64>, i64)> = tx
.query_row(
"SELECT external_user, history_deleted FROM messages WHERE id = ?1 AND storage_owner = ?2",
"SELECT external_user, external_principal, history_deleted FROM messages WHERE id = ?1 AND storage_owner = ?2",
params![message_id, storage_owner],
|row| Ok((row.get(0)?, row.get(1)?)),
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.optional()?;
let Some((external_user, history_deleted)) = message else {
let Some((external_user, external_principal, history_deleted)) = message else {
return Ok(());
};
if history_deleted != 0 {
@ -295,7 +374,7 @@ pub fn remove_message_history_in_tx(
[message_id],
)?;
tx.execute("DELETE FROM reactions WHERE message_id = ?1", [message_id])?;
tx.execute("DELETE FROM message_receipts WHERE storage_owner = ?1 AND EXISTS (SELECT 1 FROM messages WHERE id = ?2 AND relay_signer_id = message_receipts.target_signer_id AND relay_message_id = message_receipts.target_message_id)", params![storage_owner, message_id])?;
tx.execute("DELETE FROM message_receipts WHERE storage_owner = ?1 AND EXISTS (SELECT 1 FROM messages WHERE id = ?2 AND relay_signer_principal = message_receipts.target_signer_principal AND relay_message_id = message_receipts.target_message_id)", params![storage_owner, message_id])?;
tx.execute("UPDATE messages SET content = '', history_deleted = 1, history_deleted_at = ?2, expires_at = NULL, client_received_at = NULL, client_received_recorded_at = NULL, read_at = NULL, read_recorded_at = NULL WHERE id = ?1", params![message_id, sync::now_millis()])?;
sync::record_event(
tx,
@ -304,7 +383,12 @@ pub fn remove_message_history_in_tx(
message_id,
Operation::Delete,
)?;
update_contact_last_message_in_tx(tx, storage_owner, external_user)?;
update_contact_last_message_in_tx(
tx,
storage_owner,
external_user,
external_principal.map(iota_identity::PrincipalHandle),
)?;
Ok(())
}
@ -312,8 +396,12 @@ fn update_contact_last_message_in_tx(
tx: &Transaction<'_>,
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
) -> Result<(), StorageError> {
tx.execute("UPDATE contacts SET last_message_at = (SELECT MAX(COALESCE(destination_iota_received_at, origin_iota_received_at, authored_at, message_time)) FROM messages WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 AND history_deleted = 0) WHERE storage_owner = ?1 AND user_id = ?2", params![storage_owner, external_user])?;
tx.execute(
"UPDATE contacts SET last_message_at = (SELECT MAX(COALESCE(destination_iota_received_at, origin_iota_received_at, authored_at, message_time)) FROM messages WHERE storage_owner = ?1 AND ((?3 IS NULL AND external_user = ?2) OR external_principal = ?3) AND deleted_by_external = 0 AND history_deleted = 0) WHERE storage_owner = ?1 AND ((?3 IS NULL AND user_id = ?2) OR principal_handle = ?3)",
params![storage_owner, external_user, external_principal.map(|principal| principal.0)],
)?;
Ok(())
}
@ -322,15 +410,17 @@ pub fn purge_message_in_tx(
storage_owner: i64,
message_id: i64,
) -> Result<(), StorageError> {
let (external_user, relay_signer_id, relay_message_id) = tx.query_row(
"SELECT external_user, relay_signer_id, relay_message_id FROM messages WHERE id = ?1 AND storage_owner = ?2",
let (external_user, external_principal, relay_signer_principal, relay_message_id) = tx.query_row(
"SELECT external_user, external_principal, relay_signer_principal, relay_message_id FROM messages WHERE id = ?1 AND storage_owner = ?2",
params![message_id, storage_owner],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, Option<String>>(2)?)),
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, Option<i64>>(2)?, row.get::<_, Option<String>>(3)?)),
)?;
if let (Some(relay_signer_id), Some(relay_message_id)) = (relay_signer_id, relay_message_id) {
if let (Some(relay_signer_principal), Some(relay_message_id)) =
(relay_signer_principal, relay_message_id)
{
tx.execute(
"DELETE FROM message_receipts WHERE storage_owner = ?1 AND target_signer_id = ?2 AND target_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"DELETE FROM message_receipts WHERE storage_owner = ?1 AND target_signer_principal = ?2 AND target_message_id = ?3",
params![storage_owner, relay_signer_principal, relay_message_id],
)?;
}
tx.execute(
@ -346,7 +436,12 @@ pub fn purge_message_in_tx(
message_id,
Operation::Delete,
)?;
update_contact_last_message_in_tx(tx, storage_owner, external_user)?;
update_contact_last_message_in_tx(
tx,
storage_owner,
external_user,
external_principal.map(iota_identity::PrincipalHandle),
)?;
Ok(())
}
@ -356,8 +451,50 @@ pub fn delete_message(
external_user: i64,
message_time: i64,
) -> Result<(), StorageError> {
match ensure_message_direction(storage_owner, external_user, message_time, true) {
Ok(()) => hard_delete_message(storage_owner, external_user, message_time),
delete_message_with_principal(storage_owner, external_user, None, message_time, true)
}
pub fn delete_message_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
) -> Result<(), StorageError> {
delete_message_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
true,
)
}
fn delete_message_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
sent_by_self: bool,
) -> Result<(), StorageError> {
match ensure_message_direction(
storage_owner,
external_user,
external_principal,
message_time,
sent_by_self,
) {
Ok(()) if sent_by_self => hard_delete_message_with_principal(
storage_owner,
external_user,
external_principal,
message_time,
),
Ok(()) => flag_deleted_by_external_with_principal(
storage_owner,
external_user,
external_principal,
message_time,
),
Err(StorageError::Db(rusqlite::Error::QueryReturnedNoRows)) => Ok(()),
Err(error) => Err(error),
}
@ -375,16 +512,34 @@ pub fn apply_remote_delete(
"Remote sender does not match chat partner".into(),
));
}
match ensure_message_direction(storage_owner, external_user, message_time, false) {
Ok(()) => flag_deleted_by_external(storage_owner, external_user, message_time),
Err(StorageError::Db(rusqlite::Error::QueryReturnedNoRows)) => Ok(()),
Err(error) => Err(error),
delete_message_with_principal(storage_owner, external_user, None, message_time, false)
}
pub fn apply_remote_delete_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
sender_id: i64,
) -> Result<(), StorageError> {
if sender_id != external_user {
return Err(StorageError::Other(
"Remote sender does not match chat partner".into(),
));
}
delete_message_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
false,
)
}
fn ensure_message_direction(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
expected_sent_by_self: bool,
) -> Result<(), StorageError> {
@ -392,10 +547,20 @@ fn ensure_message_direction(
let sent_by_self: i64 = conn.query_row(
r#"
SELECT sent_by_self FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| row.get(0),
)?;
if (sent_by_self != 0) != expected_sent_by_self {
@ -415,6 +580,15 @@ pub fn flag_deleted_by_external(
storage_owner: i64,
external_user: i64,
message_time: i64,
) -> Result<(), StorageError> {
flag_deleted_by_external_with_principal(storage_owner, external_user, None, message_time)
}
fn flag_deleted_by_external_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
) -> Result<(), StorageError> {
db::with_db(|conn| {
let tx = conn.unchecked_transaction()?;
@ -422,14 +596,28 @@ pub fn flag_deleted_by_external(
r#"
UPDATE messages
SET deleted_by_external = 1
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
)?;
if affected == 0 {
return Err(StorageError::Other("Message not found".into()));
}
let msg_id: i64 = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3 ORDER BY id DESC LIMIT 1", params![storage_owner, external_user, message_time], |r| r.get(0))?;
let msg_id: i64 = tx.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND message_time = ?3 AND ((?4 IS NULL AND external_user = ?2) OR external_principal = ?4) ORDER BY id DESC LIMIT 1",
params![storage_owner, external_user, message_time, external_principal.map(|principal| principal.0)],
|r| r.get(0),
)?;
sync::record_event(
&tx,
storage_owner,
@ -437,7 +625,7 @@ pub fn flag_deleted_by_external(
msg_id,
Operation::Delete,
)?;
update_contact_last_message_in_tx(&tx, storage_owner, external_user)?;
update_contact_last_message_in_tx(&tx, storage_owner, external_user, external_principal)?;
tx.commit()?;
Ok(())
})
@ -490,15 +678,65 @@ pub fn add_reaction(
message_time: i64,
user_id: i64,
reaction: &str,
) -> Result<(), StorageError> {
add_reaction_with_principal(
storage_owner,
external_user,
None,
message_time,
user_id,
None,
reaction,
)
}
pub fn add_reaction_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
user_id: i64,
user_principal: iota_identity::PrincipalHandle,
reaction: &str,
) -> Result<(), StorageError> {
add_reaction_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
user_id,
Some(user_principal),
reaction,
)
}
fn add_reaction_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
user_id: i64,
user_principal: Option<iota_identity::PrincipalHandle>,
reaction: &str,
) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
let (msg_id, history_deleted): (i64, i64) = tx.query_row(
r#"
SELECT id, history_deleted FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
if history_deleted != 0 {
@ -528,10 +766,16 @@ pub fn add_reaction(
let inserted = tx.execute(
r#"
INSERT OR IGNORE INTO reactions (message_id, user_id, reaction, created_at)
VALUES (?1, ?2, ?3, ?4)
INSERT OR IGNORE INTO reactions (message_id, user_id, user_principal, reaction, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)
"#,
params![msg_id, user_id, reaction, now],
params![
msg_id,
user_id,
user_principal.map(|principal| principal.0),
reaction,
now
],
)?;
if inserted > 0 {
sync::record_event(
@ -554,15 +798,65 @@ pub fn remove_reaction(
message_time: i64,
user_id: i64,
reaction: &str,
) -> Result<(), StorageError> {
remove_reaction_with_principal(
storage_owner,
external_user,
None,
message_time,
user_id,
None,
reaction,
)
}
pub fn remove_reaction_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
user_id: i64,
user_principal: iota_identity::PrincipalHandle,
reaction: &str,
) -> Result<(), StorageError> {
remove_reaction_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
user_id,
Some(user_principal),
reaction,
)
}
fn remove_reaction_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
user_id: i64,
user_principal: Option<iota_identity::PrincipalHandle>,
reaction: &str,
) -> Result<(), StorageError> {
db::with_db(|conn| {
let (msg_id, history_deleted): (i64, i64) = conn.query_row(
r#"
SELECT id, history_deleted FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
if history_deleted != 0 {
@ -571,8 +865,13 @@ pub fn remove_reaction(
let tx = conn.unchecked_transaction()?;
tx.execute(
"DELETE FROM reactions WHERE message_id = ?1 AND user_id = ?2 AND reaction = ?3",
params![msg_id, user_id, reaction],
"DELETE FROM reactions WHERE message_id = ?1 AND ((?3 IS NULL AND user_id = ?2) OR user_principal = ?3) AND reaction = ?4",
params![
msg_id,
user_id,
user_principal.map(|principal| principal.0),
reaction
],
)?;
sync::record_event(
&tx,
@ -589,11 +888,13 @@ pub fn remove_reaction(
pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
let NewMessage {
relay_signer_id,
relay_signer_principal,
relay_message_id,
authored_at,
send_time,
storage_owner,
external_user,
external_principal,
sent_by_self,
content,
height,
@ -620,8 +921,9 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
INSERT INTO messages (
storage_owner, external_user, message_time, content, sent_by_self,
message_state, height, key_version, reply_to, relay_signer_id, relay_message_id,
authored_at, origin_iota_received_at, destination_iota_received_at, stored_at, expires_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
authored_at, origin_iota_received_at, destination_iota_received_at, stored_at,
expires_at, external_principal, relay_signer_principal
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)
"#,
params![
storage_owner,
@ -640,6 +942,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
destination_iota_received_at,
stored_at,
expires_at,
external_principal.0,
relay_signer_principal.0,
],
)?;
let msg_id = tx.last_insert_rowid();
@ -650,7 +954,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
msg_id,
Operation::Upsert,
)?;
let mut contact = crate::users::contact::Contact::new(external_user);
let mut contact =
crate::users::contact::Contact::for_principal(external_user, external_principal);
contact.set_last_message_at(
destination_iota_received_at
.or(origin_iota_received_at)
@ -659,8 +964,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
crate::util::chats_util::upsert_contact(&tx, storage_owner, &contact)?;
if !sent_by_self {
tx.execute(
"UPDATE contacts SET notifications = CASE WHEN notifications < 9223372036854775807 THEN notifications + 1 ELSE notifications END WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, external_user],
"UPDATE contacts SET notifications = CASE WHEN notifications < 9223372036854775807 THEN notifications + 1 ELSE notifications END WHERE storage_owner = ?1 AND principal_handle = ?2",
params![storage_owner, external_principal.0],
)?;
}
tx.commit()?;
@ -670,7 +975,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
pub fn change_message_state_by_relay_id(
storage_owner: i64,
relay_signer_id: i64,
_relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
new_state: MessageState,
) -> Result<(), StorageError> {
@ -678,8 +984,8 @@ pub fn change_message_state_by_relay_id(
let tx = conn.unchecked_transaction()?;
let Some((msg_id, current)) = tx
.query_row(
"SELECT id, message_state FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id, message_state FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
)
.optional()?
@ -706,8 +1012,10 @@ pub fn change_message_state_by_relay_id(
pub fn record_message_receipt(
storage_owner: i64,
target_signer_id: i64,
target_signer_principal: iota_identity::PrincipalHandle,
target_message_id: &str,
receipt_signer_id: i64,
receipt_signer_principal: iota_identity::PrincipalHandle,
receipt_message_id: &str,
receipt_type: MessageState,
event_at: i64,
@ -720,17 +1028,17 @@ pub fn record_message_receipt(
};
db::with_db(|conn| {
let tx = conn.unchecked_transaction()?;
let Some((message_id, external_user, authored_at, history_deleted)) = tx
let Some((message_id, external_principal, authored_at, history_deleted)) = tx
.query_row(
"SELECT id, external_user, authored_at, history_deleted FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, target_signer_id, target_message_id],
"SELECT id, external_principal, authored_at, history_deleted FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, target_signer_principal.0, target_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, Option<i64>>(2)?, row.get::<_, i64>(3)?)),
)
.optional()?
else {
return Err(StorageError::Other("message receipt target was not found".into()));
};
if external_user != receipt_signer_id {
if external_principal != receipt_signer_principal.0 {
return Err(StorageError::Other(
"message receipt signer is not the chat partner".into(),
));
@ -746,8 +1054,8 @@ pub fn record_message_receipt(
return Ok(());
}
tx.execute(
"INSERT OR IGNORE INTO message_receipts (storage_owner, target_signer_id, target_message_id, receipt_signer_id, receipt_message_id, receipt_type, event_at, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![storage_owner, target_signer_id, target_message_id, receipt_signer_id, receipt_message_id, receipt_type, event_at, recorded_at],
"INSERT OR IGNORE INTO message_receipts (storage_owner, target_signer_id, target_signer_principal, target_message_id, receipt_signer_id, receipt_signer_principal, receipt_message_id, receipt_type, event_at, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
params![storage_owner, target_signer_id, target_signer_principal.0, target_message_id, receipt_signer_id, receipt_signer_principal.0, receipt_message_id, receipt_type, event_at, recorded_at],
)?;
let (state_column, recorded_column) = if receipt_type == "read" {
("read_at", "read_recorded_at")
@ -780,7 +1088,7 @@ pub fn record_message_receipt(
pub fn record_destination_iota_received(
storage_owner: i64,
relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
accepted_at: i64,
) -> Result<(), StorageError> {
@ -788,8 +1096,8 @@ pub fn record_destination_iota_received(
let tx = conn.unchecked_transaction()?;
let Some(message_id) = tx
.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| row.get::<_, i64>(0),
)
.optional()?
@ -814,7 +1122,7 @@ pub fn record_destination_iota_received(
pub fn record_delivery_failure(
storage_owner: i64,
relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
failure: &str,
failed_at: i64,
@ -823,8 +1131,8 @@ pub fn record_delivery_failure(
let tx = conn.unchecked_transaction()?;
let Some(message_id) = tx
.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| row.get::<_, i64>(0),
)
.optional()?
@ -957,6 +1265,20 @@ pub fn get_messages(
external_user: i64,
loaded_messages: i64,
amount: i64,
) -> Result<Vec<StoredMessage>, StorageError> {
let Some(principal) = crate::util::chats_util::get_user(storage_owner, external_user)?
.and_then(|contact| contact.principal)
else {
return Ok(Vec::new());
};
get_messages_for_principal(storage_owner, principal, loaded_messages, amount)
}
pub fn get_messages_for_principal(
storage_owner: i64,
external_principal: iota_identity::PrincipalHandle,
loaded_messages: i64,
amount: i64,
) -> Result<Vec<StoredMessage>, StorageError> {
if amount <= 0 || loaded_messages < 0 {
return Ok(Vec::new());
@ -969,21 +1291,25 @@ pub fn get_messages(
origin_iota_received_at, destination_iota_received_at,
client_received_at, client_received_recorded_at, read_at,
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version,
reply_to, edited_count
reply_to, edited_count, external_user, relay_signer_principal
FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 AND history_deleted = 0
WHERE storage_owner = ?1 AND external_principal = ?2 AND deleted_by_external = 0 AND history_deleted = 0
ORDER BY COALESCE(destination_iota_received_at, origin_iota_received_at, authored_at, id) DESC, id DESC
LIMIT ?3 OFFSET ?4
"#,
)?;
let rows = stmt.query_map(
params![storage_owner, external_user, amount, loaded_messages],
params![storage_owner, external_principal.0, amount, loaded_messages],
|row| {
Ok(StoredMessage {
id: row.get(0)?,
external_user,
external_user: row.get(20)?,
external_principal: Some(external_principal),
relay_signer_id: row.get(1)?,
relay_signer_principal: row
.get::<_, Option<i64>>(21)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,
@ -1026,6 +1352,7 @@ pub fn get_message(
storage_owner: i64,
message_time: i64,
external_user: Option<i64>,
external_principal: Option<iota_identity::PrincipalHandle>,
) -> Result<Option<StoredMessage>, StorageError> {
db::with_db(|conn| {
let mut stmt = conn.prepare(
@ -1034,43 +1361,62 @@ pub fn get_message(
origin_iota_received_at, destination_iota_received_at,
client_received_at, client_received_recorded_at, read_at,
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version,
reply_to, edited_count, external_user
reply_to, edited_count, external_user, external_principal,
relay_signer_principal
FROM messages
WHERE storage_owner = ?1
AND message_time = ?2
AND deleted_by_external = 0
AND history_deleted = 0
AND (?3 IS NULL OR external_user = ?3)
AND (
?3 IS NULL
OR (?4 IS NULL AND external_user = ?3)
OR external_principal = ?4
)
ORDER BY id DESC
"#,
)?;
let rows = stmt.query_map(params![storage_owner, message_time, external_user], |row| {
Ok(StoredMessage {
id: row.get(0)?,
relay_signer_id: row.get(1)?,
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,
origin_iota_received_at: row.get(5)?,
destination_iota_received_at: row.get(6)?,
client_received_at: row.get(7)?,
client_received_recorded_at: row.get(8)?,
read_at: row.get(9)?,
read_recorded_at: row.get(10)?,
delivery_failed_at: row.get(11)?,
delivery_failure: row.get(12)?,
content: row.get(13)?,
sent_by_self: row.get::<_, i64>(14)? != 0,
message_state: row.get(15)?,
height: row.get(16)?,
key_version: row.get(17)?,
reply_to: row.get(18)?,
edited: row.get::<_, i64>(19)? > 0,
external_user: row.get(20)?,
reactions: Vec::new(),
})
})?;
let rows = stmt.query_map(
params![
storage_owner,
message_time,
external_user,
external_principal.map(|principal| principal.0)
],
|row| {
Ok(StoredMessage {
id: row.get(0)?,
relay_signer_id: row.get(1)?,
relay_signer_principal: row
.get::<_, Option<i64>>(22)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,
origin_iota_received_at: row.get(5)?,
destination_iota_received_at: row.get(6)?,
client_received_at: row.get(7)?,
client_received_recorded_at: row.get(8)?,
read_at: row.get(9)?,
read_recorded_at: row.get(10)?,
delivery_failed_at: row.get(11)?,
delivery_failure: row.get(12)?,
content: row.get(13)?,
sent_by_self: row.get::<_, i64>(14)? != 0,
message_state: row.get(15)?,
height: row.get(16)?,
key_version: row.get(17)?,
reply_to: row.get(18)?,
edited: row.get::<_, i64>(19)? > 0,
external_user: row.get(20)?,
external_principal: row
.get::<_, Option<i64>>(21)?
.map(iota_identity::PrincipalHandle),
reactions: Vec::new(),
})
},
)?;
let messages: Vec<StoredMessage> = rows.collect::<Result<_, _>>()?;
if messages.is_empty() {
@ -1079,7 +1425,7 @@ pub fn get_message(
if external_user.is_none()
&& messages
.iter()
.map(|message| message.external_user)
.filter_map(|message| message.external_principal)
.collect::<std::collections::HashSet<_>>()
.len()
> 1
@ -1099,7 +1445,18 @@ pub fn get_message_with_offset(
external_user: i64,
message_time: i64,
) -> Result<Option<(StoredMessage, i64)>, StorageError> {
let Some(message) = get_message(storage_owner, message_time, Some(external_user))? else {
let Some(external_principal) = crate::util::chats_util::get_user(storage_owner, external_user)?
.and_then(|contact| contact.principal)
else {
return Ok(None);
};
let Some(message) = get_message(
storage_owner,
message_time,
Some(external_user),
Some(external_principal),
)?
else {
return Ok(None);
};
let offset = db::with_db(|conn| {
@ -1108,7 +1465,7 @@ pub fn get_message_with_offset(
SELECT COUNT(*)
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
AND external_principal = ?2
AND deleted_by_external = 0
AND history_deleted = 0
AND (
@ -1120,7 +1477,7 @@ pub fn get_message_with_offset(
"#,
params![
storage_owner,
external_user,
external_principal.0,
message.destination_iota_received_at.or(message.origin_iota_received_at).or(message.authored_at),
message.id
],
@ -1142,13 +1499,19 @@ pub fn get_messages_by_ids(
// A journal id uniquely identifies a row. Load all messages for this owner and retain only
// those ids; this keeps reaction hydration identical to normal message loading.
db::with_db(|conn| {
let mut stmt = conn.prepare("SELECT id, relay_signer_id, relay_message_id, message_time, authored_at, origin_iota_received_at, destination_iota_received_at, client_received_at, client_received_recorded_at, read_at, read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version, reply_to, edited_count, external_user FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0 AND history_deleted = 0")?;
let mut stmt = conn.prepare("SELECT id, relay_signer_id, relay_message_id, message_time, authored_at, origin_iota_received_at, destination_iota_received_at, client_received_at, client_received_recorded_at, read_at, read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version, reply_to, edited_count, external_user, external_principal, relay_signer_principal FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0 AND history_deleted = 0")?;
let rows = stmt.query_map([storage_owner], |row| {
let external_user: i64 = row.get(20)?;
Ok(StoredMessage {
id: row.get(0)?,
external_user,
external_principal: row
.get::<_, Option<i64>>(21)?
.map(iota_identity::PrincipalHandle),
relay_signer_id: row.get(1)?,
relay_signer_principal: row
.get::<_, Option<i64>>(22)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,

View file

@ -9,14 +9,31 @@ pub(crate) fn upsert_contact(
storage_owner: i64,
contact: &Contact,
) -> Result<(), StorageError> {
let principal = match contact.principal {
Some(principal) => principal,
None => iota_identity::PrincipalHandle(tx.query_row(
r#"
SELECT p.principal_pk
FROM principals p
WHERE p.authority_id = COALESCE(
(SELECT omega_authority_id FROM identity_configuration WHERE singleton = 1),
'omega:central'
)
AND p.remote_user_id = ?1
"#,
[contact.user_id],
|row| row.get(0),
)?),
};
tx.execute(
r#"
INSERT INTO contacts (
storage_owner, user_id, user_name, created_at, last_message_at,
storage_owner, user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(storage_owner, user_id) DO UPDATE SET
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
ON CONFLICT(storage_owner, principal_handle) DO UPDATE SET
user_id = excluded.user_id,
user_name = COALESCE(excluded.user_name, contacts.user_name),
created_at = MIN(contacts.created_at, excluded.created_at),
last_message_at = CASE
@ -28,6 +45,7 @@ pub(crate) fn upsert_contact(
params![
storage_owner,
contact.user_id,
principal.0,
contact.user_name,
contact.created_at,
contact.last_message_at,
@ -39,63 +57,112 @@ pub(crate) fn upsert_contact(
tx,
storage_owner,
EntityType::Contact,
contact.user_id,
principal.0,
Operation::Upsert,
)?;
Ok(())
}
pub fn has_user(storage_owner: i64, user_id: i64) -> Result<bool, StorageError> {
pub fn has_principal(
storage_owner: i64,
principal: iota_identity::PrincipalHandle,
) -> Result<bool, StorageError> {
db::with_db(|conn| {
Ok(conn.query_row(
"SELECT EXISTS(SELECT 1 FROM contacts WHERE storage_owner = ?1 AND user_id = ?2)",
params![storage_owner, user_id],
"SELECT EXISTS(SELECT 1 FROM contacts WHERE storage_owner = ?1 AND principal_handle = ?2)",
params![storage_owner, principal.0],
|row| row.get(0),
)?)
})
}
pub fn has_user(storage_owner: i64, user_id: i64) -> Result<bool, StorageError> {
get_user(storage_owner, user_id).map(|contact| contact.is_some())
}
pub fn mod_user(storage_owner: i64, contact: &Contact) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| upsert_contact(tx, storage_owner, contact))
}
pub fn get_users_by_ids(storage_owner: i64, ids: &[i64]) -> Result<Vec<Contact>, StorageError> {
if ids.is_empty() {
pub fn get_users_by_principals(
storage_owner: i64,
principals: &[iota_identity::PrincipalHandle],
) -> Result<Vec<Contact>, StorageError> {
if principals.is_empty() {
return Ok(Vec::new());
}
let wanted: std::collections::HashSet<i64> = ids.iter().copied().collect();
let wanted: std::collections::HashSet<iota_identity::PrincipalHandle> =
principals.iter().copied().collect();
Ok(get_users(storage_owner)?
.into_iter()
.filter(|contact| wanted.contains(&contact.user_id))
.filter(|contact| {
contact
.principal
.is_some_and(|principal| wanted.contains(&principal))
})
.collect())
}
pub fn get_user(storage_owner: i64, user_id: i64) -> Result<Option<Contact>, StorageError> {
db::with_db(|conn| {
match conn.query_row(
let mut statement = conn.prepare(
r#"
SELECT user_id, user_name, created_at, last_message_at,
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
LIMIT 1
LIMIT 2
"#,
params![storage_owner, user_id],
|r| {
)?;
let contacts = statement
.query_map(params![storage_owner, user_id], |r| {
Ok(Contact {
user_id: r.get(0)?,
user_name: r.get(1)?,
created_at: r.get(2)?,
last_message_at: r.get(3)?,
notifications: r.get(4)?,
notifications_read_at: r.get(5)?,
principal: r
.get::<_, Option<i64>>(1)?
.map(iota_identity::PrincipalHandle),
user_name: r.get(2)?,
created_at: r.get(3)?,
last_message_at: r.get(4)?,
notifications: r.get(5)?,
notifications_read_at: r.get(6)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
match contacts.as_slice() {
[contact] => Ok(Some(contact.clone())),
_ => Ok(None),
}
})
}
pub fn get_user_by_principal(
storage_owner: i64,
principal: iota_identity::PrincipalHandle,
) -> Result<Option<Contact>, StorageError> {
db::with_db(|conn| {
conn.query_row(
r#"
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND principal_handle = ?2
"#,
params![storage_owner, principal.0],
|row| {
Ok(Contact {
user_id: row.get(0)?,
principal: Some(iota_identity::PrincipalHandle(row.get(1)?)),
user_name: row.get(2)?,
created_at: row.get(3)?,
last_message_at: row.get(4)?,
notifications: row.get(5)?,
notifications_read_at: row.get(6)?,
})
},
) {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
)
.optional()
.map_err(Into::into)
})
}
@ -103,7 +170,7 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
db::with_db(|conn| {
let mut stmt = conn.prepare(
r#"
SELECT user_id, user_name, created_at, last_message_at,
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1
@ -117,11 +184,14 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
let rows = stmt.query_map(params![storage_owner], |r| {
Ok(Contact {
user_id: r.get(0)?,
user_name: r.get(1)?,
created_at: r.get(2)?,
last_message_at: r.get(3)?,
notifications: r.get(4)?,
notifications_read_at: r.get(5)?,
principal: r
.get::<_, Option<i64>>(1)?
.map(iota_identity::PrincipalHandle),
user_name: r.get(2)?,
created_at: r.get(3)?,
last_message_at: r.get(4)?,
notifications: r.get(5)?,
notifications_read_at: r.get(6)?,
})
})?;
@ -135,14 +205,14 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
pub fn read_notifications(
storage_owner: i64,
user_id: i64,
principal: iota_identity::PrincipalHandle,
through: i64,
) -> Result<Option<Contact>, StorageError> {
db::with_immediate_transaction(|tx| {
let Some(current_read_at) = tx
.query_row(
"SELECT notifications_read_at FROM contacts WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, user_id],
"SELECT notifications_read_at FROM contacts WHERE storage_owner = ?1 AND principal_handle = ?2",
params![storage_owner, principal.0],
|row| row.get::<_, i64>(0),
)
.optional()?
@ -155,43 +225,46 @@ pub fn read_notifications(
SELECT COUNT(*)
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
AND external_principal = ?2
AND sent_by_self = 0
AND deleted_by_external = 0
AND history_deleted = 0
AND COALESCE(destination_iota_received_at, stored_at, authored_at, message_time) > ?3
"#,
params![storage_owner, user_id, read_at],
params![storage_owner, principal.0, read_at],
|row| row.get::<_, i64>(0),
)?;
tx.execute(
"UPDATE contacts SET notifications = ?3, notifications_read_at = ?4 WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, user_id, notifications, read_at],
"UPDATE contacts SET notifications = ?3, notifications_read_at = ?4 WHERE storage_owner = ?1 AND principal_handle = ?2",
params![storage_owner, principal.0, notifications, read_at],
)?;
sync::record_event(
tx,
storage_owner,
EntityType::Contact,
user_id,
principal.0,
Operation::Upsert,
)?;
Ok(Some(tx.query_row(
r#"
SELECT user_id, user_name, created_at, last_message_at,
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
WHERE storage_owner = ?1 AND principal_handle = ?2
"#,
params![storage_owner, user_id],
params![storage_owner, principal.0],
|row| {
Ok(Contact {
user_id: row.get(0)?,
user_name: row.get(1)?,
created_at: row.get(2)?,
last_message_at: row.get(3)?,
notifications: row.get(4)?,
notifications_read_at: row.get(5)?,
principal: row
.get::<_, Option<i64>>(1)?
.map(iota_identity::PrincipalHandle),
user_name: row.get(2)?,
created_at: row.get(3)?,
last_message_at: row.get(4)?,
notifications: row.get(5)?,
notifications_read_at: row.get(6)?,
})
},
)?))

View file

@ -25,8 +25,8 @@ pub fn acknowledge_client_delivery(
== message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery
{
let message_id = tx.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
rusqlite::params![destination_id, relay.signer_id, relay.message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
rusqlite::params![destination_id, relay.signer.0, relay.message_id],
|row| row.get::<_, i64>(0),
).optional()?;
if let Some(message_id) = message_id {

View file

@ -30,6 +30,16 @@ pub enum ConfigError {
#[source]
source: std::net::AddrParseError,
},
#[error("invalid federation endpoint {endpoint:?}: {source}")]
InvalidFederationEndpoint {
endpoint: String,
#[source]
source: iota_identity::IdentityError,
},
#[error("invalid relay router public key: {0}")]
InvalidRelayRouterKey(String),
#[error("relay router certificate path must not be empty")]
MissingRelayRouterCertificate,
#[error("max_ipc_clients must be greater than zero")]
InvalidMaxIpcClients,
}
@ -43,6 +53,8 @@ pub struct IotaConfig {
pub port: u16,
#[serde(default)]
pub web: WebSettings,
#[serde(default)]
pub relay_routers: Vec<RelayRouterSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub omikron_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -61,6 +73,14 @@ pub struct IotaConfig {
pub max_ipc_clients: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RelayRouterSettings {
pub endpoint: String,
pub public_key: String,
pub certificate: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WebMode {
@ -89,6 +109,10 @@ pub struct WebSettings {
pub key: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub direct_endpoints: Vec<String>,
#[serde(default)]
pub relay_hints: Vec<String>,
}
fn default_web_bind() -> String {
"127.0.0.1".into()
@ -106,6 +130,8 @@ impl Default for WebSettings {
certificate: None,
key: None,
required: false,
direct_endpoints: Vec::new(),
relay_hints: Vec::new(),
}
}
}
@ -128,6 +154,7 @@ impl Default for IotaConfig {
iota_id: None,
port: default_port(),
web: WebSettings::default(),
relay_routers: Vec::new(),
omikron_host: None,
omikron_port: None,
omikron_id: None,
@ -187,6 +214,32 @@ pub fn validate_config(config: &IotaConfig) -> Result<(), ConfigError> {
if config.max_ipc_clients == 0 {
return Err(ConfigError::InvalidMaxIpcClients);
}
for endpoint in config
.web
.direct_endpoints
.iter()
.chain(&config.web.relay_hints)
{
iota_identity::AuthorityLocator::new(endpoint.clone()).map_err(|source| {
ConfigError::InvalidFederationEndpoint {
endpoint: endpoint.clone(),
source,
}
})?;
}
for router in &config.relay_routers {
iota_identity::AuthorityLocator::new(router.endpoint.clone()).map_err(|source| {
ConfigError::InvalidFederationEndpoint {
endpoint: router.endpoint.clone(),
source,
}
})?;
iota_identity::PublicKeyBundle::from_base64(&router.public_key)
.map_err(|error| ConfigError::InvalidRelayRouterKey(error.to_string()))?;
if router.certificate.trim().is_empty() {
return Err(ConfigError::MissingRelayRouterCertificate);
}
}
Ok(())
}
@ -301,7 +354,7 @@ pub fn configure_config_path(path: PathBuf) {
#[cfg(test)]
mod tests {
use super::{ConfigError, IotaConfig, parse_config, validate_config};
use super::{ConfigError, IotaConfig, RelayRouterSettings, parse_config, validate_config};
use std::path::Path;
#[test]
@ -339,4 +392,34 @@ mod tests {
Err(ConfigError::InvalidMaxIpcClients)
));
}
#[test]
fn relay_router_requires_valid_explicit_trust_pins() {
let mut config = IotaConfig::default();
config.relay_routers.push(RelayRouterSettings {
endpoint: "router.example:1984".into(),
public_key: "invalid".into(),
certificate: "router.pem".into(),
});
assert!(matches!(
validate_config(&config),
Err(ConfigError::InvalidRelayRouterKey(_))
));
}
#[test]
fn relay_router_configuration_parses_with_all_trust_material() {
let public_key = mtp::crypto::Keyring::generate()
.public_key_bundle()
.try_to_base64()
.unwrap();
let config = parse_config(
Path::new("config.yaml"),
&format!(
"relay_routers:\n - endpoint: router.example:1984\n public_key: {public_key}\n certificate: router.pem\n"
),
)
.unwrap();
assert_eq!(config.relay_routers.len(), 1);
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,65 +4,85 @@ use crate::util::{db, sync};
use rusqlite::{OptionalExtension, params};
pub fn acknowledge_iota_delivery(
destination_iota: u64,
frame_id: u32,
signer_id: i64,
signer_principal: iota_identity::PrincipalHandle,
_legacy_signer_id: Option<i64>,
relay_message_id: &str,
destination_accepted_at: i64,
) -> Result<(), StorageError> {
let destination_iota = i64::try_from(destination_iota)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_immediate_transaction(|tx| {
let pending_id = tx.query_row("SELECT id FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", params![destination_iota, i64::from(frame_id)], |row| row.get::<_, i64>(0)).optional()?;
let pending_id = tx
.query_row(
"SELECT id FROM pending_relays WHERE signer_principal = ?1 AND frame_id = ?2",
params![signer_principal.0, i64::from(frame_id)],
|row| row.get::<_, i64>(0),
)
.optional()?;
let Some(pending_id) = pending_id else {
return Ok(());
};
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?1 AND relay_message_id = ?2", params![signer_id, relay_message_id], |row| row.get::<_, i64>(0)).optional()?;
if let Some(message_id) = message_id {
let message = tx
.query_row(
"SELECT id, storage_owner FROM messages WHERE relay_signer_principal = ?1 AND relay_message_id = ?2",
params![signer_principal.0, relay_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.optional()?;
if let Some((message_id, storage_owner)) = message {
tx.execute("UPDATE messages SET destination_iota_received_at = COALESCE(destination_iota_received_at, ?1), delivery_failed_at = NULL, delivery_failure = NULL, message_state = CASE WHEN message_state = 'sending' THEN 'sent' ELSE message_state END WHERE id = ?2", params![destination_accepted_at, message_id])?;
sync::record_event(
tx,
signer_id,
storage_owner,
sync::EntityType::Message,
message_id,
sync::Operation::Upsert,
)?;
}
tx.execute("UPDATE relay_inbox SET state = 'delivered', downstream_acked_at = COALESCE(downstream_acked_at, ?3) WHERE signer_id = ?1 AND message_id = ?2", params![signer_id, relay_message_id, sync::now_millis()])?;
tx.execute("UPDATE relay_inbox SET state = 'delivered', downstream_acked_at = COALESCE(downstream_acked_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2", params![signer_principal.0, relay_message_id, sync::now_millis()])?;
tx.execute("DELETE FROM pending_relays WHERE id = ?1", [pending_id])?;
Ok(())
})
}
pub fn reject_iota_delivery(
destination_iota: u64,
frame_id: u32,
signer_id: i64,
signer_principal: iota_identity::PrincipalHandle,
_legacy_signer_id: Option<i64>,
relay_message_id: &str,
failure: &str,
) -> Result<(), StorageError> {
let destination_iota = i64::try_from(destination_iota)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_immediate_transaction(|tx| {
let pending_id = tx.query_row("SELECT id FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", params![destination_iota, i64::from(frame_id)], |row| row.get::<_, i64>(0)).optional()?;
let pending_id = tx
.query_row(
"SELECT id FROM pending_relays WHERE signer_principal = ?1 AND frame_id = ?2",
params![signer_principal.0, i64::from(frame_id)],
|row| row.get::<_, i64>(0),
)
.optional()?;
let Some(pending_id) = pending_id else {
return Ok(());
};
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?1 AND relay_message_id = ?2", params![signer_id, relay_message_id], |row| row.get::<_, i64>(0)).optional()?;
if let Some(message_id) = message_id {
let message = tx
.query_row(
"SELECT id, storage_owner FROM messages WHERE relay_signer_principal = ?1 AND relay_message_id = ?2",
params![signer_principal.0, relay_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.optional()?;
if let Some((message_id, storage_owner)) = message {
tx.execute(
"UPDATE messages SET delivery_failed_at = ?1, delivery_failure = ?2 WHERE id = ?3",
params![sync::now_millis(), failure, message_id],
)?;
sync::record_event(
tx,
signer_id,
storage_owner,
sync::EntityType::Message,
message_id,
sync::Operation::Upsert,
)?;
}
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_id = ?1 AND message_id = ?2", params![signer_id, relay_message_id, sync::now_millis()])?;
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2", params![signer_principal.0, relay_message_id, sync::now_millis()])?;
tx.execute("DELETE FROM pending_relays WHERE id = ?1", [pending_id])?;
Ok(())
})

View file

@ -25,6 +25,68 @@ pub struct ChatSecretQuery {
pub version: Option<i64>,
}
pub fn principal_chat_id(
first: iota_identity::PrincipalHandle,
second: iota_identity::PrincipalHandle,
) -> Option<String> {
if first.0 <= 0 || second.0 <= 0 || first == second {
return None;
}
let (first, second) = if first.0 < second.0 {
(first.0, second.0)
} else {
(second.0, first.0)
};
Some(format!("principal:{first}:{second}"))
}
pub fn principal_secret_id(chat_id: &str) -> String {
format!("chat:{chat_id}:main")
}
pub fn migrate_chat_secret_namespace(
user_id: &str,
legacy_chat_id: &str,
legacy_secret_id: &str,
principal_chat_id: &str,
principal_secret_id: &str,
) -> Result<(), StorageError> {
if [
user_id,
legacy_chat_id,
legacy_secret_id,
principal_chat_id,
principal_secret_id,
]
.iter()
.any(|value| value.is_empty())
{
return Err("chat secret namespace is incomplete".into());
}
db::with_conn(&E2EE_DB, |conn| {
conn.execute(
r#"
INSERT OR IGNORE INTO chat_secrets (
user_id, chat_id, secret_id, version, encrypted_secret,
kem_ciphertext, wrapping_scheme, created_at, updated_at
)
SELECT user_id, ?4, ?5, version, encrypted_secret,
kem_ciphertext, wrapping_scheme, created_at, updated_at
FROM chat_secrets
WHERE user_id = ?1 AND chat_id = ?2 AND secret_id = ?3
"#,
params![
user_id,
legacy_chat_id,
legacy_secret_id,
principal_chat_id,
principal_secret_id
],
)?;
Ok(())
})
}
static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
let database = db::create_shared_connection(
"e2ee",
@ -186,3 +248,34 @@ fn chat_secret_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredChatS
updated_at: row.get(8)?,
})
}
#[cfg(test)]
mod tests {
use super::{principal_chat_id, principal_secret_id};
use iota_identity::PrincipalHandle;
#[test]
fn chat_secret_namespace_uses_principals() {
let first = principal_chat_id(PrincipalHandle(3), PrincipalHandle(9));
let second = principal_chat_id(PrincipalHandle(9), PrincipalHandle(3));
assert_eq!(first, Some("principal:3:9".to_string()));
assert_eq!(first, second);
assert_eq!(
principal_secret_id("principal:3:9"),
"chat:principal:3:9:main"
);
}
#[test]
fn equal_or_invalid_principals_have_no_chat_namespace() {
assert_eq!(
principal_chat_id(PrincipalHandle(7), PrincipalHandle(7)),
None
);
assert_eq!(
principal_chat_id(PrincipalHandle(0), PrincipalHandle(7)),
None
);
}
}

View file

@ -1,11 +1,10 @@
/* Accepts an outgoing relay only when its retry record and retention state commit together. */
use crate::storage_error::StorageError;
use crate::util::{chat_files, db, message_storage_policy, relay_queue, relay_replay};
use iota_util::route_target::RouteTarget;
use rusqlite::OptionalExtension;
pub struct OutgoingRelay<'a> {
pub target: RouteTarget,
pub target: relay_queue::RelayTarget,
pub identity: &'a relay_queue::RelayIdentity,
pub frame: &'a [u8],
pub created_at: i64,
@ -24,22 +23,30 @@ pub fn commit_outgoing_relay(relay: OutgoingRelay<'_>) -> Result<(), StorageErro
relay.frame_id,
relay.type_map_version,
)?;
if message_storage_policy::get_in_tx(tx, relay.identity.signer_id)?.history_mode
let signer_id = tx
.query_row(
"SELECT local_user_id FROM hosted_principals WHERE principal_handle = ?1",
[relay.identity.signer.0],
|row| row.get::<_, i64>(0),
)
.optional()?
.ok_or_else(|| StorageError::Other("outgoing signer is not hosted locally".into()))?;
if message_storage_policy::get_in_tx(tx, signer_id)?.history_mode
== message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery
{
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?1 AND relay_message_id = ?2", rusqlite::params![relay.identity.signer_id, relay.identity.message_id], |row| row.get::<_, i64>(0)).optional()?;
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3", rusqlite::params![signer_id, relay.identity.signer.0, relay.identity.message_id], |row| row.get::<_, i64>(0)).optional()?;
if let Some(message_id) = message_id {
chat_files::remove_message_history_in_tx(tx, relay.identity.signer_id, message_id)?;
chat_files::remove_message_history_in_tx(tx, signer_id, message_id)?;
}
}
relay_replay::mark_queued_in_tx(tx, relay.identity.signer_id, &relay.identity.message_id)?;
relay_replay::mark_queued_in_tx(tx, relay.identity.signer, &relay.identity.message_id)?;
Ok(())
})
}
pub fn apply_outgoing_history_policy(
storage_owner: i64,
relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
@ -49,8 +56,8 @@ pub fn apply_outgoing_history_policy(
return Ok(());
}
let message_id = tx.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
rusqlite::params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
rusqlite::params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| row.get::<_, i64>(0),
).optional()?;
if let Some(message_id) = message_id {

View file

@ -1,21 +1,44 @@
use crate::storage_error::StorageError;
use crate::util::db;
use iota_identity::{AuthorityId, IotaNodeId, PrincipalHandle};
use iota_util::route_target::RouteTarget;
use rusqlite::{OptionalExtension, Transaction, params};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingRelay {
pub id: i64,
pub target: RouteTarget,
pub target: RelayTarget,
pub frame: Vec<u8>,
pub created_at: i64,
pub frame_id: u32,
pub type_map_version: String,
pub relay_signer_id: Option<i64>,
pub relay_destination_user_id: Option<i64>,
pub signer_principal: Option<PrincipalHandle>,
pub destination_principal: Option<PrincipalHandle>,
pub relay_message_id: Option<String>,
pub delivery_state: PendingRelayDeliveryState,
pub last_error: Option<String>,
pub attempt_count: i64,
pub last_attempt_at: Option<i64>,
pub next_attempt_at: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RelayTarget {
User(u64),
Iota(IotaNodeId),
LegacyOmegaIota { omega: AuthorityId, iota_id: u64 },
}
impl RelayTarget {
pub fn legacy_wire_target(&self) -> Option<RouteTarget> {
match self {
Self::User(user_id) => Some(RouteTarget::User(*user_id)),
Self::LegacyOmegaIota { iota_id, .. } => Some(RouteTarget::Iota(*iota_id)),
Self::Iota(_) => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -38,23 +61,25 @@ impl PendingRelayDeliveryState {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RelayIdentity {
pub signer_id: i64,
pub destination_user_id: i64,
pub signer: PrincipalHandle,
pub recipient: PrincipalHandle,
pub message_id: String,
pub legacy_signer_id: Option<i64>,
pub legacy_recipient_id: Option<i64>,
}
pub fn enqueue(
target: RouteTarget,
target: RelayTarget,
relay: &RelayIdentity,
frame: &[u8],
created_at: i64,
frame_id: u32,
type_map_version: &str,
) -> Result<(), StorageError> {
if let RouteTarget::User(destination) = target {
if let RelayTarget::User(destination) = target {
let destination = i64::try_from(destination)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
if destination != relay.destination_user_id {
if Some(destination) != relay.legacy_recipient_id {
return Err(StorageError::Other(
"user relay target does not match final recipient".into(),
));
@ -75,31 +100,49 @@ pub fn enqueue(
pub fn enqueue_in_tx(
tx: &Transaction<'_>,
target: RouteTarget,
target: RelayTarget,
relay: &RelayIdentity,
frame: &[u8],
created_at: i64,
frame_id: u32,
type_map_version: &str,
) -> Result<(), StorageError> {
let destination_id = i64::try_from(target.id())
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
let target_kind = match target {
RouteTarget::User(_) => 0_i64,
RouteTarget::Iota(_) => 1_i64,
let (destination_id, target_kind, destination_node_id, destination_authority_id) = match &target
{
RelayTarget::User(user_id) => (
i64::try_from(*user_id).map_err(|_| {
StorageError::Other("relay destination ID exceeds SQLite range".into())
})?,
0_i64,
None,
None,
),
RelayTarget::LegacyOmegaIota { omega, iota_id } => (
i64::try_from(*iota_id).map_err(|_| {
StorageError::Other("relay destination ID exceeds SQLite range".into())
})?,
1_i64,
None,
Some(omega.as_str()),
),
RelayTarget::Iota(node) => (0, 2_i64, Some(node.as_str()), None),
};
tx.execute(
"INSERT OR IGNORE INTO pending_relays (destination_id, target_kind, relay_signer_id, relay_destination_user_id, relay_message_id, frame, created_at, frame_id, type_map_version) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
"INSERT OR IGNORE INTO pending_relays (destination_id, target_kind, relay_signer_id, relay_destination_user_id, relay_message_id, frame, created_at, frame_id, type_map_version, signer_principal, destination_principal, destination_node_id, destination_authority_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
params![
destination_id,
target_kind,
relay.signer_id,
relay.destination_user_id,
relay.legacy_signer_id,
relay.legacy_recipient_id,
relay.message_id,
frame,
created_at,
i64::from(frame_id),
type_map_version
type_map_version,
relay.signer.0,
relay.recipient.0,
destination_node_id,
destination_authority_id,
],
)?;
Ok(())
@ -108,7 +151,7 @@ pub fn enqueue_in_tx(
pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare(
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error FROM pending_relays ORDER BY id LIMIT ?1",
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error, destination_principal, signer_principal, destination_node_id, destination_authority_id, attempt_count, last_attempt_at, next_attempt_at FROM pending_relays ORDER BY id LIMIT ?1",
)?;
let rows = statement.query_map(params![limit.clamp(1, 500)], |row| {
let destination_id = row.get::<_, i64>(1)?;
@ -121,8 +164,26 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
)
})?;
let target = match target_kind {
0 => RouteTarget::User(destination_id),
1 => RouteTarget::Iota(destination_id),
0 => RelayTarget::User(destination_id),
1 => RelayTarget::LegacyOmegaIota {
omega: AuthorityId::new(row.get::<_, String>(15)?).map_err(|error| {
rusqlite::Error::FromSqlConversionFailure(
15,
rusqlite::types::Type::Text,
Box::new(error),
)
})?,
iota_id: destination_id,
},
2 => RelayTarget::Iota(IotaNodeId::new(row.get::<_, String>(14)?).map_err(
|error| {
rusqlite::Error::FromSqlConversionFailure(
14,
rusqlite::types::Type::Text,
Box::new(error),
)
},
)?),
_ => {
return Err(rusqlite::Error::FromSqlConversionFailure(
2,
@ -146,9 +207,16 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
type_map_version: row.get(6)?,
relay_signer_id: row.get(7)?,
relay_destination_user_id: row.get(8)?,
signer_principal: row.get::<_, Option<i64>>(13)?.map(PrincipalHandle),
destination_principal: row
.get::<_, Option<i64>>(12)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(9)?,
delivery_state: PendingRelayDeliveryState::from_db(row.get(10)?)?,
last_error: row.get(11)?,
attempt_count: row.get(16)?,
last_attempt_at: row.get(17)?,
next_attempt_at: row.get(18)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
@ -160,9 +228,12 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
pub fn list_active(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare(
"SELECT p.id, p.destination_id, p.target_kind, p.frame, p.created_at, p.frame_id, p.type_map_version, p.relay_signer_id, p.relay_destination_user_id, p.relay_message_id, p.delivery_state, p.last_error FROM pending_relays p WHERE p.delivery_state = 'ready' AND ((p.target_kind = 0 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.destination_id)) OR (p.target_kind = 1 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.relay_signer_id))) ORDER BY p.id LIMIT ?1",
"SELECT p.id, p.destination_id, p.target_kind, p.frame, p.created_at, p.frame_id, p.type_map_version, p.relay_signer_id, p.relay_destination_user_id, p.relay_message_id, p.delivery_state, p.last_error, p.destination_principal, p.signer_principal, p.destination_node_id, p.destination_authority_id, p.attempt_count, p.last_attempt_at, p.next_attempt_at FROM pending_relays p WHERE p.delivery_state = 'ready' AND p.next_attempt_at <= ?1 AND ((p.target_kind = 0 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.destination_id)) OR (p.target_kind IN (1, 2) AND p.signer_principal IS NOT NULL)) ORDER BY p.id LIMIT ?2",
)?;
let rows = statement.query_map(
params![now_millis(), limit.clamp(1, 500)],
pending_relay_from_row,
)?;
let rows = statement.query_map(params![limit.clamp(1, 500)], pending_relay_from_row)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
})
}
@ -176,7 +247,7 @@ pub fn list_without_relay_identity_after(
limit: i64,
) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare("SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error FROM pending_relays WHERE id > ?1 AND (relay_signer_id IS NULL OR relay_destination_user_id IS NULL OR relay_message_id IS NULL) ORDER BY id LIMIT ?2")?;
let mut statement = connection.prepare("SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error, destination_principal, signer_principal, destination_node_id, destination_authority_id, attempt_count, last_attempt_at, next_attempt_at FROM pending_relays WHERE id > ?1 AND (signer_principal IS NULL OR destination_principal IS NULL OR relay_message_id IS NULL) ORDER BY id LIMIT ?2")?;
let rows = statement.query_map(
params![after_id, limit.clamp(1, 500)],
pending_relay_from_row,
@ -189,8 +260,16 @@ fn pending_relay_from_row(row: &rusqlite::Row<'_>) -> Result<PendingRelay, rusql
let destination_id = u64::try_from(row.get::<_, i64>(1)?)
.map_err(|_| rusqlite::Error::IntegralValueOutOfRange(1, 0))?;
let target = match row.get::<_, i64>(2)? {
0 => RouteTarget::User(destination_id),
1 => RouteTarget::Iota(destination_id),
0 => RelayTarget::User(destination_id),
1 => RelayTarget::LegacyOmegaIota {
omega: AuthorityId::new(row.get::<_, String>(15)?)
.map_err(|_| rusqlite::Error::InvalidQuery)?,
iota_id: destination_id,
},
2 => RelayTarget::Iota(
IotaNodeId::new(row.get::<_, String>(14)?)
.map_err(|_| rusqlite::Error::InvalidQuery)?,
),
_ => return Err(rusqlite::Error::IntegralValueOutOfRange(2, 0)),
};
Ok(PendingRelay {
@ -203,12 +282,55 @@ fn pending_relay_from_row(row: &rusqlite::Row<'_>) -> Result<PendingRelay, rusql
type_map_version: row.get(6)?,
relay_signer_id: row.get(7)?,
relay_destination_user_id: row.get(8)?,
signer_principal: row.get::<_, Option<i64>>(13)?.map(PrincipalHandle),
destination_principal: row
.get::<_, Option<i64>>(12)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(9)?,
delivery_state: PendingRelayDeliveryState::from_db(row.get(10)?)?,
last_error: row.get(11)?,
attempt_count: row.get(16)?,
last_attempt_at: row.get(17)?,
next_attempt_at: row.get(18)?,
})
}
pub fn record_retry(id: i64, now: i64, error: &str) -> Result<(), StorageError> {
db::with_db(|connection| {
let attempts = connection.query_row(
"SELECT attempt_count FROM pending_relays WHERE id = ?1",
[id],
|row| row.get::<_, i64>(0),
)?;
let exponent = u32::try_from(attempts.clamp(0, 6)).unwrap_or(6);
let delay = 5_000_i64.saturating_mul(1_i64 << exponent).min(300_000);
connection.execute(
"UPDATE pending_relays SET attempt_count = attempt_count + 1, last_attempt_at = ?2, next_attempt_at = ?3, last_error = ?4 WHERE id = ?1",
params![id, now, now.saturating_add(delay), error],
)?;
Ok(())
})
}
pub fn quarantine_for_frame(frame_id: u32, error: &str) -> Result<(), StorageError> {
db::with_db(|connection| {
connection.execute(
"UPDATE pending_relays SET delivery_state = 'quarantined', last_error = ?2 WHERE frame_id = ?1",
params![i64::from(frame_id), error],
)?;
Ok(())
})
}
fn now_millis() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(i64::MAX)
}
pub fn mark_client_offline(destination_user_id: i64, frame_id: u32) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
tx.execute("UPDATE pending_relays SET delivery_state = 'waiting_client', last_error = 'client_offline' WHERE target_kind = 0 AND destination_id = ?1 AND frame_id = ?2", params![destination_user_id, i64::from(frame_id)])?;
@ -258,7 +380,7 @@ pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageE
[id],
|row| row.get::<_, i64>(0),
)?;
if destination_id != relay.destination_user_id {
if Some(destination_id) != relay.legacy_recipient_id {
connection.execute(
"UPDATE pending_relays SET delivery_state = 'quarantined', last_error = 'target_recipient_mismatch' WHERE id = ?1",
[id],
@ -266,7 +388,7 @@ pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageE
return Ok(());
}
}
connection.execute("UPDATE pending_relays SET relay_signer_id = ?2, relay_destination_user_id = ?3, relay_message_id = ?4 WHERE id = ?1", params![id, relay.signer_id, relay.destination_user_id, relay.message_id])?;
connection.execute("UPDATE pending_relays SET relay_signer_id = ?2, relay_destination_user_id = ?3, relay_message_id = ?4, signer_principal = ?5, destination_principal = ?6 WHERE id = ?1", params![id, relay.legacy_signer_id, relay.legacy_recipient_id, relay.message_id, relay.signer.0, relay.recipient.0])?;
Ok(())
})
}
@ -274,7 +396,7 @@ pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageE
pub fn has_unclassified_relays() -> Result<bool, StorageError> {
db::with_db(|connection| {
connection
.query_row("SELECT EXISTS(SELECT 1 FROM pending_relays WHERE relay_signer_id IS NULL OR relay_destination_user_id IS NULL OR relay_message_id IS NULL)", [], |row| row.get(0))
.query_row("SELECT EXISTS(SELECT 1 FROM pending_relays WHERE signer_principal IS NULL OR destination_principal IS NULL OR relay_message_id IS NULL)", [], |row| row.get(0))
.map_err(Into::into)
})
}
@ -285,14 +407,24 @@ pub fn find_user_delivery_in_tx(
frame_id: u32,
) -> Result<Option<(i64, RelayIdentity)>, StorageError> {
tx.query_row(
"SELECT id, relay_signer_id, relay_destination_user_id, relay_message_id FROM pending_relays WHERE destination_id = ?1 AND target_kind = 0 AND frame_id = ?2",
"SELECT id, relay_signer_id, relay_destination_user_id, relay_message_id, signer_principal, destination_principal FROM pending_relays WHERE destination_id = ?1 AND target_kind = 0 AND frame_id = ?2",
params![destination_id, i64::from(frame_id)],
|row| {
let signer_id = row.get::<_, Option<i64>>(1)?;
let destination_user_id = row.get::<_, Option<i64>>(2)?;
let message_id = row.get::<_, Option<String>>(3)?;
match (signer_id, destination_user_id, message_id) {
(Some(signer_id), Some(destination_user_id), Some(message_id)) => Ok((row.get(0)?, RelayIdentity { signer_id, destination_user_id, message_id })),
(Some(signer_id), Some(destination_user_id), Some(message_id)) => {
let signer = row.get::<_, Option<i64>>(4)?.ok_or(rusqlite::Error::InvalidQuery)?;
let recipient = row.get::<_, Option<i64>>(5)?.ok_or(rusqlite::Error::InvalidQuery)?;
Ok((row.get(0)?, RelayIdentity {
signer: PrincipalHandle(signer),
recipient: PrincipalHandle(recipient),
message_id,
legacy_signer_id: Some(signer_id),
legacy_recipient_id: Some(destination_user_id),
}))
}
_ => Err(rusqlite::Error::InvalidQuery),
}
},
@ -310,14 +442,14 @@ pub fn acknowledge_in_tx(tx: &Transaction<'_>, id: i64) -> Result<(), StorageErr
pub fn reject_outgoing_relay(
destination_iota: u64,
frame_id: u32,
signer_id: i64,
signer_principal: PrincipalHandle,
relay_message_id: &str,
) -> Result<(), StorageError> {
let destination_iota = i64::try_from(destination_iota)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_immediate_transaction(|tx| {
tx.execute("DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", params![destination_iota, i64::from(frame_id)])?;
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_id = ?1 AND message_id = ?2", params![signer_id, relay_message_id, crate::util::sync::now_millis()])?;
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2", params![signer_principal.0, relay_message_id, crate::util::sync::now_millis()])?;
Ok(())
})
}

View file

@ -15,16 +15,30 @@ pub enum RelayReservation {
Existing { state: String, frame_matches: bool },
}
pub fn reserve(
signer_id: u64,
message_id: &str,
created_at: u64,
accepted_at: i64,
destination_id: u64,
frame: &[u8],
frame_id: u32,
type_map_version: &str,
) -> Result<RelayReservation, StorageError> {
pub struct RelayReservationRequest<'a> {
pub signer_principal: iota_identity::PrincipalHandle,
pub signer_id: u64,
pub message_id: &'a str,
pub created_at: u64,
pub accepted_at: i64,
pub destination_id: u64,
pub frame: &'a [u8],
pub frame_id: u32,
pub type_map_version: &'a str,
}
pub fn reserve(request: RelayReservationRequest<'_>) -> Result<RelayReservation, StorageError> {
let RelayReservationRequest {
signer_principal,
signer_id,
message_id,
created_at,
accepted_at,
destination_id,
frame,
frame_id,
type_map_version,
} = request;
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let created_at = i64::try_from(created_at)
@ -34,8 +48,9 @@ pub fn reserve(
db::with_db(|connection| {
let inserted = connection.execute(
"INSERT OR IGNORE INTO relay_inbox (signer_id, message_id, created_at, accepted_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 'received')",
"INSERT OR IGNORE INTO relay_inbox (signer_principal, signer_id, message_id, created_at, accepted_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 'received')",
params![
signer_principal.0,
signer_id,
message_id,
created_at,
@ -52,8 +67,8 @@ pub fn reserve(
let (state, existing_destination_id, existing_frame, existing_type_map_version):
(String, i64, Vec<u8>, String) = connection.query_row(
"SELECT state, destination_id, frame, type_map_version FROM relay_inbox WHERE signer_id = ?1 AND message_id = ?2",
params![signer_id, message_id],
"SELECT state, destination_id, frame, type_map_version FROM relay_inbox WHERE signer_principal = ?1 AND message_id = ?2",
params![signer_principal.0, message_id],
|row| {
Ok((
row.get(0)?,
@ -119,52 +134,67 @@ pub fn mark_delivered_for_frame_in_tx(
}
fn mark_transition(
signer_id: u64,
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
state: &str,
column: &str,
) -> Result<(), StorageError> {
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
db::with_db(|connection| {
connection.execute(
&format!("UPDATE relay_inbox SET state = ?3, {column} = COALESCE({column}, ?4) WHERE signer_id = ?1 AND message_id = ?2"),
params![signer_id, message_id, state, timestamp],
&format!("UPDATE relay_inbox SET state = ?3, {column} = COALESCE({column}, ?4) WHERE signer_principal = ?1 AND message_id = ?2"),
params![signer_principal.0, message_id, state, timestamp],
)?;
Ok(())
})
}
pub fn mark_applied(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "applied", "applied_at")
pub fn mark_applied(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(signer_principal, message_id, "applied", "applied_at")
}
pub fn mark_queued(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "queued", "queued_at")
pub fn mark_queued(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(signer_principal, message_id, "queued", "queued_at")
}
pub fn mark_queued_in_tx(
tx: &rusqlite::Transaction<'_>,
signer_id: i64,
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
tx.execute(
"UPDATE relay_inbox SET state = 'queued', queued_at = COALESCE(queued_at, ?3) WHERE signer_id = ?1 AND message_id = ?2",
params![signer_id, message_id, crate::util::sync::now_millis()],
"UPDATE relay_inbox SET state = 'queued', queued_at = COALESCE(queued_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2",
params![signer_principal.0, message_id, crate::util::sync::now_millis()],
)?;
Ok(())
}
pub fn mark_downstream_acked(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "delivered", "downstream_acked_at")
pub fn mark_downstream_acked(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(
signer_principal,
message_id,
"delivered",
"downstream_acked_at",
)
}
pub fn mark_rejected(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "rejected", "rejected_at")
pub fn mark_rejected(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(signer_principal, message_id, "rejected", "rejected_at")
}
pub fn prune_completed(before_terminal_at: i64) -> Result<(), StorageError> {
@ -174,14 +204,19 @@ pub fn prune_completed(before_terminal_at: i64) -> Result<(), StorageError> {
params![before_terminal_at],
)?;
connection.execute(
"DELETE FROM relay_replay WHERE NOT EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_id = relay_replay.signer_id AND relay_inbox.message_id = relay_replay.message_id)",
"DELETE FROM relay_replay WHERE NOT EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_principal = relay_replay.signer_principal AND relay_inbox.message_id = relay_replay.message_id)",
[],
)?;
Ok(())
})
}
pub fn accept(signer_id: u64, message_id: &str, created_at: u64) -> Result<bool, StorageError> {
pub fn accept(
signer_principal: iota_identity::PrincipalHandle,
signer_id: u64,
message_id: &str,
created_at: u64,
) -> Result<bool, StorageError> {
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let created_at = i64::try_from(created_at)
@ -189,8 +224,8 @@ pub fn accept(signer_id: u64, message_id: &str, created_at: u64) -> Result<bool,
db::with_db(|connection| {
let inserted = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![signer_id, message_id, created_at],
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![signer_principal.0, signer_id, message_id, created_at],
)?;
Ok(inserted == 1)
})
@ -201,26 +236,29 @@ mod tests {
use rusqlite::{Connection, params};
#[test]
fn replay_identity_uses_signer_and_message_id() -> Result<(), rusqlite::Error> {
fn replay_identity_uses_principal_and_message_id() -> Result<(), rusqlite::Error> {
let connection = Connection::open_in_memory()?;
connection.execute_batch(
"CREATE TABLE relay_replay (signer_id INTEGER NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (signer_id, message_id));",
"CREATE TABLE relay_replay (signer_principal INTEGER NOT NULL, signer_id INTEGER NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (signer_principal, message_id));",
)?;
let first = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![7_i64, "message", 1_i64],
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![70_i64, 7_i64, "message", 1_i64],
)?;
let duplicate = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![7_i64, "message", 2_i64],
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![70_i64, 7_i64, "message", 2_i64],
)?;
let other_signer = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![8_i64, "message", 2_i64],
let other_authority_same_numeric_signer = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![71_i64, 7_i64, "message", 2_i64],
)?;
assert_eq!((first, duplicate, other_signer), (1, 0, 1));
assert_eq!(
(first, duplicate, other_authority_same_numeric_signer),
(1, 0, 1)
);
Ok(())
}
}

View file

@ -0,0 +1,132 @@
use iota_identity::{
AuthorityKind, LocalNodeIdentity, LocalUserDescriptor, LocalUserId, PrincipalHome, PrincipalId,
PrincipalStore, VerifiedPrincipalDescriptor,
};
use iota_storage::identity::SqlitePrincipalStore;
use mtp::crypto::Keyring;
#[test]
fn local_descriptor_revision_persists_changes_and_retirement() {
let storage = tempfile::tempdir().unwrap();
iota_util::file_util::configure_storage_directory(storage.path().to_owned());
iota_storage::util::db::initialize_database().unwrap();
let identity = LocalNodeIdentity::from_keyring(Keyring::generate()).unwrap();
let user_key = Keyring::generate();
let mut user = LocalUserDescriptor {
id: LocalUserId(9),
username: "alice".into(),
display_name: None,
public_key: user_key.public_key_bundle().try_to_base64().unwrap(),
};
iota_storage::util::db::with_db(|connection| {
connection.execute(
"INSERT INTO users (user_id, username, public_key, created_at) VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![user.id.0, user.username, user.public_key, 1_i64],
)?;
Ok(())
})
.unwrap();
let store = SqlitePrincipalStore;
let home = PrincipalHome::Iota(identity.node_id().clone());
store
.ensure_local_principal(
identity.authority_id(),
AuthorityKind::Iota,
&user,
home.clone(),
1_000,
)
.unwrap();
let first = store.local_principal_descriptor(user.id).unwrap();
assert_eq!(first.revision, 0);
store
.ensure_local_principal(
identity.authority_id(),
AuthorityKind::Iota,
&user,
home.clone(),
1_001,
)
.unwrap();
assert_eq!(
store.local_principal_descriptor(user.id).unwrap().revision,
0
);
user.display_name = Some("Alice".into());
store
.ensure_local_principal(
identity.authority_id(),
AuthorityKind::Iota,
&user,
home.clone(),
1_002,
)
.unwrap();
assert_eq!(
store.local_principal_descriptor(user.id).unwrap().revision,
1
);
let replacement_key = Keyring::generate().public_key_bundle();
user.public_key = replacement_key.try_to_base64().unwrap();
store
.ensure_local_principal(
identity.authority_id(),
AuthorityKind::Iota,
&user,
home.clone(),
1_003,
)
.unwrap();
let rotated = store.local_principal_descriptor(user.id).unwrap();
assert_eq!(rotated.revision, 2);
assert_eq!(rotated.public_keys.len(), 1);
assert_eq!(
rotated.public_keys[0].try_to_base64().unwrap(),
replacement_key.try_to_base64().unwrap()
);
let renewal_at = rotated.valid_until.unwrap() - 1;
store
.ensure_local_principal(
identity.authority_id(),
AuthorityKind::Iota,
&user,
home,
renewal_at,
)
.unwrap();
let renewed = store.local_principal_descriptor(user.id).unwrap();
assert_eq!(renewed.revision, 3);
let mut conflicting = renewed.clone();
conflicting.display_name = Some("Changed without revision".into());
assert!(
store
.upsert_remote_descriptor(
&VerifiedPrincipalDescriptor::from_trusted_authority(conflicting).unwrap()
)
.is_err()
);
store
.retire_local_principal(user.id, renewal_at + 1)
.unwrap();
assert!(store.local_principal_descriptor(user.id).is_err());
let principal = PrincipalId {
authority: identity.authority_id().clone(),
user_id: 9,
};
let retired = store.get_by_canonical_id(&principal).unwrap().unwrap();
assert_eq!(retired.descriptor_revision, 4);
assert!(
store
.upsert_remote_descriptor(
&VerifiedPrincipalDescriptor::from_trusted_authority(first).unwrap()
)
.is_err()
);
}

View file

@ -0,0 +1,111 @@
use iota_identity::{
AuthorityLocator, LocalNodeIdentity, NodeDescriptor, NodeDirectory, NodeIdentityResolver,
SignedNodeDescriptor,
};
use iota_storage::node_directory::SqliteNodeDirectory;
use mtp::crypto::Keyring;
#[tokio::test]
async fn node_directory_persists_identity_revision_endpoints_and_pins() {
let storage = tempfile::tempdir().unwrap();
iota_util::file_util::configure_storage_directory(storage.path().to_owned());
iota_storage::util::db::initialize_database().unwrap();
let identity = LocalNodeIdentity::from_keyring(Keyring::generate()).unwrap();
let endpoint = AuthorityLocator::new("iota.example.org:443").unwrap();
let relay = AuthorityLocator::new("relay.example.org:443").unwrap();
let now = iota_storage::util::sync::now_millis();
let signed = SignedNodeDescriptor::sign(
NodeDescriptor {
node: identity.node_id().clone(),
public_keys: identity.public_keys(),
direct_endpoints: vec![endpoint.clone()],
relay_hints: vec![relay.clone()],
revision: 1,
valid_until: Some(now + 60_000),
},
&identity,
)
.unwrap();
let verified = signed.verify(None, now).unwrap();
SqliteNodeDirectory
.upsert_node_descriptor(&verified)
.unwrap();
SqliteNodeDirectory
.pin_endpoint(&endpoint, identity.node_id(), now)
.unwrap();
let certificate = b"test certificate";
SqliteNodeDirectory
.pin_endpoint_certificate(&endpoint, certificate)
.unwrap();
SqliteNodeDirectory
.record_successful_endpoint(identity.node_id(), &endpoint)
.unwrap();
let resolved = SqliteNodeDirectory
.resolve_node(identity.node_id())
.await
.unwrap();
assert_eq!(resolved.descriptor_revision, 1);
assert_eq!(resolved.direct_endpoints, vec![endpoint.clone()]);
assert_eq!(resolved.relay_hints, vec![relay]);
assert_eq!(resolved.last_successful_endpoint, Some(endpoint.clone()));
assert_eq!(
SqliteNodeDirectory.pinned_node(&endpoint).unwrap(),
Some(identity.node_id().clone())
);
assert_eq!(
SqliteNodeDirectory
.pinned_endpoint_certificate(&endpoint)
.unwrap(),
Some(certificate.to_vec())
);
assert!(
SqliteNodeDirectory
.pin_endpoint_certificate(&endpoint, b"changed certificate")
.is_err()
);
let stale = SignedNodeDescriptor::sign(
NodeDescriptor {
node: identity.node_id().clone(),
public_keys: identity.public_keys(),
direct_endpoints: Vec::new(),
relay_hints: Vec::new(),
revision: 0,
valid_until: Some(now + 60_000),
},
&identity,
)
.unwrap()
.verify(None, now)
.unwrap();
assert!(SqliteNodeDirectory.upsert_node_descriptor(&stale).is_err());
let conflicting = SignedNodeDescriptor::sign(
NodeDescriptor {
node: identity.node_id().clone(),
public_keys: identity.public_keys(),
direct_endpoints: Vec::new(),
relay_hints: Vec::new(),
revision: 1,
valid_until: Some(now + 60_000),
},
&identity,
)
.unwrap()
.verify(None, now)
.unwrap();
assert!(
SqliteNodeDirectory
.upsert_node_descriptor(&conflicting)
.is_err()
);
let other = LocalNodeIdentity::from_keyring(Keyring::generate()).unwrap();
assert!(
SqliteNodeDirectory
.pin_endpoint(&endpoint, other.node_id(), now + 1)
.is_err()
);
}

View file

@ -0,0 +1,60 @@
use iota_identity::{IotaNodeId, PrincipalHandle};
use iota_storage::util::relay_queue::{
self, PendingRelayDeliveryState, RelayIdentity, RelayTarget,
};
use mtp::crypto::Keyring;
#[test]
fn pending_iota_relay_uses_bounded_backoff_and_quarantine() {
let storage = tempfile::tempdir().unwrap();
iota_util::file_util::configure_storage_directory(storage.path().to_owned());
iota_storage::util::db::initialize_database().unwrap();
let (signer, recipient) = iota_storage::util::db::with_db(|connection| {
connection.execute(
"INSERT INTO principals (authority_kind, authority_id, remote_user_id, descriptor_revision, last_resolved_at) VALUES ('iota', 'iota:first', 1, 1, 1)",
[],
)?;
let signer = connection.last_insert_rowid();
connection.execute(
"INSERT INTO principals (authority_kind, authority_id, remote_user_id, descriptor_revision, last_resolved_at) VALUES ('iota', 'iota:second', 1, 1, 1)",
[],
)?;
Ok((signer, connection.last_insert_rowid()))
})
.unwrap();
let node = IotaNodeId::from_public_keys(&Keyring::generate().public_key_bundle()).unwrap();
relay_queue::enqueue(
RelayTarget::Iota(node),
&RelayIdentity {
signer: PrincipalHandle(signer),
recipient: PrincipalHandle(recipient),
message_id: "retry".into(),
legacy_signer_id: Some(1),
legacy_recipient_id: Some(1),
},
&[1, 2, 3],
1,
7,
"1.0",
)
.unwrap();
let id = relay_queue::list(1).unwrap()[0].id;
let now = iota_storage::util::sync::now_millis();
for attempt in 0..8 {
relay_queue::record_retry(id, now, "offline").unwrap();
let relay = relay_queue::list(1).unwrap().remove(0);
let exponent = attempt.min(6);
let expected_delay = (5_000_i64 * (1_i64 << exponent)).min(300_000);
assert_eq!(relay.attempt_count, attempt + 1);
assert_eq!(relay.last_attempt_at, Some(now));
assert_eq!(relay.next_attempt_at, now + expected_delay);
assert_eq!(relay.last_error.as_deref(), Some("offline"));
}
assert!(relay_queue::list_active(1).unwrap().is_empty());
relay_queue::quarantine_for_frame(7, "wrong acknowledgement").unwrap();
let relay = relay_queue::list(1).unwrap().remove(0);
assert_eq!(relay.delivery_state, PendingRelayDeliveryState::Quarantined);
assert_eq!(relay.last_error.as_deref(), Some("wrong acknowledgement"));
}