use calimero_context::group_store::{GroupKeyring, NamespaceRepository};
use calimero_crypto::Nonce;
use calimero_primitives::identity::PrivateKey;
use calimero_storage::action::Action;
use eyre::{bail, OptionExt, Result};
use tracing::debug;
pub(super) const STATE_DELTA_KEY_LOOKUP_WAIT: std::time::Duration =
std::time::Duration::from_secs(3);
const STATE_DELTA_KEY_LOOKUP_POLL: std::time::Duration = std::time::Duration::from_millis(100);
pub(super) async fn lookup_group_key_with_wait(
context_client: &calimero_context_client::client::ContextClient,
group_id: &calimero_context_config::types::ContextGroupId,
key_id: &[u8; 32],
max_wait: std::time::Duration,
) -> Result<Option<calimero_primitives::identity::PrivateKey>> {
use tokio::time::{sleep, Instant};
let single_shot = max_wait.is_zero();
let deadline = Instant::now() + max_wait;
let mut logged_wait = false;
loop {
let resolved = {
let store = context_client.datastore();
let direct = GroupKeyring::new(store, *group_id).load_key_by_id(key_id)?;
match direct {
Some(k) => Some(k),
None => {
let ns_id = NamespaceRepository::new(store).resolve(group_id)?;
if &ns_id != group_id {
GroupKeyring::new(store, ns_id).load_key_by_id(key_id)?
} else {
None
}
}
}
};
if let Some(k) = resolved {
return Ok(Some(calimero_primitives::identity::PrivateKey::from(k)));
}
if single_shot {
return Ok(None);
}
if Instant::now() + STATE_DELTA_KEY_LOOKUP_POLL > deadline {
return Ok(None);
}
if !logged_wait {
debug!(
?group_id,
key_id = %hex::encode(key_id),
wait_ms = max_wait.as_millis(),
"Group key not yet available — polling for KeyDelivery"
);
logged_wait = true;
}
sleep(STATE_DELTA_KEY_LOOKUP_POLL).await;
}
}
pub(super) fn decrypt_delta_actions(
artifact: Vec<u8>,
nonce: Nonce,
sender_key: PrivateKey,
) -> Result<Vec<Action>> {
let shared_key = calimero_crypto::SharedKey::from_sk(&sender_key);
let decrypted_artifact = shared_key
.decrypt(artifact, nonce)
.ok_or_eyre("failed to decrypt artifact")?;
let storage_delta: calimero_storage::delta::StorageDelta =
borsh::from_slice(&decrypted_artifact)?;
match storage_delta {
calimero_storage::delta::StorageDelta::Actions(actions) => Ok(actions),
_ => bail!("Expected Actions variant in state delta"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use calimero_crypto::{SharedKey, NONCE_LEN};
use calimero_storage::delta::StorageDelta;
use rand::thread_rng;
#[test]
fn decrypt_delta_actions_roundtrip() -> Result<()> {
let mut rng = thread_rng();
let sender_key = PrivateKey::random(&mut rng);
let shared_key = SharedKey::from_sk(&sender_key);
let nonce = [7u8; NONCE_LEN];
let storage_delta = StorageDelta::Actions(Vec::new());
let plaintext = borsh::to_vec(&storage_delta)?;
let cipher = shared_key
.encrypt(plaintext, nonce)
.ok_or_eyre("encryption failed")?;
let decrypted = decrypt_delta_actions(cipher, nonce, sender_key)?;
assert!(decrypted.is_empty());
Ok(())
}
#[test]
fn decrypt_delta_actions_rejects_bad_cipher() {
let mut rng = thread_rng();
let sender_key = PrivateKey::random(&mut rng);
let nonce = [9u8; NONCE_LEN];
let result = decrypt_delta_actions(vec![1, 2, 3, 4], nonce, sender_key);
assert!(result.is_err());
}
}