radix_engine/blueprints/transaction_tracker/
package.rs

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