diff --git a/contracts/adventurer/src/adventurer.cairo b/contracts/adventurer/src/adventurer.cairo index 5f4490866..66e24f710 100644 --- a/contracts/adventurer/src/adventurer.cairo +++ b/contracts/adventurer/src/adventurer.cairo @@ -1693,7 +1693,7 @@ impl ImplAdventurer of IAdventurer { #[inline(always)] fn block_changed_since_last_action(self: Adventurer, block_number: u64) -> bool { - self.last_action_block == (block_number % MAX_ADVENTURER_BLOCKS.into()).try_into().unwrap() + self.last_action_block != (block_number % MAX_ADVENTURER_BLOCKS.into()).try_into().unwrap() } #[inline(always)] diff --git a/contracts/game/src/lib.cairo b/contracts/game/src/lib.cairo index aa7e5961a..49342a7ff 100644 --- a/contracts/game/src/lib.cairo +++ b/contracts/game/src/lib.cairo @@ -207,7 +207,7 @@ mod Game { ref adventurer, adventurer_id, adventurer_entropy, - game_entropy.get_hash(), + 123, till_beast ); } else { @@ -248,9 +248,12 @@ mod Game { @self, adventurer_id ); + let block_number = starknet::get_block_info().unbox().block_number; // ensure player is not exceeding the rate limit // @player this is to protect you from bots - _assert_rate_limit(adventurer.actions_per_block, game_entropy.get_rate_limit()); + if !adventurer.block_changed_since_last_action(block_number) { + _assert_rate_limit(adventurer.actions_per_block, game_entropy.get_rate_limit()); + } // update actions per block adventurer.update_actions_per_block(starknet::get_block_info().unbox().block_number); @@ -287,7 +290,7 @@ mod Game { adventurer_entropy, beast, beast_seed, - game_entropy.get_hash(), + game_entropy.hash, to_the_death ); } else { @@ -351,7 +354,7 @@ mod Game { ref adventurer, adventurer_id, adventurer_entropy, - game_entropy.get_hash(), + game_entropy.hash, beast_seed, beast, to_the_death @@ -410,7 +413,7 @@ mod Game { // get two random numbers let (rnd1, rnd2) = AdventurerUtils::get_randomness( - adventurer.xp, adventurer_entropy, game_entropy.get_hash() + adventurer.xp, adventurer_entropy, game_entropy.hash ); // process beast attack @@ -2144,11 +2147,11 @@ mod Game { __event_GameEntropyRotated( ref self, GameEntropyRotatedEvent { - prev_hash: prev_game_entropy.get_hash(), + prev_hash: prev_game_entropy.hash, prev_block_number: prev_game_entropy.last_updated_block, prev_block_timestamp: prev_game_entropy.last_updated_time, prev_next_rotation_block: prev_game_entropy.next_update_block, - new_hash: new_game_entropy.get_hash(), + new_hash: new_game_entropy.hash, new_block_number: new_game_entropy.last_updated_block, new_block_timestamp: new_game_entropy.last_updated_time, new_next_rotation_block: new_game_entropy.next_update_block, @@ -2338,7 +2341,7 @@ mod Game { } fn _assert_rate_limit(actions_per_block: u8, rate_limit: u64) { - assert(actions_per_block.into() > rate_limit, messages::RATE_LIMIT_EXCEEDED); + assert(actions_per_block.into() <= rate_limit, messages::RATE_LIMIT_EXCEEDED); } fn _is_idle(self: @ContractState, adventurer: Adventurer) -> (bool, u16) { diff --git a/contracts/game/src/tests/test_game.cairo b/contracts/game/src/tests/test_game.cairo index 260f2f39e..8ebdcc919 100644 --- a/contracts/game/src/tests/test_game.cairo +++ b/contracts/game/src/tests/test_game.cairo @@ -174,19 +174,13 @@ mod tests { game } - fn new_adventurer_lvl3(stat: u8) -> IGameDispatcher { + fn new_adventurer_lvl3(starting_block: u64) -> IGameDispatcher { // start game on lvl 2 - let mut game = new_adventurer_lvl2(1000); + let mut game = new_adventurer_lvl2(starting_block); let shopping_cart = ArrayTrait::::new(); let stat_upgrades = Stats { - strength: stat, - dexterity: 0, - vitality: 0, - intelligence: 0, - wisdom: 0, - charisma: 1, - luck: 0 + strength: 0, dexterity: 0, vitality: 0, intelligence: 0, wisdom: 0, charisma: 1, luck: 0 }; game.upgrade(ADVENTURER_ID, 0, stat_upgrades, shopping_cart); @@ -208,7 +202,7 @@ mod tests { fn new_adventurer_lvl4(stat: u8) -> IGameDispatcher { // start game on lvl 2 - let mut game = new_adventurer_lvl3(stat); + let mut game = new_adventurer_lvl3(123); // upgrade charisma let shopping_cart = ArrayTrait::::new(); @@ -1420,7 +1414,10 @@ mod tests { fn test_get_last_action_block() { let mut game = new_adventurer(1000); let adventurer = game.get_adventurer(ADVENTURER_ID); - assert(adventurer.last_action_block == game.get_last_action_block(ADVENTURER_ID), 'wrong last action'); + assert( + adventurer.last_action_block == game.get_last_action_block(ADVENTURER_ID), + 'wrong last action' + ); } #[test] #[available_gas(20000000)] @@ -1758,6 +1755,7 @@ mod tests { // get original adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); + let game_entropy = game.get_game_entropy(); let original_charisma = adventurer.stats.charisma; let original_health = adventurer.health; @@ -1815,4 +1813,46 @@ mod tests { assert(!adventurer.is_equipped(waist_armor_id), 'waist should not be equipped'); } } + + #[test] + #[available_gas(57023944)] + #[should_panic(expected: ('rate limit exceeded', 'ENTRYPOINT_FAILED'))] + fn test_exceed_rate_limit() { + let starting_block = 1000; + let mut game = new_adventurer_lvl2(starting_block); + let shopping_cart = ArrayTrait::::new(); + let stat_upgrades = Stats { + strength: 0, dexterity: 0, vitality: 0, intelligence: 0, wisdom: 0, charisma: 1, luck: 0 + }; + + game.upgrade(ADVENTURER_ID, 0, stat_upgrades, shopping_cart); + game.explore(ADVENTURER_ID, false); + game.attack(ADVENTURER_ID, false); + game.attack(ADVENTURER_ID, false); + } + + #[test] + #[available_gas(944417840)] + fn test_exceed_rate_limit_block_rotation() { + let starting_block = 1000; + let mut game = new_adventurer_lvl2(starting_block); + let shopping_cart = ArrayTrait::::new(); + let stat_upgrades = Stats { + strength: 0, dexterity: 0, vitality: 0, intelligence: 0, wisdom: 0, charisma: 1, luck: 0 + }; + + game.upgrade(ADVENTURER_ID, 0, stat_upgrades, shopping_cart); + game.explore(ADVENTURER_ID, false); + game.attack(ADVENTURER_ID, false); + + // advancing block resets players action per block + starknet::testing::set_block_number(starting_block + 1); + + // player can continue playing + game.attack(ADVENTURER_ID, false); + game.attack(ADVENTURER_ID, false); + game.attack(ADVENTURER_ID, false); + game.attack(ADVENTURER_ID, false); + + } } diff --git a/contracts/game_entropy/src/game_entropy.cairo b/contracts/game_entropy/src/game_entropy.cairo index 8cbf190af..0a1705ad2 100644 --- a/contracts/game_entropy/src/game_entropy.cairo +++ b/contracts/game_entropy/src/game_entropy.cairo @@ -1,11 +1,13 @@ +use debug::PrintTrait; use poseidon::poseidon_hash_span; use starknet::{StorePacking}; #[derive(Drop, Copy, Serde)] struct GameEntropy { - last_updated_block: u64, - last_updated_time: u64, - next_update_block: u64, + hash: felt252, // not stored + last_updated_block: u64, // 64 bits in storage + last_updated_time: u64, // 64 bits in storage + next_update_block: u64, // 64 bits in storage } impl GameEntropyPacking of StorePacking { @@ -38,7 +40,7 @@ impl GameEntropyPacking of StorePacking { let last_updated_block = last_updated_block.try_into().unwrap(); let last_updated_time = last_updated_time.try_into().unwrap(); let next_update_block = next_update_block.try_into().unwrap(); - GameEntropy { last_updated_block, last_updated_time, next_update_block } + ImplGameEntropy::new(last_updated_block, last_updated_time, next_update_block) } } @@ -50,16 +52,17 @@ impl ImplGameEntropy of IGameEntropy { /// @param next_update_block The block number for the next scheduled update. /// @return A new instance of GameEntropy. fn new(last_updated_block: u64, last_updated_time: u64, next_update_block: u64) -> GameEntropy { - GameEntropy { last_updated_block, last_updated_time, next_update_block } + let hash = ImplGameEntropy::get_hash(last_updated_block, last_updated_time, next_update_block); + GameEntropy { hash, last_updated_block, last_updated_time, next_update_block } } /// @notice Calculate a hash based on the properties of the GameEntropy struct /// @return A 252-bit hash value - fn get_hash(self: GameEntropy) -> felt252 { + fn get_hash(last_updated_block: u64, last_updated_time: u64, next_update_block: u64) -> felt252 { let mut hash_span = ArrayTrait::::new(); - hash_span.append(self.last_updated_block.into()); - hash_span.append(self.last_updated_time.into()); - hash_span.append(self.next_update_block.into()); + hash_span.append(last_updated_block.into()); + hash_span.append(last_updated_time.into()); + hash_span.append(next_update_block.into()); poseidon_hash_span(hash_span.span()) } @@ -138,16 +141,40 @@ const U128_MAX: u128 = 340282366920938463463374607431768211455; #[cfg(test)] mod tests { use game_entropy::game_entropy::{GameEntropy, ImplGameEntropy, GameEntropyPacking}; + #[test] + #[available_gas(22080)] + fn test_new_entropy() { + let last_updated_block = 282360; + let last_updated_time = 1696209920; + let next_update_block = 282364; + + let game_entropy = ImplGameEntropy::new(last_updated_block, last_updated_time, next_update_block); + assert(game_entropy.hash != 0, 'hash should be set'); + assert(game_entropy.last_updated_block == last_updated_block, 'wrong last_updated_block'); + assert(game_entropy.last_updated_time == last_updated_time, 'wrong last_updated_time'); + assert(game_entropy.next_update_block == next_update_block, 'wrong next_update_block'); + } + + #[test] + #[available_gas(18580)] + fn test_get_hash() { + // zero case + let last_updated_block = 0; + let last_updated_time = 0; + let next_update_block = 0; + let hash = ImplGameEntropy::get_hash(last_updated_block, last_updated_time, next_update_block); + } #[test] - #[available_gas(14180)] + #[available_gas(14280)] fn test_is_adventurer_idle() { + let hash = 0x123; let last_updated_block = 282360; let last_updated_time = 1696209920; let next_update_block = 282364; let game_entropy = GameEntropy { - last_updated_block, last_updated_time, next_update_block, + hash, last_updated_block, last_updated_time, next_update_block, }; let adventurer_idle_blocks = 3; @@ -160,7 +187,7 @@ mod tests { } #[test] - #[available_gas(19360)] + #[available_gas(15960)] fn test_calculate_next_update_block() { let current_block = 1; @@ -185,17 +212,17 @@ mod tests { } #[test] - #[available_gas(8350)] + #[available_gas(6350)] fn test_current_blocks_per_hour() { let game_entropy = GameEntropy { - last_updated_block: 282465, last_updated_time: 1696214108, next_update_block: 282481, + hash: 0x123, last_updated_block: 282465, last_updated_time: 1696214108, next_update_block: 282481, }; let blocks_per_hour = game_entropy.current_blocks_per_hour(); assert(blocks_per_hour == 96, 'wrong blocks per hour') } #[test] - #[available_gas(30680)] + #[available_gas(29280)] fn test_calculate_blocks_per_hour() { // normal case using current starknet goerli data let previous_block_number = 876324; @@ -226,12 +253,12 @@ mod tests { } #[test] - #[available_gas(240920)] + #[available_gas(243220)] fn test_game_entropy_packing() { // max value case - // hash is calculated during unpack so this value does not matter let game_entropy = GameEntropy { + hash: 0x123, last_updated_block: 0xFFFFFFFFFFFFFFFF, last_updated_time: 0xFFFFFFFFFFFFFFFF, next_update_block: 0xFFFFFFFFFFFFFFFF, @@ -254,7 +281,7 @@ mod tests { // zero case let game_entropy = GameEntropy { - last_updated_block: 0, last_updated_time: 0, next_update_block: 0 + hash: 0, last_updated_block: 0, last_updated_time: 0, next_update_block: 0 }; let unpacked: GameEntropy = GameEntropyPacking::unpack( GameEntropyPacking::pack(game_entropy)