Skip to content

Commit

Permalink
reduce start delay blocks (#559)
Browse files Browse the repository at this point in the history
* allows clients to optimistically provide contract with the block hash for start_block + 1
* contract will verify provided hash was correct as part of highscore check and prior to minting collectible beast
* contract provides a public slay_invalid_adventurers that can be used to slay adventurers started with wrong hash
* this change is backwards compatible with existing clients. Calling attack() after 11 blocks will work same as it did previously but now clients can call set_starting_entropy(adventurer_id, block_hash) when start_block + 1 hash is available and proceed to call attack() immediately after.
  • Loading branch information
loothero authored Mar 8, 2024
1 parent 43c0eb3 commit 8474dd9
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 61 deletions.
2 changes: 1 addition & 1 deletion Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "openzeppelin"
version = "0.9.0"
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#861fc416f87addbe23a3b47f9d19ab27c10d5dc8"
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#364db5b1aecc1335d2e65db887291d19aa28937d"

[[package]]
name = "survivor"
Expand Down
21 changes: 21 additions & 0 deletions contracts/adventurer/src/adventurer.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,17 @@ impl ImplAdventurer of IAdventurer {
hash_span.append(start_hash);
poseidon_hash_span(hash_span.span())
}

/// @title invalidate_adventurer
/// @notice This function invalidates an adventurer by setting its xp to 1 and gold to 0.
/// @dev This function directly modifies the state of the adventurer.
/// @param self The Adventurer struct instance to be invalidated.
#[inline(always)]
fn invalidate_game(ref self: Adventurer) {
self.health = 0;
self.xp = 1;
self.gold = 0;
}
}


Expand Down Expand Up @@ -5255,4 +5266,14 @@ mod tests {
assert(adventurer.is_ambushed(5), 'should be ambushed 5');
assert(!adventurer.is_ambushed(6), 'should not be ambushed 6');
}

#[test]
#[available_gas(1000000)]
fn test_invalidate_game() {
let mut adventurer = ImplAdventurer::new(ItemId::Wand);
adventurer.invalidate_game();
assert(adventurer.health == 0, 'adventurer health should be 0');
assert(adventurer.xp == 1, 'adventurer xp should be 1');
assert(adventurer.gold == 0, 'adventurer gold should be 0');
}
}
5 changes: 5 additions & 0 deletions contracts/game/src/game/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ mod messages {
const NOT_OWNER_OF_TOKEN: felt252 = 'Not owner of token';
const MA_PERIOD_LESS_THAN_WEEK: felt252 = 'MA period too small';
const TERMINAL_TIME_REACHED: felt252 = 'terminal time reached';
const STARTING_ENTROPY_ALREADY_SET: felt252 = 'starting entropy already set';
const STARTING_ENTROPY_ZERO: felt252 = 'block hash should not be zero';
const GAME_ALREADY_STARTED: felt252 = 'game already started';
const STARTING_ENTROPY_IS_VALID: felt252 = 'starting entropy is valid';
const VALID_BLOCK_HASH_UNAVAILABLE: felt252 = 'valid hash not yet available';
}

