oasis_runtime_sdk/modules/rofl/
mod.rs

1//! On-chain coordination for ROFL components.
2use 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
29/// Unique module name.
30const MODULE_NAME: &str = "rofl";
31
32pub use config::Config;
33pub use error::Error;
34pub use event::Event;
35use policy::EndorsementPolicyEvaluator;
36
37/// Parameters for the module.
38#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
39pub struct Parameters {}
40
41/// Errors emitted during parameter validation.
42#[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/// Genesis state for the module.
54#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
55pub struct Genesis {
56    pub parameters: Parameters,
57
58    /// Application configurations.
59    pub apps: Vec<types::AppConfig>,
60}
61
62/// Interface that can be called from other modules.
63pub trait API {
64    /// Get the identifier of the calling ROFL app in case the origin transaction is signed by a
65    /// ROFL instance. Otherwise `None` is returned.
66    ///
67    /// # Panics
68    ///
69    /// This method will panic if called outside a transaction environment.
70    fn get_origin_app() -> Option<app_id::AppId>;
71
72    /// Get the Runtime Attestation Key of the ROFL app instance in case the origin transaction is
73    /// signed by a ROFL instance. Otherwise `None` is returned.
74    ///
75    /// # Panics
76    ///
77    /// This method will panic if called outside a transaction environment.
78    fn get_origin_rak() -> Option<PublicKey>;
79
80    /// Get the registration descriptor of the ROFL app instance in case the origin transaction is
81    /// signed by a ROFL instance of the specified app. Otherwise `None` is returned.
82    ///
83    /// # Panics
84    ///
85    /// This method will panic if called outside a transaction environment.
86    fn get_origin_registration(app: app_id::AppId) -> Option<types::Registration>;
87
88    /// Verify whether the origin transaction is signed by an authorized ROFL instance for the given
89    /// application.
90    ///
91    /// # Panics
92    ///
93    /// This method will panic if called outside a transaction environment.
94    fn is_authorized_origin(app: app_id::AppId) -> bool;
95
96    /// Get a specific registered instance for an application.
97    fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result<types::Registration, Error>;
98
99    /// Get an application's configuration.
100    fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error>;
101
102    /// Get all application configurations.
103    fn get_apps() -> Result<Vec<types::AppConfig>, Error>;
104
105    /// Get all registered instances for an application.
106    fn get_instances(id: app_id::AppId) -> Result<Vec<types::Registration>, Error>;
107}
108
109/// Module's address that has the application stake pool.
110///
111/// oasis1qza6sddnalgzexk3ct30gqfvntgth5m4hsyywmff
112pub static ADDRESS_APP_STAKE_POOL: Lazy<Address> =
113    Lazy::new(|| Address::from_module(MODULE_NAME, "app-stake-pool"));
114
115/// Key derivation context.
116pub static ROFL_DERIVE_KEY_CONTEXT: &[u8] = b"oasis-runtime-sdk/rofl: derive key v1";
117/// Secrets encryption key identifier.
118pub 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        // Resolve RAK as the call may be made by an extra key.
134        state::get_endorser(&caller_pk).map(|kei| match kei {
135            // It may point to a RAK.
136            state::KeyEndorsementInfo { rak: Some(rak), .. } => rak.into(),
137            // Or it points to itself.
138            _ => 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        // Set genesis parameters.
185        Self::set_params(genesis.parameters);
186
187        // Insert all applications.
188        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    /// Create a new ROFL application.
198    #[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        // Sanity check that the application doesn't already exist.
236        if state::get_app(app_id).is_some() {
237            return Err(Error::AppAlreadyExists);
238        }
239
240        // Transfer stake.
241        <C::Runtime as Runtime>::Accounts::transfer(
242            creator,
243            *ADDRESS_APP_STAKE_POOL,
244            &Cfg::STAKE_APP_CREATE,
245        )?;
246
247        // Generate the secret encryption (public) key.
248        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        // Register the application.
260        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    /// Ensure caller is the current administrator, return an error otherwise.
277    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    /// Update a ROFL application.
286    #[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        // Ensure caller is the admin and is allowed to update the configuration.
318        Self::ensure_caller_is_admin(&cfg)?;
319
320        if CurrentState::with_env(|env| env.is_check_only()) {
321            return Ok(());
322        }
323
324        // If there is no SEK defined, regenerate it.
325        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    /// Remove a ROFL application.
350    #[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        // Ensure caller is the admin and is allowed to update the configuration.
357        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        // Return stake to the administrator account.
366        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    /// Register a new ROFL instance.
380    #[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        // Ensure that the transaction is signed by RAK (and co-signed by extra keys).
407        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        // Verify policy.
428        let verified_ect = body
429            .ect
430            .verify(&cfg.policy.quotes)
431            .map_err(|_| Error::InvalidArgument)?;
432
433        // Verify enclave identity.
434        if !cfg
435            .policy
436            .enclaves
437            .contains(&verified_ect.verified_attestation.quote.identity)
438        {
439            return Err(Error::UnknownEnclave);
440        }
441
442        // Verify allowed endorsement.
443        let node = Cfg::EndorsementPolicyEvaluator::verify(
444            ctx,
445            &cfg.policy.endorsements,
446            &body.ect,
447            &body.metadata,
448        )?;
449
450        // Update registration.
451        let registration = types::Registration {
452            app: body.app,
453            node_id: verified_ect.node_id.unwrap(), // Verified above.
454            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)?, // REK required.
457            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    /// Derive a ROFL application-specific key.
474    #[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        // Ensure call is encrypted to avoid leaking any keys by accident.
482        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        // Currently only generation zero keys are supported.
488        if body.generation != 0 {
489            return Err(Error::InvalidArgument);
490        }
491
492        // Ensure key identifier is not too long.
493        if body.key_id.len() > Cfg::DERIVE_KEY_MAX_KEY_ID_LENGTH {
494            return Err(Error::InvalidArgument);
495        }
496
497        // Disallow invocation from subcalls.
498        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        // Ensure caller is an authorized instance of the given application.
507        let reg = Self::get_origin_registration(body.app).ok_or(Error::Forbidden)?;
508
509        // Derive application key.
510        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        // Build the base key identifier.
534        //
535        // We use the following tuple elements which are fed into TupleHash to derive the final key
536        // identifier, in order:
537        //
538        // - V1 context domain separator.
539        // - App ID.
540        // - Encoded kind.
541        // - Key ID.
542        // - Optional CBOR-serialized extra domain separation.
543        //
544        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                // Nothing to do here, global keys don't include an explicit scope for backwards
551                // compatibility.
552            }
553            types::KeyScope::Node => {
554                // Fetch node identifier corresponding to the application instance.
555                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                // Fetch entity identifier corresponding to the application instance.
562                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        // Add optional extra domain separation.
573        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        // Finalize the key identifier.
583        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    /// Verify whether the origin transaction is signed by an authorized ROFL instance for the given
606    /// application.
607    #[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    /// Returns the configuration for the given ROFL application.
647    #[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    /// Returns all ROFL app configurations.
653    #[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    /// Returns a specific registered instance for the given ROFL application.
659    #[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    /// Returns a list of all registered instances for the given ROFL application.
668    #[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    /// Returns the minimum stake thresholds for managing ROFL.
677    #[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    /// Returns the minimum stake thresholds for managing ROFL.
688    #[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                // For registration transactions, extract endorsing node.
713                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                // Ensure that the transaction is signed by RAK.
719                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                // Checking other details is not relevant for authorizing fee payments as if the
727                // node signed a TEE capability then it is authorizing fees to be spent on its
728                // behalf.
729
730                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                // For others, check if caller is one of the endorsed keys.
737                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        // Look up the per-ROFL app policy.
767        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                // Application needs to figure out a way to pay, defer to regular handler.
776                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        // Only do work in case the epoch has changed since the last processed block.
789        if !<C::Runtime as Runtime>::Core::has_epoch_changed() {
790            return;
791        }
792
793        // Process enclave expirations.
794        // TODO: Consider processing unprocessed in the next block(s) if there are too many.
795        state::expire_registrations(ctx.epoch(), 128);
796    }
797}
798
799impl<Cfg: Config> module::InvariantHandler for Module<Cfg> {}