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 alloc::{format, vec::Vec};
6use codec::{Decode, Encode};
7use scale_decode::{
8 IntoVisitor, TypeResolver, Visitor,
9 ext::scale_type_resolver,
10 visitor::{TypeIdFor, types::Composite, types::Variant},
11};
12use scale_encode::EncodeAsType;
13
14// Dev note: This and related bits taken from `sp_runtime::generic::Era`
15/// An era to describe the longevity of a transaction.
16#[derive(
17 PartialEq,
18 Default,
19 Eq,
20 Clone,
21 Copy,
22 Debug,
23 serde::Serialize,
24 serde::Deserialize,
25 scale_info::TypeInfo,
26)]
27pub enum Era {
28 /// The transaction is valid forever. The genesis hash must be present in the signed content.
29 #[default]
30 Immortal,
31
32 /// The transaction will expire. Use [`Era::mortal`] to construct this with correct values.
33 ///
34 /// When used on `FRAME`-based runtimes, `period` cannot exceed `BlockHashCount` parameter
35 /// of `system` module.
36 Mortal {
37 /// The number of blocks that the tx will be valid for after the checkpoint block
38 /// hash found in the signer payload.
39 period: u64,
40 /// The phase in the period that this transaction's lifetime begins (and, importantly,
41 /// implies which block hash is included in the signature material). If the `period` is
42 /// greater than 1 << 12, then it will be a factor of the times greater than 1<<12 that
43 /// `period` is.
44 phase: u64,
45 },
46}
47
48// E.g. with period == 4:
49// 0 10 20 30 40
50// 0123456789012345678901234567890123456789012
51// |...|
52// authored -/ \- expiry
53// phase = 1
54// n = Q(current - phase, period) + phase
55impl Era {
56 /// Create a new era based on a period (which should be a power of two between 4 and 65536
57 /// inclusive) and a block number on which it should start (or, for long periods, be shortly
58 /// after the start).
59 ///
60 /// If using `Era` in the context of `FRAME` runtime, make sure that `period`
61 /// does not exceed `BlockHashCount` parameter passed to `system` module, since that
62 /// prunes old blocks and renders transactions immediately invalid.
63 pub fn mortal(period: u64, current: u64) -> Self {
64 let period = period
65 .checked_next_power_of_two()
66 .unwrap_or(1 << 16)
67 .clamp(4, 1 << 16);
68 let phase = current % period;
69 let quantize_factor = (period >> 12).max(1);
70 let quantized_phase = phase / quantize_factor * quantize_factor;
71
72 Self::Mortal {
73 period,
74 phase: quantized_phase,
75 }
76 }
77}
78
79// Both copied from `sp_runtime::generic::Era`; this is the wire interface and so
80// it's really the most important bit here.
81impl codec::Encode for Era {
82 fn encode_to<T: codec::Output + ?Sized>(&self, output: &mut T) {
83 match self {
84 Self::Immortal => output.push_byte(0),
85 Self::Mortal { period, phase } => {
86 let quantize_factor = (*period >> 12).max(1);
87 let encoded = (period.trailing_zeros() - 1).clamp(1, 15) as u16
88 | ((phase / quantize_factor) << 4) as u16;
89 encoded.encode_to(output);
90 }
91 }
92 }
93}
94impl codec::Decode for Era {
95 fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
96 let first = input.read_byte()?;
97 if first == 0 {
98 Ok(Self::Immortal)
99 } else {
100 let encoded = first as u64 + ((input.read_byte()? as u64) << 8);
101 let period = 2 << (encoded % (1 << 4));
102 let quantize_factor = (period >> 12).max(1);
103 let phase = (encoded >> 4) * quantize_factor;
104 if period >= 4 && phase < period {
105 Ok(Self::Mortal { period, phase })
106 } else {
107 Err("Invalid period and phase".into())
108 }
109 }
110 }
111}
112
113/// Define manually how to encode an Era given some type information. Here we
114/// basically check that the type we're targeting is called "Era" and then codec::Encode.
115impl EncodeAsType for Era {
116 fn encode_as_type_to<R: TypeResolver>(
117 &self,
118 type_id: R::TypeId,
119 types: &R,
120 out: &mut Vec<u8>,
121 ) -> Result<(), scale_encode::Error> {
122 // Visit the type to check that it is an Era. This is only a rough check.
123 let visitor = scale_type_resolver::visitor::new((), |_, _| false)
124 .visit_variant(|_, path, _variants| path.last() == Some("Era"));
125
126 let is_era = types
127 .resolve_type(type_id.clone(), visitor)
128 .unwrap_or_default();
129 if !is_era {
130 return Err(scale_encode::Error::custom_string(format!(
131 "Type {type_id:?} is not a valid Era type; expecting either Immortal or MortalX variant"
132 )));
133 }
134
135 // if the type looks valid then just scale encode our Era.
136 self.encode_to(out);
137 Ok(())
138 }
139}
140
141/// Define manually how to decode an Era given some type information. Here we check that the
142/// variant we're decoding is one of the expected Era variants, and that the field is correct if so,
143/// ensuring that this will fail if trying to decode something that isn't an Era.
144pub struct EraVisitor<R>(core::marker::PhantomData<R>);
145
146impl IntoVisitor for Era {
147 type AnyVisitor<R: TypeResolver> = EraVisitor<R>;
148 fn into_visitor<R: TypeResolver>() -> Self::AnyVisitor<R> {
149 EraVisitor(core::marker::PhantomData)
150 }
151}
152
153impl<R: TypeResolver> Visitor for EraVisitor<R> {
154 type Value<'scale, 'resolver> = Era;
155 type Error = scale_decode::Error;
156 type TypeResolver = R;
157
158 // Unwrap any newtype wrappers around the era, eg the CheckMortality extension (which actually
159 // has 2 fields, but scale_info seems to automatically ignore the PhantomData field). This
160 // allows us to decode directly from CheckMortality into Era.
161 fn visit_composite<'scale, 'resolver>(
162 self,
163 value: &mut Composite<'scale, 'resolver, Self::TypeResolver>,
164 _type_id: TypeIdFor<Self>,
165 ) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
166 if value.remaining() != 1 {
167 return Err(scale_decode::Error::custom_string(format!(
168 "Expected any wrapper around Era to have exactly one field, but got {} fields",
169 value.remaining()
170 )));
171 }
172
173 value
174 .decode_item(self)
175 .expect("1 field expected; checked above.")
176 }
177
178 fn visit_variant<'scale, 'resolver>(
179 self,
180 value: &mut Variant<'scale, 'resolver, Self::TypeResolver>,
181 _type_id: TypeIdFor<Self>,
182 ) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
183 let variant = value.name();
184
185 // If the variant is immortal, we know the outcome.
186 if variant == "Immortal" {
187 return Ok(Era::Immortal);
188 }
189
190 // Otherwise, we expect a variant Mortal1..Mortal255 where the number
191 // here is the first byte, and the second byte is conceptually a field of this variant.
192 // This weird encoding is because the Era is compressed to just 1 byte if immortal and
193 // just 2 bytes if mortal.
194 //
195 // Note: We _could_ just assume we'll have 2 bytes to work with and decode the era directly,
196 // but checking the variant names ensures that the thing we think is an Era actually _is_
197 // one, based on the type info for it.
198 let first_byte = variant
199 .strip_prefix("Mortal")
200 .and_then(|s| s.parse::<u8>().ok())
201 .ok_or_else(|| {
202 scale_decode::Error::custom_string(format!(
203 "Expected MortalX variant, but got {variant}"
204 ))
205 })?;
206
207 // We need 1 field in the MortalN variant containing the second byte.
208 let mortal_fields = value.fields();
209 if mortal_fields.remaining() != 1 {
210 return Err(scale_decode::Error::custom_string(format!(
211 "Expected Mortal{} to have one u8 field, but got {} fields",
212 first_byte,
213 mortal_fields.remaining()
214 )));
215 }
216
217 let second_byte = mortal_fields
218 .decode_item(u8::into_visitor())
219 .expect("At least one field should exist; checked above.")
220 .map_err(|e| {
221 scale_decode::Error::custom_string(format!(
222 "Expected mortal variant field to be u8, but: {e}"
223 ))
224 })?;
225
226 // Now that we have both bytes we can decode them into the era using
227 // the same logic as the codec::Decode impl does.
228 Era::decode(&mut &[first_byte, second_byte][..]).map_err(|e| {
229 scale_decode::Error::custom_string(format!(
230 "Failed to codec::Decode Era from Mortal bytes: {e}"
231 ))
232 })
233 }
234}