Skip to content

Commit

Permalink
added last update enforcement to push and remove operations
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Feb 12, 2025
1 parent 7e27b41 commit c2c7160
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 2 deletions.
1 change: 1 addition & 0 deletions contracts/delegation/dao-vote-delegation/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub const DELEGATIONS: SnapshotVectorMap<Addr, Delegation> = SnapshotVectorMap::
"d__active",
"d__active__checkpoints",
"d__active__changelog",
"d__active__last_update",
);

/// map (delegator, delegate) -> (ID, expiration_block) of the delegation in the
Expand Down
1 change: 1 addition & 0 deletions packages/cw-snapshot-vector-map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ let svm: SnapshotVectorMap<Addr, String> = SnapshotVectorMap::new(
"svm__active",
"svm__active__checkpoints",
"svm__active__changelog",
"svm__active__last_update",
);
let key = Addr::unchecked("leaf");
let first = "first".to_string();
Expand Down
39 changes: 38 additions & 1 deletion packages/cw-snapshot-vector-map/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]

use std::cmp::Ordering;

use serde::de::DeserializeOwned;
use serde::Serialize;

use cosmwasm_std::{StdResult, Storage};
use cosmwasm_std::{StdError, StdResult, Storage};
use cw_storage_plus::{KeyDeserialize, Map, Prefixer, PrimaryKey, SnapshotMap, Strategy};

/// Map to a vector that allows reading the subset of items that existed at a
Expand All @@ -17,6 +19,9 @@ pub struct SnapshotVectorMap<'a, K, V> {
/// The IDs of the items that are active for a key at a given height, and
/// optionally the height at which they expire.
active: SnapshotMap<'a, K, Vec<(u64, Option<u64>)>>,
/// The last height at which the active list was updated for each key, to
/// enforce that updates (push/remove) are done in order.
last_active_update: Map<'a, K, u64>,
}

/// A loaded item from the vector, including its ID and expiration.
Expand Down Expand Up @@ -45,6 +50,7 @@ impl<K, V> SnapshotVectorMap<'_, K, V> {
/// "data__active",
/// "data__active__checkpoints",
/// "data__active__changelog",
/// "data__active__last_update",
/// );
/// ```
pub const fn new(
Expand All @@ -53,6 +59,7 @@ impl<K, V> SnapshotVectorMap<'_, K, V> {
active_key: &'static str,
active_checkpoints_key: &'static str,
active_changelog_key: &'static str,
last_active_update_key: &'static str,
) -> Self {
SnapshotVectorMap {
items: Map::new(items_key),
Expand All @@ -63,6 +70,7 @@ impl<K, V> SnapshotVectorMap<'_, K, V> {
active_changelog_key,
Strategy::EveryBlock,
),
last_active_update: Map::new(last_active_update_key),
}
}
}
Expand All @@ -89,6 +97,9 @@ where
curr_height: u64,
expire_in: Option<u64>,
) -> StdResult<((u64, Option<u64>), usize)> {
// ensure push operations are performed at or after the last update
self.validate_update_order(store, k, curr_height)?;

// get next ID for the key, defaulting to 0
let next_id = self
.next_ids
Expand Down Expand Up @@ -130,6 +141,9 @@ where
id: u64,
curr_height: u64,
) -> StdResult<(V, usize)> {
// ensure remove operations are performed at or after the last update
self.validate_update_order(store, k, curr_height)?;

// get active list for the key
let mut active = self.active.may_load(store, k.clone())?.unwrap_or_default();

Expand All @@ -148,6 +162,29 @@ where
Ok((item, active.len()))
}

/// Validate that updates are performed at or after the last update.
pub fn validate_update_order(
&self,
store: &mut dyn Storage,
k: &K,
curr_height: u64,
) -> StdResult<()> {
let last_active_update = self
.last_active_update
.may_load(store, k.clone())?
.unwrap_or_default();
match curr_height.cmp(&last_active_update) {
Ordering::Less => Err(StdError::generic_err(format!(
"update must be performed at or after the last update ({last_active_update})",
))),
Ordering::Equal => Ok(()),
Ordering::Greater => {
// update store if greater than the last active update
self.last_active_update.save(store, k.clone(), &curr_height)
}
}
}

/// Loads paged items at the given block height that are not expired. This
/// takes 1 block to reflect updates made earlier in the same block, due to
/// how [`SnapshotMap`] is implemented. When accessing historical data, you
Expand Down
29 changes: 28 additions & 1 deletion packages/cw-snapshot-vector-map/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cosmwasm_std::{testing::mock_dependencies, Addr};
use cosmwasm_std::{testing::mock_dependencies, Addr, StdError};

use crate::{LoadedItem, SnapshotVectorMap};

Expand All @@ -11,6 +11,7 @@ fn test_basic() {
"svm__active",
"svm__active__checkpoints",
"svm__active__changelog",
"svm__active__last_update",
);
let k1 = &Addr::unchecked("haon");
let k2 = &Addr::unchecked("ekez");
Expand Down Expand Up @@ -89,6 +90,31 @@ fn test_basic() {
}
]
);

// cannot push in the past
for i in 0..4 {
let err = svm.push(storage, k1, &4, i, None).unwrap_err();
assert_eq!(
err,
StdError::generic_err("update must be performed at or after the last update (4)")
);
}

// can push at the same height as the last update (block 4)
let ((added_id, _), _) = svm.push(storage, k1, &4, 4, None).unwrap();

// cannot remove in the past
for i in 0..4 {
let err = svm.remove(storage, k1, 0, i).unwrap_err();
assert_eq!(
err,
StdError::generic_err("update must be performed at or after the last update (4)")
);
}

// can remove at the same height as the last update
// remove item we just added from k1 at block 4
svm.remove(storage, k1, added_id, 4).unwrap();
}

#[test]
Expand All @@ -100,6 +126,7 @@ fn test_expiration() {
"svm__active",
"svm__active__checkpoints",
"svm__active__changelog",
"svm__active__last_update",
);
let k1 = &Addr::unchecked("haon");

Expand Down

0 comments on commit c2c7160

Please sign in to comment.