use crate::{
core::{
common::crypto::{hash::Hash, signature::PublicKey as CorePublicKey},
consensus::beacon::EpochTime,
},
crypto::signature::PublicKey,
state::CurrentState,
storage::{self, Store},
};
use super::{app_id::AppId, types, Error, MODULE_NAME};
const APPS: &[u8] = &[0x01];
const REGISTRATIONS: &[u8] = &[0x02];
const ENDORSERS: &[u8] = &[0x03];
const EXPIRATION_QUEUE: &[u8] = &[0x04];
#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
#[cbor(as_array)]
pub struct KeyEndorsementInfo {
pub node_id: CorePublicKey,
pub rak: Option<CorePublicKey>,
}
impl KeyEndorsementInfo {
pub fn for_rak(node_id: CorePublicKey) -> Self {
Self {
node_id,
..Default::default()
}
}
pub fn for_extra_key(node_id: CorePublicKey, rak: CorePublicKey) -> Self {
Self {
node_id,
rak: Some(rak),
}
}
}
pub fn get_app(app_id: AppId) -> Option<types::AppConfig> {
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let apps = storage::TypedStore::new(storage::PrefixStore::new(store, &APPS));
apps.get(app_id)
})
}
pub fn set_app(cfg: types::AppConfig) {
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let mut apps = storage::TypedStore::new(storage::PrefixStore::new(store, &APPS));
apps.insert(cfg.id, cfg);
})
}
pub fn remove_app(app_id: AppId) {
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let mut apps = storage::TypedStore::new(storage::PrefixStore::new(store, &APPS));
apps.remove(app_id);
})
}
pub fn update_registration(registration: types::Registration) -> Result<(), Error> {
let hrak = hash_rak(®istration.rak);
if let Some(existing) = get_registration_hrak(registration.app, hrak) {
if existing.extra_keys != registration.extra_keys {
return Err(Error::ExtraKeyUpdateNotAllowed);
}
remove_expiration_queue(existing.expiration, registration.app, hrak);
}
insert_expiration_queue(registration.expiration, registration.app, hrak);
CurrentState::with_store(|mut root_store| {
let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
let mut endorsers = storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
endorsers.insert(hrak, KeyEndorsementInfo::for_rak(registration.node_id));
for pk in ®istration.extra_keys {
endorsers.insert(
hash_pk(pk),
KeyEndorsementInfo::for_extra_key(registration.node_id, registration.rak),
);
}
let app_id = registration.app;
let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
let mut app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
app.insert(hrak, registration);
});
Ok(())
}
fn remove_registration_hrak(app_id: AppId, hrak: Hash) {
let registration = match get_registration_hrak(app_id, hrak) {
Some(registration) => registration,
None => return,
};
remove_expiration_queue(registration.expiration, registration.app, hrak);
CurrentState::with_store(|mut root_store| {
let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
let mut endorsers = storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
endorsers.remove(hrak);
for pk in ®istration.extra_keys {
endorsers.remove(hash_pk(pk));
}
let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
let mut app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
app.remove(hrak);
});
}
pub fn remove_registration(app_id: AppId, rak: &CorePublicKey) {
remove_registration_hrak(app_id, hash_rak(rak))
}
fn get_registration_hrak(app_id: AppId, hrak: Hash) -> Option<types::Registration> {
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
let app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
app.get(hrak)
})
}
pub fn get_registration(app_id: AppId, rak: &CorePublicKey) -> Option<types::Registration> {
get_registration_hrak(app_id, hash_rak(rak))
}
pub fn get_registrations_for_app(app_id: AppId) -> Vec<types::Registration> {
CurrentState::with_store(|mut root_store| {
let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
let app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
app.iter()
.map(|(_, registration): (Hash, types::Registration)| registration)
.collect()
})
}
pub fn get_endorser(pk: &PublicKey) -> Option<KeyEndorsementInfo> {
let hpk = hash_pk(pk);
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let endorsers = storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
endorsers.get(hpk)
})
}
fn hash_rak(rak: &CorePublicKey) -> Hash {
hash_pk(&PublicKey::Ed25519(rak.into()))
}
fn hash_pk(pk: &PublicKey) -> Hash {
Hash::digest_bytes_list(&[pk.key_type().as_bytes(), pk.as_ref()])
}
fn queue_entry_key(epoch: EpochTime, app_id: AppId, hrak: Hash) -> Vec<u8> {
[&epoch.to_be_bytes(), app_id.as_ref(), hrak.as_ref()].concat()
}
fn insert_expiration_queue(epoch: EpochTime, app_id: AppId, hrak: Hash) {
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let mut queue = storage::PrefixStore::new(store, &EXPIRATION_QUEUE);
queue.insert(&queue_entry_key(epoch, app_id, hrak), &[]);
})
}
fn remove_expiration_queue(epoch: EpochTime, app_id: AppId, hrak: Hash) {
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let mut queue = storage::PrefixStore::new(store, &EXPIRATION_QUEUE);
queue.remove(&queue_entry_key(epoch, app_id, hrak));
})
}
struct ExpirationQueueEntry {
epoch: EpochTime,
app_id: AppId,
hrak: Hash,
}
impl<'a> TryFrom<&'a [u8]> for ExpirationQueueEntry {
type Error = anyhow::Error;
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
if value.len() != 8 + AppId::SIZE + Hash::len() {
anyhow::bail!("incorrect expiration queue key size");
}
Ok(Self {
epoch: EpochTime::from_be_bytes(value[..8].try_into()?),
app_id: value[8..8 + AppId::SIZE].try_into()?,
hrak: value[8 + AppId::SIZE..].into(),
})
}
}
pub fn expire_registrations(epoch: EpochTime, limit: usize) {
let expired: Vec<_> = CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let queue = storage::TypedStore::new(storage::PrefixStore::new(store, &EXPIRATION_QUEUE));
queue
.iter()
.take_while(|(e, _): &(ExpirationQueueEntry, CorePublicKey)| e.epoch <= epoch)
.map(|(e, _)| (e.app_id, e.hrak))
.take(limit)
.collect()
});
for (app_id, hrak) in expired {
remove_registration_hrak(app_id, hrak);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::testing::{keys, mock};
#[test]
fn test_app_cfg() {
let _mock = mock::Mock::default();
let app_id = AppId::from_creator_round_index(keys::alice::address(), 0, 0);
let app = get_app(app_id);
assert!(app.is_none());
let cfg = types::AppConfig {
id: app_id,
policy: Default::default(),
admin: Some(keys::alice::address()),
stake: Default::default(),
};
set_app(cfg.clone());
let app = get_app(app_id).expect("application config should be created");
assert_eq!(app, cfg);
let cfg = types::AppConfig { admin: None, ..cfg };
set_app(cfg.clone());
let app = get_app(app_id).expect("application config should be updated");
assert_eq!(app, cfg);
remove_app(app_id);
let app = get_app(app_id);
assert!(app.is_none(), "application should have been removed");
}
#[test]
fn test_registration() {
let _mock = mock::Mock::default();
let app_id = Default::default();
let rak = keys::alice::pk().try_into().unwrap(); let rak_pk = keys::alice::pk();
let registration = get_registration(app_id, &rak);
assert!(registration.is_none());
let endorser = get_endorser(&rak_pk);
assert!(endorser.is_none());
let endorser = get_endorser(&keys::dave::pk());
assert!(endorser.is_none());
let new_registration = types::Registration {
app: app_id,
rak,
expiration: 42,
extra_keys: vec![
keys::dave::pk(), ],
..Default::default()
};
update_registration(new_registration.clone()).expect("registration update should work");
let bad_registration = types::Registration {
app: app_id,
extra_keys: vec![],
..new_registration.clone()
};
update_registration(bad_registration.clone())
.expect_err("extra endorsed key update should not be allowed");
let registration = get_registration(app_id, &rak).expect("registration should be present");
assert_eq!(registration, new_registration);
let endorser = get_endorser(&rak_pk).expect("endorser should be present");
assert_eq!(endorser.node_id, new_registration.node_id);
assert!(endorser.rak.is_none());
let endorser = get_endorser(&keys::dave::pk()).expect("extra keys should be endorsed");
assert_eq!(endorser.node_id, new_registration.node_id);
assert_eq!(endorser.rak, Some(rak));
let registrations = get_registrations_for_app(new_registration.app);
assert_eq!(registrations.len(), 1);
expire_registrations(42, 128);
let registration = get_registration(app_id, &rak);
assert!(registration.is_none());
let endorser = get_endorser(&rak_pk);
assert!(endorser.is_none());
let endorser = get_endorser(&keys::dave::pk());
assert!(endorser.is_none());
let registrations = get_registrations_for_app(new_registration.app);
assert_eq!(registrations.len(), 0);
}
}