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 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    // parameters
179    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        // Check if epoch is within range
189        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        // Calculate the destination partition number
198        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    /// This method will shift the start partition by 1, considering the partition range as a buffer.
211    /// Protocol-specific implementation is within transaction executor.
212    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 // Targeted epoch duration: 5 mins
273            / 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}