From d65f5cf954f8d02da5dd3e2b0e855212a9ac771a Mon Sep 17 00:00:00 2001 From: loothero Date: Mon, 2 Oct 2023 04:26:12 +0000 Subject: [PATCH] adds rate limit to reduce power of bots * adds actions_per_block to Adventurer which is stored via 4 bits (0-15) * rate limit is dynamic based on starknet block speed * target is two actions per minute --- .gitignore | 4 +- contracts/adventurer/src/adventurer.cairo | 214 +++++++++++------- .../adventurer/src/adventurer_utils.cairo | 4 +- .../src/constants/adventurer_constants.cairo | 1 + contracts/adventurer/src/exploration.cairo | 8 +- contracts/adventurer/src/leaderboard.cairo | 28 +-- contracts/game/src/game/constants.cairo | 1 + contracts/game/src/game/interfaces.cairo | 2 +- contracts/game/src/lib.cairo | 79 +++++-- contracts/game/src/tests/test_game.cairo | 18 +- contracts/game_entropy/src/game_entropy.cairo | 63 +++++- 11 files changed, 277 insertions(+), 145 deletions(-) diff --git a/.gitignore b/.gitignore index db231c3fe..0f3d2f550 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ contracts/beasts/target/CACHEDIR.TAG keys account -loot-survivor.pem \ No newline at end of file +loot-survivor.pem +contracts/game_entropy/target/CACHEDIR.TAG +contracts/game_entropy/target/dev/game_entropy.sierra diff --git a/contracts/adventurer/src/adventurer.cairo b/contracts/adventurer/src/adventurer.cairo index 341cc6fb2..5f4490866 100644 --- a/contracts/adventurer/src/adventurer.cairo +++ b/contracts/adventurer/src/adventurer.cairo @@ -21,7 +21,7 @@ use super::{ JEWELRY_BONUS_NAME_MATCH_PERCENT_PER_GREATNESS, NECKLACE_ARMOR_BONUS, MINIMUM_DAMAGE_FROM_BEASTS, OBSTACLE_CRITICAL_HIT_CHANCE, BEAST_CRITICAL_HIT_CHANCE, SILVER_RING_LUCK_BONUS_PER_GREATNESS, MINIMUM_DAMAGE_FROM_OBSTACLES, - MINIMUM_DAMAGE_TO_BEASTS + MINIMUM_DAMAGE_TO_BEASTS, MAX_ACTIONS_PER_BLOCK }, discovery_constants::DiscoveryEnums::{ExploreResult, DiscoveryType} } @@ -39,7 +39,7 @@ use beasts::{beast::{ImplBeast, Beast}, constants::BeastSettings}; #[derive(Drop, Copy, Serde)] struct Adventurer { - last_action: u16, // 9 bits + last_action_block: u16, // 9 bits health: u16, // 9 bits xp: u16, // 13 bits stats: Stats, // 30 bits @@ -54,12 +54,13 @@ struct Adventurer { ring: ItemPrimitive, // 21 bits beast_health: u16, // 9 bits stat_points_available: u8, // 3 bits + actions_per_block: u8, // 4 bits mutated: bool, // not packed } impl AdventurerPacking of StorePacking { fn pack(value: Adventurer) -> felt252 { - (value.last_action.into() + (value.last_action_block.into() + value.health.into() * TWO_POW_9 + value.xp.into() * TWO_POW_18 + StatsPacking::pack(value.stats).into() * TWO_POW_31 @@ -73,14 +74,15 @@ impl AdventurerPacking of StorePacking { + ItemPrimitivePacking::pack(value.neck).into() * TWO_POW_190 + ItemPrimitivePacking::pack(value.ring).into() * TWO_POW_211 + value.beast_health.into() * TWO_POW_232 - + value.stat_points_available.into() * TWO_POW_241) + + value.stat_points_available.into() * TWO_POW_241 + + value.actions_per_block.into() * TWO_POW_244) .try_into() .unwrap() } fn unpack(value: felt252) -> Adventurer { let packed = value.into(); - let (packed, last_action) = integer::U256DivRem::div_rem( + let (packed, last_action_block) = integer::U256DivRem::div_rem( packed, TWO_POW_9.try_into().unwrap() ); let (packed, health) = integer::U256DivRem::div_rem(packed, TWO_POW_9.try_into().unwrap()); @@ -98,12 +100,15 @@ impl AdventurerPacking of StorePacking { let (packed, beast_health) = integer::U256DivRem::div_rem( packed, TWO_POW_9.try_into().unwrap() ); - let (_, stat_points_available) = integer::U256DivRem::div_rem( + let (packed, stat_points_available) = integer::U256DivRem::div_rem( packed, TWO_POW_3.try_into().unwrap() ); + let (_, actions_per_block) = integer::U256DivRem::div_rem( + packed, TWO_POW_4.try_into().unwrap() + ); Adventurer { - last_action: last_action.try_into().unwrap(), + last_action_block: last_action_block.try_into().unwrap(), health: health.try_into().unwrap(), xp: xp.try_into().unwrap(), stats: StatsPacking::unpack(stats.try_into().unwrap()), @@ -118,6 +123,7 @@ impl AdventurerPacking of StorePacking { ring: ItemPrimitivePacking::unpack(ring.try_into().unwrap()), beast_health: beast_health.try_into().unwrap(), stat_points_available: stat_points_available.try_into().unwrap(), + actions_per_block: actions_per_block.try_into().unwrap(), mutated: false, } } @@ -141,7 +147,7 @@ impl ImplAdventurer of IAdventurer { let starting_stats = AdventurerUtils::generate_starting_stats(entropy, num_starting_stats); let mut adventurer = Adventurer { - last_action: current_block_modulo_512, + last_action_block: current_block_modulo_512, health: STARTING_HEALTH, xp: 0, stats: starting_stats, @@ -156,6 +162,7 @@ impl ImplAdventurer of IAdventurer { ring: ItemPrimitive { id: 0, xp: 0, metadata: 0, }, beast_health: BeastSettings::STARTER_BEAST_HEALTH, stat_points_available: 0, + actions_per_block: 0, mutated: false, }; @@ -1244,20 +1251,20 @@ impl ImplAdventurer of IAdventurer { #[inline(always)] fn get_idle_blocks(self: Adventurer, current_block: u64) -> u16 { // adventurer only has 9 bits of storage for block numbers - // the last_action on the adventurer is 0-511 which is based on + // the last_action_block on the adventurer is 0-511 which is based on // the current starknet block % 512. As such, when calculating the number Of // idle blocks, we need to % 512 the current block let current_block_modulo_512: u16 = (current_block % MAX_ADVENTURER_BLOCKS.into()) .try_into() .unwrap(); - // if the current block is greater than or equal to the last last_action - if (current_block_modulo_512 >= self.last_action) { + // if the current block is greater than or equal to the last last_action_block + if (current_block_modulo_512 >= self.last_action_block) { // we can just subtract the two to get idle blocks - current_block_modulo_512 - self.last_action + current_block_modulo_512 - self.last_action_block } else { // otherwise we need to add the two and subtract 512 - MAX_ADVENTURER_BLOCKS - self.last_action + current_block_modulo_512 + MAX_ADVENTURER_BLOCKS - self.last_action_block + current_block_modulo_512 } } @@ -1274,14 +1281,14 @@ impl ImplAdventurer of IAdventurer { } } - // @notice: last_action.set_last_action sets the last action on the adventurer to the current block + // @notice: last_action_block.set_last_action_block sets the last action on the adventurer to the current block // @dev: we only have 9 bits of storage for block numbers so we need to modulo the current block // @dev: by 512 to ensure we don't overflow the storage // @param self A reference to the Adventurer instance. // @param current_block The current block number. #[inline(always)] - fn set_last_action(ref self: Adventurer, current_block: u64) { - self.last_action = (current_block % MAX_BLOCK_COUNT).try_into().unwrap(); + fn set_last_action_block(ref self: Adventurer, current_block: u64) { + self.last_action_block = (current_block % MAX_BLOCK_COUNT).try_into().unwrap(); } @@ -1674,9 +1681,36 @@ impl ImplAdventurer of IAdventurer { base_damage } } + + #[inline(always)] + fn update_actions_per_block(ref self: Adventurer, current_block: u64) { + if self.block_changed_since_last_action(current_block) { + self.reset_actions_per_block(); + } else { + self.increment_actions_per_block(); + } + } + + #[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() + } + + #[inline(always)] + fn increment_actions_per_block(ref self: Adventurer) { + if self.actions_per_block < MAX_ACTIONS_PER_BLOCK { + self.actions_per_block += 1; + } + } + + #[inline(always)] + fn reset_actions_per_block(ref self: Adventurer) { + self.actions_per_block = 1; + } } const TWO_POW_3: u256 = 0x8; +const TWO_POW_4: u256 = 0x10; const TWO_POW_9: u256 = 0x200; const TWO_POW_13: u256 = 0x2000; const TWO_POW_18: u256 = 0x40000; @@ -1694,6 +1728,7 @@ const TWO_POW_190: u256 = 0x400000000000000000000000000000000000000000000000; const TWO_POW_211: u256 = 0x80000000000000000000000000000000000000000000000000000; const TWO_POW_232: u256 = 0x10000000000000000000000000000000000000000000000000000000000; const TWO_POW_241: u256 = 0x2000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_244: u256 = 0x10000000000000000000000000000000000000000000000000000000000000; // --------------------------- // ---------- Tests ---------- @@ -2637,23 +2672,23 @@ mod tests { } #[test] - #[available_gas(216684)] - fn test_set_last_action() { + #[available_gas(217684)] + fn test_set_last_action_block() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); - adventurer.set_last_action(0); - assert(adventurer.last_action == 0, 'last action should be 0'); - adventurer.set_last_action(511); - assert(adventurer.last_action == 511, 'last action should be 511'); - adventurer.set_last_action(512); - assert(adventurer.last_action == 0, 'last action should be 0'); - adventurer.set_last_action(1023); - assert(adventurer.last_action == 511, 'last action should be 511'); - adventurer.set_last_action(1024); - assert(adventurer.last_action == 0, 'last action should be 0'); + adventurer.set_last_action_block(0); + assert(adventurer.last_action_block == 0, 'last action should be 0'); + adventurer.set_last_action_block(511); + assert(adventurer.last_action_block == 511, 'last action should be 511'); + adventurer.set_last_action_block(512); + assert(adventurer.last_action_block == 0, 'last action should be 0'); + adventurer.set_last_action_block(1023); + assert(adventurer.last_action_block == 511, 'last action should be 511'); + adventurer.set_last_action_block(1024); + assert(adventurer.last_action_block == 0, 'last action should be 0'); } #[test] - #[available_gas(253944)] + #[available_gas(254644)] fn test_charisma_adjusted_item_price() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -2677,7 +2712,7 @@ mod tests { } #[test] - #[available_gas(288554)] + #[available_gas(289254)] fn test_charisma_adjusted_potion_price() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -2708,10 +2743,10 @@ mod tests { } #[test] - #[available_gas(241184)] + #[available_gas(241584)] fn test_get_idle_blocks() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); - adventurer.last_action = 1; + adventurer.last_action_block = 1; // test with current block greater than last action assert(adventurer.get_idle_blocks(3) == 2, 'idle blocks should be 2'); @@ -2719,7 +2754,7 @@ mod tests { // test with current block less than last action assert(adventurer.get_idle_blocks(0) == 511, 'idle blocks should be 511'); - adventurer.last_action = 511; + adventurer.last_action_block = 511; assert(adventurer.get_idle_blocks(511) == 0, 'idle blocks should be 0'); assert(adventurer.get_idle_blocks(0) == 1, 'idle blocks should be 1'); } @@ -2728,7 +2763,7 @@ mod tests { #[available_gas(3000000)] fn test_packing_and_unpacking_adventurer() { let adventurer = Adventurer { - last_action: 511, + last_action_block: 511, health: 511, xp: 8191, stats: Stats { @@ -2751,11 +2786,12 @@ mod tests { ring: ItemPrimitive { id: 1, xp: 511, metadata: 8, }, beast_health: 511, stat_points_available: 7, + actions_per_block: 0, mutated: false }; let packed = AdventurerPacking::pack(adventurer); let unpacked: Adventurer = AdventurerPacking::unpack(packed); - assert(adventurer.last_action == unpacked.last_action, 'last_action'); + assert(adventurer.last_action_block == unpacked.last_action_block, 'last_action_block'); assert(adventurer.health == unpacked.health, 'health'); assert(adventurer.xp == unpacked.xp, 'xp'); assert(adventurer.stats.strength == unpacked.stats.strength, 'strength'); @@ -2807,7 +2843,7 @@ mod tests { } #[test] - #[available_gas(304164)] + #[available_gas(305064)] fn test_increase_health() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -2862,7 +2898,7 @@ mod tests { } #[test] - #[available_gas(196364)] + #[available_gas(197164)] fn test_decrease_health() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let starting_health = adventurer.health; @@ -2878,7 +2914,7 @@ mod tests { } #[test] - #[available_gas(196364)] + #[available_gas(197064)] fn test_deduct_gold() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let starting_gold = adventurer.gold; @@ -2894,7 +2930,7 @@ mod tests { } #[test] - #[available_gas(337814)] + #[available_gas(339614)] fn test_increase_adventurer_xp() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // base case level increase @@ -2956,7 +2992,7 @@ mod tests { } #[test] - #[available_gas(182964)] + #[available_gas(192164)] fn test_increase_strength() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -2968,7 +3004,7 @@ mod tests { } #[test] - #[available_gas(182964)] + #[available_gas(192164)] fn test_increase_dexterity() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -2980,7 +3016,7 @@ mod tests { } #[test] - #[available_gas(182964)] + #[available_gas(192164)] fn test_increase_vitality() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -2992,7 +3028,7 @@ mod tests { } #[test] - #[available_gas(182964)] + #[available_gas(192164)] fn test_increase_intelligence() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3004,7 +3040,7 @@ mod tests { } #[test] - #[available_gas(182964)] + #[available_gas(192164)] fn test_increase_wisdom() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3016,7 +3052,7 @@ mod tests { } #[test] - #[available_gas(182964)] + #[available_gas(192164)] fn test_increase_charisma() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3028,7 +3064,7 @@ mod tests { } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_strength() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3042,7 +3078,7 @@ mod tests { } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_dexterity() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3056,7 +3092,7 @@ mod tests { } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_vitality() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3070,7 +3106,7 @@ mod tests { } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_intelligence() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3084,14 +3120,14 @@ mod tests { } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_wisdom_gas() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); adventurer.stats.decrease_wisdom(1); } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_wisdom() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3105,7 +3141,7 @@ mod tests { } #[test] - #[available_gas(191964)] + #[available_gas(192164)] fn test_decrease_charisma() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // basic case @@ -3140,7 +3176,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(171984)] fn test_equip_valid_weapon() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3176,7 +3212,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(171984)] fn test_equip_valid_chest() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // equip Divine Robe as chest item @@ -3207,7 +3243,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(171984)] fn test_equip_valid_head() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // equip Crown as head item @@ -3237,7 +3273,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(171984)] fn test_equip_valid_waist() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3269,7 +3305,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(172184)] fn test_equip_valid_foot() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3302,7 +3338,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(172184)] fn test_equip_valid_hand() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3335,7 +3371,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(172184)] fn test_equip_valid_neck() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3368,7 +3404,7 @@ mod tests { // } #[test] - #[available_gas(171784)] + #[available_gas(172184)] fn test_equip_valid_ring() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let item = ItemPrimitive { id: ItemId::PlatinumRing, xp: 1, metadata: 0 }; @@ -3379,14 +3415,14 @@ mod tests { } #[test] - #[available_gas(196284)] + #[available_gas(198584)] fn test_increase_item_xp_at_slot_gas() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); adventurer.increase_item_xp_at_slot(Slot::Weapon(()), 1); } #[test] - #[available_gas(382584)] + #[available_gas(385184)] fn test_increase_item_xp_at_slot() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3426,7 +3462,7 @@ mod tests { } #[test] - #[available_gas(197084)] + #[available_gas(198084)] fn test_increase_item_xp_at_slot_max() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3436,7 +3472,7 @@ mod tests { } #[test] - #[available_gas(197084)] + #[available_gas(198084)] fn test_increase_item_xp_at_slot_zero() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3446,7 +3482,7 @@ mod tests { } #[test] - #[available_gas(448264)] + #[available_gas(449564)] fn test_get_equipped_items() { let mut adventurer = ImplAdventurer::new(ItemId::Wand, 0, 0, 0); @@ -3563,7 +3599,7 @@ mod tests { } #[test] - #[available_gas(184344)] + #[available_gas(184944)] fn test_set_beast_health() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3579,7 +3615,7 @@ mod tests { } #[test] - #[available_gas(194364)] + #[available_gas(194964)] fn test_deduct_beast_health() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3647,7 +3683,7 @@ mod tests { } #[test] - #[available_gas(352884)] + #[available_gas(353184)] fn test_is_slot_free() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3703,7 +3739,7 @@ mod tests { } #[test] - #[available_gas(233824)] + #[available_gas(234224)] fn test_charisma_health_discount_overflow() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3721,7 +3757,7 @@ mod tests { } #[test] - #[available_gas(233824)] + #[available_gas(234524)] fn test_charisma_item_discount_overflow() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let item_price = 15; @@ -3747,7 +3783,7 @@ mod tests { } #[test] - #[available_gas(255224)] + #[available_gas(256224)] fn test_increase_xp() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3761,7 +3797,7 @@ mod tests { } #[test] - #[available_gas(293684)] + #[available_gas(293884)] fn test_apply_suffix_boost() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3825,10 +3861,10 @@ mod tests { } #[test] - #[available_gas(337800)] + #[available_gas(337900)] fn test_get_and_apply_stat_boosts() { let mut adventurer = Adventurer { - last_action: 511, + last_action_block: 511, health: 100, xp: 1, stats: Stats { @@ -3851,6 +3887,7 @@ mod tests { ring: ItemPrimitive { id: 8, xp: 1, metadata: 8, }, beast_health: 20, stat_points_available: 0, + actions_per_block: 0, mutated: false, }; @@ -3916,7 +3953,7 @@ mod tests { // test base case #[test] - #[available_gas(207324)] + #[available_gas(207524)] fn test_apply_stat_boosts() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3936,7 +3973,7 @@ mod tests { // test zero case #[test] - #[available_gas(207324)] + #[available_gas(207524)] fn test_apply_stat_boosts_zero() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -3955,7 +3992,7 @@ mod tests { // test max value case #[test] - #[available_gas(207324)] + #[available_gas(207524)] fn test_apply_stat_boosts_max() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let boost_stats = Stats { @@ -3982,7 +4019,7 @@ mod tests { #[available_gas(53430)] fn test_remove_stat_boosts() { let mut adventurer = Adventurer { - last_action: 511, + last_action_block: 511, health: 100, xp: 1, stats: Stats { @@ -4005,6 +4042,7 @@ mod tests { ring: ItemPrimitive { id: 8, xp: 1, metadata: 8, }, beast_health: 20, stat_points_available: 0, + actions_per_block: 0, mutated: false, }; @@ -4032,7 +4070,7 @@ mod tests { #[available_gas(53430)] fn test_remove_stat_boosts_zero() { let mut adventurer = Adventurer { - last_action: 511, + last_action_block: 511, health: 100, xp: 1, stats: Stats { @@ -4055,6 +4093,7 @@ mod tests { ring: ItemPrimitive { id: 8, xp: 1, metadata: 8, }, beast_health: 20, stat_points_available: 0, + actions_per_block: 0, mutated: false, }; @@ -4076,7 +4115,7 @@ mod tests { #[available_gas(53430)] fn test_remove_stat_boosts_max() { let mut adventurer = Adventurer { - last_action: 511, + last_action_block: 511, health: 100, xp: 1, stats: Stats { @@ -4099,6 +4138,7 @@ mod tests { ring: ItemPrimitive { id: 8, xp: 1, metadata: 8, }, beast_health: 20, stat_points_available: 0, + actions_per_block: 0, mutated: false, }; @@ -4142,7 +4182,7 @@ mod tests { } #[test] - #[available_gas(244654)] + #[available_gas(245054)] fn test_calculate_luck_gas_no_luck() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let bag = ImplBag::new(); @@ -4150,7 +4190,7 @@ mod tests { } #[test] - #[available_gas(245154)] + #[available_gas(245554)] fn test_calculate_luck_gas_with_luck() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let bag = ImplBag::new(); @@ -4163,7 +4203,7 @@ mod tests { } #[test] - #[available_gas(697614)] + #[available_gas(698414)] fn test_calculate_luck() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); let bag = ImplBag::new(); @@ -4199,7 +4239,7 @@ mod tests { } #[test] - #[available_gas(177584)] + #[available_gas(177984)] fn test_in_battle() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); assert(adventurer.in_battle() == true, 'new advntr start in battle'); @@ -4396,7 +4436,7 @@ mod tests { #[test] #[should_panic(expected: ('item is not equipped',))] - #[available_gas(172784)] + #[available_gas(172984)] fn test_drop_item_not_equipped() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); // try to drop an item that isn't equipped @@ -4406,7 +4446,7 @@ mod tests { } #[test] - #[available_gas(507684)] + #[available_gas(511384)] fn test_drop_item() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -4490,7 +4530,7 @@ mod tests { } #[test] - #[available_gas(419324)] + #[available_gas(421224)] fn test_is_ambush() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); diff --git a/contracts/adventurer/src/adventurer_utils.cairo b/contracts/adventurer/src/adventurer_utils.cairo index f0eac5821..9d7a06ec3 100644 --- a/contracts/adventurer/src/adventurer_utils.cairo +++ b/contracts/adventurer/src/adventurer_utils.cairo @@ -484,7 +484,7 @@ mod tests { } #[test] - #[available_gas(259044)] + #[available_gas(259644)] fn test_is_health_full() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); @@ -518,7 +518,7 @@ mod tests { } #[test] - #[available_gas(204804)] + #[available_gas(205004)] fn test_get_max_health() { let mut adventurer = ImplAdventurer::new(12, 0, 0, 0); diff --git a/contracts/adventurer/src/constants/adventurer_constants.cairo b/contracts/adventurer/src/constants/adventurer_constants.cairo index a9c76f8db..e27f74461 100644 --- a/contracts/adventurer/src/constants/adventurer_constants.cairo +++ b/contracts/adventurer/src/constants/adventurer_constants.cairo @@ -45,6 +45,7 @@ const MAX_ADVENTURER_BLOCKS: u16 = 512; // 2^9 const STAT_UPGRADE_POINTS_PER_LEVEL: u8 = 1; const BEAST_SPECIAL_NAME_LEVEL_UNLOCK: u16 = 19; const XP_FOR_DISCOVERIES: u16 = 1; +const MAX_ACTIONS_PER_BLOCK: u8 = 15; // controls how much faster items level up compared to the player const ITEM_XP_MULTIPLIER_BEASTS: u16 = 2; diff --git a/contracts/adventurer/src/exploration.cairo b/contracts/adventurer/src/exploration.cairo index a0469837b..96eb0a962 100644 --- a/contracts/adventurer/src/exploration.cairo +++ b/contracts/adventurer/src/exploration.cairo @@ -71,7 +71,7 @@ mod tests { use lootitems::constants::ItemId; #[test] - #[available_gas(328154)] + #[available_gas(328654)] fn test_get_gold_discovery_gas() { let adventurer = ImplAdventurer::new(12, 6, 0, 0); let entropy = 0; @@ -79,7 +79,7 @@ mod tests { } #[test] - #[available_gas(328654)] + #[available_gas(329054)] fn test_get_gold_discovery() { let mut adventurer = ImplAdventurer::new(12, 6, 0, 0); @@ -90,7 +90,7 @@ mod tests { } #[test] - #[available_gas(328154)] + #[available_gas(328854)] fn test_get_health_discovery_gas() { let adventurer = ImplAdventurer::new(12, 6, 0, 0); let entropy = 12345; @@ -98,7 +98,7 @@ mod tests { } #[test] - #[available_gas(328654)] + #[available_gas(329054)] fn test_get_health_discovery() { let mut adventurer = ImplAdventurer::new(12, 6, 0, 0); diff --git a/contracts/adventurer/src/leaderboard.cairo b/contracts/adventurer/src/leaderboard.cairo index eb208b664..5c6389e4e 100644 --- a/contracts/adventurer/src/leaderboard.cairo +++ b/contracts/adventurer/src/leaderboard.cairo @@ -63,20 +63,20 @@ impl LeaderboardPacking of StorePacking { const TWO_POW_9: u256 = 0x200; const TWO_POW_13: u256 = 0x2000; -const TWO_POW_22: u256 = 0x400000; // 2^22 -const TWO_POW_44: u256 = 0x100000000000; // 2^44 -const TWO_POW_61: u256 = 0x2000000000000000; // 2^61 -const TWO_POW_66: u256 = 0x40000000000000000; // 2^66 -const TWO_POW_74: u256 = 0x4000000000000000000; // 2^74 -const TWO_POW_83: u256 = 0x800000000000000000000; // 2^83 -const TWO_POW_88: u256 = 0x10000000000000000000000; // 2^88 -const TWO_POW_110: u256 = 0x4000000000000000000000000000; // 2^110 -const TWO_POW_132: u256 = 0x1000000000000000000000000000000000; // 2^132 -const TWO_POW_154: u256 = 0x400000000000000000000000000000000000000; // 2^154 -const TWO_POW_166: u256 = 0x400000000000000000000000000000000000000000; // 2^166 -const TWO_POW_176: u256 = 0x100000000000000000000000000000000000000000000; // 2^176 -const TWO_POW_198: u256 = 0x40000000000000000000000000000000000000000000000000; // 2^198 -const TWO_POW_220: u256 = 0x10000000000000000000000000000000000000000000000000000000; // 2^220 +const TWO_POW_22: u256 = 0x400000; +const TWO_POW_44: u256 = 0x100000000000; +const TWO_POW_61: u256 = 0x2000000000000000; +const TWO_POW_66: u256 = 0x40000000000000000; +const TWO_POW_74: u256 = 0x4000000000000000000; +const TWO_POW_83: u256 = 0x800000000000000000000; +const TWO_POW_88: u256 = 0x10000000000000000000000; +const TWO_POW_110: u256 = 0x4000000000000000000000000000; +const TWO_POW_132: u256 = 0x1000000000000000000000000000000000; +const TWO_POW_154: u256 = 0x400000000000000000000000000000000000000; +const TWO_POW_166: u256 = 0x400000000000000000000000000000000000000000; +const TWO_POW_176: u256 = 0x100000000000000000000000000000000000000000000; +const TWO_POW_198: u256 = 0x40000000000000000000000000000000000000000000000000; +const TWO_POW_220: u256 = 0x10000000000000000000000000000000000000000000000000000000; // --------------------------- // ---------- Tests ---------- diff --git a/contracts/game/src/game/constants.cairo b/contracts/game/src/game/constants.cairo index 339d9789e..84a484b1b 100644 --- a/contracts/game/src/game/constants.cairo +++ b/contracts/game/src/game/constants.cairo @@ -24,6 +24,7 @@ mod messages { const MUST_USE_ALL_STATS: felt252 = 'Must use all stats'; const NO_ITEMS: felt252 = 'Must provide item ids'; const NON_ZERO_STARTING_LUCK: felt252 = 'Luck must be zero'; + const RATE_LIMIT_EXCEEDED: felt252 = 'rate limit exceeded'; } // TODO: Update for mainnet diff --git a/contracts/game/src/game/interfaces.cairo b/contracts/game/src/game/interfaces.cairo index b3010c4cf..e6c12dac4 100644 --- a/contracts/game/src/game/interfaces.cairo +++ b/contracts/game/src/game/interfaces.cairo @@ -40,7 +40,7 @@ trait IGame { fn get_level(self: @TContractState, adventurer_id: felt252) -> u8; fn get_gold(self: @TContractState, adventurer_id: felt252) -> u16; fn get_stat_upgrades_available(self: @TContractState, adventurer_id: felt252) -> u8; - fn get_last_action(self: @TContractState, adventurer_id: felt252) -> u16; + fn get_last_action_block(self: @TContractState, adventurer_id: felt252) -> u16; // adventurer stats (includes boost) fn get_stats(self: @TContractState, adventurer_id: felt252) -> Stats; diff --git a/contracts/game/src/lib.cairo b/contracts/game/src/lib.cairo index 7456578b3..aa7e5961a 100644 --- a/contracts/game/src/lib.cairo +++ b/contracts/game/src/lib.cairo @@ -190,6 +190,13 @@ mod Game { @self, adventurer_id ); + // 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()); + + // update actions per block + adventurer.update_actions_per_block(starknet::get_block_info().unbox().block_number); + // get number of blocks between actions let (idle, num_blocks) = _is_idle(@self, immutable_adventurer); @@ -200,7 +207,7 @@ mod Game { ref adventurer, adventurer_id, adventurer_entropy, - game_entropy, + game_entropy.get_hash(), till_beast ); } else { @@ -208,7 +215,7 @@ mod Game { } // update players last action block number to reset idle counter - adventurer.set_last_action(starknet::get_block_info().unbox().block_number); + adventurer.set_last_action_block(starknet::get_block_info().unbox().block_number); // pack and save adventurer _pack_adventurer_remove_stat_boost( @@ -236,16 +243,23 @@ mod Game { _assert_not_dead(immutable_adventurer); _assert_in_battle(immutable_adventurer); + // get adventurer and game entropy + let (adventurer_entropy, game_entropy) = _get_adventurer_and_game_entropy( + @self, adventurer_id + ); + + // 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()); + + // update actions per block + adventurer.update_actions_per_block(starknet::get_block_info().unbox().block_number); + // get number of blocks between actions let (idle, num_blocks) = _is_idle(@self, immutable_adventurer); // process attack or apply idle penalty if !idle { - // get adventurer and game entropy - let (adventurer_entropy, game_entropy) = _get_adventurer_and_game_entropy( - @self, adventurer_id - ); - // get weapon specials let weapon_specials = _get_item_specials(@self, adventurer_id, adventurer.weapon); @@ -273,7 +287,7 @@ mod Game { adventurer_entropy, beast, beast_seed, - game_entropy, + game_entropy.get_hash(), to_the_death ); } else { @@ -281,7 +295,7 @@ mod Game { } // update players last action block - adventurer.set_last_action(starknet::get_block_info().unbox().block_number); + adventurer.set_last_action_block(starknet::get_block_info().unbox().block_number); // pack and save adventurer _pack_adventurer_remove_stat_boost( @@ -311,16 +325,23 @@ mod Game { _assert_not_starter_beast(immutable_adventurer); _assert_dexterity_not_zero(immutable_adventurer); + // get adventurer and game entropy + let (adventurer_entropy, game_entropy) = _get_adventurer_and_game_entropy( + @self, adventurer_id + ); + + // 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()); + + // update actions per block + adventurer.update_actions_per_block(starknet::get_block_info().unbox().block_number); + // get number of blocks between actions let (idle, num_blocks) = _is_idle(@self, immutable_adventurer); // if adventurer is not idle if !idle { - // get adventurer and game entropy - let (adventurer_entropy, game_entropy) = _get_adventurer_and_game_entropy( - @self, adventurer_id - ); - // get beast and beast seed let (beast, beast_seed) = adventurer.get_beast(adventurer_entropy); @@ -330,7 +351,7 @@ mod Game { ref adventurer, adventurer_id, adventurer_entropy, - game_entropy, + game_entropy.get_hash(), beast_seed, beast, to_the_death @@ -346,7 +367,7 @@ mod Game { } // update players last action block number - adventurer.set_last_action(starknet::get_block_info().unbox().block_number); + adventurer.set_last_action_block(starknet::get_block_info().unbox().block_number); // pack and save adventurer _pack_adventurer_remove_stat_boost( @@ -389,7 +410,7 @@ mod Game { // get two random numbers let (rnd1, rnd2) = AdventurerUtils::get_randomness( - adventurer.xp, adventurer_entropy, game_entropy.into() + adventurer.xp, adventurer_entropy, game_entropy.get_hash() ); // process beast attack @@ -483,6 +504,16 @@ mod Game { _assert_not_in_battle(immutable_adventurer); _assert_valid_stat_selection(immutable_adventurer, stat_upgrades); + // load game entropy + let game_entropy = _load_game_entropy(@self); + + // 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()); + + // update actions per block + adventurer.update_actions_per_block(starknet::get_block_info().unbox().block_number); + // get number of blocks between actions let (idle, num_blocks) = _is_idle(@self, immutable_adventurer); @@ -516,7 +547,7 @@ mod Game { } // update players last action block number - adventurer.set_last_action(starknet::get_block_info().unbox().block_number); + adventurer.set_last_action_block(starknet::get_block_info().unbox().block_number); // emit adventurer upgraded event __event_AdventurerUpgraded(ref self, adventurer, adventurer_id, bag, stat_upgrades); @@ -713,8 +744,8 @@ mod Game { fn get_stat_upgrades_available(self: @ContractState, adventurer_id: felt252) -> u8 { _unpack_adventurer(self, adventurer_id).stat_points_available } - fn get_last_action(self: @ContractState, adventurer_id: felt252) -> u16 { - _unpack_adventurer(self, adventurer_id).last_action + fn get_last_action_block(self: @ContractState, adventurer_id: felt252) -> u16 { + _unpack_adventurer(self, adventurer_id).last_action_block } fn get_weapon_greatness(self: @ContractState, adventurer_id: felt252) -> u8 { _unpack_adventurer(self, adventurer_id).weapon.get_greatness() @@ -2306,6 +2337,10 @@ mod Game { assert(is_idle, messages::ADVENTURER_NOT_IDLE); } + fn _assert_rate_limit(actions_per_block: u8, rate_limit: u64) { + assert(actions_per_block.into() > rate_limit, messages::RATE_LIMIT_EXCEEDED); + } + fn _is_idle(self: @ContractState, adventurer: Adventurer) -> (bool, u16) { // get number of blocks since the players last turn let idle_blocks = adventurer @@ -2483,8 +2518,8 @@ mod Game { #[inline(always)] fn _get_adventurer_and_game_entropy( self: @ContractState, adventurer_id: felt252 - ) -> (u128, felt252) { - (_get_adventurer_entropy(self, adventurer_id), _load_game_entropy(self).get_hash()) + ) -> (u128, GameEntropy) { + (_get_adventurer_entropy(self, adventurer_id), _load_game_entropy(self)) } #[inline(always)] diff --git a/contracts/game/src/tests/test_game.cairo b/contracts/game/src/tests/test_game.cairo index e631e68dc..260f2f39e 100644 --- a/contracts/game/src/tests/test_game.cairo +++ b/contracts/game/src/tests/test_game.cairo @@ -1197,13 +1197,13 @@ mod tests { // verify last action block number is correct assert( - adventurer.last_action == STARTING_BLOCK_NUMBER.try_into().unwrap(), + adventurer.last_action_block == STARTING_BLOCK_NUMBER.try_into().unwrap(), 'unexpected last action block' ); // roll forward blockchain to make adventurer idle testing::set_block_number( - adventurer.last_action.into() + game_entropy.get_idle_penalty_blocks() + 1 + adventurer.last_action_block.into() + game_entropy.get_idle_penalty_blocks() + 1 ); // get current block number @@ -1213,7 +1213,7 @@ mod tests { // this is imperative because this test is testing the case where the adventurer last action block number // is less than (current_block_number % MAX_BLOCK_COUNT) assert( - (current_block_number % MAX_BLOCK_COUNT) < adventurer.last_action.into(), + (current_block_number % MAX_BLOCK_COUNT) < adventurer.last_action_block.into(), 'last action !> current block' ); @@ -1253,7 +1253,7 @@ mod tests { // roll forward blockchain to make adventurer idle testing::set_block_number( - adventurer.last_action.into() + game_entropy.get_idle_penalty_blocks() + 1 + adventurer.last_action_block.into() + game_entropy.get_idle_penalty_blocks() + 1 ); // get current block number @@ -1263,7 +1263,7 @@ mod tests { // this is imperative because this test is testing the case where the adventurer last action block number // is greater than the (current_block_number % MAX_BLOCK_COUNT) assert( - (current_block_number % MAX_BLOCK_COUNT) > adventurer.last_action.into(), + (current_block_number % MAX_BLOCK_COUNT) > adventurer.last_action_block.into(), 'last action !> current block' ); @@ -1305,7 +1305,7 @@ mod tests { // roll forward blockchain to make adventurer idle testing::set_block_number( - adventurer.last_action.into() + game_entropy.get_idle_penalty_blocks() + 1 + adventurer.last_action_block.into() + game_entropy.get_idle_penalty_blocks() + 1 ); // get current block number @@ -1315,7 +1315,7 @@ mod tests { // this is imperative because this test is testing the case where the adventurer last action block number // is greater than the (current_block_number % MAX_BLOCK_COUNT) assert( - (current_block_number % MAX_BLOCK_COUNT) > adventurer.last_action.into(), + (current_block_number % MAX_BLOCK_COUNT) > adventurer.last_action_block.into(), 'last action !> current block' ); @@ -1417,10 +1417,10 @@ mod tests { } #[test] #[available_gas(20000000)] - fn test_get_last_action() { + fn test_get_last_action_block() { let mut game = new_adventurer(1000); let adventurer = game.get_adventurer(ADVENTURER_ID); - assert(adventurer.last_action == game.get_last_action(ADVENTURER_ID), 'wrong last action'); + assert(adventurer.last_action_block == game.get_last_action_block(ADVENTURER_ID), 'wrong last action'); } #[test] #[available_gas(20000000)] diff --git a/contracts/game_entropy/src/game_entropy.cairo b/contracts/game_entropy/src/game_entropy.cairo index 645399ab1..8cbf190af 100644 --- a/contracts/game_entropy/src/game_entropy.cairo +++ b/contracts/game_entropy/src/game_entropy.cairo @@ -9,7 +9,9 @@ struct GameEntropy { } impl GameEntropyPacking of StorePacking { + // @notice: packs a GameEntropy struct into a felt252 // @dev: we don't store hash since it can be calculated dynamically + // @param value: the GameEntropy struct to pack fn pack(value: GameEntropy) -> felt252 { (value.last_updated_block.into() + (value.last_updated_time.into() * TWO_POW_64) @@ -18,7 +20,9 @@ impl GameEntropyPacking of StorePacking { .unwrap() } + // @notice: unpacks a felt252 into a GameEntropy struct // @dev: entropy hash is calculated during unpack + // @param value: the felt252 to unpack fn unpack(value: felt252) -> GameEntropy { let packed = value.into(); let (packed, last_updated_block) = integer::U256DivRem::div_rem( @@ -40,9 +44,17 @@ impl GameEntropyPacking of StorePacking { #[generate_trait] impl ImplGameEntropy of IGameEntropy { + /// @notice Create a new instance of the GameEntropy struct + /// @param last_updated_block The block number when the game was last updated. + /// @param last_updated_time The timestamp when the game was last updated. + /// @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 } } + + /// @notice Calculate a hash based on the properties of the GameEntropy struct + /// @return A 252-bit hash value fn get_hash(self: GameEntropy) -> felt252 { let mut hash_span = ArrayTrait::::new(); hash_span.append(self.last_updated_block.into()); @@ -51,32 +63,63 @@ impl ImplGameEntropy of IGameEntropy { poseidon_hash_span(hash_span.span()) } + /// @notice Calculate the number of blocks produced per hour + /// @param previous_block_number The previous block's number. + /// @param previous_block_timestamp The timestamp of the previous block. + /// @param current_block_number The current block's number. + /// @param current_block_timestamp The timestamp of the current block. + /// @return The number of blocks produced per hour. fn calculate_blocks_per_hour( previous_block_number: u64, previous_block_timestamp: u64, current_block_number: u64, current_block_timestamp: u64 ) -> u64 { - let failsafe: u64 = 1; let block_number_diff = current_block_number - previous_block_number; let block_timestamp_diff = current_block_timestamp - previous_block_timestamp; block_number_diff * 3600 / block_timestamp_diff } + /// @notice Calculate the current rate of blocks produced per hour, based on a ten-minute window. + /// @return The number of blocks produced per hour. + #[inline(always)] + fn current_blocks_per_hour(self: GameEntropy) -> u64 { + let blocks_per_ten_minutes = self.next_update_block - self.last_updated_block; + blocks_per_ten_minutes * 6 + } + + /// @notice Calculate the next scheduled update block based on the current block and block production rate. + /// @param current_block The current block number. + /// @param blocks_per_hour The estimated block production rate per hour. + /// @return The block number for the next update. + #[inline(always)] fn calculate_next_update_block(current_block: u64, blocks_per_hour: u64) -> u64 { let blocks_per_ten_mins = blocks_per_hour / 6; current_block + blocks_per_ten_mins } - // @dev player idleness is based on the game entropy rotation interval - // this interval dynamically adjusts to the blockspeed of starknet. - // Players must act within the interval to avoid being able to let entropy rotate and change their outcome. + /// @notice Get the rate limit based on the current block production rate. + /// @return The rate limit. + #[inline(always)] + fn get_rate_limit(self: GameEntropy) -> u64 { + let blocks_per_hour = self.current_blocks_per_hour(); + if blocks_per_hour < 120 { + return (120 / blocks_per_hour); + } else { + return 1; + } + } + + /// @notice Determine if an adventurer is idle based on the number of idle blocks. + /// @param idle_blocks The number of blocks the adventurer has been idle. + /// @return True if the adventurer is idle, false otherwise. #[inline(always)] fn is_adventurer_idle(self: GameEntropy, idle_blocks: u64) -> bool { idle_blocks > self.get_idle_penalty_blocks() } - // @dev idle penalty is one less than the game entropy rotation interval + /// @notice Get the number of penalty blocks for being idle. + /// @return The number of penalty blocks. #[inline(always)] fn get_idle_penalty_blocks(self: GameEntropy) -> u64 { self.next_update_block - self.last_updated_block - 1 @@ -141,6 +184,16 @@ mod tests { assert(next_entropy_rotation == 21, 'wrong rotation, fast speed'); } + #[test] + #[available_gas(8350)] + fn test_current_blocks_per_hour() { + let game_entropy = GameEntropy { + 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)] fn test_calculate_blocks_per_hour() {