oasis_runtime_sdk/modules/consensus/
mod.rs

1//! Consensus module.
2//!
3//! Low level consensus module for communicating with the consensus layer.
4use std::{convert::TryInto, num::NonZeroUsize, str::FromStr, sync::Mutex};
5
6use oasis_runtime_sdk_macros::handler;
7use once_cell::sync::Lazy;
8use thiserror::Error;
9
10use oasis_core_runtime::{
11    common::{namespace::Namespace, versioned::Versioned},
12    consensus::{
13        beacon::EpochTime,
14        roothash::{Message, RoundRoots, StakingMessage},
15        staking,
16        staking::{Account as ConsensusAccount, Delegation as ConsensusDelegation},
17        state::{
18            beacon::ImmutableState as BeaconImmutableState,
19            roothash::ImmutableState as RoothashImmutableState,
20            staking::ImmutableState as StakingImmutableState, StateError,
21        },
22        HEIGHT_LATEST,
23    },
24};
25
26use crate::{
27    context::Context,
28    core::common::crypto::hash::Hash,
29    history, migration, module,
30    module::{Module as _, Parameters as _},
31    modules,
32    modules::core::API as _,
33    sdk_derive,
34    state::CurrentState,
35    types::{
36        address::{Address, SignatureAddressSpec},
37        message::MessageEventHookInvocation,
38        token,
39        transaction::{AddressSpec, CallerAddress},
40    },
41    Runtime,
42};
43
44#[cfg(test)]
45mod test;
46pub mod types;
47
48/// Unique module name.
49const MODULE_NAME: &str = "consensus";
50
51/// Gas costs.
52#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
53pub struct GasCosts {
54    /// Cost of the internal round_root call.
55    pub round_root: u64,
56}
57
58/// Parameters for the consensus module.
59#[derive(Clone, Debug, PartialEq, Eq, cbor::Encode, cbor::Decode)]
60pub struct Parameters {
61    pub gas_costs: GasCosts,
62
63    pub consensus_denomination: token::Denomination,
64    pub consensus_scaling_factor: u64,
65
66    /// Minimum amount that is allowed to be delegated. This should be greater than or equal to what
67    /// is configured in the consensus layer as the consensus layer will do its own checks.
68    ///
69    /// The amount is in consensus units.
70    pub min_delegate_amount: u128,
71}
72
73impl Default for Parameters {
74    fn default() -> Self {
75        Self {
76            gas_costs: Default::default(),
77            consensus_denomination: token::Denomination::from_str("TEST").unwrap(),
78            consensus_scaling_factor: 1,
79            min_delegate_amount: 0,
80        }
81    }
82}
83
84/// Errors emitted during rewards parameter validation.
85#[derive(Error, Debug)]
86pub enum ParameterValidationError {
87    #[error("consensus scaling factor set to zero")]
88    ZeroScalingFactor,
89
90    #[error("consensus scaling factor is not a power of 10")]
91    ScalingFactorNotPowerOf10,
92}
93
94impl module::Parameters for Parameters {
95    type Error = ParameterValidationError;
96
97    fn validate_basic(&self) -> Result<(), Self::Error> {
98        if self.consensus_scaling_factor == 0 {
99            return Err(ParameterValidationError::ZeroScalingFactor);
100        }
101
102        let log = self.consensus_scaling_factor.ilog10();
103        if 10u64.pow(log) != self.consensus_scaling_factor {
104            return Err(ParameterValidationError::ScalingFactorNotPowerOf10);
105        }
106
107        Ok(())
108    }
109}
110/// Events emitted by the consensus module (none so far).
111#[derive(Debug, cbor::Encode, oasis_runtime_sdk_macros::Event)]
112#[cbor(untagged)]
113pub enum Event {}
114
115/// Genesis state for the consensus module.
116#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
117pub struct Genesis {
118    pub parameters: Parameters,
119}
120
121#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)]
122pub enum Error {
123    #[error("invalid argument")]
124    #[sdk_error(code = 1)]
125    InvalidArgument,
126
127    #[error("invalid denomination")]
128    #[sdk_error(code = 2)]
129    InvalidDenomination,
130
131    #[error("internal state: {0}")]
132    #[sdk_error(code = 3)]
133    InternalStateError(#[from] StateError),
134
135    #[error("core: {0}")]
136    #[sdk_error(transparent)]
137    Core(#[from] modules::core::Error),
138
139    #[error("consensus incompatible signer")]
140    #[sdk_error(code = 4)]
141    ConsensusIncompatibleSigner,
142
143    #[error("amount not representable")]
144    #[sdk_error(code = 5)]
145    AmountNotRepresentable,
146
147    #[error("amount is lower than the minimum delegation amount")]
148    #[sdk_error(code = 6)]
149    UnderMinDelegationAmount,
150
151    #[error("history: {0}")]
152    #[sdk_error(transparent)]
153    History(#[from] history::Error),
154}
155
156/// Interface that can be called from other modules.
157pub trait API {
158    /// Transfer an amount from the runtime account.
159    fn transfer<C: Context>(
160        ctx: &C,
161        to: Address,
162        amount: &token::BaseUnits,
163        hook: MessageEventHookInvocation,
164    ) -> Result<(), Error>;
165
166    /// Withdraw an amount into the runtime account.
167    fn withdraw<C: Context>(
168        ctx: &C,
169        from: Address,
170        amount: &token::BaseUnits,
171        hook: MessageEventHookInvocation,
172    ) -> Result<(), Error>;
173
174    /// Escrow an amount of the runtime account funds.
175    fn escrow<C: Context>(
176        ctx: &C,
177        to: Address,
178        amount: &token::BaseUnits,
179        hook: MessageEventHookInvocation,
180    ) -> Result<(), Error>;
181
182    /// Reclaim an amount of runtime staked shares.
183    fn reclaim_escrow<C: Context>(
184        ctx: &C,
185        from: Address,
186        amount: u128,
187        hook: MessageEventHookInvocation,
188    ) -> Result<(), Error>;
189
190    /// Returns consensus token denomination.
191    fn consensus_denomination() -> Result<token::Denomination, Error>;
192
193    /// Ensures transaction signer is consensus compatible.
194    fn ensure_compatible_tx_signer() -> Result<(), Error>;
195
196    /// Query consensus account info.
197    fn account<C: Context>(ctx: &C, addr: Address) -> Result<ConsensusAccount, Error>;
198
199    /// Query consensus delegation info.
200    fn delegation<C: Context>(
201        ctx: &C,
202        delegator_addr: Address,
203        escrow_addr: Address,
204    ) -> Result<ConsensusDelegation, Error>;
205
206    /// Convert runtime amount to consensus amount, scaling as needed.
207    fn amount_from_consensus<C: Context>(ctx: &C, amount: u128) -> Result<u128, Error>;
208
209    /// Convert consensus amount to runtime amount, scaling as needed.
210    fn amount_to_consensus<C: Context>(ctx: &C, amount: u128) -> Result<u128, Error>;
211
212    /// Determine consensus height corresponding to the given epoch transition. This query may be
213    /// expensive in case the epoch is far back.
214    fn height_for_epoch<C: Context>(ctx: &C, epoch: EpochTime) -> Result<u64, Error>;
215
216    /// Round roots return the round roots for the given runtime ID and round.
217    fn round_roots<C: Context>(
218        ctx: &C,
219        runtime_id: Namespace,
220        round: u64,
221    ) -> Result<Option<RoundRoots>, Error>;
222}
223
224pub struct Module;
225
226impl Module {
227    fn ensure_consensus_denomination(denomination: &token::Denomination) -> Result<(), Error> {
228        if denomination != &Self::consensus_denomination()? {
229            return Err(Error::InvalidDenomination);
230        }
231
232        Ok(())
233    }
234}
235
236#[sdk_derive(Module)]
237impl Module {
238    const NAME: &'static str = MODULE_NAME;
239    const VERSION: u32 = 1;
240    type Error = Error;
241    type Event = Event;
242    type Parameters = Parameters;
243    type Genesis = Genesis;
244
245    #[migration(init)]
246    pub fn init(genesis: Genesis) {
247        // Validate genesis parameters.
248        genesis
249            .parameters
250            .validate_basic()
251            .expect("invalid genesis parameters");
252
253        // Set genesis parameters.
254        Self::set_params(genesis.parameters);
255    }
256
257    #[handler(call = "consensus.RoundRoot", internal)]
258    fn internal_round_root<C: Context>(
259        ctx: &C,
260        body: types::RoundRootBody,
261    ) -> Result<Option<Hash>, Error> {
262        let params = Self::params();
263        <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.round_root)?;
264
265        Ok(
266            Self::round_roots(ctx, body.runtime_id, body.round)?.map(|rr| match body.kind {
267                types::RootKind::IO => rr.io_root,
268                types::RootKind::State => rr.state_root,
269            }),
270        )
271    }
272}
273
274impl API for Module {
275    fn transfer<C: Context>(
276        ctx: &C,
277        to: Address,
278        amount: &token::BaseUnits,
279        hook: MessageEventHookInvocation,
280    ) -> Result<(), Error> {
281        Self::ensure_consensus_denomination(amount.denomination())?;
282        let amount = Self::amount_to_consensus(ctx, amount.amount())?;
283
284        CurrentState::with(|state| {
285            state.emit_message(
286                ctx,
287                Message::Staking(Versioned::new(
288                    0,
289                    StakingMessage::Transfer(staking::Transfer {
290                        to: to.into(),
291                        amount: amount.into(),
292                    }),
293                )),
294                hook,
295            )
296        })?;
297
298        Ok(())
299    }
300
301    fn withdraw<C: Context>(
302        ctx: &C,
303        from: Address,
304        amount: &token::BaseUnits,
305        hook: MessageEventHookInvocation,
306    ) -> Result<(), Error> {
307        Self::ensure_consensus_denomination(amount.denomination())?;
308        let amount = Self::amount_to_consensus(ctx, amount.amount())?;
309
310        CurrentState::with(|state| {
311            state.emit_message(
312                ctx,
313                Message::Staking(Versioned::new(
314                    0,
315                    StakingMessage::Withdraw(staking::Withdraw {
316                        from: from.into(),
317                        amount: amount.into(),
318                    }),
319                )),
320                hook,
321            )
322        })?;
323
324        Ok(())
325    }
326
327    fn escrow<C: Context>(
328        ctx: &C,
329        to: Address,
330        amount: &token::BaseUnits,
331        hook: MessageEventHookInvocation,
332    ) -> Result<(), Error> {
333        Self::ensure_consensus_denomination(amount.denomination())?;
334        let amount = Self::amount_to_consensus(ctx, amount.amount())?;
335
336        if amount < Self::params().min_delegate_amount {
337            return Err(Error::UnderMinDelegationAmount);
338        }
339
340        CurrentState::with(|state| {
341            state.emit_message(
342                ctx,
343                Message::Staking(Versioned::new(
344                    0,
345                    StakingMessage::AddEscrow(staking::Escrow {
346                        account: to.into(),
347                        amount: amount.into(),
348                    }),
349                )),
350                hook,
351            )
352        })?;
353
354        Ok(())
355    }
356
357    fn reclaim_escrow<C: Context>(
358        ctx: &C,
359        from: Address,
360        shares: u128,
361        hook: MessageEventHookInvocation,
362    ) -> Result<(), Error> {
363        CurrentState::with(|state| {
364            state.emit_message(
365                ctx,
366                Message::Staking(Versioned::new(
367                    0,
368                    StakingMessage::ReclaimEscrow(staking::ReclaimEscrow {
369                        account: from.into(),
370                        shares: shares.into(),
371                    }),
372                )),
373                hook,
374            )
375        })?;
376
377        Ok(())
378    }
379
380    fn consensus_denomination() -> Result<token::Denomination, Error> {
381        Ok(Self::params().consensus_denomination)
382    }
383
384    fn ensure_compatible_tx_signer() -> Result<(), Error> {
385        CurrentState::with_env(|env| match env.tx_auth_info().signer_info[0].address_spec {
386            AddressSpec::Signature(SignatureAddressSpec::Ed25519(_)) => Ok(()),
387            AddressSpec::Internal(CallerAddress::Address(_)) if env.is_simulation() => {
388                // During simulations, the caller may be overriden in case of confidential runtimes
389                // which would cause this check to always fail, making gas estimation incorrect.
390                //
391                // Note that this is optimistic as a `CallerAddres::Address(_)` can still be
392                // incompatible, but as long as this is only allowed during simulations it shouldn't
393                // result in any problems.
394                Ok(())
395            }
396            _ => Err(Error::ConsensusIncompatibleSigner),
397        })
398    }
399
400    fn account<C: Context>(ctx: &C, addr: Address) -> Result<ConsensusAccount, Error> {
401        let state = StakingImmutableState::new(ctx.consensus_state());
402        state
403            .account(addr.into())
404            .map_err(Error::InternalStateError)
405    }
406
407    fn delegation<C: Context>(
408        ctx: &C,
409        delegator_addr: Address,
410        escrow_addr: Address,
411    ) -> Result<ConsensusDelegation, Error> {
412        let state = StakingImmutableState::new(ctx.consensus_state());
413        state
414            .delegation(delegator_addr.into(), escrow_addr.into())
415            .map_err(Error::InternalStateError)
416    }
417
418    fn amount_from_consensus<C: Context>(_ctx: &C, amount: u128) -> Result<u128, Error> {
419        let scaling_factor = Self::params().consensus_scaling_factor;
420        amount
421            .checked_mul(scaling_factor.into())
422            .ok_or(Error::AmountNotRepresentable)
423    }
424
425    fn amount_to_consensus<C: Context>(_ctx: &C, amount: u128) -> Result<u128, Error> {
426        let scaling_factor = Self::params().consensus_scaling_factor;
427        let scaled = amount
428            .checked_div(scaling_factor.into())
429            .ok_or(Error::AmountNotRepresentable)?;
430
431        // Ensure there is no remainder as that is not representable in the consensus layer.
432        let remainder = amount
433            .checked_rem(scaling_factor.into())
434            .ok_or(Error::AmountNotRepresentable)?;
435        if remainder != 0 {
436            return Err(Error::AmountNotRepresentable);
437        }
438
439        Ok(scaled)
440    }
441
442    fn height_for_epoch<C: Context>(ctx: &C, epoch: EpochTime) -> Result<u64, Error> {
443        static HEIGHT_CACHE: Lazy<Mutex<lru::LruCache<EpochTime, u64>>> =
444            Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(128).unwrap())));
445
446        // Check the cache first to avoid more expensive traversals.
447        let mut cache = HEIGHT_CACHE.lock().unwrap();
448        if let Some(height) = cache.get(&epoch) {
449            return Ok(*height);
450        }
451
452        // Resolve height for the given epoch.
453        let mut height = HEIGHT_LATEST;
454        loop {
455            let state = ctx.history().consensus_state_at(height)?;
456
457            let beacon = BeaconImmutableState::new(&state);
458
459            let mut epoch_state = beacon.future_epoch_state()?;
460            if epoch_state.height > TryInto::<i64>::try_into(state.height()).unwrap() {
461                // Use current epoch if future epoch is in the future.
462                epoch_state = beacon.epoch_state().unwrap();
463            }
464            height = epoch_state.height.try_into().unwrap();
465
466            // Cache height for later queries.
467            cache.put(epoch_state.epoch, height);
468
469            if epoch_state.epoch == epoch {
470                return Ok(height);
471            }
472
473            assert!(epoch_state.epoch > epoch);
474            assert!(height > 1);
475
476            // Go one height before epoch transition.
477            height -= 1;
478        }
479    }
480
481    fn round_roots<C: Context>(
482        ctx: &C,
483        runtime_id: Namespace,
484        round: u64,
485    ) -> Result<Option<RoundRoots>, Error> {
486        let roothash = RoothashImmutableState::new(ctx.consensus_state());
487        roothash
488            .round_roots(runtime_id, round)
489            .map_err(Error::InternalStateError)
490    }
491}
492
493impl module::TransactionHandler for Module {}
494
495impl module::BlockHandler for Module {}
496
497impl module::InvariantHandler for Module {}