1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use borsh::{BorshDeserialize, BorshSerialize};
use namada_sdk::address::{self, Address};
use namada_sdk::storage;
use namada_tx_prelude::collections::lazy_map::{
NestedMap, NestedSubKey, SubKey,
};
use namada_tx_prelude::collections::{LazyCollection, LazyMap};
use namada_tx_prelude::storage::KeySeg;
use namada_vp_prelude::collection_validation::{self, LazyCollectionExt};
use proptest::prelude::*;
use proptest::test_runner::Config;
use proptest_state_machine::{
ReferenceStateMachine, StateMachineTest, prop_state_machine,
};
use test_log::test;
use crate::tx::tx_host_env;
use crate::vp::vp_host_env;
prop_state_machine! {
#![proptest_config(Config {
// Instead of the default 256, we only run 5 because otherwise it
// takes too long and it's preferable to crank up the number of
// transitions instead, to allow each case to run for more epochs as
// some issues only manifest once the model progresses further.
// Additionally, more cases will be explored every time this test is
// executed in the CI.
cases: 5,
verbose: 1,
.. Config::default()
})]
#[test]
fn nested_lazy_map_api_state_machine_test(sequential 1..100 => ConcreteLazyMapState);
}
/// Some borsh-serializable type with arbitrary fields to be used inside
/// LazyMap state machine test
#[derive(
Clone,
Debug,
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
PartialOrd,
Ord,
)]
struct TestVal {
x: u64,
y: bool,
}
type KeyOuter = u64;
type KeyMiddle = i32;
type KeyInner = i8;
type NestedTestMap =
NestedMap<KeyOuter, NestedMap<KeyMiddle, LazyMap<KeyInner, TestVal>>>;
type NestedEagerMap =
BTreeMap<KeyOuter, BTreeMap<KeyMiddle, BTreeMap<KeyInner, TestVal>>>;
/// A `StateMachineTest` implemented on this struct manipulates it with
/// `Transition`s, which are also being accumulated into
/// `current_transitions`. It then:
///
/// - checks its state against an in-memory
/// `namada_core::collections::HashMap`
/// - runs validation and checks that the `LazyMap::Action`s reported from
/// validation match with transitions that were applied
///
/// Additionally, one of the transitions is to commit a block and/or
/// transaction, during which the currently accumulated state changes are
/// persisted, or promoted from transaction write log to block's write log.
#[derive(Debug)]
struct ConcreteLazyMapState {
/// Address is used to prefix the storage key of the `lazy_map` in
/// order to simulate a transaction and a validity predicate
/// check from changes on the `lazy_map`
address: Address,
/// In the test, we apply the same transitions on the `lazy_map` as on
/// `eager_map` to check that `lazy_map`'s state is consistent with
/// `eager_map`.
eager_map: NestedEagerMap,
/// Handle to a lazy map with nested lazy collections
lazy_map: NestedTestMap,
/// Valid LazyMap changes in the current transaction
current_transitions: Vec<Transition>,
}
#[derive(Clone, Debug, Default)]
struct AbstractLazyMapState {
/// Valid LazyMap changes in the current transaction
valid_transitions: Vec<Transition>,
/// Valid LazyMap changes committed to storage
committed_transitions: Vec<Transition>,
}
/// Possible transitions that can modify a [`NestedTestMap`].
/// This roughly corresponds to the methods that have `StorageWrite`
/// access and is very similar to [`Action`]
#[derive(Clone, Debug)]
enum Transition {
/// Commit all valid transitions in the current transaction
CommitTx,
/// Commit all valid transitions in the current transaction and also
/// commit the current block
CommitTxAndBlock,
/// Insert a key-val into a [`LazyMap`]
Insert(Key, TestVal),
/// Remove a key-val from a [`LazyMap`]
Remove(Key),
/// Update a value at key from pre to post state in a
/// [`LazyMap`]
Update(Key, TestVal),
}
/// A key for transition
type Key = (KeyOuter, KeyMiddle, KeyInner);
impl ReferenceStateMachine for AbstractLazyMapState {
type State = Self;
type Transition = Transition;
fn init_state() -> BoxedStrategy<Self::State> {
Just(Self::default()).boxed()
}
// Apply a random transition to the state
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition> {
let length = state.len();
if length == 0 {
prop_oneof![
1 => Just(Transition::CommitTx),
1 => Just(Transition::CommitTxAndBlock),
3 => (arb_map_key(), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val))
]
.boxed()
} else {
let keys = state.find_existing_keys();
let arb_existing_map_key =
|| proptest::sample::select(keys.clone());
prop_oneof![
1 => Just(Transition::CommitTx),
1 => Just(Transition::CommitTxAndBlock),
3 => (arb_existing_map_key(), arb_map_val()).prop_map(|(key, val)|
Transition::Update(key, val)),
3 => arb_existing_map_key().prop_map(Transition::Remove),
5 => (arb_map_key().prop_filter(
"insert on non-existing keys only",
move |key| !keys.contains(key)), arb_map_val())
.prop_map(|(key, val)| Transition::Insert(key, val))
]
.boxed()
}
}
fn apply(
mut state: Self::State,
transition: &Self::Transition,
) -> Self::State {
match transition {
Transition::CommitTx | Transition::CommitTxAndBlock => {
let valid_actions_to_commit =
std::mem::take(&mut state.valid_transitions);
state.committed_transitions.extend(valid_actions_to_commit);
}
_ => state.valid_transitions.push(transition.clone()),
}
state
}
fn preconditions(
state: &Self::State,
transition: &Self::Transition,
) -> bool {
let length = state.len();
// Ensure that the remove or update transitions are not applied
// to an empty state
if length == 0
&& matches!(
transition,
Transition::Remove(_) | Transition::Update(_, _)
)
{
return false;
}
match transition {
Transition::Update(key, _) | Transition::Remove(key) => {
let keys = state.find_existing_keys();
// Ensure that the update/remove key is an existing one
keys.contains(key)
}
Transition::Insert(key, _) => {
let keys = state.find_existing_keys();
// Ensure that the insert key is not an existing one
!keys.contains(key)
}
_ => true,
}
}
}
impl StateMachineTest for ConcreteLazyMapState {
type Reference = AbstractLazyMapState;
type SystemUnderTest = Self;
fn init_test(
_initial_state: &<Self::Reference as ReferenceStateMachine>::State,
) -> Self::SystemUnderTest {
// Init transaction env in which we'll be applying the transitions
tx_host_env::init();
// The lazy_map's path must be prefixed by the address to be able
// to trigger a validity predicate on it
let address = address::testing::established_address_1();
tx_host_env::with(|env| env.spawn_accounts([&address]));
let lazy_map_prefix: storage::Key = address.to_db_key().into();
Self {
address,
eager_map: BTreeMap::new(),
lazy_map: NestedTestMap::open(
lazy_map_prefix.push(&"arbitrary".to_string()).unwrap(),
),
current_transitions: vec![],
}
}
fn apply(
mut state: Self::SystemUnderTest,
_ref_state: &<Self::Reference as ReferenceStateMachine>::State,
transition: <Self::Reference as ReferenceStateMachine>::Transition,
) -> Self::SystemUnderTest {
// Apply transitions in transaction env
let ctx = tx_host_env::ctx();
// Persist the transitions in the current tx, or clear previous ones
// if we're committing a tx
match &transition {
Transition::CommitTx | Transition::CommitTxAndBlock => {
state.current_transitions = vec![];
}
_ => {
state.current_transitions.push(transition.clone());
}
}
// Transition application on lazy map and post-conditions:
match &transition {
Transition::CommitTx => {
// commit the tx without committing the block
tx_host_env::with(|env| env.state.commit_tx_batch());
}
Transition::CommitTxAndBlock => {
// commit the tx and the block
tx_host_env::commit_tx_and_block();
}
Transition::Insert(
(key_outer, key_middle, key_inner),
value,
) => {
let inner = state.lazy_map.at(key_outer).at(key_middle);
inner.insert(ctx, *key_inner, value.clone()).unwrap();
// Post-conditions:
let stored_value =
inner.get(ctx, key_inner).unwrap().unwrap();
assert_eq!(
&stored_value, value,
"the new item must be added to the back"
);
state.assert_validation_accepted();
}
Transition::Remove((key_outer, key_middle, key_inner)) => {
let inner = state.lazy_map.at(key_outer).at(key_middle);
let removed =
inner.remove(ctx, key_inner).unwrap().unwrap();
// Post-conditions:
assert_eq!(
&removed,
state
.eager_map
.get(key_outer)
.unwrap()
.get(key_middle)
.unwrap()
.get(key_inner)
.unwrap(),
"removed element matches the value in eager map \
before it's updated"
);
state.assert_validation_accepted();
}
Transition::Update(
(key_outer, key_middle, key_inner),
value,
) => {
let inner = state.lazy_map.at(key_outer).at(key_middle);
let old_val = inner.get(ctx, key_inner).unwrap().unwrap();
inner.insert(ctx, *key_inner, value.clone()).unwrap();
// Post-conditions:
let new_val = inner.get(ctx, key_inner).unwrap().unwrap();
assert_eq!(
&old_val,
state
.eager_map
.get(key_outer)
.unwrap()
.get(key_middle)
.unwrap()
.get(key_inner)
.unwrap(),
"old value must match the value at the same key in \
the eager map before it's updated"
);
assert_eq!(
&new_val, value,
"new value must match that which was passed into the \
Transition::Update"
);
state.assert_validation_accepted();
}
}
// Apply transition in the eager map for comparison
apply_transition_on_eager_map(&mut state.eager_map, &transition);
// Global post-conditions:
// All items in eager map must be present in lazy map
for (key_outer, middle) in state.eager_map.iter() {
for (key_middle, inner) in middle {
for (key_inner, expected_item) in inner {
let got = state
.lazy_map
.at(key_outer)
.at(key_middle)
.get(ctx, key_inner)
.unwrap()
.expect(
"The expected item must be present in lazy map",
);
assert_eq!(
expected_item, &got,
"at key {key_outer}, {key_middle} {key_inner}"
);
}
}
}
// All items in lazy map must be present in eager map
for key_val in state.lazy_map.iter(ctx).unwrap() {
let (
NestedSubKey::Data {
key: key_outer,
nested_sub_key:
NestedSubKey::Data {
key: key_middle,
nested_sub_key: SubKey::Data(key_inner),
},
},
expected_val,
) = key_val.unwrap();
let got = state
.eager_map
.get(&key_outer)
.unwrap()
.get(&key_middle)
.unwrap()
.get(&key_inner)
.expect("The expected item must be present in eager map");
assert_eq!(
&expected_val, got,
"at key {key_outer}, {key_middle} {key_inner})"
);
}
state
}
}
impl AbstractLazyMapState {
/// Find the length of the map from the applied transitions
fn len(&self) -> u64 {
(map_len_diff_from_transitions(self.committed_transitions.iter())
+ map_len_diff_from_transitions(self.valid_transitions.iter()))
.try_into()
.expect(
"It shouldn't be possible to underflow length from all \
transactions applied in abstract state",
)
}
/// Build an eager map from the committed and current transitions
fn eager_map(&self) -> NestedEagerMap {
let mut eager_map = BTreeMap::new();
for transition in &self.committed_transitions {
apply_transition_on_eager_map(&mut eager_map, transition);
}
for transition in &self.valid_transitions {
apply_transition_on_eager_map(&mut eager_map, transition);
}
eager_map
}
/// Find the keys currently present in the map
fn find_existing_keys(&self) -> Vec<Key> {
let outer_map = self.eager_map();
outer_map
.into_iter()
.fold(vec![], |acc, (outer, middle_map)| {
middle_map.into_iter().fold(
acc,
|mut acc, (middle, inner_map)| {
acc.extend(
inner_map
.into_keys()
.map(|inner| (outer, middle, inner)),
);
acc
},
)
})
}
}
/// Find the difference in length of the map from the applied transitions
fn map_len_diff_from_transitions<'a>(
transitions: impl Iterator<Item = &'a Transition>,
) -> i64 {
let mut insert_count: i64 = 0;
let mut remove_count: i64 = 0;
for trans in transitions {
match trans {
Transition::CommitTx
| Transition::CommitTxAndBlock
| Transition::Update(_, _) => {}
Transition::Insert(_, _) => insert_count += 1,
Transition::Remove(_) => remove_count += 1,
}
}
insert_count - remove_count
}
impl ConcreteLazyMapState {
fn assert_validation_accepted(&self) {
// Init the VP env from tx env in which we applied the map
// transitions
let tx_env = tx_host_env::take();
vp_host_env::init_from_tx(self.address.clone(), tx_env, |_| {});
// Simulate a validity predicate run using the lazy map's validation
// helpers
let changed_keys =
vp_host_env::with(|env| env.all_touched_storage_keys());
let mut validation_builder = None;
// Push followed by pop is a no-op, in which case we'd still see the
// changed keys for these actions, but they wouldn't affect the
// validation result and they never get persisted, but we'd still
// them as changed key here. To guard against this case,
// we check that `map_len_from_transitions` is not empty.
let map_len_diff =
map_len_diff_from_transitions(self.current_transitions.iter());
// To help debug validation issues...
dbg!(
&self.current_transitions,
&changed_keys
.iter()
.map(storage::Key::to_string)
.collect::<Vec<_>>()
);
for key in &changed_keys {
let is_sub_key = self
.lazy_map
.accumulate(
vp_host_env::ctx(),
&mut validation_builder,
key,
)
.unwrap();
assert!(
is_sub_key,
"We're only modifying the lazy_map's keys here. Key: \
\"{key}\", map length diff {map_len_diff}"
);
}
if !changed_keys.is_empty() && map_len_diff != 0 {
assert!(
validation_builder.is_some(),
"If some keys were changed, the builder must get filled in"
);
let actions =
NestedTestMap::validate(validation_builder.unwrap())
.unwrap();
let mut actions_to_check = actions.clone();
// Check that every transition has a corresponding action from
// validation. We drop the found actions to check that all
// actions are matched too.
let current_transitions =
normalize_transitions(&self.current_transitions);
for transition in ¤t_transitions {
use collection_validation::lazy_map::Action;
use collection_validation::lazy_map::NestedAction::At;
match transition {
Transition::CommitTx | Transition::CommitTxAndBlock => {
}
Transition::Insert(expected_key, expected_val) => {
for (ix, action) in
actions_to_check.iter().enumerate()
{
if let At(
key_outer,
At(
key_middle,
Action::Insert(key_inner, val),
),
) = action
{
let key =
(*key_outer, *key_middle, *key_inner);
if expected_key == &key
&& expected_val == val
{
actions_to_check.remove(ix);
break;
}
}
}
}
Transition::Remove(expected_key) => {
for (ix, action) in
actions_to_check.iter().enumerate()
{
if let At(
key_outer,
At(
key_middle,
Action::Remove(key_inner, _val),
),
) = action
{
let key =
(*key_outer, *key_middle, *key_inner);
if expected_key == &key {
actions_to_check.remove(ix);
break;
}
}
}
}
Transition::Update(expected_key, value) => {
for (ix, action) in
actions_to_check.iter().enumerate()
{
if let At(
key_outer,
At(
key_middle,
Action::Update {
key: key_inner,
pre: _,
post,
},
),
) = action
{
let key =
(*key_outer, *key_middle, *key_inner);
if expected_key == &key && post == value {
actions_to_check.remove(ix);
break;
}
}
}
}
}
}
assert!(
actions_to_check.is_empty(),
"All the actions reported from validation {actions:#?} \
should have been matched with SM transitions \
{current_transitions:#?}, but these actions didn't \
match: {actions_to_check:#?}",
)
}
// Put the tx_env back before checking the result
tx_host_env::set_from_vp_env(vp_host_env::take());
}
}
/// Generate an arbitrary `TestKey`
fn arb_map_key() -> impl Strategy<Value = (KeyOuter, KeyMiddle, KeyInner)> {
(any::<u64>(), any::<i32>(), any::<i8>())
}
/// Generate an arbitrary `TestVal`
fn arb_map_val() -> impl Strategy<Value = TestVal> {
(any::<u64>(), any::<bool>()).prop_map(|(x, y)| TestVal { x, y })
}
/// Apply `Transition` on an eager `Map`.
fn apply_transition_on_eager_map(
map: &mut NestedEagerMap,
transition: &Transition,
) {
match transition {
Transition::CommitTx | Transition::CommitTxAndBlock => {}
Transition::Insert((key_outer, key_middle, key_inner), value)
| Transition::Update((key_outer, key_middle, key_inner), value) => {
let middle = map.entry(*key_outer).or_default();
let inner = middle.entry(*key_middle).or_default();
inner.insert(*key_inner, value.clone());
}
Transition::Remove((key_outer, key_middle, key_inner)) => {
let middle = map.entry(*key_outer).or_default();
let inner = middle.entry(*key_middle).or_default();
let _popped = inner.remove(key_inner);
}
}
}
/// Normalize transitions:
/// - remove(key) + insert(key, val) -> update(key, val)
/// - insert(key, val) + update(key, new_val) -> insert(key, new_val)
/// - update(key, val) + update(key, new_val) -> update(key, new_val)
///
/// Note that the normalizable transitions pairs do not have to be directly
/// next to each other, but their order does matter.
fn normalize_transitions(transitions: &[Transition]) -> Vec<Transition> {
let mut collapsed = vec![];
'outer: for transition in transitions {
match transition {
Transition::CommitTx
| Transition::CommitTxAndBlock
| Transition::Remove(_) => collapsed.push(transition.clone()),
Transition::Insert(key, val) => {
for (ix, collapsed_transition) in
collapsed.iter().enumerate()
{
if let Transition::Remove(remove_key) =
collapsed_transition
{
if key == remove_key {
// remove(key) + insert(key, val) -> update(key,
// val)
// Replace the Remove with an Update instead of
// inserting the Insert
*collapsed.get_mut(ix).unwrap() =
Transition::Update(*key, val.clone());
continue 'outer;
}
}
}
collapsed.push(transition.clone());
}
Transition::Update(key, value) => {
for (ix, collapsed_transition) in
collapsed.iter().enumerate()
{
if let Transition::Insert(insert_key, _) =
collapsed_transition
{
if key == insert_key {
// insert(key, val) + update(key, new_val) ->
// insert(key, new_val)
// Replace the insert with the new update's
// value instead of inserting it
*collapsed.get_mut(ix).unwrap() =
Transition::Insert(*key, value.clone());
continue 'outer;
}
} else if let Transition::Update(update_key, _) =
collapsed_transition
{
if key == update_key {
// update(key, val) + update(key, new_val) ->
// update(key, new_val)
// Replace the insert with the new update's
// value instead of inserting it
*collapsed.get_mut(ix).unwrap() =
Transition::Update(*key, value.clone());
continue 'outer;
}
}
}
collapsed.push(transition.clone());
}
}
}
collapsed
}
}