use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use crate::{
context::Context,
core::{
common::crypto::signature::PublicKey as CorePublicKey,
consensus::{
registry::{Node, RolesMask, VerifiedEndorsedCapabilityTEE},
state::registry::ImmutableState as RegistryImmutableState,
},
},
crypto::signature::PublicKey,
handler, migration,
module::{self, Module as _, Parameters as _},
modules::{self, accounts::API as _, core::API as _},
sdk_derive,
state::CurrentState,
types::{address::Address, transaction::Transaction},
Runtime,
};
pub mod app;
pub mod app_id;
mod config;
mod error;
mod event;
pub mod policy;
pub mod state;
#[cfg(test)]
mod test;
pub mod types;
const MODULE_NAME: &str = "rofl";
pub use config::Config;
pub use error::Error;
pub use event::Event;
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
pub struct Parameters {}
#[derive(thiserror::Error, Debug)]
pub enum ParameterValidationError {}
impl module::Parameters for Parameters {
type Error = ParameterValidationError;
fn validate_basic(&self) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
pub struct Genesis {
pub parameters: Parameters,
pub apps: Vec<types::AppConfig>,
}
pub trait API {
fn get_origin_rak() -> Option<PublicKey>;
fn get_origin_registration(app: app_id::AppId) -> Option<types::Registration>;
fn is_authorized_origin(app: app_id::AppId) -> bool;
fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result<types::Registration, Error>;
fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error>;
fn get_instances(id: app_id::AppId) -> Result<Vec<types::Registration>, Error>;
}
pub static ADDRESS_APP_STAKE_POOL: Lazy<Address> =
Lazy::new(|| Address::from_module(MODULE_NAME, "app-stake-pool"));
pub struct Module<Cfg: Config> {
_cfg: std::marker::PhantomData<Cfg>,
}
impl<Cfg: Config> API for Module<Cfg> {
fn get_origin_rak() -> Option<PublicKey> {
let caller_pk = CurrentState::with_env_origin(|env| env.tx_caller_public_key())?;
state::get_endorser(&caller_pk).map(|kei| match kei {
state::KeyEndorsementInfo { rak: Some(rak), .. } => rak.into(),
_ => caller_pk,
})
}
fn get_origin_registration(app: app_id::AppId) -> Option<types::Registration> {
Self::get_origin_rak()
.and_then(|rak| state::get_registration(app, &rak.try_into().unwrap()))
}
fn is_authorized_origin(app: app_id::AppId) -> bool {
Self::get_origin_registration(app).is_some()
}
fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result<types::Registration, Error> {
state::get_registration(app, &rak.try_into().map_err(|_| Error::InvalidArgument)?)
.ok_or(Error::UnknownInstance)
}
fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error> {
state::get_app(id).ok_or(Error::UnknownApp)
}
fn get_instances(id: app_id::AppId) -> Result<Vec<types::Registration>, Error> {
Ok(state::get_registrations_for_app(id))
}
}
#[sdk_derive(Module)]
impl<Cfg: Config> Module<Cfg> {
const NAME: &'static str = MODULE_NAME;
type Error = Error;
type Event = Event;
type Parameters = Parameters;
type Genesis = Genesis;
#[migration(init)]
fn init(genesis: Genesis) {
genesis
.parameters
.validate_basic()
.expect("invalid genesis parameters");
Self::set_params(genesis.parameters);
for cfg in genesis.apps {
if state::get_app(cfg.id).is_some() {
panic!("duplicate application in genesis: {:?}", cfg.id);
}
state::set_app(cfg);
}
}
#[handler(call = "rofl.Create")]
fn tx_create<C: Context>(ctx: &C, body: types::Create) -> Result<app_id::AppId, Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_CREATE)?;
if CurrentState::with_env(|env| env.is_check_only()) {
return Ok(Default::default());
}
let (creator, tx_index) =
CurrentState::with_env(|env| (env.tx_caller_address(), env.tx_index()));
let app_id = match body.scheme {
types::IdentifierScheme::CreatorRoundIndex => app_id::AppId::from_creator_round_index(
creator,
ctx.runtime_header().round,
tx_index.try_into().map_err(|_| Error::InvalidArgument)?,
),
types::IdentifierScheme::CreatorNonce => {
let nonce = <C::Runtime as Runtime>::Accounts::get_nonce(creator)?;
app_id::AppId::from_creator_nonce(creator, nonce)
}
};
if state::get_app(app_id).is_some() {
return Err(Error::AppAlreadyExists);
}
<C::Runtime as Runtime>::Accounts::transfer(
creator,
*ADDRESS_APP_STAKE_POOL,
&Cfg::STAKE_APP_CREATE,
)?;
let cfg = types::AppConfig {
id: app_id,
policy: body.policy,
admin: Some(creator),
stake: Cfg::STAKE_APP_CREATE,
};
state::set_app(cfg);
CurrentState::with(|state| state.emit_event(Event::AppCreated { id: app_id }));
Ok(app_id)
}
fn ensure_caller_is_admin(cfg: &types::AppConfig) -> Result<(), Error> {
let caller = CurrentState::with_env(|env| env.tx_caller_address());
if cfg.admin != Some(caller) {
return Err(Error::Forbidden);
}
Ok(())
}
#[handler(call = "rofl.Update")]
fn tx_update<C: Context>(ctx: &C, body: types::Update) -> Result<(), Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_UPDATE)?;
let mut cfg = state::get_app(body.id).ok_or(Error::UnknownApp)?;
Self::ensure_caller_is_admin(&cfg)?;
if CurrentState::with_env(|env| env.is_check_only()) {
return Ok(());
}
if cfg.policy == body.policy && cfg.admin == body.admin {
return Ok(());
}
cfg.policy = body.policy;
cfg.admin = body.admin;
state::set_app(cfg);
CurrentState::with(|state| state.emit_event(Event::AppUpdated { id: body.id }));
Ok(())
}
#[handler(call = "rofl.Remove")]
fn tx_remove<C: Context>(ctx: &C, body: types::Remove) -> Result<(), Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_REMOVE)?;
let cfg = state::get_app(body.id).ok_or(Error::UnknownApp)?;
Self::ensure_caller_is_admin(&cfg)?;
if CurrentState::with_env(|env| env.is_check_only()) {
return Ok(());
}
state::remove_app(body.id);
if let Some(admin) = cfg.admin {
<C::Runtime as Runtime>::Accounts::transfer(
*ADDRESS_APP_STAKE_POOL,
admin,
&cfg.stake,
)?;
}
CurrentState::with(|state| state.emit_event(Event::AppRemoved { id: body.id }));
Ok(())
}
#[handler(call = "rofl.Register")]
fn tx_register<C: Context>(ctx: &C, body: types::Register) -> Result<(), Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_REGISTER)?;
if body.expiration <= ctx.epoch() {
return Err(Error::RegistrationExpired);
}
let cfg = state::get_app(body.app).ok_or(Error::UnknownApp)?;
if body.expiration - ctx.epoch() > cfg.policy.max_expiration {
return Err(Error::InvalidArgument);
}
let signer_pks: BTreeSet<PublicKey> = CurrentState::with_env(|env| {
env.tx_auth_info()
.signer_info
.iter()
.filter_map(|si| si.address_spec.public_key())
.collect()
});
if !signer_pks.contains(&body.ect.capability_tee.rak.into()) {
return Err(Error::NotSignedByRAK);
}
for extra_pk in &body.extra_keys {
if !signer_pks.contains(extra_pk) {
return Err(Error::NotSignedByExtraKey);
}
}
if CurrentState::with_env(|env| env.is_check_only()) {
return Ok(());
}
let verified_ect = body
.ect
.verify(&cfg.policy.quotes)
.map_err(|_| Error::InvalidArgument)?;
if !cfg
.policy
.enclaves
.contains(&verified_ect.verified_attestation.quote.identity)
{
return Err(Error::UnknownEnclave);
}
let node = Self::verify_endorsement(ctx, &cfg.policy, &verified_ect)?;
let registration = types::Registration {
app: body.app,
node_id: verified_ect.node_id.unwrap(), entity_id: node.map(|n| n.entity_id),
rak: body.ect.capability_tee.rak,
rek: body.ect.capability_tee.rek.ok_or(Error::InvalidArgument)?, expiration: body.expiration,
extra_keys: body.extra_keys,
};
state::update_registration(registration)?;
Ok(())
}
fn verify_endorsement<C: Context>(
ctx: &C,
app_policy: &policy::AppAuthPolicy,
ect: &VerifiedEndorsedCapabilityTEE,
) -> Result<Option<Node>, Error> {
use policy::AllowedEndorsement;
let endorsing_node_id = ect.node_id.ok_or(Error::UnknownNode)?;
let maybe_node = || -> Result<Option<Node>, Error> {
let registry = RegistryImmutableState::new(ctx.consensus_state());
let node = registry
.node(&endorsing_node_id)
.map_err(|_| Error::UnknownNode)?;
let node = if let Some(node) = node {
node
} else {
return Ok(None);
};
if node.expiration < ctx.epoch() {
return Ok(None);
}
Ok(Some(node))
}()?;
let has_runtime = |node: &Node| -> bool {
let version = &<C::Runtime as Runtime>::VERSION;
node.get_runtime(ctx.runtime_id(), version).is_some()
};
for allowed in &app_policy.endorsements {
match (allowed, &maybe_node) {
(AllowedEndorsement::Any, _) => {
return Ok(maybe_node);
}
(AllowedEndorsement::ComputeRole, Some(node)) => {
if node.has_roles(RolesMask::ROLE_COMPUTE_WORKER) && has_runtime(node) {
return Ok(maybe_node);
}
}
(AllowedEndorsement::ObserverRole, Some(node)) => {
if node.has_roles(RolesMask::ROLE_OBSERVER) && has_runtime(node) {
return Ok(maybe_node);
}
}
(AllowedEndorsement::Entity(entity_id), Some(node)) => {
if &node.entity_id == entity_id {
return Ok(maybe_node);
}
}
(AllowedEndorsement::Node(node_id), _) => {
if endorsing_node_id == *node_id {
return Ok(maybe_node);
}
}
_ => continue,
}
}
Err(Error::NodeNotAllowed)
}
#[handler(call = "rofl.IsAuthorizedOrigin", internal)]
fn internal_is_authorized_origin<C: Context>(
_ctx: &C,
app: app_id::AppId,
) -> Result<bool, Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_IS_AUTHORIZED_ORIGIN)?;
Ok(Self::is_authorized_origin(app))
}
#[handler(call = "rofl.AuthorizedOriginNode", internal)]
fn internal_authorized_origin_node<C: Context>(
_ctx: &C,
app: app_id::AppId,
) -> Result<CorePublicKey, Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_AUTHORIZED_ORIGIN_NODE)?;
let registration = Self::get_origin_registration(app).ok_or(Error::UnknownInstance)?;
Ok(registration.node_id)
}
#[handler(call = "rofl.AuthorizedOriginEntity", internal)]
fn internal_authorized_origin_entity<C: Context>(
_ctx: &C,
app: app_id::AppId,
) -> Result<Option<CorePublicKey>, Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_AUTHORIZED_ORIGIN_ENTITY)?;
let registration = Self::get_origin_registration(app).ok_or(Error::UnknownInstance)?;
Ok(registration.entity_id)
}
#[handler(query = "rofl.App")]
fn query_app<C: Context>(_ctx: &C, args: types::AppQuery) -> Result<types::AppConfig, Error> {
Self::get_app(args.id)
}
#[handler(query = "rofl.AppInstance")]
fn query_app_instance<C: Context>(
_ctx: &C,
args: types::AppInstanceQuery,
) -> Result<types::Registration, Error> {
Self::get_registration(args.app, args.rak)
}
#[handler(query = "rofl.AppInstances", expensive)]
fn query_app_instances<C: Context>(
_ctx: &C,
args: types::AppQuery,
) -> Result<Vec<types::Registration>, Error> {
Self::get_instances(args.id)
}
#[handler(query = "rofl.StakeThresholds")]
fn query_stake_thresholds<C: Context>(
_ctx: &C,
_args: (),
) -> Result<types::StakeThresholds, Error> {
Ok(types::StakeThresholds {
app_create: Cfg::STAKE_APP_CREATE,
})
}
#[handler(call = "rofl.StakeThresholds", internal)]
fn internal_query_stake_thresholds<C: Context>(
ctx: &C,
_args: (),
) -> Result<types::StakeThresholds, Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_STAKE_THRESHOLDS)?;
Ok(types::StakeThresholds {
app_create: Cfg::STAKE_APP_CREATE,
})
}
fn resolve_payer_from_tx<C: Context>(
ctx: &C,
tx: &Transaction,
app_policy: &policy::AppAuthPolicy,
) -> Result<Option<Address>, anyhow::Error> {
let caller_pk = tx
.auth_info
.signer_info
.first()
.and_then(|si| si.address_spec.public_key());
match tx.call.method.as_str() {
"rofl.Register" => {
let body: types::Register = cbor::from_value(tx.call.body.clone())?;
if body.expiration <= ctx.epoch() {
return Err(Error::RegistrationExpired.into());
}
let caller_pk = caller_pk.ok_or(Error::NotSignedByRAK)?;
if caller_pk != body.ect.capability_tee.rak {
return Err(Error::NotSignedByRAK.into());
}
body.ect.verify_endorsement()?;
let node_id = body.ect.node_endorsement.public_key;
let payer = Address::from_consensus_pk(&node_id);
Ok(Some(payer))
}
_ => {
let caller_pk = match caller_pk {
Some(pk) => pk,
None => return Ok(None),
};
Ok(state::get_endorser(&caller_pk)
.map(|ei| Address::from_consensus_pk(&ei.node_id)))
}
}
}
}
impl<Cfg: Config> module::FeeProxyHandler for Module<Cfg> {
fn resolve_payer<C: Context>(
ctx: &C,
tx: &Transaction,
) -> Result<Option<Address>, modules::core::Error> {
use policy::FeePolicy;
let proxy = if let Some(ref proxy) = tx.auth_info.fee.proxy {
proxy
} else {
return Ok(None);
};
if proxy.module != MODULE_NAME {
return Ok(None);
}
let app_id = app_id::AppId::try_from(proxy.id.as_slice())
.map_err(|err| modules::core::Error::InvalidArgument(err.into()))?;
let app_policy = state::get_app(app_id).map(|cfg| cfg.policy).ok_or(
modules::core::Error::InvalidArgument(Error::UnknownApp.into()),
)?;
match app_policy.fees {
FeePolicy::InstancePays => {
Ok(None)
}
FeePolicy::EndorsingNodePays => Self::resolve_payer_from_tx(ctx, tx, &app_policy)
.map_err(modules::core::Error::InvalidArgument),
}
}
}
impl<Cfg: Config> module::TransactionHandler for Module<Cfg> {}
impl<Cfg: Config> module::BlockHandler for Module<Cfg> {
fn end_block<C: Context>(ctx: &C) {
if !<C::Runtime as Runtime>::Core::has_epoch_changed() {
return;
}
state::expire_registrations(ctx.epoch(), 128);
}
}
impl<Cfg: Config> module::InvariantHandler for Module<Cfg> {}