1use 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
48const MODULE_NAME: &str = "consensus";
50
51#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
53pub struct GasCosts {
54 pub round_root: u64,
56}
57
58#[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 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#[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#[derive(Debug, cbor::Encode, oasis_runtime_sdk_macros::Event)]
112#[cbor(untagged)]
113pub enum Event {}
114
115#[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
156pub trait API {
158 fn transfer<C: Context>(
160 ctx: &C,
161 to: Address,
162 amount: &token::BaseUnits,
163 hook: MessageEventHookInvocation,
164 ) -> Result<(), Error>;
165
166 fn withdraw<C: Context>(
168 ctx: &C,
169 from: Address,
170 amount: &token::BaseUnits,
171 hook: MessageEventHookInvocation,
172 ) -> Result<(), Error>;
173
174 fn escrow<C: Context>(
176 ctx: &C,
177 to: Address,
178 amount: &token::BaseUnits,
179 hook: MessageEventHookInvocation,
180 ) -> Result<(), Error>;
181
182 fn reclaim_escrow<C: Context>(
184 ctx: &C,
185 from: Address,
186 amount: u128,
187 hook: MessageEventHookInvocation,
188 ) -> Result<(), Error>;
189
190 fn consensus_denomination() -> Result<token::Denomination, Error>;
192
193 fn ensure_compatible_tx_signer() -> Result<(), Error>;
195
196 fn account<C: Context>(ctx: &C, addr: Address) -> Result<ConsensusAccount, Error>;
198
199 fn delegation<C: Context>(
201 ctx: &C,
202 delegator_addr: Address,
203 escrow_addr: Address,
204 ) -> Result<ConsensusDelegation, Error>;
205
206 fn amount_from_consensus<C: Context>(ctx: &C, amount: u128) -> Result<u128, Error>;
208
209 fn amount_to_consensus<C: Context>(ctx: &C, amount: u128) -> Result<u128, Error>;
211
212 fn height_for_epoch<C: Context>(ctx: &C, epoch: EpochTime) -> Result<u64, Error>;
215
216 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 genesis
249 .parameters
250 .validate_basic()
251 .expect("invalid genesis parameters");
252
253 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 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 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 let mut cache = HEIGHT_CACHE.lock().unwrap();
448 if let Some(height) = cache.get(&epoch) {
449 return Ok(*height);
450 }
451
452 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 epoch_state = beacon.epoch_state().unwrap();
463 }
464 height = epoch_state.height.try_into().unwrap();
465
466 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 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 {}