1use std::{
3 collections::{BTreeMap, BTreeSet},
4 convert::{TryFrom, TryInto},
5 fmt::Display,
6};
7
8use anyhow::anyhow;
9use oasis_runtime_sdk_macros::{handler, sdk_derive};
10use thiserror::Error;
11
12use crate::{
13 callformat,
14 context::Context,
15 core::consensus::beacon::EpochTime,
16 dispatcher,
17 error::Error as SDKError,
18 keymanager, migration,
19 module::{
20 self, CallResult, InvariantHandler as _, MethodHandler as _, Module as _,
21 ModuleInfoHandler as _,
22 },
23 sender::SenderMeta,
24 state::{CurrentState, Mode, Options, TransactionWithMeta},
25 storage::{self},
26 types::{
27 token::{self, Denomination},
28 transaction::{
29 self, AddressSpec, AuthProof, Call, CallFormat, CallerAddress, SignerInfo, Transaction,
30 UnverifiedTransaction,
31 },
32 },
33 Runtime,
34};
35
36use self::types::RuntimeInfoResponse;
37
38#[cfg(test)]
39mod test;
40pub mod types;
41
42pub const MODULE_NAME: &str = "core";
44
45#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)]
47pub enum Error {
48 #[error("malformed transaction: {0}")]
49 #[sdk_error(code = 1)]
50 MalformedTransaction(#[source] anyhow::Error),
51
52 #[error("invalid transaction: {0}")]
53 #[sdk_error(code = 2)]
54 InvalidTransaction(#[from] transaction::Error),
55
56 #[error("invalid method: {0}")]
57 #[sdk_error(code = 3)]
58 InvalidMethod(String),
59
60 #[error("invalid nonce")]
61 #[sdk_error(code = 4)]
62 InvalidNonce,
63
64 #[error("insufficient balance to pay fees")]
65 #[sdk_error(code = 5)]
66 InsufficientFeeBalance,
67
68 #[error("out of message slots")]
69 #[sdk_error(code = 6)]
70 OutOfMessageSlots,
71
72 #[error("message handler not invoked")]
73 #[sdk_error(code = 8)]
74 MessageHandlerNotInvoked,
75
76 #[error("missing message handler")]
77 #[sdk_error(code = 9)]
78 MessageHandlerMissing(u32),
79
80 #[error("invalid argument: {0}")]
81 #[sdk_error(code = 10)]
82 InvalidArgument(#[source] anyhow::Error),
83
84 #[error("gas overflow")]
85 #[sdk_error(code = 11)]
86 GasOverflow,
87
88 #[error("out of gas (limit: {0} wanted: {1})")]
89 #[sdk_error(code = 12)]
90 OutOfGas(u64, u64),
91
92 #[error("too many authentication slots")]
93 #[sdk_error(code = 15)]
94 TooManyAuth,
95
96 #[error("multisig too many signers")]
97 #[sdk_error(code = 16)]
98 MultisigTooManySigners,
99
100 #[error("invariant violation: {0}")]
101 #[sdk_error(code = 17)]
102 InvariantViolation(String),
103
104 #[error("invalid call format: {0}")]
105 #[sdk_error(code = 18)]
106 InvalidCallFormat(#[source] anyhow::Error),
107
108 #[error("{0}")]
109 #[sdk_error(transparent, abort)]
110 Abort(#[source] dispatcher::Error),
111
112 #[error("no module could authenticate the transaction")]
113 #[sdk_error(code = 19)]
114 NotAuthenticated,
115
116 #[error("gas price too low")]
117 #[sdk_error(code = 20)]
118 GasPriceTooLow,
119
120 #[error("forbidden in secure build")]
121 #[sdk_error(code = 21)]
122 ForbiddenInSecureBuild,
123
124 #[error("forbidden by node policy")]
125 #[sdk_error(code = 22)]
126 Forbidden,
127
128 #[error("transaction is too large")]
129 #[sdk_error(code = 23)]
130 OversizedTransaction,
131
132 #[error("transaction is expired or not yet valid")]
133 #[sdk_error(code = 24)]
134 ExpiredTransaction,
135
136 #[error("read-only transaction attempted modifications")]
137 #[sdk_error(code = 25)]
138 ReadOnlyTransaction,
139
140 #[error("future nonce")]
141 #[sdk_error(code = 26)]
142 FutureNonce,
143
144 #[error("call depth exceeded (depth: {0} max: {1})")]
145 #[sdk_error(code = 27)]
146 CallDepthExceeded(u16, u16),
147
148 #[error("{0}")]
149 #[sdk_error(transparent)]
150 TxSimulationFailed(#[from] TxSimulationFailure),
151}
152
153impl Error {
154 pub fn out_of_gas<Cfg: Config>(limit: u64, wanted: u64) -> Self {
157 if Cfg::EMIT_GAS_USED_EVENTS {
158 Self::OutOfGas(limit, wanted)
159 } else {
160 Self::OutOfGas(0, 0)
162 }
163 }
164}
165
166#[derive(Error, Debug)]
168pub struct TxSimulationFailure {
169 message: String,
170 module_name: String,
171 code: u32,
172}
173
174impl TxSimulationFailure {
175 pub fn is_error_core_out_of_gas(&self) -> bool {
177 self.module_name == MODULE_NAME && self.code == 12
178 }
179}
180
181impl Display for TxSimulationFailure {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 write!(f, "{}", self.message)
184 }
185}
186
187impl SDKError for TxSimulationFailure {
188 fn module_name(&self) -> &str {
189 &self.module_name
190 }
191
192 fn code(&self) -> u32 {
193 self.code
194 }
195}
196
197impl TryFrom<CallResult> for TxSimulationFailure {
198 type Error = anyhow::Error;
199
200 fn try_from(value: CallResult) -> Result<Self, Self::Error> {
201 match value {
202 CallResult::Failed {
203 module,
204 code,
205 message,
206 } => Ok(TxSimulationFailure {
207 code,
208 module_name: module,
209 message,
210 }),
211 _ => Err(anyhow!("CallResult not Failed")),
212 }
213 }
214}
215
216#[derive(Debug, PartialEq, Eq, cbor::Encode, oasis_runtime_sdk_macros::Event)]
218#[cbor(untagged)]
219pub enum Event {
220 #[sdk_event(code = 1)]
221 GasUsed { amount: u64 },
222}
223
224#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
226pub struct GasCosts {
227 pub tx_byte: u64,
228 pub storage_byte: u64,
229
230 pub auth_signature: u64,
231 pub auth_multisig_signer: u64,
232
233 pub callformat_x25519_deoxysii: u64,
234}
235
236#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
238pub struct DynamicMinGasPrice {
239 pub enabled: bool,
244
245 pub target_block_gas_usage_percentage: u8,
251 pub min_price_max_change_denominator: u8,
257}
258
259#[derive(Error, Debug)]
261pub enum ParameterValidationError {
262 #[error("invalid dynamic target block gas usage percentage (10-100)")]
263 InvalidTargetBlockGasUsagePercentage,
264 #[error("invalid dynamic min price max change denominator (1-50)")]
265 InvalidMinPriceMaxChangeDenominator,
266}
267#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
269pub struct Parameters {
270 pub max_batch_gas: u64,
271 pub max_tx_size: u32,
272 pub max_tx_signers: u32,
273 pub max_multisig_signers: u32,
274 pub gas_costs: GasCosts,
275 pub min_gas_price: BTreeMap<token::Denomination, u128>,
276 pub dynamic_min_gas_price: DynamicMinGasPrice,
277}
278
279impl module::Parameters for Parameters {
280 type Error = ParameterValidationError;
281
282 fn validate_basic(&self) -> Result<(), Self::Error> {
283 let dmgp = &self.dynamic_min_gas_price;
285 if dmgp.enabled {
286 if dmgp.target_block_gas_usage_percentage < 10
287 || dmgp.target_block_gas_usage_percentage > 100
288 {
289 return Err(ParameterValidationError::InvalidTargetBlockGasUsagePercentage);
290 }
291 if dmgp.min_price_max_change_denominator < 1
292 || dmgp.min_price_max_change_denominator > 50
293 {
294 return Err(ParameterValidationError::InvalidMinPriceMaxChangeDenominator);
295 }
296 }
297 Ok(())
298 }
299}
300
301pub trait API {
303 type Config: Config;
305
306 fn use_batch_gas(gas: u64) -> Result<(), Error>;
310
311 fn use_tx_gas(gas: u64) -> Result<(), Error>;
315
316 fn remaining_batch_gas() -> u64;
318
319 fn used_batch_gas() -> u64;
321
322 fn remaining_tx_gas() -> u64;
324
325 fn used_tx_gas() -> u64;
327
328 fn max_batch_gas() -> u64;
330
331 fn min_gas_price(denom: &token::Denomination) -> Option<u128>;
333
334 fn set_priority(priority: u64);
336
337 fn take_priority() -> u64;
339
340 fn set_sender_meta(meta: SenderMeta);
342
343 fn take_sender_meta() -> SenderMeta;
345
346 fn estimate_gas_search_max_iters<C: Context>(ctx: &C) -> u64;
349
350 fn has_epoch_changed() -> bool;
352}
353
354#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
356pub struct Genesis {
357 pub parameters: Parameters,
358}
359
360#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
362pub struct LocalConfig {
363 #[cbor(optional)]
365 pub min_gas_price: BTreeMap<token::Denomination, u128>,
366
367 #[cbor(optional)]
372 pub max_estimated_gas: u64,
373
374 #[cbor(optional)]
380 pub estimate_gas_search_max_iters: u64,
381}
382
383pub mod state {
385 pub const METADATA: &[u8] = &[0x01];
387 pub const MESSAGE_HANDLERS: &[u8] = &[0x02];
389 pub const LAST_EPOCH: &[u8] = &[0x03];
391 pub const DYNAMIC_MIN_GAS_PRICE: &[u8] = &[0x04];
393}
394
395#[allow(clippy::declare_interior_mutable_const)]
397pub trait Config: 'static {
398 const DEFAULT_LOCAL_MIN_GAS_PRICE: once_cell::unsync::Lazy<
401 BTreeMap<token::Denomination, u128>,
402 > = once_cell::unsync::Lazy::new(BTreeMap::new);
403
404 const DEFAULT_LOCAL_ESTIMATE_GAS_SEARCH_MAX_ITERS: u64 = 0;
407
408 const ESTIMATE_GAS_EXTRA_FAIL: once_cell::unsync::Lazy<BTreeMap<&'static str, u64>> =
410 once_cell::unsync::Lazy::new(BTreeMap::new);
411
412 const MIN_GAS_PRICE_EXEMPT_METHODS: once_cell::unsync::Lazy<BTreeSet<&'static str>> =
414 once_cell::unsync::Lazy::new(BTreeSet::new);
415
416 const EMIT_GAS_USED_EVENTS: bool = true;
420
421 const ALLOW_INTERACTIVE_READ_ONLY_TRANSACTIONS: bool = false;
425
426 const GAS_COST_CALL_CALLDATA_PUBLIC_KEY: u64 = 20;
428 const GAS_COST_CALL_CURRENT_EPOCH: u64 = 10;
430}
431
432pub struct Module<Cfg: Config> {
433 _cfg: std::marker::PhantomData<Cfg>,
434}
435
436const CONTEXT_KEY_GAS_USED: &str = "core.GasUsed";
437const CONTEXT_KEY_PRIORITY: &str = "core.Priority";
438const CONTEXT_KEY_SENDER_META: &str = "core.SenderMeta";
439const CONTEXT_KEY_EPOCH_CHANGED: &str = "core.EpochChanged";
440
441impl<Cfg: Config> API for Module<Cfg> {
442 type Config = Cfg;
443
444 fn use_batch_gas(gas: u64) -> Result<(), Error> {
445 if CurrentState::with_env(|env| env.is_check_only()) {
447 return Ok(());
448 }
449 let batch_gas_limit = Self::params().max_batch_gas;
450 let batch_gas_used = Self::used_batch_gas();
451 let batch_new_gas_used = batch_gas_used
455 .checked_add(gas)
456 .ok_or(Error::Abort(dispatcher::Error::BatchOutOfGas))?;
457 if batch_new_gas_used > batch_gas_limit {
458 return Err(Error::Abort(dispatcher::Error::BatchOutOfGas));
459 }
460
461 CurrentState::with(|state| {
462 state
463 .block_value::<u64>(CONTEXT_KEY_GAS_USED)
464 .set(batch_new_gas_used);
465 });
466
467 Ok(())
468 }
469
470 fn use_tx_gas(gas: u64) -> Result<(), Error> {
471 let (gas_limit, gas_used) = CurrentState::with(|state| {
472 (
473 state.env().tx_auth_info().fee.gas,
474 *state.local_value::<u64>(CONTEXT_KEY_GAS_USED).or_default(),
475 )
476 });
477 let new_gas_used = {
478 let sum = gas_used.checked_add(gas).ok_or(Error::GasOverflow)?;
479 if sum > gas_limit {
480 return Err(Error::out_of_gas::<Cfg>(gas_limit, sum));
481 }
482 sum
483 };
484
485 Self::use_batch_gas(gas)?;
486
487 CurrentState::with(|state| {
488 *state.local_value::<u64>(CONTEXT_KEY_GAS_USED).or_default() = new_gas_used;
489 });
490
491 Ok(())
492 }
493
494 fn remaining_batch_gas() -> u64 {
495 let batch_gas_limit = Self::params().max_batch_gas;
496 batch_gas_limit.saturating_sub(Self::used_batch_gas())
497 }
498
499 fn used_batch_gas() -> u64 {
500 CurrentState::with(|state| {
501 state
502 .block_value::<u64>(CONTEXT_KEY_GAS_USED)
503 .get()
504 .cloned()
505 .unwrap_or_default()
506 })
507 }
508
509 fn remaining_tx_gas() -> u64 {
510 let (gas_limit, gas_used) = CurrentState::with(|state| {
511 (
512 state.env().tx_auth_info().fee.gas,
513 *state.local_value::<u64>(CONTEXT_KEY_GAS_USED).or_default(),
514 )
515 });
516 let remaining_tx = gas_limit.saturating_sub(gas_used);
517 let remaining_batch = Self::remaining_batch_gas();
519 std::cmp::min(remaining_tx, remaining_batch)
520 }
521
522 fn used_tx_gas() -> u64 {
523 CurrentState::with(|state| *state.local_value::<u64>(CONTEXT_KEY_GAS_USED).or_default())
524 }
525
526 fn max_batch_gas() -> u64 {
527 Self::params().max_batch_gas
528 }
529
530 fn min_gas_price(denom: &token::Denomination) -> Option<u128> {
531 Self::min_gas_prices().get(denom).copied()
532 }
533
534 fn set_priority(priority: u64) {
535 CurrentState::with(|state| {
536 state.block_value::<u64>(CONTEXT_KEY_PRIORITY).set(priority);
537 })
538 }
539
540 fn take_priority() -> u64 {
541 CurrentState::with(|state| {
542 state
543 .block_value::<u64>(CONTEXT_KEY_PRIORITY)
544 .take()
545 .unwrap_or_default()
546 })
547 }
548
549 fn set_sender_meta(meta: SenderMeta) {
550 CurrentState::with(|state| {
551 state
552 .block_value::<SenderMeta>(CONTEXT_KEY_SENDER_META)
553 .set(meta);
554 });
555 }
556
557 fn take_sender_meta() -> SenderMeta {
558 CurrentState::with(|state| {
559 state
560 .block_value::<SenderMeta>(CONTEXT_KEY_SENDER_META)
561 .take()
562 .unwrap_or_default()
563 })
564 }
565
566 fn estimate_gas_search_max_iters<C: Context>(ctx: &C) -> u64 {
567 ctx.local_config(MODULE_NAME)
568 .as_ref()
569 .map(|cfg: &LocalConfig| cfg.estimate_gas_search_max_iters)
570 .unwrap_or(Cfg::DEFAULT_LOCAL_ESTIMATE_GAS_SEARCH_MAX_ITERS)
571 }
572
573 fn has_epoch_changed() -> bool {
574 CurrentState::with(|state| {
575 *state
576 .block_value(CONTEXT_KEY_EPOCH_CHANGED)
577 .get()
578 .unwrap_or(&false)
579 })
580 }
581}
582
583#[sdk_derive(Module)]
584impl<Cfg: Config> Module<Cfg> {
585 const NAME: &'static str = MODULE_NAME;
586 type Error = Error;
587 type Event = Event;
588 type Parameters = Parameters;
589 type Genesis = Genesis;
590
591 #[migration(init)]
592 pub fn init(genesis: Genesis) {
593 Self::set_params(genesis.parameters);
595 }
596
597 #[handler(query = "core.EstimateGas", allow_private_km)]
602 pub fn query_estimate_gas<C: Context>(
603 ctx: &C,
604 mut args: types::EstimateGasQuery,
605 ) -> Result<u64, Error> {
606 let mut extra_gas = 0u64;
607 if ctx.is_confidential() {
610 args.caller = Some(
611 args.caller
612 .unwrap_or_else(|| {
613 args.tx
614 .auth_info
615 .signer_info
616 .first()
617 .map(|si| si.address_spec.caller_address())
618 .unwrap_or(CallerAddress::Address(Default::default()))
619 })
620 .zeroized(),
621 );
622 args.propagate_failures = false; }
624 args.tx.auth_info.fee.gas = {
626 let local_max_estimated_gas = Self::get_local_max_estimated_gas(ctx);
627 if local_max_estimated_gas == 0 {
628 Self::params().max_batch_gas
629 } else {
630 local_max_estimated_gas
631 }
632 };
633 args.tx.auth_info.fee.amount = token::BaseUnits::native(u64::MAX.into());
634 args.tx.auth_info.fee.consensus_messages = ctx.max_messages();
635 let auth_proofs: Result<_, Error> = args
638 .tx
639 .auth_info
640 .signer_info
641 .iter()
642 .map(|si| match si.address_spec {
643 transaction::AddressSpec::Signature(_) => {
645 Ok(transaction::AuthProof::Signature(vec![0; 64].into()))
646 }
647 transaction::AddressSpec::Multisig(ref cfg) => {
649 Ok(transaction::AuthProof::Multisig(
650 cfg.signers
651 .iter()
652 .map(|_| Some(vec![0; 64].into()))
653 .collect(),
654 ))
655 }
656 transaction::AddressSpec::Internal(_) => Err(Error::MalformedTransaction(anyhow!(
658 "internal address spec used"
659 ))),
660 })
661 .collect();
662 let tx_envelope =
663 transaction::UnverifiedTransaction(cbor::to_vec(args.tx.clone()), auth_proofs?);
664 let tx_size: u32 = cbor::to_vec(tx_envelope)
665 .len()
666 .try_into()
667 .map_err(|_| Error::InvalidArgument(anyhow!("transaction too large")))?;
668 let propagate_failures = args.propagate_failures;
669 let bs_max_iters = Self::estimate_gas_search_max_iters(ctx);
670
671 if let Some(caller) = args.caller.clone() {
673 extra_gas = extra_gas.saturating_add(
676 Self::compute_signature_verification_cost(
677 &Self::params(),
678 &args.tx.auth_info.signer_info,
679 )
680 .unwrap_or_default(),
681 );
682
683 args.tx.auth_info.signer_info = vec![transaction::SignerInfo {
684 address_spec: transaction::AddressSpec::Internal(caller),
685 nonce: args
686 .tx
687 .auth_info
688 .signer_info
689 .first()
690 .map(|si| si.nonce)
691 .unwrap_or_default(),
692 }];
693 }
694
695 #[allow(clippy::borrow_interior_mutable_const)]
697 let extra_gas_fail = *Cfg::ESTIMATE_GAS_EXTRA_FAIL
698 .get(args.tx.call.method.as_str())
699 .unwrap_or(&0);
700
701 let simulate = |tx: &transaction::Transaction, gas: u64, report_failure: bool| {
703 let mut tx = tx.clone();
704 tx.auth_info.fee.gas = gas;
705 let call = tx.call.clone(); CurrentState::with_transaction_opts(
708 Options::new()
709 .with_mode(Mode::Simulate)
710 .with_tx(TransactionWithMeta {
711 data: tx,
712 size: tx_size,
713 index: 0,
714 hash: Default::default(),
715 }),
716 || {
717 let (result, _) = dispatcher::Dispatcher::<C::Runtime>::dispatch_tx_call(
718 ctx,
719 call,
720 &Default::default(),
721 );
722 if !result.is_success() && report_failure {
723 let err: TxSimulationFailure = result.try_into().unwrap(); return Err(Error::TxSimulationFailed(err));
726 }
727 let gas_used = Self::used_batch_gas();
730 if result.is_success() {
731 Ok(gas_used)
732 } else {
733 Ok(gas_used.saturating_add(extra_gas_fail).clamp(0, gas))
734 }
735 },
736 )
737 };
738
739 let (cap, mut lo, mut hi) = (
741 args.tx.auth_info.fee.gas,
742 10_u128,
743 args.tx.auth_info.fee.gas as u128,
744 ); let (mut iters, mut fast_path_tried) = (0, false);
748 let (mut has_succeeded, mut tried_with_max_gas) = (false, false);
753 while (lo + 1 < hi) && iters < bs_max_iters {
754 iters += 1;
755
756 let mid = (hi + lo) / 2;
757 match simulate(&args.tx, mid as u64, true) {
758 Ok(r) => {
759 hi = mid;
761
762 has_succeeded = true;
765
766 if !fast_path_tried && (lo + 1 < hi) {
769 fast_path_tried = true;
770
771 match simulate(&args.tx, r, true) {
774 Ok(_) => hi = r as u128,
775 _ => continue,
776 }
777 match simulate(&args.tx, r - 1, true) {
779 Err(_) => {
780 break;
782 }
783 _ => continue,
784 }
785 }
786 }
787 Err(_) if has_succeeded => {
788 lo = mid
792 }
793 Err(Error::TxSimulationFailed(failure)) if failure.is_error_core_out_of_gas() => {
794 lo = mid
796 }
797 r @ Err(_) => {
798 let mut res = r;
799 if !tried_with_max_gas {
800 tried_with_max_gas = true;
801 res = simulate(&args.tx, cap, true)
807 }
808 match res {
809 Ok(_) => {
810 has_succeeded = true;
811 lo = mid
813 }
814 err if propagate_failures => {
815 return err;
817 }
818 _ => {
819 break;
824 }
825 }
826 }
827 }
828 }
829
830 let result = if hi == Into::<u128>::into(cap) {
832 simulate(&args.tx, cap, propagate_failures)
834 } else {
835 Ok(hi as u64)
836 };
837
838 result.map(|est| est.saturating_add(extra_gas).clamp(0, cap))
840 }
841
842 #[handler(query = "core.CheckInvariants", expensive)]
844 fn query_check_invariants<C: Context>(ctx: &C, _args: ()) -> Result<(), Error> {
845 <C::Runtime as Runtime>::Modules::check_invariants(ctx)
846 }
847
848 fn calldata_public_key_common<C: Context>(
849 ctx: &C,
850 ) -> Result<types::CallDataPublicKeyQueryResponse, Error> {
851 let key_manager = ctx
852 .key_manager()
853 .ok_or_else(|| Error::InvalidArgument(anyhow!("key manager not available")))?;
854 let epoch = ctx.epoch();
855 let public_key = key_manager
856 .get_public_ephemeral_key(callformat::get_key_pair_id(epoch), epoch)
857 .map_err(|err| match err {
858 keymanager::KeyManagerError::InvalidEpoch(..) => {
859 Error::InvalidCallFormat(anyhow!("invalid epoch"))
860 }
861 _ => Error::Abort(err.into()),
862 })?;
863
864 Ok(types::CallDataPublicKeyQueryResponse { public_key, epoch })
865 }
866
867 #[handler(query = "core.CallDataPublicKey")]
869 fn query_calldata_public_key<C: Context>(
870 ctx: &C,
871 _args: (),
872 ) -> Result<types::CallDataPublicKeyQueryResponse, Error> {
873 Self::calldata_public_key_common(ctx)
874 }
875
876 #[handler(call = "core.CallDataPublicKey", internal)]
878 fn internal_calldata_public_key<C: Context>(
879 ctx: &C,
880 _args: (),
881 ) -> Result<types::CallDataPublicKeyQueryResponse, Error> {
882 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_CALLDATA_PUBLIC_KEY)?;
883 Self::calldata_public_key_common(ctx)
884 }
885
886 #[handler(call = "core.CurrentEpoch", internal)]
888 fn internal_current_epoch<C: Context>(ctx: &C, _args: ()) -> Result<u64, Error> {
889 <C::Runtime as Runtime>::Core::use_tx_gas(Cfg::GAS_COST_CALL_CURRENT_EPOCH)?;
890 Ok(ctx.epoch())
891 }
892
893 #[handler(query = "core.MinGasPrice")]
895 fn query_min_gas_price<C: Context>(
896 ctx: &C,
897 _args: (),
898 ) -> Result<BTreeMap<token::Denomination, u128>, Error> {
899 let mut mgp = Self::min_gas_prices();
900
901 for (denom, price) in mgp.iter_mut() {
903 let local_mgp = Self::get_local_min_gas_price(ctx, denom);
904 if local_mgp > *price {
905 *price = local_mgp;
906 }
907 }
908
909 Ok(mgp)
910 }
911
912 #[handler(query = "core.RuntimeInfo")]
914 fn query_runtime_info<C: Context>(ctx: &C, _args: ()) -> Result<RuntimeInfoResponse, Error> {
915 Ok(RuntimeInfoResponse {
916 runtime_version: <C::Runtime as Runtime>::VERSION,
917 state_version: <C::Runtime as Runtime>::STATE_VERSION,
918 modules: <C::Runtime as Runtime>::Modules::module_info(ctx),
919 })
920 }
921
922 #[handler(query = "core.ExecuteReadOnlyTx", expensive, allow_private_km)]
928 fn query_execute_read_only_tx<C: Context>(
929 ctx: &C,
930 args: types::ExecuteReadOnlyTxQuery,
931 ) -> Result<types::ExecuteReadOnlyTxResponse, Error> {
932 if !Cfg::ALLOW_INTERACTIVE_READ_ONLY_TRANSACTIONS {
933 return Err(Error::Forbidden);
934 }
935
936 CurrentState::with_transaction_opts(Options::new().with_mode(Mode::Simulate), || {
937 let tx_size = args
941 .tx
942 .len()
943 .try_into()
944 .map_err(|_| Error::OversizedTransaction)?;
945 let tx = dispatcher::Dispatcher::<C::Runtime>::decode_tx(ctx, &args.tx)?;
946
947 if !tx.call.read_only {
949 return Err(Error::InvalidArgument(anyhow::anyhow!(
950 "only read-only transactions are allowed"
951 )));
952 }
953 if tx.auth_info.not_before.is_none() || tx.auth_info.not_after.is_none() {
955 return Err(Error::InvalidArgument(anyhow::anyhow!(
956 "only read-only transactions with expiry are allowed"
957 )));
958 }
959
960 let (result, _) = dispatcher::Dispatcher::<C::Runtime>::execute_tx_opts(
962 ctx,
963 tx,
964 &dispatcher::DispatchOptions {
965 tx_size,
966 method_authorizer: Some(&|method| {
967 <C::Runtime as Runtime>::Modules::is_allowed_interactive_call(method)
970 && <C::Runtime as Runtime>::is_allowed_interactive_call(method)
971 }),
972 ..Default::default()
973 },
974 )
975 .map_err(|err| Error::InvalidArgument(err.into()))?;
976
977 Ok(types::ExecuteReadOnlyTxResponse { result })
978 })
979 }
980}
981
982impl<Cfg: Config> Module<Cfg> {
983 fn min_gas_prices() -> BTreeMap<Denomination, u128> {
984 let params = Self::params();
985 if params.dynamic_min_gas_price.enabled {
986 CurrentState::with_store(|store| {
987 let store =
988 storage::TypedStore::new(storage::PrefixStore::new(store, &MODULE_NAME));
989 store
990 .get(state::DYNAMIC_MIN_GAS_PRICE)
991 .unwrap_or(params.min_gas_price)
993 })
994 } else {
995 params.min_gas_price
996 }
997 }
998
999 fn get_local_min_gas_price<C: Context>(ctx: &C, denom: &token::Denomination) -> u128 {
1000 #[allow(clippy::borrow_interior_mutable_const)]
1001 ctx.local_config(MODULE_NAME)
1002 .as_ref()
1003 .map(|cfg: &LocalConfig| cfg.min_gas_price.get(denom).copied())
1004 .unwrap_or_else(|| Cfg::DEFAULT_LOCAL_MIN_GAS_PRICE.get(denom).copied())
1005 .unwrap_or_default()
1006 }
1007
1008 fn get_local_max_estimated_gas<C: Context>(ctx: &C) -> u64 {
1009 ctx.local_config(MODULE_NAME)
1010 .as_ref()
1011 .map(|cfg: &LocalConfig| cfg.max_estimated_gas)
1012 .unwrap_or_default()
1013 }
1014
1015 fn enforce_min_gas_price<C: Context>(ctx: &C, call: &Call) -> Result<(), Error> {
1016 #[allow(clippy::borrow_interior_mutable_const)]
1018 if Cfg::MIN_GAS_PRICE_EXEMPT_METHODS.contains(call.method.as_str()) {
1019 return Ok(());
1020 }
1021
1022 let fee = CurrentState::with_env(|env| env.tx_auth_info().fee.clone());
1023 let denom = fee.amount.denomination();
1024
1025 match Self::min_gas_price(denom) {
1026 None => return Err(Error::GasPriceTooLow),
1028
1029 Some(min_gas_price) => {
1031 if CurrentState::with_env(|env| env.is_check_only()) {
1032 let local_mgp = Self::get_local_min_gas_price(ctx, denom);
1033
1034 if fee.gas_price() < local_mgp {
1036 return Err(Error::GasPriceTooLow);
1037 }
1038 }
1039
1040 if fee.gas_price() < min_gas_price {
1041 return Err(Error::GasPriceTooLow);
1042 }
1043 }
1044 }
1045
1046 Ok(())
1047 }
1048
1049 fn compute_signature_verification_cost(
1050 params: &Parameters,
1051 signer_info: &[SignerInfo],
1052 ) -> Option<u64> {
1053 let mut num_signature: u64 = 0;
1054 let mut num_multisig_signer: u64 = 0;
1055 for si in signer_info {
1056 match &si.address_spec {
1057 AddressSpec::Signature(_) => {
1058 num_signature = num_signature.checked_add(1)?;
1059 }
1060 AddressSpec::Multisig(config) => {
1061 num_multisig_signer =
1062 num_multisig_signer.checked_add(config.signers.len() as u64)?;
1063 }
1064 AddressSpec::Internal(_) => {}
1065 }
1066 }
1067
1068 let signature_cost = num_signature.checked_mul(params.gas_costs.auth_signature)?;
1069 let multisig_signer_cost =
1070 num_multisig_signer.checked_mul(params.gas_costs.auth_multisig_signer)?;
1071 let sum = signature_cost.checked_add(multisig_signer_cost)?;
1072
1073 Some(sum)
1074 }
1075}
1076
1077impl<Cfg: Config> module::TransactionHandler for Module<Cfg> {
1078 fn approve_raw_tx<C: Context>(_ctx: &C, tx: &[u8]) -> Result<(), Error> {
1079 let params = Self::params();
1080 if tx.len() > TryInto::<usize>::try_into(params.max_tx_size).unwrap() {
1081 return Err(Error::OversizedTransaction);
1082 }
1083 Ok(())
1084 }
1085
1086 fn approve_unverified_tx<C: Context>(
1087 _ctx: &C,
1088 utx: &UnverifiedTransaction,
1089 ) -> Result<(), Error> {
1090 let params = Self::params();
1091 if utx.1.len() > params.max_tx_signers as usize {
1092 return Err(Error::TooManyAuth);
1093 }
1094 for auth_proof in &utx.1 {
1095 if let AuthProof::Multisig(config) = auth_proof {
1096 if config.len() > params.max_multisig_signers as usize {
1097 return Err(Error::MultisigTooManySigners);
1098 }
1099 }
1100 }
1101 Ok(())
1102 }
1103
1104 fn authenticate_tx<C: Context>(
1105 ctx: &C,
1106 tx: &Transaction,
1107 ) -> Result<module::AuthDecision, Error> {
1108 let round = ctx.runtime_header().round;
1110 if let Some(not_before) = tx.auth_info.not_before {
1111 if round < not_before {
1112 return Err(Error::ExpiredTransaction);
1114 }
1115 }
1116 if let Some(not_after) = tx.auth_info.not_after {
1117 if round > not_after {
1118 return Err(Error::ExpiredTransaction);
1120 }
1121 }
1122
1123 Ok(module::AuthDecision::Continue)
1124 }
1125
1126 fn before_handle_call<C: Context>(ctx: &C, call: &Call) -> Result<(), Error> {
1127 let params = Self::params();
1129 let fee = CurrentState::with_env(|env| env.tx_auth_info().fee.clone());
1130 if fee.gas > params.max_batch_gas {
1131 return Err(Error::GasOverflow);
1132 }
1133 if fee.consensus_messages > ctx.max_messages() {
1134 return Err(Error::OutOfMessageSlots);
1135 }
1136
1137 if CurrentState::with_env(|env| env.is_internal()) {
1139 return Ok(());
1140 }
1141
1142 Self::enforce_min_gas_price(ctx, call)?;
1144
1145 let tx_size = CurrentState::with_env(|env| env.tx_size());
1147 Self::use_tx_gas(
1148 params
1149 .gas_costs
1150 .tx_byte
1151 .checked_mul(tx_size.into())
1152 .ok_or(Error::GasOverflow)?,
1153 )?;
1154
1155 let total = CurrentState::with_env(|env| {
1157 Self::compute_signature_verification_cost(¶ms, &env.tx_auth_info().signer_info)
1158 })
1159 .ok_or(Error::GasOverflow)?;
1160 Self::use_tx_gas(total)?;
1161
1162 match call.format {
1164 CallFormat::Plain => {} CallFormat::EncryptedX25519DeoxysII => {
1166 Self::use_tx_gas(params.gas_costs.callformat_x25519_deoxysii)?
1167 }
1168 }
1169
1170 Ok(())
1171 }
1172
1173 fn after_handle_call<C: Context>(ctx: &C, _result: &module::CallResult) -> Result<(), Error> {
1174 if CurrentState::with_env(|env| env.is_internal()) {
1176 return Ok(());
1177 }
1178
1179 let params = Self::params();
1180
1181 let storage_gas = if params.gas_costs.storage_byte > 0 {
1183 let storage_update_bytes =
1184 CurrentState::with(|state| state.pending_store_update_byte_size());
1185 params
1186 .gas_costs
1187 .storage_byte
1188 .saturating_mul(storage_update_bytes as u64)
1189 } else {
1190 0
1191 };
1192
1193 let message_gas = {
1195 let emitted_message_count =
1196 CurrentState::with(|state| state.emitted_messages_local_count());
1197 let message_gas_cost = params
1200 .max_batch_gas
1201 .checked_div(ctx.max_messages().into())
1202 .unwrap_or(u64::MAX); message_gas_cost.saturating_mul(emitted_message_count as u64)
1204 };
1205
1206 let used_gas = Self::used_tx_gas();
1208 let max_gas = std::cmp::max(used_gas, std::cmp::max(storage_gas, message_gas));
1209
1210 let maybe_out_of_gas = Self::use_tx_gas(max_gas - used_gas); if Cfg::EMIT_GAS_USED_EVENTS {
1217 let used_gas = Self::used_tx_gas();
1218 CurrentState::with(|state| {
1219 state.emit_unconditional_event(Event::GasUsed { amount: used_gas });
1220 });
1221 }
1222
1223 maybe_out_of_gas?;
1225
1226 Ok(())
1227 }
1228}
1229
1230fn min_gas_price_update(
1238 gas_used: u128,
1239 target_gas_used: u128,
1240 min_price_max_change_denominator: u128,
1241 current_price: u128,
1242) -> u128 {
1243 if target_gas_used == 0 || min_price_max_change_denominator == 0 {
1245 return current_price;
1246 }
1247
1248 let delta = (gas_used.max(target_gas_used) - gas_used.min(target_gas_used)).saturating_mul(100)
1250 / target_gas_used;
1251
1252 let price_change =
1254 (current_price.saturating_mul(delta) / 100) / min_price_max_change_denominator;
1255
1256 if gas_used > target_gas_used {
1258 current_price.saturating_add(price_change)
1259 } else {
1260 current_price.saturating_sub(price_change)
1261 }
1262}
1263
1264impl<Cfg: Config> module::BlockHandler for Module<Cfg> {
1265 fn begin_block<C: Context>(ctx: &C) {
1266 CurrentState::with(|state| {
1267 let epoch = ctx.epoch();
1268
1269 let mut store = storage::PrefixStore::new(state.store(), &MODULE_NAME);
1271 let mut tstore = storage::TypedStore::new(&mut store);
1272 let previous_epoch: EpochTime = tstore.get(state::LAST_EPOCH).unwrap_or_default();
1273 if epoch != previous_epoch {
1274 tstore.insert(state::LAST_EPOCH, epoch);
1275 }
1276
1277 state
1279 .block_value(CONTEXT_KEY_EPOCH_CHANGED)
1280 .set(epoch != previous_epoch);
1281 });
1282 }
1283
1284 fn end_block<C: Context>(_ctx: &C) {
1285 let params = Self::params();
1286 if !params.dynamic_min_gas_price.enabled {
1287 return;
1288 }
1289
1290 let gas_used = Self::used_batch_gas() as u128;
1295 let max_batch_gas = Self::max_batch_gas() as u128;
1296 let target_gas_used = max_batch_gas.saturating_mul(
1297 params
1298 .dynamic_min_gas_price
1299 .target_block_gas_usage_percentage as u128,
1300 ) / 100;
1301
1302 let mut mgp = Self::min_gas_prices();
1304 mgp.iter_mut().for_each(|(d, price)| {
1305 let mut new_min_price = min_gas_price_update(
1306 gas_used,
1307 target_gas_used,
1308 params
1309 .dynamic_min_gas_price
1310 .min_price_max_change_denominator as u128,
1311 *price,
1312 );
1313
1314 if let Some(min_price) = params.min_gas_price.get(d) {
1316 if new_min_price < *min_price {
1317 new_min_price = *min_price;
1318 }
1319 }
1320 *price = new_min_price;
1321 });
1322
1323 CurrentState::with_store(|store| {
1325 let mut store = storage::PrefixStore::new(store, &MODULE_NAME);
1326 let mut tstore = storage::TypedStore::new(&mut store);
1327 tstore.insert(state::DYNAMIC_MIN_GAS_PRICE, mgp);
1328 });
1329 }
1330}
1331
1332impl<Cfg: Config> module::InvariantHandler for Module<Cfg> {}