1use std::collections::{BTreeMap, BTreeSet};
3
4use once_cell::sync::Lazy;
5
6use crate::{
7 context::Context,
8 core::common::crypto::signature::PublicKey as CorePublicKey,
9 crypto::signature::PublicKey,
10 dispatcher, handler, keymanager, migration,
11 module::{self, Module as _, Parameters as _},
12 modules::{self, accounts::API as _, core::API as _},
13 sdk_derive,
14 state::CurrentState,
15 types::{address::Address, transaction::Transaction},
16 Runtime,
17};
18
19pub mod app_id;
20mod config;
21mod error;
22mod event;
23pub mod policy;
24pub mod state;
25#[cfg(test)]
26mod test;
27pub mod types;
28
29const MODULE_NAME: &str = "rofl";
31
32pub use config::Config;
33pub use error::Error;
34pub use event::Event;
35use policy::EndorsementPolicyEvaluator;
36
37#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
39pub struct Parameters {}
40
41#[derive(thiserror::Error, Debug)]
43pub enum ParameterValidationError {}
44
45impl module::Parameters for Parameters {
46 type Error = ParameterValidationError;
47
48 fn validate_basic(&self) -> Result<(), Self::Error> {
49 Ok(())
50 }
51}
52
53#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
55pub struct Genesis {
56 pub parameters: Parameters,
57
58 pub apps: Vec<types::AppConfig>,
60}
61
62pub trait API {
64 fn get_origin_app() -> Option<app_id::AppId>;
71
72 fn get_origin_rak() -> Option<PublicKey>;
79
80 fn get_origin_registration(app: app_id::AppId) -> Option<types::Registration>;
87
88 fn is_authorized_origin(app: app_id::AppId) -> bool;
95
96 fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result<types::Registration, Error>;
98
99 fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error>;
101
102 fn get_apps() -> Result<Vec<types::AppConfig>, Error>;
104
105 fn get_instances(id: app_id::AppId) -> Result<Vec<types::Registration>, Error>;
107}
108
109pub static ADDRESS_APP_STAKE_POOL: Lazy<Address> =
113 Lazy::new(|| Address::from_module(MODULE_NAME, "app-stake-pool"));
114
115pub static ROFL_DERIVE_KEY_CONTEXT: &[u8] = b"oasis-runtime-sdk/rofl: derive key v1";
117pub static ROFL_KEY_ID_SEK: &[u8] = b"oasis-runtime-sdk/rofl: secrets encryption key v1";
119
120pub struct Module<Cfg: Config> {
121 _cfg: std::marker::PhantomData<Cfg>,
122}
123
124impl<Cfg: Config> API for Module<Cfg> {
125 fn get_origin_app() -> Option<app_id::AppId> {
126 let caller_pk = CurrentState::with_env_origin(|env| env.tx_caller_public_key())?;
127 state::get_endorser(&caller_pk).map(|kei| kei.app_id)
128 }
129
130 fn get_origin_rak() -> Option<PublicKey> {
131 let caller_pk = CurrentState::with_env_origin(|env| env.tx_caller_public_key())?;
132
133 state::get_endorser(&caller_pk).map(|kei| match kei {
135 state::KeyEndorsementInfo { rak: Some(rak), .. } => rak.into(),
137 _ => caller_pk,
139 })
140 }
141
142 fn get_origin_registration(app: app_id::AppId) -> Option<types::Registration> {
143 Self::get_origin_rak()
144 .and_then(|rak| state::get_registration(app, &rak.try_into().unwrap()))
145 }
146
147 fn is_authorized_origin(app: app_id::AppId) -> bool {
148 Self::get_origin_registration(app).is_some()
149 }
150
151 fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result<types::Registration, Error> {
152 state::get_registration(app, &rak.try_into().map_err(|_| Error::InvalidArgument)?)
153 .ok_or(Error::UnknownInstance)
154 }
155
156 fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error> {
157 state::get_app(id).ok_or(Error::UnknownApp)
158 }
159
160 fn get_apps() -> Result<Vec<types::AppConfig>, Error> {
161 Ok(state::get_apps())
162 }
163
164 fn get_instances(id: app_id::AppId) -> Result<Vec<types::Registration>, Error> {
165 Ok(state::get_registrations_for_app(id))
166 }
167}
168
169#[sdk_derive(Module)]
170impl<Cfg: Config> Module<Cfg> {
171 const NAME: &'static str = MODULE_NAME;
172 type Error = Error;
173 type Event = Event;
174 type Parameters = Parameters;
175 type Genesis = Genesis;
176
177 #[migration(init)]
178 fn init(genesis: Genesis) {
179 genesis
180 .parameters
181 .validate_basic()
182 .expect("invalid genesis parameters");
183
184 Self::set_params(genesis.parameters);
186
187 for cfg in genesis.apps {
189 if state::get_app(cfg.id).is_some() {
190 panic!("duplicate application in genesis: {:?}", cfg.id);
191 }
192
193 state::set_app(cfg);
194 }
195 }
196
197 #[handler(call = "rofl.Create")]
199 fn tx_create<C: Context>(ctx: &C, body: types::Create) -> Result<app_id::AppId, Error> {
200 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_CREATE)?;
201
202 if body.metadata.len() > Cfg::MAX_METADATA_PAIRS {
203 return Err(Error::InvalidArgument);
204 }
205 for (key, value) in &body.metadata {
206 if key.len() > Cfg::MAX_METADATA_KEY_SIZE {
207 return Err(Error::InvalidArgument);
208 }
209 if value.len() > Cfg::MAX_METADATA_VALUE_SIZE {
210 return Err(Error::InvalidArgument);
211 }
212 }
213
214 body.policy.validate::<Cfg>()?;
215
216 if CurrentState::with_env(|env| env.is_check_only()) {
217 return Ok(Default::default());
218 }
219
220 let (creator, tx_index) =
221 CurrentState::with_env(|env| (env.tx_caller_address(), env.tx_index()));
222 let app_id = match body.scheme {
223 types::IdentifierScheme::CreatorRoundIndex => app_id::AppId::from_creator_round_index(
224 creator,
225 ctx.runtime_header().round,
226 tx_index.try_into().map_err(|_| Error::InvalidArgument)?,
227 ),
228 types::IdentifierScheme::CreatorNonce => {
229 let nonce = <C::Runtime as Runtime>::Accounts::get_nonce(creator)?;
230
231 app_id::AppId::from_creator_nonce(creator, nonce)
232 }
233 };
234
235 if state::get_app(app_id).is_some() {
237 return Err(Error::AppAlreadyExists);
238 }
239
240 <C::Runtime as Runtime>::Accounts::transfer(
242 creator,
243 *ADDRESS_APP_STAKE_POOL,
244 &Cfg::STAKE_APP_CREATE,
245 )?;
246
247 let sek = Self::derive_app_key(
249 ctx,
250 &app_id,
251 types::KeyKind::X25519,
252 types::KeyScope::Global,
253 ROFL_KEY_ID_SEK,
254 None,
255 )?
256 .input_keypair
257 .pk;
258
259 let cfg = types::AppConfig {
261 id: app_id,
262 policy: body.policy,
263 admin: Some(creator),
264 stake: Cfg::STAKE_APP_CREATE,
265 metadata: body.metadata,
266 sek,
267 ..Default::default()
268 };
269 state::set_app(cfg);
270
271 CurrentState::with(|state| state.emit_event(Event::AppCreated { id: app_id }));
272
273 Ok(app_id)
274 }
275
276 fn ensure_caller_is_admin(cfg: &types::AppConfig) -> Result<(), Error> {
278 let caller = CurrentState::with_env(|env| env.tx_caller_address());
279 if cfg.admin != Some(caller) {
280 return Err(Error::Forbidden);
281 }
282 Ok(())
283 }
284
285 #[handler(call = "rofl.Update")]
287 fn tx_update<C: Context>(ctx: &C, body: types::Update) -> Result<(), Error> {
288 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_UPDATE)?;
289
290 if body.metadata.len() > Cfg::MAX_METADATA_PAIRS {
291 return Err(Error::InvalidArgument);
292 }
293 for (key, value) in &body.metadata {
294 if key.len() > Cfg::MAX_METADATA_KEY_SIZE {
295 return Err(Error::InvalidArgument);
296 }
297 if value.len() > Cfg::MAX_METADATA_VALUE_SIZE {
298 return Err(Error::InvalidArgument);
299 }
300 }
301 if body.secrets.len() > Cfg::MAX_METADATA_PAIRS {
302 return Err(Error::InvalidArgument);
303 }
304 for (key, value) in &body.secrets {
305 if key.len() > Cfg::MAX_METADATA_KEY_SIZE {
306 return Err(Error::InvalidArgument);
307 }
308 if value.len() > Cfg::MAX_METADATA_VALUE_SIZE {
309 return Err(Error::InvalidArgument);
310 }
311 }
312
313 body.policy.validate::<Cfg>()?;
314
315 let mut cfg = state::get_app(body.id).ok_or(Error::UnknownApp)?;
316
317 Self::ensure_caller_is_admin(&cfg)?;
319
320 if CurrentState::with_env(|env| env.is_check_only()) {
321 return Ok(());
322 }
323
324 if cfg.sek == Default::default() {
326 cfg.sek = Self::derive_app_key(
327 ctx,
328 &body.id,
329 types::KeyKind::X25519,
330 types::KeyScope::Global,
331 ROFL_KEY_ID_SEK,
332 None,
333 )?
334 .input_keypair
335 .pk;
336 }
337
338 cfg.policy = body.policy;
339 cfg.admin = body.admin;
340 cfg.metadata = body.metadata;
341 cfg.secrets = body.secrets;
342 state::set_app(cfg);
343
344 CurrentState::with(|state| state.emit_event(Event::AppUpdated { id: body.id }));
345
346 Ok(())
347 }
348
349 #[handler(call = "rofl.Remove")]
351 fn tx_remove<C: Context>(ctx: &C, body: types::Remove) -> Result<(), Error> {
352 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_REMOVE)?;
353
354 let cfg = state::get_app(body.id).ok_or(Error::UnknownApp)?;
355
356 Self::ensure_caller_is_admin(&cfg)?;
358
359 if CurrentState::with_env(|env| env.is_check_only()) {
360 return Ok(());
361 }
362
363 state::remove_app(body.id);
364
365 if let Some(admin) = cfg.admin {
367 <C::Runtime as Runtime>::Accounts::transfer(
368 *ADDRESS_APP_STAKE_POOL,
369 admin,
370 &cfg.stake,
371 )?;
372 }
373
374 CurrentState::with(|state| state.emit_event(Event::AppRemoved { id: body.id }));
375
376 Ok(())
377 }
378
379 #[handler(call = "rofl.Register")]
381 fn tx_register<C: Context>(ctx: &C, body: types::Register) -> Result<(), Error> {
382 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_REGISTER)?;
383
384 if body.expiration <= ctx.epoch() {
385 return Err(Error::RegistrationExpired);
386 }
387
388 if body.metadata.len() > Cfg::MAX_METADATA_PAIRS {
389 return Err(Error::InvalidArgument);
390 }
391 for (key, value) in &body.metadata {
392 if key.len() > Cfg::MAX_METADATA_KEY_SIZE {
393 return Err(Error::InvalidArgument);
394 }
395 if value.len() > Cfg::MAX_METADATA_VALUE_SIZE {
396 return Err(Error::InvalidArgument);
397 }
398 }
399
400 let cfg = state::get_app(body.app).ok_or(Error::UnknownApp)?;
401
402 if body.expiration - ctx.epoch() > cfg.policy.max_expiration {
403 return Err(Error::InvalidArgument);
404 }
405
406 let signer_pks: BTreeSet<PublicKey> = CurrentState::with_env(|env| {
408 env.tx_auth_info()
409 .signer_info
410 .iter()
411 .filter_map(|si| si.address_spec.public_key())
412 .collect()
413 });
414 if !signer_pks.contains(&body.ect.capability_tee.rak.into()) {
415 return Err(Error::NotSignedByRAK);
416 }
417 for extra_pk in &body.extra_keys {
418 if !signer_pks.contains(extra_pk) {
419 return Err(Error::NotSignedByExtraKey);
420 }
421 }
422
423 if CurrentState::with_env(|env| env.is_check_only()) {
424 return Ok(());
425 }
426
427 let verified_ect = body
429 .ect
430 .verify(&cfg.policy.quotes)
431 .map_err(|_| Error::InvalidArgument)?;
432
433 if !cfg
435 .policy
436 .enclaves
437 .contains(&verified_ect.verified_attestation.quote.identity)
438 {
439 return Err(Error::UnknownEnclave);
440 }
441
442 let node = Cfg::EndorsementPolicyEvaluator::verify(
444 ctx,
445 &cfg.policy.endorsements,
446 &body.ect,
447 &body.metadata,
448 )?;
449
450 let registration = types::Registration {
452 app: body.app,
453 node_id: verified_ect.node_id.unwrap(), entity_id: node.map(|n| n.entity_id),
455 rak: body.ect.capability_tee.rak,
456 rek: body.ect.capability_tee.rek.ok_or(Error::InvalidArgument)?, expiration: body.expiration,
458 extra_keys: body.extra_keys,
459 metadata: body.metadata,
460 };
461 state::update_registration(registration)?;
462
463 CurrentState::with(|state| {
464 state.emit_event(Event::InstanceRegistered {
465 app_id: body.app,
466 rak: body.ect.capability_tee.rak.into(),
467 })
468 });
469
470 Ok(())
471 }
472
473 #[handler(call = "rofl.DeriveKey")]
475 fn tx_derive_key<C: Context>(
476 ctx: &C,
477 body: types::DeriveKey,
478 ) -> Result<types::DeriveKeyResponse, Error> {
479 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_DERIVE_KEY)?;
480
481 let call_format = CurrentState::with_env(|env| env.tx_call_format());
483 if !call_format.is_encrypted() {
484 return Err(Error::PlainCallFormatNotAllowed);
485 }
486
487 if body.generation != 0 {
489 return Err(Error::InvalidArgument);
490 }
491
492 if body.key_id.len() > Cfg::DERIVE_KEY_MAX_KEY_ID_LENGTH {
494 return Err(Error::InvalidArgument);
495 }
496
497 if CurrentState::with_env(|env| env.is_internal()) {
499 return Err(Error::Forbidden);
500 }
501
502 if CurrentState::with_env(|env| env.is_check_only()) {
503 return Ok(Default::default());
504 }
505
506 let reg = Self::get_origin_registration(body.app).ok_or(Error::Forbidden)?;
508
509 let key = Self::derive_app_key(
511 ctx,
512 &body.app,
513 body.kind,
514 body.scope,
515 &body.key_id,
516 Some(reg),
517 )?;
518 let key = match body.kind {
519 types::KeyKind::EntropyV0 => key.state_key.0.into(),
520 types::KeyKind::X25519 => key.input_keypair.sk.as_ref().into(),
521 };
522
523 Ok(types::DeriveKeyResponse { key })
524 }
525
526 fn derive_app_key_id(
527 app: &app_id::AppId,
528 kind: types::KeyKind,
529 scope: types::KeyScope,
530 key_id: &[u8],
531 reg: Option<types::Registration>,
532 ) -> Result<keymanager::KeyPairId, Error> {
533 let kind_id = &[kind as u8];
545 let mut key_id = vec![ROFL_DERIVE_KEY_CONTEXT, app.as_ref(), kind_id, key_id];
546 let mut extra_dom: BTreeMap<&str, Vec<u8>> = BTreeMap::new();
547
548 match scope {
549 types::KeyScope::Global => {
550 }
553 types::KeyScope::Node => {
554 let node_id = reg.ok_or(Error::InvalidArgument)?.node_id;
556
557 extra_dom.insert("scope", [scope as u8].to_vec());
558 extra_dom.insert("node_id", node_id.as_ref().to_vec());
559 }
560 types::KeyScope::Entity => {
561 let entity_id = reg
563 .ok_or(Error::InvalidArgument)?
564 .entity_id
565 .ok_or(Error::InvalidArgument)?;
566
567 extra_dom.insert("scope", [scope as u8].to_vec());
568 extra_dom.insert("entity_id", entity_id.as_ref().to_vec());
569 }
570 };
571
572 let extra_dom = if !extra_dom.is_empty() {
574 cbor::to_vec(extra_dom)
575 } else {
576 vec![]
577 };
578 if !extra_dom.is_empty() {
579 key_id.push(&extra_dom)
580 }
581
582 Ok(keymanager::get_key_pair_id(key_id))
584 }
585
586 fn derive_app_key<C: Context>(
587 ctx: &C,
588 app: &app_id::AppId,
589 kind: types::KeyKind,
590 scope: types::KeyScope,
591 key_id: &[u8],
592 reg: Option<types::Registration>,
593 ) -> Result<keymanager::KeyPair, Error> {
594 let key_id = Self::derive_app_key_id(app, kind, scope, key_id, reg)?;
595
596 let km = ctx
597 .key_manager()
598 .ok_or(Error::Abort(dispatcher::Error::KeyManagerFailure(
599 keymanager::KeyManagerError::NotInitialized,
600 )))?;
601 km.get_or_create_keys(key_id)
602 .map_err(|err| Error::Abort(dispatcher::Error::KeyManagerFailure(err)))
603 }
604
605 #[handler(call = "rofl.IsAuthorizedOrigin", internal)]
608 fn internal_is_authorized_origin<C: Context>(
609 _ctx: &C,
610 app: app_id::AppId,
611 ) -> Result<bool, Error> {
612 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_IS_AUTHORIZED_ORIGIN)?;
613
614 Ok(Self::is_authorized_origin(app))
615 }
616
617 #[handler(call = "rofl.AuthorizedOriginNode", internal)]
618 fn internal_authorized_origin_node<C: Context>(
619 _ctx: &C,
620 app: app_id::AppId,
621 ) -> Result<CorePublicKey, Error> {
622 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_AUTHORIZED_ORIGIN_NODE)?;
623
624 let registration = Self::get_origin_registration(app).ok_or(Error::UnknownInstance)?;
625 Ok(registration.node_id)
626 }
627
628 #[handler(call = "rofl.AuthorizedOriginEntity", internal)]
629 fn internal_authorized_origin_entity<C: Context>(
630 _ctx: &C,
631 app: app_id::AppId,
632 ) -> Result<Option<CorePublicKey>, Error> {
633 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_AUTHORIZED_ORIGIN_ENTITY)?;
634
635 let registration = Self::get_origin_registration(app).ok_or(Error::UnknownInstance)?;
636 Ok(registration.entity_id)
637 }
638
639 #[handler(call = "rofl.OriginApp", internal)]
640 fn internal_origin_app<C: Context>(_ctx: &C, _args: ()) -> Result<app_id::AppId, Error> {
641 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_ORIGIN_APP)?;
642
643 Self::get_origin_app().ok_or(Error::UnknownInstance)
644 }
645
646 #[handler(query = "rofl.App")]
648 fn query_app<C: Context>(_ctx: &C, args: types::AppQuery) -> Result<types::AppConfig, Error> {
649 Self::get_app(args.id)
650 }
651
652 #[handler(query = "rofl.Apps", expensive)]
654 fn query_apps<C: Context>(_ctx: &C, _args: ()) -> Result<Vec<types::AppConfig>, Error> {
655 Self::get_apps()
656 }
657
658 #[handler(query = "rofl.AppInstance")]
660 fn query_app_instance<C: Context>(
661 _ctx: &C,
662 args: types::AppInstanceQuery,
663 ) -> Result<types::Registration, Error> {
664 Self::get_registration(args.app, args.rak)
665 }
666
667 #[handler(query = "rofl.AppInstances", expensive)]
669 fn query_app_instances<C: Context>(
670 _ctx: &C,
671 args: types::AppQuery,
672 ) -> Result<Vec<types::Registration>, Error> {
673 Self::get_instances(args.id)
674 }
675
676 #[handler(query = "rofl.StakeThresholds")]
678 fn query_stake_thresholds<C: Context>(
679 _ctx: &C,
680 _args: (),
681 ) -> Result<types::StakeThresholds, Error> {
682 Ok(types::StakeThresholds {
683 app_create: Cfg::STAKE_APP_CREATE,
684 })
685 }
686
687 #[handler(call = "rofl.StakeThresholds", internal)]
689 fn internal_query_stake_thresholds<C: Context>(
690 ctx: &C,
691 _args: (),
692 ) -> Result<types::StakeThresholds, Error> {
693 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_STAKE_THRESHOLDS)?;
694 Ok(types::StakeThresholds {
695 app_create: Cfg::STAKE_APP_CREATE,
696 })
697 }
698
699 fn resolve_payer_from_tx<C: Context>(
700 ctx: &C,
701 tx: &Transaction,
702 app_policy: &policy::AppAuthPolicy,
703 ) -> Result<Option<Address>, anyhow::Error> {
704 let caller_pk = tx
705 .auth_info
706 .signer_info
707 .first()
708 .and_then(|si| si.address_spec.public_key());
709
710 match tx.call.method.as_str() {
711 "rofl.Register" => {
712 let body: types::Register = cbor::from_value(tx.call.body.clone())?;
714 if body.expiration <= ctx.epoch() {
715 return Err(Error::RegistrationExpired.into());
716 }
717
718 let caller_pk = caller_pk.ok_or(Error::NotSignedByRAK)?;
720 if caller_pk != body.ect.capability_tee.rak {
721 return Err(Error::NotSignedByRAK.into());
722 }
723
724 body.ect.verify_endorsement()?;
725
726 let node_id = body.ect.node_endorsement.public_key;
731 let payer = Address::from_consensus_pk(&node_id);
732
733 Ok(Some(payer))
734 }
735 _ => {
736 let caller_pk = match caller_pk {
738 Some(pk) => pk,
739 None => return Ok(None),
740 };
741
742 Ok(state::get_endorser(&caller_pk)
743 .map(|ei| Address::from_consensus_pk(&ei.node_id)))
744 }
745 }
746 }
747}
748
749impl<Cfg: Config> module::FeeProxyHandler for Module<Cfg> {
750 fn resolve_payer<C: Context>(
751 ctx: &C,
752 tx: &Transaction,
753 ) -> Result<Option<Address>, modules::core::Error> {
754 use policy::FeePolicy;
755
756 let proxy = if let Some(ref proxy) = tx.auth_info.fee.proxy {
757 proxy
758 } else {
759 return Ok(None);
760 };
761
762 if proxy.module != MODULE_NAME {
763 return Ok(None);
764 }
765
766 let app_id = app_id::AppId::try_from(proxy.id.as_slice())
768 .map_err(|err| modules::core::Error::InvalidArgument(err.into()))?;
769 let app_policy = state::get_app(app_id).map(|cfg| cfg.policy).ok_or(
770 modules::core::Error::InvalidArgument(Error::UnknownApp.into()),
771 )?;
772
773 match app_policy.fees {
774 FeePolicy::InstancePays => {
775 Ok(None)
777 }
778 FeePolicy::EndorsingNodePays => Self::resolve_payer_from_tx(ctx, tx, &app_policy)
779 .map_err(modules::core::Error::InvalidArgument),
780 }
781 }
782}
783
784impl<Cfg: Config> module::TransactionHandler for Module<Cfg> {}
785
786impl<Cfg: Config> module::BlockHandler for Module<Cfg> {
787 fn end_block<C: Context>(ctx: &C) {
788 if !<C::Runtime as Runtime>::Core::has_epoch_changed() {
790 return;
791 }
792
793 state::expire_registrations(ctx.epoch(), 128);
796 }
797}
798
799impl<Cfg: Config> module::InvariantHandler for Module<Cfg> {}