// TODO: Update for mainnet
Expand Down
5 changes: 5 additions & 0 deletions contracts/game/src/game/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ trait IGame<TContractState> {
fn new_game(
ref self: TContractState, client_reward_address: ContractAddress, weapon: u8, name: u128, golden_token_id: u256, interface_camel: bool
);
fn set_starting_entropy(ref self: TContractState, adventurer_id: felt252, block_hash: felt252);
fn explore(ref self: TContractState, adventurer_id: felt252, till_beast: bool);
fn attack(ref self: TContractState, adventurer_id: felt252, to_the_death: bool);
fn flee(ref self: TContractState, adventurer_id: felt252, to_the_death: bool);
Expand All @@ -29,6 +30,7 @@ trait IGame<TContractState> {
items: Array<ItemPurchase>,
);
fn slay_idle_adventurers(ref self: TContractState, adventurer_ids: Array<felt252>);
fn slay_invalid_adventurers(ref self: TContractState, adventurer_ids: Array<felt252>);
fn rotate_game_entropy(ref self: TContractState);
fn update_cost_to_play(ref self: TContractState);
fn initiate_price_change(ref self: TContractState);
Expand All @@ -39,6 +41,8 @@ trait IGame<TContractState> {
fn get_adventurer(self: @TContractState, adventurer_id: felt252) -> Adventurer;
fn get_adventurer_no_boosts(self: @TContractState, adventurer_id: felt252) -> Adventurer;
fn get_adventurer_meta(self: @TContractState, adventurer_id: felt252) -> AdventurerMetadata;
fn get_adventurer_starting_entropy(self: @TContractState, adventurer_id: felt252) -> felt252;
fn get_adventurer_entropy(self: @TContractState, adventurer_id: felt252) -> felt252;
fn get_health(self: @TContractState, adventurer_id: felt252) -> u16;
fn get_xp(self: @TContractState, adventurer_id: felt252) -> u16;
fn get_level(self: @TContractState, adventurer_id: felt252) -> u8;
Expand All @@ -48,6 +52,7 @@ trait IGame<TContractState> {
fn get_actions_per_block(self: @TContractState, adventurer_id: felt252) -> u8;
fn get_reveal_block(self: @TContractState, adventurer_id: felt252) -> u64;
fn is_idle(self: @TContractState, adventurer_id: felt252) -> (bool, u16);
fn get_contract_calculated_entropy(self: @TContractState, adventurer_id: felt252) -> felt252;

// adventurer stats (includes boost)
fn get_stats(self: @TContractState, adventurer_id: felt252) -> Stats;
Expand Down
158 changes: 144 additions & 14 deletions contracts/game/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ mod Game {
_cost_to_play: u128,
_games_played_snapshot: GamesPlayedSnapshot,
_terminal_timestamp: u64,
_starting_entropy: LegacyMap::<felt252, felt252>,
}

#[event]
Expand Down Expand Up @@ -224,6 +225,31 @@ mod Game {
_start_game(ref self, weapon, name, interface_camel);
}

fn set_starting_entropy(
ref self: ContractState, adventurer_id: felt252, block_hash: felt252
) {
// only owner of the adventurer can set starting entropy
_assert_ownership(@self, adventurer_id);

// player can only call this before starting game (defeating starter beast)
assert(_load_adventurer(@self, adventurer_id).xp == 0, messages::GAME_ALREADY_STARTED);

// prevent client from trying to set start entropy prior to valid block hash being available
_assert_valid_block_hash_available(@self, adventurer_id);

// verify the block_hash is not zero
assert(block_hash != 0, messages::STARTING_ENTROPY_ZERO);

// starting entropy can only be set once
assert(
self._starting_entropy.read(adventurer_id) == 0,
messages::STARTING_ENTROPY_ALREADY_SET
);

// save starting entropy
self._starting_entropy.write(adventurer_id, block_hash);
}

/// @title Explore Function
///
/// @notice Allows an adventurer to explore
Expand Down Expand Up @@ -433,7 +459,7 @@ mod Game {

// if adventurer died while attempting to flee, process death
if adventurer.health == 0 {
_process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0);
}
} else {
_apply_idle_penalty(ref self, adventurer_id, ref adventurer, num_blocks);
Expand Down Expand Up @@ -490,7 +516,7 @@ mod Game {

// if adventurer died from counter attack, process death
if (adventurer.health == 0) {
_process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0);
}
}

Expand Down Expand Up @@ -654,6 +680,19 @@ mod Game {
}
}

fn slay_invalid_adventurers(ref self: ContractState, adventurer_ids: Array<felt252>) {
let mut adventurer_index: u32 = 0;
loop {
if adventurer_index == adventurer_ids.len() {
break;
}
let adventurer_id = *adventurer_ids.at(adventurer_index);
_slay_invalid_adventurer(ref self, adventurer_id);
adventurer_index += 1;
}
}


/// @title Rotate Game Entropy Function
///
/// @notice Rotates the game entropy
Expand Down Expand Up @@ -687,6 +726,19 @@ mod Game {
fn get_adventurer_meta(self: @ContractState, adventurer_id: felt252) -> AdventurerMetadata {
_load_adventurer_metadata(self, adventurer_id)
}
fn get_adventurer_starting_entropy(
self: @ContractState, adventurer_id: felt252
) -> felt252 {
self._starting_entropy.read(adventurer_id)
}
fn get_contract_calculated_entropy(
self: @ContractState, adventurer_id: felt252
) -> felt252 {
_get_starting_block_hash(self, adventurer_id)
}
fn get_adventurer_entropy(self: @ContractState, adventurer_id: felt252) -> felt252 {
_get_adventurer_entropy(self, adventurer_id)
}
fn get_bag(self: @ContractState, adventurer_id: felt252) -> Bag {
_load_bag(self, adventurer_id)
}
Expand Down Expand Up @@ -1077,7 +1129,27 @@ mod Game {
adventurer.health = 0;

// handle adventurer death
_process_adventurer_death(ref self, adventurer, adventurer_id, 0, 0,);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, 0,);

// save adventurer (gg)
_save_adventurer_no_boosts(ref self, adventurer, adventurer_id);
}

fn _slay_invalid_adventurer(ref self: ContractState, adventurer_id: felt252) {
// unpack adventurer from storage (no need for stat boosts)
let mut adventurer = _load_adventurer_no_boosts(@self, adventurer_id);

// assert adventurer is not already dead
_assert_not_dead(adventurer);

// assert adventurer is invalid
_assert_invalid_starting_entropy(@self, adventurer_id);

// slay adventurer by setting health to 0
adventurer.health = 0;

// handle adventurer death
_process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, 0,);

// save adventurer (gg)
_save_adventurer_no_boosts(ref self, adventurer, adventurer_id);
Expand Down Expand Up @@ -1150,8 +1222,15 @@ mod Game {
owner_address,
_load_adventurer_metadata(@self, adventurer_id).interface_camel
);
// adventurers gets the beast
_mint_beast(@self, beast, primary_address);
// check if starting entropy is valid
if _is_starting_entropy_valid(@self, adventurer_id) {
// if starting entropy is valid, mint the beast
_mint_beast(@self, beast, primary_address);
} else {
// if starting entropy is not valid, kill adventurer
adventurer.health = 0;
_process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0);
}
}
}

