1use crate::{
2 core::{
3 common::crypto::{hash::Hash, signature::PublicKey as CorePublicKey},
4 consensus::beacon::EpochTime,
5 },
6 crypto::signature::PublicKey,
7 state::CurrentState,
8 storage::{self, Store},
9};
10
11use super::{app_id::AppId, types, Error, MODULE_NAME};
12
13const APPS: &[u8] = &[0x01];
15const REGISTRATIONS: &[u8] = &[0x02];
17const ENDORSERS: &[u8] = &[0x03];
20const EXPIRATION_QUEUE: &[u8] = &[0x04];
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
25#[cbor(as_array)]
26pub struct KeyEndorsementInfo {
27 pub node_id: CorePublicKey,
29 pub rak: Option<CorePublicKey>,
31 pub app_id: AppId,
33}
34
35#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
37#[cbor(as_array)]
38struct LegacyKeyEndorsementInfo {
39 pub node_id: CorePublicKey,
41 pub rak: Option<CorePublicKey>,
43}
44
45impl KeyEndorsementInfo {
46 pub fn for_rak(node_id: CorePublicKey, app_id: AppId) -> Self {
48 Self {
49 node_id,
50 app_id,
51 ..Default::default()
52 }
53 }
54
55 pub fn for_extra_key(node_id: CorePublicKey, app_id: AppId, rak: CorePublicKey) -> Self {
57 Self {
58 node_id,
59 rak: Some(rak),
60 app_id,
61 }
62 }
63}
64
65impl From<LegacyKeyEndorsementInfo> for KeyEndorsementInfo {
66 fn from(value: LegacyKeyEndorsementInfo) -> Self {
67 Self {
68 node_id: value.node_id,
69 rak: value.rak,
70 app_id: AppId::default(),
72 }
73 }
74}
75
76fn apps<S: Store>(store: S) -> storage::TypedStore<impl Store> {
77 let store = storage::PrefixStore::new(store, &MODULE_NAME);
78 storage::TypedStore::new(storage::PrefixStore::new(store, &APPS))
79}
80
81pub fn get_app(app_id: AppId) -> Option<types::AppConfig> {
83 CurrentState::with_store(|store| apps(store).get(app_id))
84}
85
86pub fn get_apps() -> Vec<types::AppConfig> {
88 CurrentState::with_store(|store| {
89 let store = storage::PrefixStore::new(store, &MODULE_NAME);
90 let apps = storage::TypedStore::new(storage::PrefixStore::new(store, &APPS));
91 apps.iter()
92 .map(|(_, cfg): (AppId, types::AppConfig)| cfg)
93 .collect()
94 })
95}
96
97pub fn set_app(cfg: types::AppConfig) {
99 CurrentState::with_store(|store| {
100 apps(store).insert(cfg.id, cfg);
101 })
102}
103
104pub fn remove_app(app_id: AppId) {
106 CurrentState::with_store(|store| {
107 apps(store).remove(app_id);
108 })
109}
110
111pub fn update_registration(registration: types::Registration) -> Result<(), Error> {
113 let hrak = hash_rak(®istration.rak);
114
115 if let Some(existing) = get_registration_hrak(registration.app, hrak) {
117 if existing.extra_keys != registration.extra_keys {
119 return Err(Error::ExtraKeyUpdateNotAllowed);
120 }
121
122 remove_expiration_queue(existing.expiration, registration.app, hrak);
123 }
124 insert_expiration_queue(registration.expiration, registration.app, hrak);
125
126 CurrentState::with_store(|mut root_store| {
128 let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
129 let mut endorsers = storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
130 endorsers.insert(
131 hrak,
132 KeyEndorsementInfo::for_rak(registration.node_id, registration.app),
133 );
134
135 for pk in ®istration.extra_keys {
136 endorsers.insert(
137 hash_pk(pk),
138 KeyEndorsementInfo::for_extra_key(
139 registration.node_id,
140 registration.app,
141 registration.rak,
142 ),
143 );
144 }
145
146 let app_id = registration.app;
147 let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
148 let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
149 let mut app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
150 app.insert(hrak, registration);
151 });
152
153 Ok(())
154}
155
156fn remove_registration_hrak(app_id: AppId, hrak: Hash) {
157 let registration = match get_registration_hrak(app_id, hrak) {
158 Some(registration) => registration,
159 None => return,
160 };
161
162 remove_expiration_queue(registration.expiration, registration.app, hrak);
164
165 CurrentState::with_store(|mut root_store| {
167 let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
168 let mut endorsers = storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
169 endorsers.remove(hrak);
170
171 for pk in ®istration.extra_keys {
172 endorsers.remove(hash_pk(pk));
173 }
174
175 let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
176 let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
177 let mut app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
178 app.remove(hrak);
179 });
180}
181
182pub fn remove_registration(app_id: AppId, rak: &CorePublicKey) {
184 remove_registration_hrak(app_id, hash_rak(rak))
185}
186
187fn get_registration_hrak(app_id: AppId, hrak: Hash) -> Option<types::Registration> {
188 CurrentState::with_store(|store| {
189 let store = storage::PrefixStore::new(store, &MODULE_NAME);
190 let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
191 let app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
192 app.get(hrak)
193 })
194}
195
196pub fn get_registration(app_id: AppId, rak: &CorePublicKey) -> Option<types::Registration> {
199 get_registration_hrak(app_id, hash_rak(rak))
200}
201
202pub fn get_registrations_for_app(app_id: AppId) -> Vec<types::Registration> {
204 CurrentState::with_store(|mut root_store| {
205 let store = storage::PrefixStore::new(&mut root_store, &MODULE_NAME);
206 let registrations = storage::PrefixStore::new(store, ®ISTRATIONS);
207 let app = storage::TypedStore::new(storage::PrefixStore::new(registrations, app_id));
208
209 app.iter()
210 .map(|(_, registration): (Hash, types::Registration)| registration)
211 .collect()
212 })
213}
214
215pub fn get_endorser(pk: &PublicKey) -> Option<KeyEndorsementInfo> {
217 let hpk = hash_pk(pk);
218
219 CurrentState::with_store(|store| {
220 let store = storage::PrefixStore::new(store, &MODULE_NAME);
221 let endorsers = storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
222 let kei: Option<cbor::Value> = endorsers.get(hpk);
223 kei.map(|kei| match kei {
224 cbor::Value::Array(ref items) if items.len() == 2 => {
225 let legacy: LegacyKeyEndorsementInfo = cbor::from_value(kei).unwrap();
227 legacy.into()
228 }
229 _ => cbor::from_value(kei).unwrap(),
230 })
231 })
232}
233
234fn hash_rak(rak: &CorePublicKey) -> Hash {
235 hash_pk(&PublicKey::Ed25519(rak.into()))
236}
237
238fn hash_pk(pk: &PublicKey) -> Hash {
239 Hash::digest_bytes_list(&[pk.key_type().as_bytes(), pk.as_ref()])
240}
241
242fn queue_entry_key(epoch: EpochTime, app_id: AppId, hrak: Hash) -> Vec<u8> {
243 [&epoch.to_be_bytes(), app_id.as_ref(), hrak.as_ref()].concat()
244}
245
246fn insert_expiration_queue(epoch: EpochTime, app_id: AppId, hrak: Hash) {
247 CurrentState::with_store(|store| {
248 let store = storage::PrefixStore::new(store, &MODULE_NAME);
249 let mut queue = storage::PrefixStore::new(store, &EXPIRATION_QUEUE);
250 queue.insert(&queue_entry_key(epoch, app_id, hrak), &[]);
251 })
252}
253
254fn remove_expiration_queue(epoch: EpochTime, app_id: AppId, hrak: Hash) {
255 CurrentState::with_store(|store| {
256 let store = storage::PrefixStore::new(store, &MODULE_NAME);
257 let mut queue = storage::PrefixStore::new(store, &EXPIRATION_QUEUE);
258 queue.remove(&queue_entry_key(epoch, app_id, hrak));
259 })
260}
261
262struct ExpirationQueueEntry {
263 epoch: EpochTime,
264 app_id: AppId,
265 hrak: Hash,
266}
267
268impl<'a> TryFrom<&'a [u8]> for ExpirationQueueEntry {
269 type Error = anyhow::Error;
270
271 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
272 if value.len() != 8 + AppId::SIZE + Hash::len() {
274 anyhow::bail!("incorrect expiration queue key size");
275 }
276
277 Ok(Self {
278 epoch: EpochTime::from_be_bytes(value[..8].try_into()?),
279 app_id: value[8..8 + AppId::SIZE].try_into()?,
280 hrak: value[8 + AppId::SIZE..].into(),
281 })
282 }
283}
284
285pub fn expire_registrations(epoch: EpochTime, limit: usize) {
288 let expired: Vec<_> = CurrentState::with_store(|store| {
289 let store = storage::PrefixStore::new(store, &MODULE_NAME);
290 let queue = storage::TypedStore::new(storage::PrefixStore::new(store, &EXPIRATION_QUEUE));
291
292 queue
293 .iter()
294 .take_while(|(e, _): &(ExpirationQueueEntry, CorePublicKey)| e.epoch <= epoch)
295 .map(|(e, _)| (e.app_id, e.hrak))
296 .take(limit)
297 .collect()
298 });
299
300 for (app_id, hrak) in expired {
301 remove_registration_hrak(app_id, hrak);
302 }
303}
304
305#[cfg(test)]
306mod test {
307 use super::*;
308 use crate::testing::{keys, mock};
309
310 #[test]
311 fn test_app_cfg() {
312 let _mock = mock::Mock::default();
313
314 let app_id = AppId::from_creator_round_index(keys::alice::address(), 0, 0);
315 let app = get_app(app_id);
316 assert!(app.is_none());
317
318 let cfg = types::AppConfig {
319 id: app_id,
320 admin: Some(keys::alice::address()),
321 ..Default::default()
322 };
323 set_app(cfg.clone());
324 let app = get_app(app_id).expect("application config should be created");
325 assert_eq!(app, cfg);
326
327 let cfg = types::AppConfig { admin: None, ..cfg };
328 set_app(cfg.clone());
329 let app = get_app(app_id).expect("application config should be updated");
330 assert_eq!(app, cfg);
331
332 let apps = get_apps();
333 assert_eq!(apps.len(), 1);
334 assert_eq!(apps[0], cfg);
335
336 remove_app(app_id);
337 let app = get_app(app_id);
338 assert!(app.is_none(), "application should have been removed");
339 let apps = get_apps();
340 assert_eq!(apps.len(), 0);
341 }
342
343 #[test]
344 fn test_registration() {
345 let _mock = mock::Mock::default();
346 let app_id = Default::default();
347 let rak = keys::alice::pk().try_into().unwrap(); let rak_pk = keys::alice::pk();
349
350 let registration = get_registration(app_id, &rak);
351 assert!(registration.is_none());
352 let endorser = get_endorser(&rak_pk);
353 assert!(endorser.is_none());
354 let endorser = get_endorser(&keys::dave::pk());
355 assert!(endorser.is_none());
356
357 let new_registration = types::Registration {
358 app: app_id,
359 rak,
360 expiration: 42,
361 extra_keys: vec![
362 keys::dave::pk(), ],
364 ..Default::default()
365 };
366 update_registration(new_registration.clone()).expect("registration update should work");
367
368 let bad_registration = types::Registration {
370 app: app_id,
371 extra_keys: vec![],
372 ..new_registration.clone()
373 };
374 update_registration(bad_registration.clone())
375 .expect_err("extra endorsed key update should not be allowed");
376
377 let registration = get_registration(app_id, &rak).expect("registration should be present");
378 assert_eq!(registration, new_registration);
379 let endorser = get_endorser(&rak_pk).expect("endorser should be present");
380 assert_eq!(endorser.node_id, new_registration.node_id);
381 assert!(endorser.rak.is_none());
382 let endorser = get_endorser(&keys::dave::pk()).expect("extra keys should be endorsed");
383 assert_eq!(endorser.node_id, new_registration.node_id);
384 assert_eq!(endorser.rak, Some(rak));
385 let registrations = get_registrations_for_app(new_registration.app);
386 assert_eq!(registrations.len(), 1);
387
388 expire_registrations(42, 128);
389
390 let registration = get_registration(app_id, &rak);
391 assert!(registration.is_none());
392 let endorser = get_endorser(&rak_pk);
393 assert!(endorser.is_none());
394 let endorser = get_endorser(&keys::dave::pk());
395 assert!(endorser.is_none());
396 let registrations = get_registrations_for_app(new_registration.app);
397 assert_eq!(registrations.len(), 0);
398 }
399
400 #[test]
401 fn test_legacy_kei() {
402 let _mock = mock::Mock::default();
403 let rak_pk = keys::alice::pk();
404 let hpk = hash_pk(&rak_pk);
405
406 CurrentState::with_store(|store| {
407 let store = storage::PrefixStore::new(store, &MODULE_NAME);
408 let mut endorsers =
409 storage::TypedStore::new(storage::PrefixStore::new(store, &ENDORSERS));
410 endorsers.insert(
411 hpk,
412 LegacyKeyEndorsementInfo {
413 node_id: keys::bob::pk_ed25519().into(),
414 rak: None,
415 },
416 );
417 });
418
419 let kei = get_endorser(&rak_pk).unwrap();
420 assert_eq!(kei.node_id, keys::bob::pk_ed25519().into());
421 assert!(kei.rak.is_none());
422 assert_eq!(kei.app_id, AppId::default());
423 }
424}