radix_engine/blueprints/transaction_tracker/
package.rs1use crate::errors::{ApplicationError, RuntimeError};
2use crate::internal_prelude::*;
3use radix_blueprint_schema_init::{
4 BlueprintCollectionSchema, BlueprintEventSchemaInit, BlueprintFunctionsSchemaInit, FieldSchema,
5 FunctionSchemaInit, TypeRef,
6};
7use radix_blueprint_schema_init::{BlueprintSchemaInit, BlueprintStateSchemaInit};
8use radix_engine_interface::api::{AttachedModuleId, FieldValue, SystemApi};
9use radix_engine_interface::blueprints::package::{
10 AuthConfig, BlueprintDefinitionInit, BlueprintType, FunctionAuth, MethodAuthTemplate,
11 PackageDefinition,
12};
13use radix_engine_interface::blueprints::transaction_tracker::*;
14use radix_native_sdk::modules::metadata::Metadata;
15use radix_native_sdk::modules::role_assignment::RoleAssignment;
16use radix_native_sdk::runtime::Runtime;
17
18pub use radix_common::prelude::TRANSACTION_TRACKER_BLUEPRINT;
19
20pub type TransactionTrackerCreateOutput = ComponentAddress;
21
22pub struct TransactionTrackerNativePackage;
23
24pub const PARTITION_RANGE_START: u8 = MAIN_BASE_PARTITION.0 + 1;
25pub const PARTITION_RANGE_END: u8 = u8::MAX;
26pub const EPOCHS_PER_PARTITION: u64 = 100;
27
28impl TransactionTrackerNativePackage {
29 pub fn definition() -> PackageDefinition {
30 let mut aggregator = TypeAggregator::<ScryptoCustomTypeKind>::new();
31 let key_type_id = aggregator.add_child_type_and_descendents::<Hash>();
32 let value_type_id = aggregator.add_child_type_and_descendents::<TransactionStatus>();
33
34 let mut collections: Vec<BlueprintCollectionSchema<TypeRef<LocalTypeId>>> = vec![];
35 for _ in PARTITION_RANGE_START..=PARTITION_RANGE_END {
36 collections.push(BlueprintCollectionSchema::KeyValueStore(
37 BlueprintKeyValueSchema {
38 key: TypeRef::Static(key_type_id),
39 value: TypeRef::Static(value_type_id),
40 allow_ownership: false,
41 },
42 ))
43 }
44
45 let mut fields = Vec::new();
46 fields.push(FieldSchema::static_field(
47 aggregator.add_child_type_and_descendents::<TransactionTrackerSubstate>(),
48 ));
49
50 let mut functions = index_map_new();
51 functions.insert(
52 TRANSACTION_TRACKER_CREATE_IDENT.to_string(),
53 FunctionSchemaInit {
54 receiver: None,
55 input: TypeRef::Static(
56 aggregator.add_child_type_and_descendents::<TransactionTrackerCreateInput>(),
57 ),
58 output: TypeRef::Static(
59 aggregator.add_child_type_and_descendents::<TransactionTrackerCreateOutput>(),
60 ),
61 export: TRANSACTION_TRACKER_CREATE_EXPORT_NAME.to_string(),
62 },
63 );
64
65 let schema = generate_full_schema(aggregator);
66 let blueprints = indexmap!(
67 TRANSACTION_TRACKER_BLUEPRINT.to_string() => BlueprintDefinitionInit {
68 blueprint_type: BlueprintType::default(),
69 is_transient: false,
70 dependencies: indexset!(
71 ),
72 feature_set: indexset!(),
73 schema: BlueprintSchemaInit {
74 generics: vec![],
75 schema,
76 state: BlueprintStateSchemaInit {
77 fields,
78 collections,
79 },
80 events: BlueprintEventSchemaInit::default(),
81 types: BlueprintTypeSchemaInit::default(),
82 functions: BlueprintFunctionsSchemaInit {
83 functions,
84 },
85 hooks: BlueprintHooksInit::default(),
86 },
87
88 royalty_config: PackageRoyaltyConfig::default(),
89 auth_config: AuthConfig {
90 function_auth: FunctionAuth::AccessRules(
91 indexmap!(
92 TRANSACTION_TRACKER_CREATE_IDENT.to_string() => rule!(require(system_execution(SystemExecution::Protocol))),
93 )
94 ),
95 method_auth: MethodAuthTemplate::default(),
96 },
97 }
98 );
99
100 PackageDefinition { blueprints }
101 }
102
103 pub fn invoke_export<Y: SystemApi<RuntimeError>>(
104 export_name: &str,
105 input: &IndexedScryptoValue,
106 api: &mut Y,
107 ) -> Result<IndexedScryptoValue, RuntimeError> {
108 match export_name {
109 TRANSACTION_TRACKER_CREATE_EXPORT_NAME => {
110 let input: TransactionTrackerCreateInput = input.as_typed().map_err(|e| {
111 RuntimeError::ApplicationError(ApplicationError::InputDecodeError(e))
112 })?;
113
114 let rtn = TransactionTrackerBlueprint::create(input.address_reservation, api)?;
115
116 Ok(IndexedScryptoValue::from_typed(&rtn))
117 }
118
119 _ => Err(RuntimeError::ApplicationError(
120 ApplicationError::ExportDoesNotExist(export_name.to_string()),
121 )),
122 }
123 }
124}
125
126#[derive(Debug, Clone, ScryptoSbor)]
127pub enum TransactionStatus {
128 V1(TransactionStatusV1),
129}
130
131impl TransactionStatus {
132 pub fn into_v1(self) -> TransactionStatusV1 {
133 match self {
134 TransactionStatus::V1(status) => status,
135 }
136 }
137}
138
139#[derive(Debug, Clone, ScryptoSbor)]
140pub enum TransactionStatusV1 {
141 CommittedSuccess,
142 CommittedFailure,
143 Cancelled,
144}
145
146pub type TransactionStatusSubstateContents = TransactionStatus;
147
148#[derive(Debug, Clone, ScryptoSbor)]
149pub enum TransactionTrackerSubstate {
150 V1(TransactionTrackerSubstateV1),
151}
152
153impl TransactionTrackerSubstate {
154 pub fn v1(&self) -> &TransactionTrackerSubstateV1 {
155 match self {
156 TransactionTrackerSubstate::V1(tracker) => tracker,
157 }
158 }
159
160 pub fn into_v1(self) -> TransactionTrackerSubstateV1 {
161 match self {
162 TransactionTrackerSubstate::V1(tracker) => tracker,
163 }
164 }
165
166 pub fn v1_mut(&mut self) -> &mut TransactionTrackerSubstateV1 {
167 match self {
168 TransactionTrackerSubstate::V1(tracker) => tracker,
169 }
170 }
171}
172
173#[derive(Debug, Clone, ScryptoSbor)]
174pub struct TransactionTrackerSubstateV1 {
175 pub start_epoch: u64,
176 pub start_partition: u8,
177
178 pub partition_range_start_inclusive: u8,
180 pub partition_range_end_inclusive: u8,
181 pub epochs_per_partition: u64,
182}
183
184impl TransactionTrackerSubstateV1 {
185 pub fn partition_for_expiry_epoch(&self, epoch: Epoch) -> Option<u8> {
186 let epoch = epoch.number();
187
188 let num_partitions =
190 self.partition_range_end_inclusive - self.partition_range_start_inclusive + 1;
191 let max_epoch_exclusive =
192 self.start_epoch + num_partitions as u64 * self.epochs_per_partition;
193 if epoch < self.start_epoch || epoch >= max_epoch_exclusive {
194 return None;
195 }
196
197 let mut partition_number =
199 self.start_partition as u64 + (epoch - self.start_epoch) / self.epochs_per_partition;
200 if partition_number > self.partition_range_end_inclusive as u64 {
201 partition_number -= num_partitions as u64;
202 }
203
204 assert!(partition_number >= self.partition_range_start_inclusive as u64);
205 assert!(partition_number <= self.partition_range_end_inclusive as u64);
206
207 Some(partition_number as u8)
208 }
209
210 pub fn advance(&mut self) -> u8 {
213 let old_start_partition = self.start_partition;
214 self.start_epoch += self.epochs_per_partition;
215 self.start_partition = if self.start_partition == self.partition_range_end_inclusive {
216 self.partition_range_start_inclusive
217 } else {
218 self.start_partition + 1
219 };
220 old_start_partition
221 }
222}
223
224pub struct TransactionTrackerBlueprint;
225
226impl TransactionTrackerBlueprint {
227 pub fn create<Y: SystemApi<RuntimeError>>(
228 address_reservation: GlobalAddressReservation,
229 api: &mut Y,
230 ) -> Result<GlobalAddress, RuntimeError> {
231 let current_epoch = Runtime::current_epoch(api)?;
232 let intent_store = api.new_simple_object(
233 TRANSACTION_TRACKER_BLUEPRINT,
234 indexmap!(
235 0u8 => FieldValue::new(&TransactionTrackerSubstate::V1(TransactionTrackerSubstateV1{
236 start_epoch: current_epoch.number(),
237 start_partition: PARTITION_RANGE_START,
238 partition_range_start_inclusive: PARTITION_RANGE_START,
239 partition_range_end_inclusive: PARTITION_RANGE_END,
240 epochs_per_partition: EPOCHS_PER_PARTITION,
241 }))
242 ),
243 )?;
244 let role_assignment = RoleAssignment::create(OwnerRole::None, indexmap!(), api)?.0;
245 let metadata = Metadata::create(api)?;
246
247 let address = api.globalize(
248 intent_store,
249 indexmap!(
250 AttachedModuleId::RoleAssignment => role_assignment.0,
251 AttachedModuleId::Metadata => metadata.0,
252 ),
253 Some(address_reservation),
254 )?;
255 Ok(address)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use radix_transactions::validation::TransactionValidationConfig;
262
263 use super::*;
264
265 #[test]
266 fn calculate_coverage() {
267 let max_epoch_range = TransactionValidationConfig::latest().max_epoch_range;
268 let covered_epochs = (EPOCHS_PER_PARTITION as f64
269 * (PARTITION_RANGE_END as f64 - (PARTITION_RANGE_START as f64 - 1.0) - 1.0))
270 .floor() as u64;
271 let covered_days = covered_epochs
272 * 5 / 60
274 / 24;
275 assert!(covered_epochs >= max_epoch_range);
276 assert_eq!(covered_days, 65);
277 }
278
279 #[test]
280 fn test_partition_calculation() {
281 let mut store = TransactionTrackerSubstate::V1(TransactionTrackerSubstateV1 {
282 start_epoch: 256,
283 start_partition: 70,
284 partition_range_start_inclusive: PARTITION_RANGE_START,
285 partition_range_end_inclusive: PARTITION_RANGE_END,
286 epochs_per_partition: EPOCHS_PER_PARTITION,
287 });
288 let num_partitions = (PARTITION_RANGE_END - PARTITION_RANGE_START + 1) as u64;
289
290 assert_eq!(store.v1().partition_for_expiry_epoch(Epoch::of(0)), None);
291 assert_eq!(
292 store.v1().partition_for_expiry_epoch(Epoch::of(256)),
293 Some(70)
294 );
295 assert_eq!(
296 store
297 .v1()
298 .partition_for_expiry_epoch(Epoch::of(256 + EPOCHS_PER_PARTITION - 1)),
299 Some(70)
300 );
301 assert_eq!(
302 store
303 .v1()
304 .partition_for_expiry_epoch(Epoch::of(256 + EPOCHS_PER_PARTITION)),
305 Some(71)
306 );
307 assert_eq!(
308 store.v1().partition_for_expiry_epoch(Epoch::of(
309 256 + EPOCHS_PER_PARTITION * num_partitions - 1
310 )),
311 Some(69)
312 );
313 assert_eq!(
314 store
315 .v1()
316 .partition_for_expiry_epoch(Epoch::of(256 + EPOCHS_PER_PARTITION * num_partitions)),
317 None,
318 );
319
320 store.v1_mut().advance();
321 assert_eq!(store.v1().start_epoch, 256 + EPOCHS_PER_PARTITION);
322 assert_eq!(store.v1().start_partition, 71);
323 }
324}