Expand Down Expand Up @@ -1212,7 +1291,7 @@ mod Game {

fn _process_adventurer_death(
ref self: ContractState,
adventurer: Adventurer,
ref adventurer: Adventurer,
adventurer_id: felt252,
beast_id: u8,
obstacle_id: u8
Expand All @@ -1229,8 +1308,47 @@ mod Game {

__event_AdventurerDied(ref self, AdventurerDied { adventurer_state, death_details });

if _is_top_score(@self, adventurer.xp) {
_update_leaderboard(ref self, adventurer_id, adventurer);
// if starting entropy is valid
if _is_starting_entropy_valid(@self, adventurer_id) {
// and adventurer got a top score
if _is_top_score(@self, adventurer.xp) {
// update the leaderboard
_update_leaderboard(ref self, adventurer_id, adventurer);
}
} else {
// if starting entropy is not valid we invalidate adventurer's game
adventurer.invalidate_game();
}
}

fn _assert_invalid_starting_entropy(self: @ContractState, adventurer_id: felt252) {
assert(
!_is_starting_entropy_valid(self, adventurer_id), messages::STARTING_ENTROPY_IS_VALID
);
}

// @title Assert Valid Block Hash Availability
// @notice This function asserts that a valid block hash is available for the given adventurer.
// @dev If this function is called prior to 2 blocks from the start of the game for the given adventurer, an exception will be thrown.
// @param adventurer_id The ID of the adventurer for which to check the block hash availability.
fn _assert_valid_block_hash_available(self: @ContractState, adventurer_id: felt252) {
let current_block = starknet::get_block_info().unbox().block_number;
let adventurer_start_block = _load_adventurer_metadata(self, adventurer_id).start_block;
let block_hash_available = adventurer_start_block + 2;
assert(current_block >= block_hash_available, messages::VALID_BLOCK_HASH_UNAVAILABLE);
}

// @title Check if starting entropy is valid
// @notice This function checks if the starting entropy provided to the contract is equal to the entropy generated by the contract based on the block hash of the block after the player committed to playing the game.
// @dev If no manual starting entropy was provided, the starting entropy is considered valid. If a starting entropy was manually provided, it is compared with the block hash of the block after the player committed to playing the game.
// @param adventurer_id The ID of the adventurer for which to check the starting entropy.
// @return Returns true if the starting entropy is valid, false otherwise.
fn _is_starting_entropy_valid(self: @ContractState, adventurer_id: felt252) -> bool {
let starting_entropy = self._starting_entropy.read(adventurer_id);
if starting_entropy == 0 {
true
} else {
starting_entropy == _get_starting_block_hash(self, adventurer_id)
}
}

Expand Down Expand Up @@ -1575,7 +1693,7 @@ mod Game {
);
__event_AmbushedByBeast(ref self, adventurer, adventurer_id, beast_battle_details);
if (adventurer.health == 0) {
_process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0);
return;
}
} else {
Expand Down Expand Up @@ -1634,7 +1752,7 @@ mod Game {
ref self, adventurer, adventurer_id, dodged, obstacle_details
);
// process death
_process_adventurer_death(ref self, adventurer, adventurer_id, 0, obstacle.id);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, obstacle.id);
// return without granting xp to adventurer or items
return;
}
Expand Down Expand Up @@ -1858,7 +1976,7 @@ mod Game {

// if adventurer is dead
if (adventurer.health == 0) {
_process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0);
return;
}

Expand Down Expand Up @@ -2740,7 +2858,7 @@ mod Game {
__event_IdleDeathPenalty(ref self, adventurer, adventurer_id, idle_blocks);

// process adventurer death
_process_adventurer_death(ref self, adventurer, adventurer_id, 0, 0);
_process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, 0);
}
#[inline(always)]
fn _lords_address(self: @ContractState) -> ContractAddress {
Expand Down Expand Up @@ -2875,10 +2993,22 @@ mod Game {

#[inline(always)]
fn _get_adventurer_entropy(self: @ContractState, adventurer_id: felt252) -> felt252 {
// get the block the adventurer started the game on
// if player used optimistic start and manually set starting hash
let starting_entropy = self._starting_entropy.read(adventurer_id);
if starting_entropy != 0 {
// use it
starting_entropy
} else {
// otherwise use block hash
_get_starting_block_hash(self, adventurer_id)
}
}

fn _get_starting_block_hash(self: @ContractState, adventurer_id: felt252) -> felt252 {
// otherwise we'll get start entropy from the block hash after the start block
let start_block = _load_adventurer_metadata(self, adventurer_id).start_block;

// adventurer_
// don't force block delay on testnet to remove this source of friction
let chain_id = starknet::get_execution_info().unbox().tx_info.unbox().chain_id;
if chain_id == MAINNET_CHAIN_ID {
_get_mainnet_entropy(adventurer_id, start_block)
Expand Down
Loading

0 comments on commit 8474dd9

Please sign in to comment.