subxt_core/utils/era.rs
1// Copyright 2019-2024 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5use scale_decode::DecodeAsType;
6use scale_encode::EncodeAsType;
7
8// Dev note: This and related bits taken from `sp_runtime::generic::Era`
9/// An era to describe the longevity of a transaction.
10#[derive(
11 PartialEq,
12 Default,
13 Eq,
14 Clone,
15 Copy,
16 Debug,
17 serde::Serialize,
18 serde::Deserialize,
19 DecodeAsType,
20 EncodeAsType,
21 scale_info::TypeInfo,
22)]
23pub enum Era {
24 /// The transaction is valid forever. The genesis hash must be present in the signed content.
25 #[default]
26 Immortal,
27
28 /// The transaction will expire. Use [`Era::mortal`] to construct this with correct values.
29 ///
30 /// When used on `FRAME`-based runtimes, `period` cannot exceed `BlockHashCount` parameter
31 /// of `system` module.
32 Mortal {
33 /// The number of blocks that the tx will be valid for after the checkpoint block
34 /// hash found in the signer payload.
35 period: u64,
36 /// The phase in the period that this transaction's lifetime begins (and, importantly,
37 /// implies which block hash is included in the signature material). If the `period` is
38 /// greater than 1 << 12, then it will be a factor of the times greater than 1<<12 that
39 /// `period` is.
40 phase: u64,
41 },
42}
43
44// E.g. with period == 4:
45// 0 10 20 30 40
46// 0123456789012345678901234567890123456789012
47// |...|
48// authored -/ \- expiry
49// phase = 1
50// n = Q(current - phase, period) + phase
51impl Era {
52 /// Create a new era based on a period (which should be a power of two between 4 and 65536
53 /// inclusive) and a block number on which it should start (or, for long periods, be shortly
54 /// after the start).
55 ///
56 /// If using `Era` in the context of `FRAME` runtime, make sure that `period`
57 /// does not exceed `BlockHashCount` parameter passed to `system` module, since that
58 /// prunes old blocks and renders transactions immediately invalid.
59 pub fn mortal(period: u64, current: u64) -> Self {
60 let period = period
61 .checked_next_power_of_two()
62 .unwrap_or(1 << 16)
63 .clamp(4, 1 << 16);
64 let phase = current % period;
65 let quantize_factor = (period >> 12).max(1);
66 let quantized_phase = phase / quantize_factor * quantize_factor;
67
68 Self::Mortal {
69 period,
70 phase: quantized_phase,
71 }
72 }
73}
74
75// Both copied from `sp_runtime::generic::Era`; this is the wire interface and so
76// it's really the most important bit here.
77impl codec::Encode for Era {
78 fn encode_to<T: codec::Output + ?Sized>(&self, output: &mut T) {
79 match self {
80 Self::Immortal => output.push_byte(0),
81 Self::Mortal { period, phase } => {
82 let quantize_factor = (*period >> 12).max(1);
83 let encoded = (period.trailing_zeros() - 1).clamp(1, 15) as u16
84 | ((phase / quantize_factor) << 4) as u16;
85 encoded.encode_to(output);
86 }
87 }
88 }
89}
90impl codec::Decode for Era {
91 fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
92 let first = input.read_byte()?;
93 if first == 0 {
94 Ok(Self::Immortal)
95 } else {
96 let encoded = first as u64 + ((input.read_byte()? as u64) << 8);
97 let period = 2 << (encoded % (1 << 4));
98 let quantize_factor = (period >> 12).max(1);
99 let phase = (encoded >> 4) * quantize_factor;
100 if period >= 4 && phase < period {
101 Ok(Self::Mortal { period, phase })
102 } else {
103 Err("Invalid period and phase".into())
104 }
105 }
106 }
107}