From 76141cdbd9cbe1895336afd5fc00ae7ab81dd52e Mon Sep 17 00:00:00 2001 From: Miles Date: Mon, 26 May 2025 09:40:27 +0800 Subject: [PATCH 01/23] feat: add settle_version field in game_context --- core/src/context.rs | 151 +++++++++++++++--- transactor/src/component/event_loop.rs | 11 +- .../src/component/event_loop/event_handler.rs | 121 +++++++++----- 3 files changed, 218 insertions(+), 65 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index 24334665..f5fcb1c0 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1,16 +1,19 @@ use crate::checkpoint::{Checkpoint, CheckpointOffChain, VersionedData}; -use crate::random::{RandomState, RandomStatus}; -use crate::types::{ClientMode, EntryType, GameAccount, GameSpec}; use crate::decision::DecisionState; use crate::error::{Error, Result}; +use crate::random::{RandomState, RandomStatus}; +use crate::types::{ClientMode, EntryType, GameAccount, GameSpec}; use borsh::{BorshDeserialize, BorshSerialize}; -use race_api::effect::{Ask, Assign, Effect, EmitBridgeEvent, Log, Release, Reveal, SubGame, Withdraw}; +use race_api::effect::{ + Ask, Assign, Effect, EmitBridgeEvent, Log, Release, Reveal, SubGame, Withdraw, +}; use race_api::engine::GameHandler; use race_api::event::{CustomEvent, Event}; use race_api::prelude::InitAccount; use race_api::random::RandomSpec; use race_api::types::{ - Award, BalanceChange, Ciphertext, DecisionId, EntryLock, GameId, GamePlayer, GameStatus, PlayerBalance, RandomId, SecretDigest, SecretShare, Settle, Transfer + Award, BalanceChange, Ciphertext, DecisionId, EntryLock, GameId, GamePlayer, GameStatus, + PlayerBalance, RandomId, SecretDigest, SecretShare, Settle, Transfer, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -29,7 +32,10 @@ pub struct Versions { impl Versions { pub fn new(access_version: u64, settle_version: u64) -> Self { - Self { access_version, settle_version } + Self { + access_version, + settle_version, + } } } @@ -123,7 +129,6 @@ impl ContextPlayer { } } - #[derive(Debug, Clone)] pub enum SubGameInitSource { FromCheckpoint(VersionedData), @@ -137,7 +142,6 @@ pub struct SubGameInit { pub source: SubGameInitSource, } - /// The effects of an event, indicates what actions should be taken /// after the event handling. /// @@ -159,6 +163,22 @@ pub struct EventEffects { pub reject_deposits: Vec, } +#[derive(Clone, Debug)] +pub struct CheckpointParams { + pub game_id: GameId, + pub settles: Vec, + pub transfer: Option, + pub awards: Vec, + pub checkpoint: Checkpoint, + pub access_version: u64, + pub settle_version: u64, + pub previous_settle_version: u64, + pub state_sha: String, + pub entry_lock: Option, + pub accept_deposits: Vec, + pub settle_locks: HashMap, +} + /// The context for public data. /// /// This information is not transmitted over the network, instead it's @@ -217,11 +237,17 @@ pub struct GameContext { pub(crate) state_sha: String, /// Accepted deposits, saved for later use in settlement pub(crate) accept_deposits: Vec, + pub(crate) pending_checkpoints: Vec, + pub(crate) next_settle_locks: HashMap, } impl GameContext { pub fn try_new_with_sub_game_spec(init: SubGameInit) -> Result { - let SubGameInit { spec, nodes, source } = init; + let SubGameInit { + spec, + nodes, + source, + } = init; let (handler_state, versions, init_data, checkpoint) = match source { SubGameInitSource::FromCheckpoint(versioned_data) => { @@ -283,10 +309,7 @@ impl GameContext { game_account.players.iter().for_each(|p| { // Only include those players joined before last checkpoint if p.access_version <= access_version { - players.push(ContextPlayer::new( - p.access_version, - p.position, - )); + players.push(ContextPlayer::new(p.access_version, p.position)); } nodes.push(Node::new_pending( @@ -329,7 +352,7 @@ impl GameContext { init_account: InitAccount { max_players: checkpoint_data.game_spec.max_players, data: vec![], // Not necessary? - } + }, }); } } @@ -351,6 +374,8 @@ impl GameContext { entry_type: game_account.entry_type.clone(), state_sha, accept_deposits: vec![], + pending_checkpoints: vec![], + next_settle_locks: HashMap::new(), }) } @@ -413,6 +438,48 @@ impl GameContext { &mut self.checkpoint } + pub fn pending_checkpoints_mut(&mut self) -> &mut Vec { + &mut self.pending_checkpoints + } + + pub fn take_first_ready_checkpoint(&mut self) -> Option { + if self + .pending_checkpoints + .first() + .is_some_and(|cp| cp.settle_locks.is_empty()) + { + let cp = self.pending_checkpoints.remove(0); + Some(cp) + } else { + None + } + } + + pub fn get_settle_locks(&self) -> HashMap { + self.next_settle_locks.clone() + } + + pub fn reset_settle_locks(&mut self) { + self.next_settle_locks.clear(); + } + + pub fn remove_settle_lock(&mut self, game_id: GameId, settle_version: u64) { + // remove from next_settle_lock + if let Some(&val) = self.next_settle_locks.get(&game_id) { + if settle_version > val { + self.next_settle_locks.remove(&game_id); + } + } + // remove from Checkpoints + for checkpoint in &mut self.pending_checkpoints { + if let Some(&val) = checkpoint.settle_locks.get(&game_id) { + if settle_version > val { + checkpoint.settle_locks.remove(&game_id); + } + } + } + } + pub fn set_handler_state(&mut self, handler: &H) where H: GameHandler, @@ -642,7 +709,10 @@ impl GameContext { } pub fn has_sub_game(&self, game_id: GameId) -> bool { - self.sub_games.iter().find(|sub_game| sub_game.id == game_id).is_some() + self.sub_games + .iter() + .find(|sub_game| sub_game.id == game_id) + .is_some() } pub fn init_random_state(&mut self, spec: RandomSpec) -> Result { @@ -932,12 +1002,19 @@ impl GameContext { self.decision_states.clear(); self.bump_settle_version()?; self.checkpoint.set_data(self.spec.game_id, state)?; - self.checkpoint.set_access_version(self.versions.access_version); + self.checkpoint + .set_access_version(self.versions.access_version); self.set_game_status(GameStatus::Idle); } else if is_init { self.bump_settle_version()?; - self.checkpoint.init_data(self.spec.game_id, self.spec.clone(), self.versions, state)?; - self.checkpoint.set_access_version(self.versions.access_version); + self.checkpoint.init_data( + self.spec.game_id, + self.spec.clone(), + self.versions, + state, + )?; + self.checkpoint + .set_access_version(self.versions.access_version); self.set_game_status(GameStatus::Idle); } @@ -953,6 +1030,21 @@ impl GameContext { settles = settles_map.into_values().collect(); } + if self.spec.game_id == 0 { + for sub_game in launch_sub_games.iter().cloned() { + self.next_settle_locks.insert(sub_game.id, 0); + } + + for evt in bridge_events.clone() { + let settle_version = self + .checkpoint + .get_versioned_data(evt.dest) + .map(|d| d.versions.settle_version) + .unwrap_or(0); + self.next_settle_locks.insert(evt.dest, settle_version); + } + } + return Ok(EventEffects { transfer, awards, @@ -1033,34 +1125,44 @@ impl Default for GameContext { entry_type: EntryType::Disabled, state_sha: "".into(), accept_deposits: vec![], + pending_checkpoints: vec![], + next_settle_locks: HashMap::new(), } } } -fn build_settles_map(withdraws: &[Withdraw], ejects: &[u64], old_balances: &[PlayerBalance], new_balances: &[PlayerBalance]) -> HashMap { - +fn build_settles_map( + withdraws: &[Withdraw], + ejects: &[u64], + old_balances: &[PlayerBalance], + new_balances: &[PlayerBalance], +) -> HashMap { // Build settles // Settle is a combination of Withdraw, Balance Diff, and Eject let mut settles_map: HashMap = HashMap::new(); for withdraw in withdraws { - settles_map.entry(withdraw.player_id) + settles_map + .entry(withdraw.player_id) .and_modify(|e| e.withdraw += withdraw.amount) .or_insert_with(|| Settle::new(withdraw.player_id, withdraw.amount, None, false)); } for eject in ejects.iter() { - settles_map.entry(*eject) + settles_map + .entry(*eject) .and_modify(|e| e.eject = true) .or_insert_with(|| Settle::new(*eject, 0, None, true)); } - let mut balances_change: HashMap = old_balances.iter() + let mut balances_change: HashMap = old_balances + .iter() .map(|orig_balance| (orig_balance.player_id, -(orig_balance.balance as i128))) .collect(); for balance in new_balances.iter() { - balances_change.entry(balance.player_id) + balances_change + .entry(balance.player_id) .and_modify(|e| *e += balance.balance as i128) .or_insert(balance.balance as i128); } @@ -1072,7 +1174,8 @@ fn build_settles_map(withdraws: &[Withdraw], ejects: &[u64], old_balances: &[Pla _ => None, }; if change.is_some() { - settles_map.entry(player_id) + settles_map + .entry(player_id) .and_modify(|e| e.change = change) .or_insert_with(|| Settle::new(player_id, 0, change, false)); } diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index d87bbc2d..204f4820 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -272,6 +272,9 @@ impl Component for EventLoop { } => { if ctx.game_mode == GameMode::Main { info!("SubGameReady: Update checkpoint for sub game: {}", game_id); + game_context + .remove_settle_lock(game_id, checkpoint_state.versions.settle_version); + if let Err(e) = game_context .checkpoint_mut() .init_versioned_data(checkpoint_state) @@ -280,7 +283,11 @@ impl Component for EventLoop { ports.send(EventFrame::Shutdown).await; } let timestamp = current_timestamp(); - let event = Event::SubGameReady { game_id, max_players, init_data }; + let event = Event::SubGameReady { + game_id, + max_players, + init_data, + }; if let Some(close_reason) = event_handler::handle_event( &mut handler, &mut game_context, @@ -313,6 +320,8 @@ impl Component for EventLoop { if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { info!("BridgeEvent: Update checkpoint for sub game: {}", from); + game_context.remove_settle_lock(from, settle_version); + if let Err(e) = game_context .checkpoint_mut() .update_versioned_data(checkpoint_state) diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index ec917dbb..58a34315 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -5,7 +5,9 @@ use race_api::{ types::{Award, EntryLock, Settle, Transfer}, }; use race_core::{ - context::{EventEffects, GameContext, SubGameInit, SubGameInitSource, Versions}, + context::{ + CheckpointParams, EventEffects, GameContext, SubGameInit, SubGameInitSource, Versions, + }, error::Error, types::{ClientMode, GameMode, GameSpec}, }; @@ -29,11 +31,7 @@ fn print_logs(logs: &[Log], env: &ComponentEnv) { }) } -async fn broadcast_event( - event: Event, - game_context: &GameContext, - ports: &PipelinePorts, -) { +async fn broadcast_event(event: Event, game_context: &GameContext, ports: &PipelinePorts) { ports .send(EventFrame::Broadcast { event, @@ -98,49 +96,96 @@ async fn send_reject_deposits( ports: &PipelinePorts, env: &ComponentEnv, ) { - - info!("{} Send reject deposits, {:?}", env.log_prefix, reject_deposits); + info!( + "{} Send reject deposits, {:?}", + env.log_prefix, reject_deposits + ); let ef = EventFrame::RejectDeposits { reject_deposits }; ports.send(ef).await; } -async fn send_settlement( +fn append_pending_checkpoint( transfer: Option, settles: Vec, awards: Vec, entry_lock: Option, original_versions: Versions, game_context: &mut GameContext, - ports: &PipelinePorts, - env: &ComponentEnv, ) { let checkpoint = game_context.checkpoint().clone(); - let checkpoint_size = checkpoint.get_data(game_context.game_id()).map(|d| d.len()); - info!( - "{} Create checkpoint, settle_version: {}, size: {:?}", - env.log_prefix, - game_context.settle_version(), - checkpoint_size, - ); - + // let checkpoint_size = checkpoint.get_data(game_context.game_id()).map(|d| d.len()); + let game_id = game_context.game_id(); let accept_deposits = game_context.take_accept_deposits(); - - ports - .send(EventFrame::Checkpoint { - access_version: game_context.access_version(), - settle_version: game_context.settle_version(), + let access_version = game_context.access_version(); + let settle_version = game_context.settle_version(); + let state_sha = game_context.state_sha(); + let settle_locks = game_context.get_settle_locks(); + game_context.reset_settle_locks(); + + game_context + .pending_checkpoints_mut() + .push(CheckpointParams { + game_id, + access_version, + settle_version, previous_settle_version: original_versions.settle_version, checkpoint: checkpoint.clone(), settles, transfer, awards, - state_sha: game_context.state_sha(), + state_sha, entry_lock, accept_deposits, - }) - .await; + settle_locks, + }); +} + +async fn do_send_settlements( + game_context: &mut GameContext, + ports: &PipelinePorts, + env: &ComponentEnv, +) { + while let Some(cp) = game_context.take_first_ready_checkpoint() { + let CheckpointParams { + game_id, + settles, + transfer, + awards, + checkpoint, + access_version, + settle_version, + previous_settle_version, + state_sha, + entry_lock, + accept_deposits, + .. + } = cp; + + let checkpoint_size = checkpoint.get_data(game_id).map(|d| d.len()); + info!( + "{} Create checkpoint, settle_version: {}, size: {:?}", + env.log_prefix, + game_context.settle_version(), + checkpoint_size, + ); + + ports + .send(EventFrame::Checkpoint { + access_version, + settle_version, + previous_settle_version, + checkpoint, + settles, + transfer, + awards, + state_sha, + entry_lock, + accept_deposits, + }) + .await; + } } async fn launch_sub_game( @@ -205,9 +250,7 @@ pub async fn init_state( } }; - let EventEffects { - checkpoint, .. - } = effects; + let EventEffects { checkpoint, .. } = effects; info!( "{} Initialize game state, access_version: {}, settle_version: {}, SHA: {}", @@ -222,17 +265,16 @@ pub async fn init_state( return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); }; - send_settlement( + append_pending_checkpoint( None, vec![], vec![], None, original_versions, &mut game_context, - ports, - env, - ) - .await; + ); + + do_send_settlements(game_context, ports, env).await; // Dispatch the initial Ready event if running in Transactor mode. if client_mode == ClientMode::Transactor { @@ -361,19 +403,18 @@ pub async fn handle_event( } if checkpoint.is_some() { - send_settlement( + append_pending_checkpoint( transfer, settles, awards, entry_lock, original_versions, game_context, - ports, - env, - ) - .await; + ); } + do_send_settlements(game_context, ports, env).await; + // Emit bridge events if client_mode == ClientMode::Transactor { send_bridge_event(bridge_events, &game_context, ports, env).await; From af484adccae86bf68e47774f830f2fd35f1ca09d Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Tue, 27 May 2025 01:33:49 +0800 Subject: [PATCH 02/23] Refactor Checkpoint into Checkpoint and Settle --- api/src/types.rs | 2 +- core/src/context.rs | 169 +++++++++++++---- transactor/src/component/broadcaster.rs | 103 ++++++++++- transactor/src/component/event_loop.rs | 8 +- .../src/component/event_loop/event_handler.rs | 173 ++++++------------ transactor/src/component/submitter.rs | 54 +++--- transactor/src/component/wrapped_client.rs | 1 + transactor/src/component/wrapped_handler.rs | 3 + transactor/src/frame.rs | 16 +- 9 files changed, 334 insertions(+), 195 deletions(-) diff --git a/api/src/types.rs b/api/src/types.rs index 159de474..5067ebc5 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -66,7 +66,7 @@ impl Settle { } } -#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct Award { diff --git a/core/src/context.rs b/core/src/context.rs index f5fcb1c0..5bc8903b 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -151,21 +151,16 @@ pub struct SubGameInit { /// - start_game: to start game. #[derive(Debug, Default, PartialEq, Eq)] pub struct EventEffects { - pub settles: Vec, - pub transfer: Option, - pub awards: Vec, - pub checkpoint: Option, pub launch_sub_games: Vec, pub bridge_events: Vec, pub start_game: bool, - pub entry_lock: Option, pub logs: Vec, pub reject_deposits: Vec, + pub checkpoint: Option, } -#[derive(Clone, Debug)] -pub struct CheckpointParams { - pub game_id: GameId, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SettleDetails { pub settles: Vec, pub transfer: Option, pub awards: Vec, @@ -173,7 +168,6 @@ pub struct CheckpointParams { pub access_version: u64, pub settle_version: u64, pub previous_settle_version: u64, - pub state_sha: String, pub entry_lock: Option, pub accept_deposits: Vec, pub settle_locks: HashMap, @@ -237,7 +231,8 @@ pub struct GameContext { pub(crate) state_sha: String, /// Accepted deposits, saved for later use in settlement pub(crate) accept_deposits: Vec, - pub(crate) pending_checkpoints: Vec, + /// The pending settle details. They are here because the settle is blocked by the settle locks. + pub(crate) pending_settle_details: Vec, pub(crate) next_settle_locks: HashMap, } @@ -374,16 +369,16 @@ impl GameContext { entry_type: game_account.entry_type.clone(), state_sha, accept_deposits: vec![], - pending_checkpoints: vec![], + pending_settle_details: vec![], next_settle_locks: HashMap::new(), }) } - pub fn init_account(&self) -> Result { - Ok(InitAccount { + pub fn init_account(&self) -> InitAccount { + InitAccount { max_players: self.spec.max_players, data: self.init_data.clone(), - }) + } } pub fn checkpoint_is_empty(&self) -> bool { @@ -438,31 +433,23 @@ impl GameContext { &mut self.checkpoint } - pub fn pending_checkpoints_mut(&mut self) -> &mut Vec { - &mut self.pending_checkpoints + pub fn pending_settle_details_mut(&mut self) -> &mut Vec { + &mut self.pending_settle_details } - pub fn take_first_ready_checkpoint(&mut self) -> Option { + pub fn take_first_ready_settle_details(&mut self) -> Option { if self - .pending_checkpoints + .pending_settle_details .first() .is_some_and(|cp| cp.settle_locks.is_empty()) { - let cp = self.pending_checkpoints.remove(0); + let cp = self.pending_settle_details.remove(0); Some(cp) } else { None } } - pub fn get_settle_locks(&self) -> HashMap { - self.next_settle_locks.clone() - } - - pub fn reset_settle_locks(&mut self) { - self.next_settle_locks.clear(); - } - pub fn remove_settle_lock(&mut self, game_id: GameId, settle_version: u64) { // remove from next_settle_lock if let Some(&val) = self.next_settle_locks.get(&game_id) { @@ -471,7 +458,7 @@ impl GameContext { } } // remove from Checkpoints - for checkpoint in &mut self.pending_checkpoints { + for checkpoint in &mut self.pending_settle_details { if let Some(&val) = checkpoint.settle_locks.get(&game_id) { if settle_version > val { checkpoint.settle_locks.remove(&game_id); @@ -946,6 +933,7 @@ impl GameContext { reject_deposits, mut accept_deposits, balances, + entry_lock, .. } = effect; @@ -994,6 +982,8 @@ impl GameContext { self.init_random_state(spec)?; } + let previous_settle_version = self.settle_version(); + if let Some(state) = handler_state { self.set_handler_state_raw(state.clone()); @@ -1028,8 +1018,10 @@ impl GameContext { let settles_map = build_settles_map(&withdraws, &ejects, &self.balances, &balances); self.balances = balances; settles = settles_map.into_values().collect(); + settles.sort_by_key(|s| s.player_id); } + if self.spec.game_id == 0 { for sub_game in launch_sub_games.iter().cloned() { self.next_settle_locks.insert(sub_game.id, 0); @@ -1045,17 +1037,29 @@ impl GameContext { } } + if is_checkpoint || is_init { + let settle_details = SettleDetails { + settles, + transfer, + awards, + checkpoint: self.checkpoint.clone(), + access_version: self.access_version(), + settle_version: self.settle_version(), + previous_settle_version, + entry_lock, + accept_deposits: self.accept_deposits.drain(..).collect(), + settle_locks: self.next_settle_locks.drain().collect(), + }; + self.pending_settle_details.push(settle_details); + } + return Ok(EventEffects { - transfer, - awards, - settles, - checkpoint: (is_checkpoint || is_init).then(|| self.checkpoint.clone()), launch_sub_games, bridge_events, start_game, - entry_lock: effect.entry_lock, logs: effect.logs, reject_deposits, + checkpoint: (is_checkpoint || is_init).then(|| self.checkpoint.clone()), }); } else if let Some(e) = error { return Err(Error::HandleError(e)); @@ -1125,12 +1129,13 @@ impl Default for GameContext { entry_type: EntryType::Disabled, state_sha: "".into(), accept_deposits: vec![], - pending_checkpoints: vec![], + pending_settle_details: vec![], next_settle_locks: HashMap::new(), } } } + fn build_settles_map( withdraws: &[Withdraw], ejects: &[u64], @@ -1184,13 +1189,105 @@ fn build_settles_map( return settles_map; } + #[cfg(test)] mod tests { - use race_api::effect::Withdraw; - + use race_api::effect::{EmitBridgeEvent, SubGame, Withdraw}; use super::*; + #[test] + fn test_apply_effect_with_all_fields() { + // Setting up a GameContext + let mut game_context = GameContext::default(); + game_context.versions = Versions::new(10, 10); + game_context.checkpoint = Checkpoint::new(0, GameSpec::default(), game_context.versions(), vec![1]); + game_context.add_node("server".into(), 0, ClientMode::Transactor); + game_context.add_node("alice".into(), 1, ClientMode::Player); + game_context.add_node("bob".into(), 2, ClientMode::Player); + game_context.set_node_ready(10); + + // Defining handler state + let handler_state = vec![1, 2, 3, 4, 5]; + + // Creating Effect with non-empty fields + let effect = Effect { + start_game: true, + stop_game: false, + cancel_dispatch: true, + action_timeout: None, + wait_timeout: Some(1000), + timestamp: 1234567890, + curr_random_id: 1, + curr_decision_id: 1, + nodes_count: 2, + asks: vec![], + assigns: vec![], + reveals: vec![], + releases: vec![], + init_random_states: vec![RandomSpec::deck_of_cards()], + revealed: HashMap::new(), + answered: HashMap::new(), + is_checkpoint: true, + withdraws: vec![Withdraw { + player_id: 1, + amount: 1000, + }], + ejects: vec![1], + handler_state: Some(handler_state.clone()), + error: None, + transfer: Some(Transfer { amount: 200 }), + launch_sub_games: vec![SubGame { + id: 1, + bundle_addr: String::from("address"), + init_account: InitAccount { + max_players: 4, + data: vec![], + }, + }], + bridge_events: vec![EmitBridgeEvent { + dest: 1, + raw: vec![1], + }], + is_init: false, + entry_lock: Some(EntryLock::Closed), + logs: vec![], + awards: vec![Award::new(1, String::from("bonus"))], + reject_deposits: vec![1, 2, 3], + accept_deposits: vec![4, 5, 6], + curr_sub_game_id: 1, + balances: vec![ + PlayerBalance::new(1, 1000), + PlayerBalance::new(2, 2000), + ], + }; + + // Apply the effect + let event_effects = game_context.apply_effect(effect).unwrap(); + + assert_eq!(event_effects.launch_sub_games.len(), 1); + assert_eq!(event_effects.bridge_events.len(), 1); + assert!(event_effects.start_game); + assert!(event_effects.checkpoint.is_some()); + assert_eq!(event_effects.reject_deposits, vec![1, 2, 3]); + assert_eq!(game_context.settle_version(), 11); + assert_eq!(game_context.pending_settle_details.len(), 1); + let settle_details = game_context.pending_settle_details[0].clone(); + assert_eq!(settle_details.settles, [ + Settle { player_id: 1, withdraw: 1000, change: Some(BalanceChange::Add(1000)), eject: true }, + Settle { player_id: 2, withdraw: 0, change: Some(BalanceChange::Add(2000)), eject: false }, + ]); + assert_eq!(settle_details.transfer, Some(Transfer { amount: 200 })); + assert_eq!(settle_details.awards, vec![Award::new(1, "bonus".into())]); + assert!(!settle_details.checkpoint.root.is_empty()); + assert_eq!(settle_details.access_version, 10); + assert_eq!(settle_details.settle_version, 11); + assert_eq!(settle_details.previous_settle_version, 10); + assert_eq!(settle_details.entry_lock, Some(EntryLock::Closed)); + assert_eq!(settle_details.accept_deposits, vec![4, 5, 6]); + assert_eq!(settle_details.settle_locks, HashMap::from([(1, 0)])); + } + // #[test] // fn test_build_settle_map() { // let withdraws = vec![ diff --git a/transactor/src/component/broadcaster.rs b/transactor/src/component/broadcaster.rs index eaeca2c9..7fcf1054 100644 --- a/transactor/src/component/broadcaster.rs +++ b/transactor/src/component/broadcaster.rs @@ -5,8 +5,8 @@ use std::collections::LinkedList; use std::sync::Arc; use async_trait::async_trait; -use race_api::types::GameId; use race_api::event::Event; +use race_api::types::GameId; use race_core::checkpoint::CheckpointOffChain; use race_core::types::{BroadcastFrame, BroadcastSync, TxState}; use tokio::sync::{broadcast, Mutex}; @@ -142,8 +142,10 @@ impl Broadcaster { // By default, returns the histories with settle_version // greater than the given one + info!("Get backlog with settle_version: {}", settle_version); if settle_version > 0 { for group in event_backup_groups.iter() { + info!("group settle version: {:?}", group.settle_version); if group.settle_version == settle_version { checkpoint_off_chain = group.checkpoint_off_chain.clone(); state_sha = group.state_sha.clone(); @@ -197,7 +199,6 @@ impl Component for Broadcaster { settle_version, checkpoint, state_sha, - .. } => { info!( "{} Create new history group (via Checkpoint) with access_version = {}, settle_version = {}", @@ -213,6 +214,7 @@ impl Component for Broadcaster { checkpoint_off_chain: Some(checkpoint.derive_offchain_part()), }); } + EventFrame::InitState { access_version, settle_version, @@ -233,6 +235,7 @@ impl Component for Broadcaster { state_sha: "".into(), }); } + EventFrame::TxState { tx_state } => match tx_state { TxState::SettleSucceed { .. } => { let r = ctx.broadcast_tx.send(BroadcastFrame::TxState { tx_state }); @@ -256,6 +259,7 @@ impl Component for Broadcaster { } } }, + EventFrame::Broadcast { event, timestamp, @@ -274,7 +278,7 @@ impl Component for Broadcaster { } else { error!("{} Received event without checkpoint", env.log_prefix); } - // Keep 10 groups at most + // Keep 200 groups at most if event_backup_groups.len() > 200 { event_backup_groups.pop_front(); } @@ -291,6 +295,7 @@ impl Component for Broadcaster { debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); } } + EventFrame::Sync { new_servers, new_players, @@ -306,7 +311,11 @@ impl Component for Broadcaster { transactor_addr, }; - ctx.event_backup_groups.lock().await.back_mut().map(|g| g.merge_sync(&sync)); + ctx.event_backup_groups + .lock() + .await + .back_mut() + .map(|g| g.merge_sync(&sync)); let broadcast_frame = BroadcastFrame::Sync { sync }; let r = ctx.broadcast_tx.send(broadcast_frame); @@ -332,7 +341,11 @@ impl Component for Broadcaster { transactor_addr, }; - ctx.event_backup_groups.lock().await.back_mut().map(|g| g.merge_sync(&sync)); + ctx.event_backup_groups + .lock() + .await + .back_mut() + .map(|g| g.merge_sync(&sync)); let broadcast_frame = BroadcastFrame::Sync { sync }; let r = ctx.broadcast_tx.send(broadcast_frame); @@ -359,9 +372,87 @@ impl Component for Broadcaster { #[cfg(test)] mod tests { use super::*; - use race_core::types::PlayerJoin; + use race_core::{checkpoint::Checkpoint, types::PlayerJoin}; use race_test::prelude::*; + #[tokio::test(flavor = "multi_thread", worker_threads = 3)] + async fn test_event_reorder() { + // Test how the events are grouped when the checkpoint is delayed due to settle_lock. + // Assume we have the following case, where in event handler, things happen in this order: + // InitState, CustomEvent, Checkpoint, CustomEvent + // Due to the settle lock, the emitted frames are in this order: + // InitState, CustomEvent, CustomEvent, Checkpoint + + tracing_subscriber::fmt::init(); + + let mut alice = TestClient::player("alice"); + let mut bob = TestClient::player("bob"); + let game_account = TestGameAccountBuilder::default() + .add_player(&mut alice, 100) + .add_player(&mut bob, 100) + .build(); + + let (broadcaster, ctx) = Broadcaster::init(game_account.addr.clone(), 0); + let handle = broadcaster.start("", ctx); + + { + let event_frame = EventFrame::InitState { + access_version: 0, + settle_version: 1, + checkpoint: Checkpoint::default(), + }; + handle.send_unchecked(event_frame).await; + + let event_frame = EventFrame::Broadcast { + event: Event::Custom { + sender: 1, + raw: vec![], + }, + state_sha: "1".into(), + timestamp: 1, + }; + handle.send_unchecked(event_frame).await; + + let event_frame = EventFrame::Checkpoint { + checkpoint: Checkpoint::default(), + access_version: 1, + settle_version: 2, + state_sha: "2".into(), + }; + handle.send_unchecked(event_frame).await; + + let event_frame = EventFrame::Broadcast { + event: Event::Custom { + sender: 2, + raw: vec![], + }, + state_sha: "3".into(), + timestamp: 1, + }; + handle.send_unchecked(event_frame).await; + } + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let broadcast_frame = broadcaster.get_backlogs(0).await; + assert_eq!( + broadcast_frame, + BroadcastFrame::Backlogs { + checkpoint_off_chain: Some(Checkpoint::default().derive_offchain_part()), + backlogs: Box::new(vec![BroadcastFrame::Sync { + sync: BroadcastSync { + new_players: vec![], + new_servers: vec![], + new_deposits: vec![], + transactor_addr: "".into(), + access_version: 1 + } + }]), + state_sha: "2".into(), + } + ); + } + #[tokio::test] async fn test_broadcast_event() { let mut alice = TestClient::player("alice"); diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 204f4820..b467fcf9 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -56,7 +56,7 @@ impl Component for EventLoop { // An init account is created during the process and is passed to the init_state // function of the game handler. // - // Recover from chcekpoint. When the checkpoint is available, there's no need to call + // Recover from checkpoint. When the checkpoint is available, there's no need to call // init_state from the game handler. if !game_context.handler_is_initialized() { if let Some(close_reason) = event_handler::init_state( @@ -265,7 +265,7 @@ impl Component for EventLoop { } EventFrame::SubGameReady { - checkpoint_state, + versioned_data, game_id, init_data, max_players, @@ -273,11 +273,11 @@ impl Component for EventLoop { if ctx.game_mode == GameMode::Main { info!("SubGameReady: Update checkpoint for sub game: {}", game_id); game_context - .remove_settle_lock(game_id, checkpoint_state.versions.settle_version); + .remove_settle_lock(game_id, versioned_data.versions.settle_version); if let Err(e) = game_context .checkpoint_mut() - .init_versioned_data(checkpoint_state) + .init_versioned_data(versioned_data) { error!("{} Failed to init checkpoint data: {:?}", env.log_prefix, e); ports.send(EventFrame::Shutdown).await; diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 58a34315..ad3f8fe2 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -2,12 +2,11 @@ use super::misc::log_execution_context; use race_api::{ effect::{EmitBridgeEvent, Log, SubGame}, event::Event, - types::{Award, EntryLock, Settle, Transfer}, + prelude::InitAccount, }; use race_core::{ - context::{ - CheckpointParams, EventEffects, GameContext, SubGameInit, SubGameInitSource, Versions, - }, + checkpoint::{Checkpoint, VersionedData}, + context::{EventEffects, GameContext, SubGameInit, SubGameInitSource}, error::Error, types::{ClientMode, GameMode, GameSpec}, }; @@ -41,6 +40,23 @@ async fn broadcast_event(event: Event, game_context: &GameContext, ports: &Pipel .await; } +async fn send_checkpoint( + checkpoint: Option, + game_context: &GameContext, + ports: &PipelinePorts, +) { + if let Some(checkpoint) = checkpoint { + ports + .send(EventFrame::Checkpoint { + checkpoint, + access_version: game_context.access_version(), + settle_version: game_context.settle_version(), + state_sha: game_context.state_sha(), + }) + .await; + } +} + async fn update_local_client(game_context: &GameContext, ports: &PipelinePorts) { ports .send(EventFrame::ContextUpdated { @@ -57,6 +73,22 @@ async fn send_start_game(game_context: &GameContext, ports: &PipelinePorts) { .await; } +async fn send_subgame_ready( + versioned_data: VersionedData, + game_context: &GameContext, + init_account: InitAccount, + ports: &PipelinePorts, +) { + ports + .send(EventFrame::SubGameReady { + versioned_data, + game_id: game_context.game_id(), + max_players: game_context.max_players(), + init_data: init_account.data, + }) + .await; +} + async fn send_bridge_event( bridge_events: Vec, game_context: &GameContext, @@ -106,83 +138,21 @@ async fn send_reject_deposits( ports.send(ef).await; } -fn append_pending_checkpoint( - transfer: Option, - settles: Vec, - awards: Vec, - entry_lock: Option, - original_versions: Versions, - game_context: &mut GameContext, -) { - let checkpoint = game_context.checkpoint().clone(); - // let checkpoint_size = checkpoint.get_data(game_context.game_id()).map(|d| d.len()); - let game_id = game_context.game_id(); - let accept_deposits = game_context.take_accept_deposits(); - let access_version = game_context.access_version(); - let settle_version = game_context.settle_version(); - let state_sha = game_context.state_sha(); - let settle_locks = game_context.get_settle_locks(); - game_context.reset_settle_locks(); - - game_context - .pending_checkpoints_mut() - .push(CheckpointParams { - game_id, - access_version, - settle_version, - previous_settle_version: original_versions.settle_version, - checkpoint: checkpoint.clone(), - settles, - transfer, - awards, - state_sha, - entry_lock, - accept_deposits, - settle_locks, - }); -} - async fn do_send_settlements( game_context: &mut GameContext, ports: &PipelinePorts, env: &ComponentEnv, ) { - while let Some(cp) = game_context.take_first_ready_checkpoint() { - let CheckpointParams { - game_id, - settles, - transfer, - awards, - checkpoint, - access_version, - settle_version, - previous_settle_version, - state_sha, - entry_lock, - accept_deposits, - .. - } = cp; - - let checkpoint_size = checkpoint.get_data(game_id).map(|d| d.len()); + while let Some(settle_details) = game_context.take_first_ready_settle_details() { info!( - "{} Create checkpoint, settle_version: {}, size: {:?}", + "{} Send settlement, settle_version: {}", env.log_prefix, game_context.settle_version(), - checkpoint_size, ); ports - .send(EventFrame::Checkpoint { - access_version, - settle_version, - previous_settle_version, - checkpoint, - settles, - transfer, - awards, - state_sha, - entry_lock, - accept_deposits, + .send(EventFrame::Settle { + settle_details: Box::new(settle_details), }) .await; } @@ -230,12 +200,7 @@ pub async fn init_state( game_mode: GameMode, env: &ComponentEnv, ) -> Option { - let init_account = match game_context.init_account() { - Ok(init_account) => init_account, - Err(e) => return Some(CloseReason::Fault(e)), - }; - - let original_versions = game_context.versions(); + let init_account = game_context.init_account(); let effects = match handler.init_state(&mut game_context, &init_account) { Ok(effects) => { @@ -250,8 +215,6 @@ pub async fn init_state( } }; - let EventEffects { checkpoint, .. } = effects; - info!( "{} Initialize game state, access_version: {}, settle_version: {}, SHA: {}", env.log_prefix, @@ -260,19 +223,12 @@ pub async fn init_state( game_context.state_sha() ); - let Some(checkpoint) = checkpoint else { + let Some(checkpoint) = effects.checkpoint else { ports.send(EventFrame::Shutdown).await; return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); }; - append_pending_checkpoint( - None, - vec![], - vec![], - None, - original_versions, - &mut game_context, - ); + send_checkpoint(Some(checkpoint.clone()), game_context, ports).await; do_send_settlements(game_context, ports, env).await; @@ -284,16 +240,8 @@ pub async fn init_state( // Tell master game the subgame is successfully created. if game_mode == GameMode::Sub { let game_id = game_context.game_id(); - let checkpoint_state = checkpoint.get_versioned_data(game_id); - if let Some(checkpoint_state) = checkpoint_state { - ports - .send(EventFrame::SubGameReady { - checkpoint_state: checkpoint_state.clone(), - game_id: game_context.game_id(), - max_players: game_context.max_players(), - init_data: init_account.data, - }) - .await; + if let Some(versioned_data) = checkpoint.get_versioned_data(game_id) { + send_subgame_ready(versioned_data.clone(), game_context, init_account, ports).await; } else { ports.send(EventFrame::Shutdown).await; return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); @@ -342,6 +290,17 @@ pub async fn recover_from_checkpoint( game_context.dispatch_safe(Event::Ready, 0); } + // Tell master game the subgame is successfully created. + if game_mode == GameMode::Sub { + let game_id = game_context.game_id(); + if let Some(versioned_data) = game_context.checkpoint().get_versioned_data(game_id) { + send_subgame_ready(versioned_data.clone(), game_context, game_context.init_account(), ports).await; + } else { + ports.send(EventFrame::Shutdown).await; + return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); + } + } + None } @@ -361,21 +320,16 @@ pub async fn handle_event( ); game_context.set_timestamp(timestamp); - let original_versions = game_context.versions(); match handler.handle_event(game_context, &event) { Ok(effects) => { let EventEffects { - settles, - transfer, - awards, - checkpoint, launch_sub_games, bridge_events, start_game, - entry_lock, logs, reject_deposits, + checkpoint, } = effects; print_logs(&logs, env); @@ -385,6 +339,8 @@ pub async fn handle_event( broadcast_event(event, &game_context, ports).await; } + send_checkpoint(checkpoint, game_context, ports).await; + // Update the local client update_local_client(&game_context, ports).await; @@ -402,17 +358,6 @@ pub async fn handle_event( send_reject_deposits(reject_deposits, ports, env).await; } - if checkpoint.is_some() { - append_pending_checkpoint( - transfer, - settles, - awards, - entry_lock, - original_versions, - game_context, - ); - } - do_send_settlements(game_context, ports, env).await; // Emit bridge events diff --git a/transactor/src/component/submitter.rs b/transactor/src/component/submitter.rs index ce24a5d3..b0d850e3 100644 --- a/transactor/src/component/submitter.rs +++ b/transactor/src/component/submitter.rs @@ -3,6 +3,7 @@ use std::time::Duration; use async_trait::async_trait; use race_api::types::{Settle, Transfer}; +use race_core::context::SettleDetails; use race_core::error::Error; use race_core::storage::StorageT; use race_core::types::{GameAccount, SaveCheckpointParams, SettleParams, SettleResult, TxState}; @@ -50,7 +51,6 @@ fn merge_settle_with_same_player_id(a: &mut Settle, b: Settle) { } fn merge_settles(a: &mut Vec, mut b: Vec) { - for settle_from_a in a.iter_mut() { let (drained, v) = b .into_iter() @@ -242,18 +242,20 @@ impl Component for Submitter { while let Some(event) = ports.recv().await { match event { - EventFrame::Checkpoint { - settles, - transfer, - checkpoint, - awards, - access_version, - settle_version, - previous_settle_version, - entry_lock, - accept_deposits, - .. - } => { + EventFrame::Settle { settle_details } => { + let SettleDetails { + settles, + transfer, + awards, + checkpoint, + access_version, + settle_version, + previous_settle_version, + entry_lock, + accept_deposits, + .. + } = *settle_details; + let checkpoint_onchain = checkpoint.derive_onchain_part(); let checkpoint_offchain = checkpoint.derive_offchain_part(); @@ -350,18 +352,20 @@ mod tests { merge_settles(&mut settles1, settles2); assert_eq!( settles1, - vec![Settle { - player_id: 0, - withdraw: 400, - change: Some(race_api::types::BalanceChange::Add(100)), - eject: false - }, - Settle { - player_id: 1, - withdraw: 0, - change: Some(race_api::types::BalanceChange::Add(500)), - eject: false - }] + vec![ + Settle { + player_id: 0, + withdraw: 400, + change: Some(race_api::types::BalanceChange::Add(100)), + eject: false + }, + Settle { + player_id: 1, + withdraw: 0, + change: Some(race_api::types::BalanceChange::Add(500)), + eject: false + } + ] ) } } diff --git a/transactor/src/component/wrapped_client.rs b/transactor/src/component/wrapped_client.rs index d19a4e6e..4d6e0306 100644 --- a/transactor/src/component/wrapped_client.rs +++ b/transactor/src/component/wrapped_client.rs @@ -101,6 +101,7 @@ impl Component for WrappedClient { } } EventFrame::Checkpoint { .. } => { + info!("{} Flush secrets states", env.log_prefix); client.flush_secret_states(); } EventFrame::Shutdown => { diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index faa75142..0db0d30f 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -286,6 +286,7 @@ mod tests { WrappedHandler::load_by_path(bundle_path).unwrap() } + #[ignore] #[test] fn test_init_state() { let mut hdlr = make_wrapped_handler(); @@ -299,6 +300,7 @@ mod tests { ); } + #[ignore] #[test] fn test_handle_event() { let mut hdlr = make_wrapped_handler(); @@ -316,6 +318,7 @@ mod tests { assert_eq!(ctx.get_status(), GameStatus::Running); } + #[ignore] #[test] fn test_handle_custom_event() { let mut hdlr = make_wrapped_handler(); diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index bececa69..242cd605 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -1,8 +1,8 @@ use race_api::{ - event::{Event, Message}, types::{Award, EntryLock, Settle, GameId} + event::{Event, Message}, types::GameId }; use race_core::{ - checkpoint::{Checkpoint, VersionedData}, context::{GameContext, SubGameInit}, types::{ClientMode, PlayerDeposit, PlayerJoin, ServerJoin, Transfer, TxState, VoteType} + checkpoint::{Checkpoint, VersionedData}, context::{GameContext, SubGameInit, SettleDetails}, types::{ClientMode, PlayerDeposit, PlayerJoin, ServerJoin, TxState, VoteType} }; use crate::component::BridgeToParent; @@ -56,16 +56,13 @@ pub enum EventFrame { timestamp: u64, }, Checkpoint { - settles: Vec, - transfer: Option, - awards: Vec, checkpoint: Checkpoint, access_version: u64, settle_version: u64, - previous_settle_version: u64, state_sha: String, - entry_lock: Option, - accept_deposits: Vec, + }, + Settle { + settle_details: Box, }, Broadcast { event: Event, @@ -120,7 +117,7 @@ pub enum EventFrame { /// Subgames send this frame after they made their first checkpoint. SubGameReady { game_id: GameId, - checkpoint_state: VersionedData, + versioned_data: VersionedData, max_players: u16, init_data: Vec, }, @@ -165,6 +162,7 @@ impl std::fmt::Display for EventFrame { EventFrame::PlayerLeaving { .. } => write!(f, "PlayerLeaving"), EventFrame::SendEvent { event, .. } => write!(f, "SendEvent: {}", event), EventFrame::SendServerEvent { event, .. } => write!(f, "SendServerEvent: {}", event), + EventFrame::Settle { .. } => write!(f, "Settle"), EventFrame::Checkpoint { .. } => write!(f, "Checkpoint"), EventFrame::Broadcast { event, .. } => write!(f, "Broadcast: {}", event), EventFrame::SendMessage { message } => write!(f, "SendMessage: {}", message.sender), From 3ce04a38c629e51e6ef0cbf6f9d99647db5e4e8f Mon Sep 17 00:00:00 2001 From: Miles Date: Tue, 27 May 2025 06:26:45 +0800 Subject: [PATCH 03/23] fix: update pending settles subgame versioned data after recv bridge and subgame ready --- transactor/src/component/event_loop.rs | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index b467fcf9..b35e1426 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -3,6 +3,7 @@ use race_api::types::GameDeposit; use async_trait::async_trait; use race_api::event::Event; use race_core::context::GameContext; +use race_core::error::Error; use tracing::{error, info, warn}; use crate::component::common::{Component, PipelinePorts}; @@ -275,6 +276,19 @@ impl Component for EventLoop { game_context .remove_settle_lock(game_id, versioned_data.versions.settle_version); + if let Err(e) = game_context + .pending_settle_details_mut() + .iter_mut() + .map(|sd| sd.checkpoint.init_versioned_data(versioned_data.clone())) + .collect::, Error>>() + { + error!( + "{} Failed to init checkpoint data for pending settles: {:?}", + env.log_prefix, e + ); + ports.send(EventFrame::Shutdown).await; + } + if let Err(e) = game_context .checkpoint_mut() .init_versioned_data(versioned_data) @@ -322,6 +336,22 @@ impl Component for EventLoop { info!("BridgeEvent: Update checkpoint for sub game: {}", from); game_context.remove_settle_lock(from, settle_version); + if let Err(e) = game_context + .pending_settle_details_mut() + .iter_mut() + .map(|sd| { + sd.checkpoint + .update_versioned_data(checkpoint_state.clone()) + }) + .collect::, Error>>() + { + error!( + "{} Failed to set checkpoint data for pending settles: {:?}", + env.log_prefix, e + ); + ports.send(EventFrame::Shutdown).await; + } + if let Err(e) = game_context .checkpoint_mut() .update_versioned_data(checkpoint_state) From 50eff6555e92cbe140fce9f512dd465e27e5c46d Mon Sep 17 00:00:00 2001 From: Miles Date: Wed, 28 May 2025 02:28:55 +0800 Subject: [PATCH 04/23] fix: validator can not start up --- core/src/types/transactor_params.rs | 1 + transactor/src/component/connection.rs | 26 ++++++++-- transactor/src/handle/validator.rs | 66 ++++++++++++++------------ 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/core/src/types/transactor_params.rs b/core/src/types/transactor_params.rs index 9565229c..c46b68a3 100644 --- a/core/src/types/transactor_params.rs +++ b/core/src/types/transactor_params.rs @@ -16,6 +16,7 @@ pub struct AttachGameParams { #[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct CheckpointParams { pub settle_version: u64, } diff --git a/transactor/src/component/connection.rs b/transactor/src/component/connection.rs index 9bb60169..e01468cd 100644 --- a/transactor/src/component/connection.rs +++ b/transactor/src/component/connection.rs @@ -20,17 +20,21 @@ use jsonrpsee::{ rpc_params, ws_client::{WsClient, WsClientBuilder}, }; -use race_core::error::{Error, Result}; use race_core::types::{BroadcastFrame, SubscribeEventParams}; +use race_core::{ + checkpoint::CheckpointOffChain, + error::{Error, Result}, + types::CheckpointParams, +}; use race_core::{ connection::ConnectionT, encryptor::EncryptorT, types::{AttachGameParams, ExitGameParams, SubmitEventParams}, }; -use crate::{frame::EventFrame, utils::current_timestamp}; use crate::utils::base64_decode; use crate::{component::common::Attachable, utils::base64_encode}; +use crate::{frame::EventFrame, utils::current_timestamp}; /// A connection to local event bus, for transactor loopback. #[allow(dead_code)] @@ -187,7 +191,7 @@ impl RemoteConnection { } Err(e) => { warn!("Error in request[{}]: {:?}", method, e); - }, + } } if retries < self.max_retries { @@ -199,6 +203,22 @@ impl RemoteConnection { } } + pub async fn get_checkpoint_off_chain( + &self, + game_addr: &str, + params: CheckpointParams, + ) -> Result> { + let req = self.make_request_no_sig(game_addr, ¶ms)?; + + match self.request::>>("get_checkpoint", req).await { + Ok(Some(res)) => { + let checkpoint_off_chain = CheckpointOffChain::try_from_slice(&res)?; + Ok(Some(checkpoint_off_chain)) + } + _ => Ok(None), + } + } + pub async fn subscribe_events( &self, game_addr: &str, diff --git a/transactor/src/handle/validator.rs b/transactor/src/handle/validator.rs index 267a0c90..c94c5b38 100644 --- a/transactor/src/handle/validator.rs +++ b/transactor/src/handle/validator.rs @@ -5,11 +5,11 @@ use crate::component::{ Voter, WrappedClient, WrappedHandler, }; use crate::frame::{EventFrame, SignalFrame}; -use race_core::error::{Error, Result}; use race_core::context::GameContext; +use race_core::error::{Error, Result}; use race_core::storage::StorageT; use race_core::transport::TransportT; -use race_core::types::{ClientMode, GameMode, GetCheckpointParams, ServerAccount}; +use race_core::types::{CheckpointParams, ClientMode, GameMode, ServerAccount}; use race_encryptor::Encryptor; use race_env::TransactorConfig; use tokio::sync::mpsc; @@ -29,43 +29,57 @@ impl ValidatorHandle { server_account: &ServerAccount, encryptor: Arc, transport: Arc, - storage: Arc, + _storage: Arc, signal_tx: mpsc::Sender, _config: &TransactorConfig, ) -> Result { - info!( - "Start game handle for {} with Validator mode", - game_addr, - ); + info!("Start game handle for {} with Validator mode", game_addr,); let Some(game_account) = transport.get_game_account(game_addr).await? else { return Err(Error::GameAccountNotFound); }; - let checkpoint_off_chain = storage - .get_checkpoint(GetCheckpointParams { - game_addr: game_addr.to_owned(), + let transactor_addr = game_account + .transactor_addr + .as_ref() + .ok_or(Error::GameNotServed)?; + let transactor_account = transport + .get_server_account(transactor_addr) + .await? + .ok_or(Error::CantFindTransactor)?; + + let connection = Arc::new( + RemoteConnection::try_new( + &server_account.addr, + &transactor_account.endpoint, + encryptor.clone(), + ) + .await?, + ); + + let checkpoint_off_chain = connection.get_checkpoint_off_chain( + game_addr, + CheckpointParams { settle_version: game_account.settle_version, - }) - .await?; + }, + ).await?; + + // let checkpoint_off_chain = storage + // .get_checkpoint(GetCheckpointParams { + // game_addr: game_addr.to_owned(), + // settle_version: game_account.settle_version, + // }) + // .await?; let game_context = GameContext::try_new(&game_account, checkpoint_off_chain)?; let checkpoint = game_context.checkpoint().clone(); - let Some(bundle_account) = transport.get_game_bundle(&game_account.bundle_addr).await? else { + let Some(bundle_account) = transport.get_game_bundle(&game_account.bundle_addr).await? + else { return Err(Error::GameBundleNotFound); }; let handler = WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?; - let transactor_addr = game_account - .transactor_addr - .as_ref() - .ok_or(Error::GameNotServed)?; - let transactor_account = transport - .get_server_account(transactor_addr) - .await? - .ok_or(Error::CantFindTransactor)?; - info!("Creating components"); let event_bus = EventBus::new(game_account.addr.clone()); @@ -76,14 +90,6 @@ impl ValidatorHandle { EventLoop::init(handler, game_context, ClientMode::Validator, GameMode::Main); let mut event_loop_handle = event_loop.start(&game_account.addr, event_loop_ctx); - let connection = Arc::new( - RemoteConnection::try_new( - &server_account.addr, - &transactor_account.endpoint, - encryptor.clone(), - ) - .await?, - ); let (subscriber, subscriber_context) = Subscriber::init(&game_account, server_account, connection.clone()); let mut subscriber_handle = subscriber.start(&game_account.addr, subscriber_context); From 7c3be663ce40a59601eb48ad191192f7129a39be Mon Sep 17 00:00:00 2001 From: Miles Date: Thu, 29 May 2025 23:58:03 +0800 Subject: [PATCH 05/23] dev: add mtt-multi layout --- dev/Justfile | 3 +++ dev/layouts/mtt-multi.kdl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 dev/layouts/mtt-multi.kdl diff --git a/dev/Justfile b/dev/Justfile index ecd81ec7..3196d89c 100644 --- a/dev/Justfile +++ b/dev/Justfile @@ -10,6 +10,9 @@ demo-app: mtt-simple: zellij --layout layouts/mtt-simple.kdl +mtt-multi: + zellij --layout layouts/mtt-multi.kdl + ltmtt-simple: zellij --layout layouts/ltmtt-simple.kdl diff --git a/dev/layouts/mtt-multi.kdl b/dev/layouts/mtt-multi.kdl new file mode 100644 index 00000000..364c2e54 --- /dev/null +++ b/dev/layouts/mtt-multi.kdl @@ -0,0 +1,30 @@ +// The layout to launch facade and one transactors in Zellij + +layout { + pane split_direction="vertical" { + pane { + name "Facade" + focus true + cwd "../" + command "bash" + args "dev/scripts/facade-mtt-game.sh" + start_suspended true + } + pane split_direction="horizontal" { + pane { + name "Server 1" + command "just" + cwd "../" + args "dev-transactor" "dev/server-confs/server1.toml" + start_suspended true + } + pane { + name "Server 2" + command "just" + cwd "../" + args "dev-transactor" "dev/server-confs/server2.toml" + start_suspended true + } + } + } +} From aba81b7bcdb94ff89737016511776ebd0c3216db Mon Sep 17 00:00:00 2001 From: Miles Date: Sun, 1 Jun 2025 11:06:21 +0800 Subject: [PATCH 06/23] fix: avoid handle event when recv bridge event in validator mode --- transactor/src/component/event_loop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index b35e1426..46c5fcb7 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -271,7 +271,7 @@ impl Component for EventLoop { init_data, max_players, } => { - if ctx.game_mode == GameMode::Main { + if ctx.game_mode == GameMode::Main && ctx.client_mode == ClientMode::Transactor { info!("SubGameReady: Update checkpoint for sub game: {}", game_id); game_context .remove_settle_lock(game_id, versioned_data.versions.settle_version); From 50b05880fb304c14da7aecfc0146bd959b2d2d00 Mon Sep 17 00:00:00 2001 From: miles Date: Thu, 12 Jun 2025 23:59:18 +0800 Subject: [PATCH 07/23] chore: add event into versioned data --- core/src/checkpoint.rs | 14 +- core/src/context.rs | 4 +- js/sdk-core/src/checkpoint.ts | 366 +++++++++--------- test/src/context_helpers.rs | 2 +- test/src/handler_helpers.rs | 6 +- transactor/src/component/event_bridge.rs | 16 +- transactor/src/component/event_loop.rs | 8 +- .../src/component/event_loop/event_handler.rs | 21 +- transactor/src/frame.rs | 18 +- 9 files changed, 248 insertions(+), 207 deletions(-) diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs index 3c9a7118..bfc998ab 100644 --- a/core/src/checkpoint.rs +++ b/core/src/checkpoint.rs @@ -10,7 +10,7 @@ use crate::{context::Versions, error::Error}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use race_api::types::GameId; +use race_api::{event::Event, types::GameId}; use crate::types::GameSpec; /// Checkpoint represents the state snapshot of game. @@ -50,6 +50,7 @@ pub struct VersionedData { pub data: Vec, pub sha: Vec, pub game_spec: GameSpec, + pub event: Option, } impl Display for Checkpoint { @@ -77,6 +78,7 @@ impl Checkpoint { versions, data: root_data, sha: sha.into(), + event: None, }, )]), proofs: HashMap::new(), @@ -156,6 +158,7 @@ impl Checkpoint { sha: sha.into(), game_spec, versions, + event: None, }; v.insert(versioned_data); self.update_root_and_proofs(); @@ -188,6 +191,15 @@ impl Checkpoint { } } + pub fn set_event_in_versinoed_data(&mut self, id: GameId, event: Option) -> Result<(), Error> { + if let Some(versioned_data) = self.data.get_mut(&id) { + versioned_data.event = event; + Ok(()) + } else { + Ok(()) + } + } + pub fn list_versioned_data(&self) -> Vec<&VersionedData> { self.data.values().collect() } diff --git a/core/src/context.rs b/core/src/context.rs index 5bc8903b..2e0b5dee 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1021,7 +1021,6 @@ impl GameContext { settles.sort_by_key(|s| s.player_id); } - if self.spec.game_id == 0 { for sub_game in launch_sub_games.iter().cloned() { self.next_settle_locks.insert(sub_game.id, 0); @@ -1262,6 +1261,9 @@ mod tests { ], }; + // prepare event + let event = Event::Bridge { dest_game_id: 0, from_game_id: 1, raw: vec![] }; + // Apply the effect let event_effects = game_context.apply_effect(effect).unwrap(); diff --git a/js/sdk-core/src/checkpoint.ts b/js/sdk-core/src/checkpoint.ts index 3b46a3d0..557f2d87 100644 --- a/js/sdk-core/src/checkpoint.ts +++ b/js/sdk-core/src/checkpoint.ts @@ -1,222 +1,236 @@ -import { array, deserialize, field, map, option, struct } from '@race-foundation/borsh' +import { + array, + deserialize, + field, + map, + option, + struct, + enums, +} from '@race-foundation/borsh' import { sha256 } from './encryptor' import { Fields } from './types' +import { GameEvent } from './events' export class Versions { - @field('u64') - accessVersion!: bigint + @field('u64') + accessVersion!: bigint - @field('u64') - settleVersion!: bigint + @field('u64') + settleVersion!: bigint - static default(): Versions { - return new Versions({ accessVersion: 0n, settleVersion: 0n }) - } + static default(): Versions { + return new Versions({ accessVersion: 0n, settleVersion: 0n }) + } - constructor(fields: Fields) { - Object.assign(this, fields) - } + constructor(fields: Fields) { + Object.assign(this, fields) + } } export class CheckpointOnChain { - @field('u8-array') - root!: Uint8Array + @field('u8-array') + root!: Uint8Array - @field('usize') - size!: number + @field('usize') + size!: number - @field('u64') - accessVersion!: bigint + @field('u64') + accessVersion!: bigint - constructor(fields: any) { - Object.assign(this, fields) - } + constructor(fields: any) { + Object.assign(this, fields) + } - static fromRaw(raw: Uint8Array): CheckpointOnChain { - return deserialize(CheckpointOnChain, raw) - } + static fromRaw(raw: Uint8Array): CheckpointOnChain { + return deserialize(CheckpointOnChain, raw) + } } export class GameSpec { - @field('string') - readonly gameAddr!: string + @field('string') + readonly gameAddr!: string - @field('usize') - readonly gameId!: number + @field('usize') + readonly gameId!: number - @field('string') - readonly bundleAddr!: string + @field('string') + readonly bundleAddr!: string - @field('u16') - readonly maxPlayers!: number + @field('u16') + readonly maxPlayers!: number - constructor(fields: Fields) { - Object.assign(this, fields) - } + constructor(fields: Fields) { + Object.assign(this, fields) + } } export class VersionedData { - @field('usize') - id!: number + @field('usize') + id!: number - @field(struct(Versions)) - versions!: Versions + @field(struct(Versions)) + versions!: Versions - @field('u8-array') - data!: Uint8Array + @field('u8-array') + data!: Uint8Array - @field('u8-array') - sha!: Uint8Array + @field('u8-array') + sha!: Uint8Array - @field(struct(GameSpec)) - spec!: GameSpec + @field(struct(GameSpec)) + spec!: GameSpec - constructor(fields: any) { - Object.assign(this, fields) - } + @field(option(enums(GameEvent))) + event!: GameEvent | undefined + + constructor(fields: any) { + Object.assign(this, fields) + } } export class CheckpointOffChain { - @field(map('usize', struct(VersionedData))) - data!: Map + @field(map('usize', struct(VersionedData))) + data!: Map - @field(map('usize', 'u8-array')) - proofs!: Map + @field(map('usize', 'u8-array')) + proofs!: Map - constructor(fields: any) { - Object.assign(this, fields) - } + constructor(fields: any) { + Object.assign(this, fields) + } - static deserialize(raw: Uint8Array): CheckpointOffChain { - return deserialize(CheckpointOffChain, raw) - } + static deserialize(raw: Uint8Array): CheckpointOffChain { + return deserialize(CheckpointOffChain, raw) + } } export class CheckpointOffChainList { + @field(array(option(struct(CheckpointOffChain)))) + checkpoints!: (CheckpointOffChain | undefined)[] - @field(array(option(struct(CheckpointOffChain)))) - checkpoints!: (CheckpointOffChain | undefined)[] - - constructor(fields: any) { - Object.assign(this, fields) - } + constructor(fields: any) { + Object.assign(this, fields) + } - static deserialize(raw: Uint8Array): CheckpointOffChainList { - return deserialize(CheckpointOffChainList, raw) - } + static deserialize(raw: Uint8Array): CheckpointOffChainList { + return deserialize(CheckpointOffChainList, raw) + } } /// Represent the on-chain checkpoint. export class Checkpoint { - @field('u8-array') - root!: Uint8Array - - @field('u64') - accessVersion!: bigint - - @field(map('usize', struct(VersionedData))) - data!: Map - - @field(map('usize', 'u8-array')) - proofs!: Map - - constructor(fields: any) { - Object.assign(this, fields) - } - - static default(): Checkpoint { - return new Checkpoint({ accessVersion: 0n, data: new Map() }) - } - - static fromParts(offchainPart: CheckpointOffChain, onchainPart: CheckpointOnChain): Checkpoint { - let checkpoint = Checkpoint.default() - checkpoint.proofs = offchainPart.proofs - checkpoint.data = offchainPart.data - checkpoint.accessVersion = onchainPart.accessVersion - checkpoint.root = onchainPart.root - return checkpoint - } - - static fromRaw(raw: Uint8Array): Checkpoint { - if (raw.length === 0) { - return Checkpoint.default() - } - return deserialize(Checkpoint, raw) - } - - static fromData(id: number, version: bigint, data: Uint8Array): Checkpoint { - return new Checkpoint({ - data: new Map([ - [ - id, - new VersionedData({ - version, - data, - }), - ], - ]), - }) - } - - clone(): Checkpoint { - return new Checkpoint({ - accessVersion: this.accessVersion, - data: new Map(this.data.entries()), - }) - } - - getData(id: number): Uint8Array | undefined { - return this.data.get(id)?.data - } - - getSha(id: number): Uint8Array | undefined { - return this.data.get(id)?.sha - } - - setAccessVersion(accessVersion: bigint) { - this.accessVersion = accessVersion - } - - async initData(id: number, data: Uint8Array, spec: GameSpec) { - if (this.data.has(id)) { - throw new Error(`Checkpoint ${id} already exists`) - } - const sha = await sha256(data) - const versionedData = new VersionedData({ - id, + @field('u8-array') + root!: Uint8Array + + @field('u64') + accessVersion!: bigint + + @field(map('usize', struct(VersionedData))) + data!: Map + + @field(map('usize', 'u8-array')) + proofs!: Map + + constructor(fields: any) { + Object.assign(this, fields) + } + + static default(): Checkpoint { + return new Checkpoint({ accessVersion: 0n, data: new Map() }) + } + + static fromParts( + offchainPart: CheckpointOffChain, + onchainPart: CheckpointOnChain + ): Checkpoint { + let checkpoint = Checkpoint.default() + checkpoint.proofs = offchainPart.proofs + checkpoint.data = offchainPart.data + checkpoint.accessVersion = onchainPart.accessVersion + checkpoint.root = onchainPart.root + return checkpoint + } + + static fromRaw(raw: Uint8Array): Checkpoint { + if (raw.length === 0) { + return Checkpoint.default() + } + return deserialize(Checkpoint, raw) + } + + static fromData(id: number, version: bigint, data: Uint8Array): Checkpoint { + return new Checkpoint({ + data: new Map([ + [ + id, + new VersionedData({ + version, data, - spec, - sha, - versions: Versions.default(), - }) - this.data.set(id, versionedData) - this.updateRootAndProofs() - } - - async initVersionedData(versionedData: VersionedData) { - this.data.set(versionedData.id, versionedData) - } - - async setData(id: number, data: Uint8Array) { - const sha = await sha256(data) - const old = this.data.get(id) - if (old !== undefined) { - old.data = data - old.versions.settleVersion += 1n - old.sha = sha - } else { - throw new Error(`Checkpoint ${id} is missing`) - } - this.updateRootAndProofs() - } - - getVersionedData(id: number): VersionedData | undefined { - return this.data.get(id) - } - - containsVersionedData(id: number): boolean { - return this.data.has(id) - } - - updateRootAndProofs() {} + }), + ], + ]), + }) + } + + clone(): Checkpoint { + return new Checkpoint({ + accessVersion: this.accessVersion, + data: new Map(this.data.entries()), + }) + } + + getData(id: number): Uint8Array | undefined { + return this.data.get(id)?.data + } + + getSha(id: number): Uint8Array | undefined { + return this.data.get(id)?.sha + } + + setAccessVersion(accessVersion: bigint) { + this.accessVersion = accessVersion + } + + async initData(id: number, data: Uint8Array, spec: GameSpec) { + if (this.data.has(id)) { + throw new Error(`Checkpoint ${id} already exists`) + } + const sha = await sha256(data) + const versionedData = new VersionedData({ + id, + data, + spec, + sha, + versions: Versions.default(), + }) + this.data.set(id, versionedData) + this.updateRootAndProofs() + } + + async initVersionedData(versionedData: VersionedData) { + this.data.set(versionedData.id, versionedData) + } + + async setData(id: number, data: Uint8Array) { + const sha = await sha256(data) + const old = this.data.get(id) + if (old !== undefined) { + old.data = data + old.versions.settleVersion += 1n + old.sha = sha + } else { + throw new Error(`Checkpoint ${id} is missing`) + } + this.updateRootAndProofs() + } + + getVersionedData(id: number): VersionedData | undefined { + return this.data.get(id) + } + + containsVersionedData(id: number): boolean { + return this.data.has(id) + } + + updateRootAndProofs() {} } diff --git a/test/src/context_helpers.rs b/test/src/context_helpers.rs index 953cc901..f0839b9e 100644 --- a/test/src/context_helpers.rs +++ b/test/src/context_helpers.rs @@ -71,7 +71,7 @@ impl TestContext { } pub fn init_account(&self) -> Result { - self.context.init_account() + Ok(self.context.init_account()) } pub fn state(&self) -> &H { diff --git a/test/src/handler_helpers.rs b/test/src/handler_helpers.rs index 4f5313b8..4dec9498 100644 --- a/test/src/handler_helpers.rs +++ b/test/src/handler_helpers.rs @@ -29,10 +29,10 @@ where impl TestHandler { pub fn init_state(context: &mut GameContext) -> Result<(Self, EventEffects)> { let mut new_context = context.clone(); - let init_account = new_context.init_account()?; + let init_account = new_context.init_account(); let effect = new_context.derive_effect(true); let handler = H::init_state(init_account)?; - let event_effects = new_context.apply_effect(effect)?; + let event_effects = new_context.apply_effect(effect, None)?; patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); Ok((Self { handler }, event_effects)) @@ -44,7 +44,7 @@ impl TestHandler { general_handle_event(&mut new_context, event, &encryptor)?; let mut effect = new_context.derive_effect(false); self.handler.handle_event(&mut effect, event.to_owned())?; - let event_effects = new_context.apply_effect(effect)?; + let event_effects = new_context.apply_effect(effect, Some(event.clone()))?; patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); Ok(event_effects) diff --git a/transactor/src/component/event_bridge.rs b/transactor/src/component/event_bridge.rs index d3ccb9a4..2f92be46 100644 --- a/transactor/src/component/event_bridge.rs +++ b/transactor/src/component/event_bridge.rs @@ -111,8 +111,8 @@ impl Component for EventBridgeParent { from, dest, event, - access_version, - settle_version, + // access_version, + // settle_version, checkpoint_state, } => { info!("{} Receives event: {}", env.log_prefix, event); @@ -121,8 +121,8 @@ impl Component for EventBridgeParent { from, dest, event, - access_version, - settle_version, + // access_version, + // settle_version, checkpoint_state, }) .await; @@ -277,8 +277,8 @@ impl Component for EventBridgeChild { from, dest, event, - access_version, - settle_version, + // access_version, + // settle_version, checkpoint_state, } if dest == ctx.game_id => { info!("{} Receives {}", env.log_prefix, event); @@ -287,8 +287,8 @@ impl Component for EventBridgeChild { from, dest, event, - access_version, - settle_version, + // access_version, + // settle_version, checkpoint_state, }) .await; diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 46c5fcb7..08fbbdff 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -271,7 +271,8 @@ impl Component for EventLoop { init_data, max_players, } => { - if ctx.game_mode == GameMode::Main && ctx.client_mode == ClientMode::Transactor { + if ctx.game_mode == GameMode::Main && ctx.client_mode == ClientMode::Transactor + { info!("SubGameReady: Update checkpoint for sub game: {}", game_id); game_context .remove_settle_lock(game_id, versioned_data.versions.settle_version); @@ -324,13 +325,12 @@ impl Component for EventLoop { dest, from, checkpoint_state, - settle_version, .. } => { - // In the case of parent, update the child game's - // checkpoint value. + // In the case of parent, update the child game' checkpoint value. let timestamp = current_timestamp(); + let settle_version = checkpoint_state.versions.settle_version; if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { info!("BridgeEvent: Update checkpoint for sub game: {}", from); diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index ad3f8fe2..5373833e 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -115,8 +115,8 @@ async fn send_bridge_event( from_game_id: game_context.game_id(), raw: be.raw, }, - access_version: game_context.access_version(), - settle_version: game_context.settle_version(), + // access_version: game_context.access_version(), + // settle_version: game_context.settle_version(), checkpoint_state: checkpoint_state.clone(), }; ports.send(ef).await; @@ -241,6 +241,7 @@ pub async fn init_state( if game_mode == GameMode::Sub { let game_id = game_context.game_id(); if let Some(versioned_data) = checkpoint.get_versioned_data(game_id) { + println!("init sub game state"); send_subgame_ready(versioned_data.clone(), game_context, init_account, ports).await; } else { ports.send(EventFrame::Shutdown).await; @@ -291,10 +292,22 @@ pub async fn recover_from_checkpoint( } // Tell master game the subgame is successfully created. - if game_mode == GameMode::Sub { + if game_mode == GameMode::Sub && client_mode == ClientMode::Transactor { let game_id = game_context.game_id(); + if let Some(versioned_data) = game_context.checkpoint().get_versioned_data(game_id) { - send_subgame_ready(versioned_data.clone(), game_context, game_context.init_account(), ports).await; + println!("recover from checkpoint {:?}", versioned_data); + if let Some(event) = &versioned_data.event { + let frame = EventFrame::SendBridgeEvent { + from: game_id, + dest: 0, + event: event.clone(), + checkpoint_state: versioned_data.clone(), + }; + ports.send(frame).await; + } + + // send_subgame_ready(versioned_data.clone(), game_context, game_context.init_account(), ports).await; } else { ports.send(EventFrame::Shutdown).await; return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index 242cd605..be2933a3 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -86,8 +86,8 @@ pub enum EventFrame { from: GameId, dest: GameId, event: Event, - access_version: u64, - settle_version: u64, + // access_version: u64, + // settle_version: u64, checkpoint_state: VersionedData, }, /// Similar to `SendBridgeEvent`, but for receiver's event bus. @@ -95,9 +95,9 @@ pub enum EventFrame { from: GameId, dest: GameId, event: Event, - #[allow(unused)] - access_version: u64, - settle_version: u64, + // #[allow(unused)] + // access_version: u64, + // settle_version: u64, checkpoint_state: VersionedData, }, @@ -171,11 +171,11 @@ impl std::fmt::Display for EventFrame { EventFrame::Vote { votee, vote_type } => { write!(f, "Vote: to {} for {:?}", votee, vote_type) } - EventFrame::SendBridgeEvent { dest, event, settle_version, .. } => { - write!(f, "SendBridgeEvent: dest {}, settle_version: {}, event: {}", dest, settle_version, event) + EventFrame::SendBridgeEvent { dest, event, .. } => { + write!(f, "SendBridgeEvent: dest {}, event: {}", dest, event) } - EventFrame::RecvBridgeEvent { dest, event, settle_version, .. } => { - write!(f, "RecvBridgeEvent: dest {}, settle_version: {}, event: {}", dest, settle_version, event) + EventFrame::RecvBridgeEvent { dest, event, .. } => { + write!(f, "RecvBridgeEvent: dest {}, event: {}", dest, event) } EventFrame::LaunchSubGame { sub_game_init } => { write!(f, "LaunchSubGame: {}#{}", sub_game_init.spec.game_addr, sub_game_init.spec.game_id) From bc7a1919875a34a4b16feca62bb4410a691234b0 Mon Sep 17 00:00:00 2001 From: Miles Date: Fri, 13 Jun 2025 06:12:45 +0800 Subject: [PATCH 08/23] chore: change settle lock struct --- core/src/checkpoint.rs | 4 +- core/src/context.rs | 114 +++++++++++++++----- local-db/src/lib.rs | 3 + transactor/src/component/event_loop.rs | 5 +- transactor/src/component/wrapped_handler.rs | 4 +- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs index bfc998ab..116f39b6 100644 --- a/core/src/checkpoint.rs +++ b/core/src/checkpoint.rs @@ -191,11 +191,13 @@ impl Checkpoint { } } - pub fn set_event_in_versinoed_data(&mut self, id: GameId, event: Option) -> Result<(), Error> { + pub fn set_event_in_versioned_data(&mut self, id: GameId, event: Option) -> Result<(), Error> { if let Some(versioned_data) = self.data.get_mut(&id) { versioned_data.event = event; + println!("set event in versioned data: {:?} {:?}", id, versioned_data.event); Ok(()) } else { + println!("set event in versioned data no versioned data"); Ok(()) } } diff --git a/core/src/context.rs b/core/src/context.rs index 2e0b5dee..bab1d0af 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -170,7 +170,13 @@ pub struct SettleDetails { pub previous_settle_version: u64, pub entry_lock: Option, pub accept_deposits: Vec, - pub settle_locks: HashMap, + pub settle_locks: HashMap, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SettleLock { + Locked(u64), + Unlocked(VersionedData), } /// The context for public data. @@ -233,7 +239,7 @@ pub struct GameContext { pub(crate) accept_deposits: Vec, /// The pending settle details. They are here because the settle is blocked by the settle locks. pub(crate) pending_settle_details: Vec, - pub(crate) next_settle_locks: HashMap, + pub(crate) next_settle_locks: HashMap, } impl GameContext { @@ -450,19 +456,23 @@ impl GameContext { } } - pub fn remove_settle_lock(&mut self, game_id: GameId, settle_version: u64) { + pub fn remove_settle_lock(&mut self, game_id: GameId, versioned_data: VersionedData) { // remove from next_settle_lock - if let Some(&val) = self.next_settle_locks.get(&game_id) { - if settle_version > val { - self.next_settle_locks.remove(&game_id); + match self.next_settle_locks.entry(game_id) { + std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { + occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); + } + std::collections::hash_map::Entry::Vacant(vacant_entry) => { + vacant_entry.insert(SettleLock::Unlocked(versioned_data.clone())); } } // remove from Checkpoints - for checkpoint in &mut self.pending_settle_details { - if let Some(&val) = checkpoint.settle_locks.get(&game_id) { - if settle_version > val { - checkpoint.settle_locks.remove(&game_id); + for checkpoint in self.pending_settle_details.iter_mut() { + match checkpoint.settle_locks.entry(game_id) { + std::collections::hash_map::Entry::Occupied(occupied_entry) => { + occupied_entry.remove(); } + std::collections::hash_map::Entry::Vacant(_vacant_entry) => {} } } } @@ -908,7 +918,7 @@ impl GameContext { } } - pub fn apply_effect(&mut self, effect: Effect) -> Result { + pub fn apply_effect(&mut self, effect: Effect, event: Option) -> Result { let Effect { action_timeout, wait_timeout, @@ -1021,9 +1031,14 @@ impl GameContext { settles.sort_by_key(|s| s.player_id); } + let game_id = self.game_id(); + self.checkpoint_mut() + .set_event_in_versioned_data(game_id, event)?; + if self.spec.game_id == 0 { for sub_game in launch_sub_games.iter().cloned() { - self.next_settle_locks.insert(sub_game.id, 0); + self.next_settle_locks + .insert(sub_game.id, SettleLock::Locked(0)); } for evt in bridge_events.clone() { @@ -1032,7 +1047,25 @@ impl GameContext { .get_versioned_data(evt.dest) .map(|d| d.versions.settle_version) .unwrap_or(0); - self.next_settle_locks.insert(evt.dest, settle_version); + self.next_settle_locks + .insert(evt.dest, SettleLock::Locked(settle_version)); + } + } + + println!("settle locks: {:?}", self.next_settle_locks); + + let settle_locks: Vec<_> = self.next_settle_locks.drain().collect(); + let mut locked_settle_locks: HashMap = HashMap::new(); + + for (game_id, settle_lock) in settle_locks { + match settle_lock { + SettleLock::Locked(_) => { + locked_settle_locks.insert(game_id, settle_lock); + } + SettleLock::Unlocked(versioned_data) => { + self.checkpoint_mut() + .update_versioned_data(versioned_data)?; + } } } @@ -1047,7 +1080,7 @@ impl GameContext { previous_settle_version, entry_lock, accept_deposits: self.accept_deposits.drain(..).collect(), - settle_locks: self.next_settle_locks.drain().collect(), + settle_locks: locked_settle_locks, }; self.pending_settle_details.push(settle_details); } @@ -1134,7 +1167,6 @@ impl Default for GameContext { } } - fn build_settles_map( withdraws: &[Withdraw], ejects: &[u64], @@ -1188,19 +1220,19 @@ fn build_settles_map( return settles_map; } - #[cfg(test)] mod tests { - use race_api::effect::{EmitBridgeEvent, SubGame, Withdraw}; use super::*; + use race_api::effect::{EmitBridgeEvent, SubGame, Withdraw}; #[test] fn test_apply_effect_with_all_fields() { // Setting up a GameContext let mut game_context = GameContext::default(); game_context.versions = Versions::new(10, 10); - game_context.checkpoint = Checkpoint::new(0, GameSpec::default(), game_context.versions(), vec![1]); + game_context.checkpoint = + Checkpoint::new(0, GameSpec::default(), game_context.versions(), vec![1]); game_context.add_node("server".into(), 0, ClientMode::Transactor); game_context.add_node("alice".into(), 1, ClientMode::Player); game_context.add_node("bob".into(), 2, ClientMode::Player); @@ -1255,17 +1287,20 @@ mod tests { reject_deposits: vec![1, 2, 3], accept_deposits: vec![4, 5, 6], curr_sub_game_id: 1, - balances: vec![ - PlayerBalance::new(1, 1000), - PlayerBalance::new(2, 2000), - ], + balances: vec![PlayerBalance::new(1, 1000), PlayerBalance::new(2, 2000)], }; // prepare event - let event = Event::Bridge { dest_game_id: 0, from_game_id: 1, raw: vec![] }; + let event = Event::Bridge { + dest_game_id: 0, + from_game_id: 1, + raw: vec![], + }; // Apply the effect - let event_effects = game_context.apply_effect(effect).unwrap(); + let event_effects = game_context + .apply_effect(effect, Some(event.clone())) + .unwrap(); assert_eq!(event_effects.launch_sub_games.len(), 1); assert_eq!(event_effects.bridge_events.len(), 1); @@ -1275,10 +1310,23 @@ mod tests { assert_eq!(game_context.settle_version(), 11); assert_eq!(game_context.pending_settle_details.len(), 1); let settle_details = game_context.pending_settle_details[0].clone(); - assert_eq!(settle_details.settles, [ - Settle { player_id: 1, withdraw: 1000, change: Some(BalanceChange::Add(1000)), eject: true }, - Settle { player_id: 2, withdraw: 0, change: Some(BalanceChange::Add(2000)), eject: false }, - ]); + assert_eq!( + settle_details.settles, + [ + Settle { + player_id: 1, + withdraw: 1000, + change: Some(BalanceChange::Add(1000)), + eject: true + }, + Settle { + player_id: 2, + withdraw: 0, + change: Some(BalanceChange::Add(2000)), + eject: false + }, + ] + ); assert_eq!(settle_details.transfer, Some(Transfer { amount: 200 })); assert_eq!(settle_details.awards, vec![Award::new(1, "bonus".into())]); assert!(!settle_details.checkpoint.root.is_empty()); @@ -1287,7 +1335,15 @@ mod tests { assert_eq!(settle_details.previous_settle_version, 10); assert_eq!(settle_details.entry_lock, Some(EntryLock::Closed)); assert_eq!(settle_details.accept_deposits, vec![4, 5, 6]); - assert_eq!(settle_details.settle_locks, HashMap::from([(1, 0)])); + assert_eq!(settle_details.settle_locks, HashMap::from([(1, SettleLock::Locked(0))])); + + let evt_in_checkpoint = game_context + .checkpoint() + .get_versioned_data(game_context.game_id()) + .unwrap() + .event + .clone(); + assert_eq!(evt_in_checkpoint, Some(event.clone())); } // #[test] diff --git a/local-db/src/lib.rs b/local-db/src/lib.rs index 7eb72e35..192cd813 100644 --- a/local-db/src/lib.rs +++ b/local-db/src/lib.rs @@ -20,6 +20,7 @@ pub struct LocalDbStorage { impl StorageT for LocalDbStorage { async fn save_checkpoint(&self, params: SaveCheckpointParams) -> Result<()> { let conn = self.conn.lock().await; + println!("save_checkpoint: {:?}", params.checkpoint); let checkpoint_bs = borsh::to_vec(¶ms.checkpoint).or(Err(Error::MalformedCheckpoint))?; let sha = digest(&checkpoint_bs); conn.execute( @@ -51,6 +52,8 @@ impl StorageT for LocalDbStorage { let checkpoint = CheckpointOffChain::try_from_slice(&checkpoint_bs) .map_err(|e| Error::StorageError(e.to_string()))?; + println!("get_checkpoint: {:?}", checkpoint); + Ok(Some(checkpoint)) } else { Ok(None) diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 08fbbdff..105a0d1e 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -274,8 +274,7 @@ impl Component for EventLoop { if ctx.game_mode == GameMode::Main && ctx.client_mode == ClientMode::Transactor { info!("SubGameReady: Update checkpoint for sub game: {}", game_id); - game_context - .remove_settle_lock(game_id, versioned_data.versions.settle_version); + game_context.remove_settle_lock(game_id, versioned_data.clone()); if let Err(e) = game_context .pending_settle_details_mut() @@ -334,7 +333,7 @@ impl Component for EventLoop { if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { info!("BridgeEvent: Update checkpoint for sub game: {}", from); - game_context.remove_settle_lock(from, settle_version); + game_context.remove_settle_lock(from, checkpoint_state.clone()); if let Err(e) = game_context .pending_settle_details_mut() diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index 0db0d30f..cfc748f0 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -138,7 +138,7 @@ impl WrappedHandler { if let Some(e) = effect.__take_error() { Err(e.into()) } else { - context.apply_effect(effect) + context.apply_effect(effect, None) } } @@ -213,7 +213,7 @@ impl WrappedHandler { error!("Effect: {:?}", effect_bs); Err(e.into()) } else { - context.apply_effect(effect) + context.apply_effect(effect, Some(event.clone())) } } From e1e3c939050e4bb104eb7e82dd3a1a6699c0c341 Mon Sep 17 00:00:00 2001 From: miles Date: Sat, 14 Jun 2025 02:19:21 +0800 Subject: [PATCH 09/23] chore: add dispatch and bridge_events into versioned_data --- api/src/effect.rs | 8 ++- core/src/checkpoint.rs | 67 +++++++++++++++---- core/src/context.rs | 42 +++++++----- js/sdk-core/src/checkpoint.ts | 19 ++++++ test/src/context_helpers.rs | 1 + transactor/src/component/event_loop.rs | 2 + .../src/component/event_loop/event_handler.rs | 48 ++++++++++--- 7 files changed, 147 insertions(+), 40 deletions(-) diff --git a/api/src/effect.rs b/api/src/effect.rs index e5078add..cd00766b 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use crate::{ engine::GameHandler, @@ -10,7 +12,10 @@ use crate::{ event::BridgeEvent, prelude::InitAccount, random::RandomSpec, - types::{Award, DecisionId, EntryLock, GameDeposit, GameId, GamePlayer, PlayerBalance, RandomId, Transfer}, + types::{ + Award, DecisionId, EntryLock, GameDeposit, GameId, GamePlayer, PlayerBalance, RandomId, + Transfer, + }, }; #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] @@ -92,6 +97,7 @@ impl SubGame { } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct EmitBridgeEvent { pub dest: GameId, pub raw: Vec, diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs index 116f39b6..105892d5 100644 --- a/core/src/checkpoint.rs +++ b/core/src/checkpoint.rs @@ -1,17 +1,17 @@ use std::{collections::HashMap, fmt::Display}; -use borsh::{BorshDeserialize, BorshSerialize}; -use rs_merkle::{ - algorithms::Sha256, proof_serializers::ReverseHashesOrder, Hasher, - MerkleTree, +use crate::{ + context::{DispatchEvent, Versions}, + error::Error, }; -use crate::{context::Versions, error::Error}; +use borsh::{BorshDeserialize, BorshSerialize}; +use rs_merkle::{algorithms::Sha256, proof_serializers::ReverseHashesOrder, Hasher, MerkleTree}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use race_api::{event::Event, types::GameId}; use crate::types::GameSpec; +use race_api::{effect::EmitBridgeEvent, event::Event, types::GameId}; /// Checkpoint represents the state snapshot of game. /// It is used as a submission to the blockchain. @@ -51,6 +51,8 @@ pub struct VersionedData { pub sha: Vec, pub game_spec: GameSpec, pub event: Option, + pub dispatch: Option, + pub bridge_events: Vec, } impl Display for Checkpoint { @@ -79,6 +81,8 @@ impl Checkpoint { data: root_data, sha: sha.into(), event: None, + dispatch: None, + bridge_events: vec![], }, )]), proofs: HashMap::new(), @@ -87,7 +91,10 @@ impl Checkpoint { ret } - pub fn new_from_parts(offchain_part: CheckpointOffChain, onchain_part: CheckpointOnChain) -> Self { + pub fn new_from_parts( + offchain_part: CheckpointOffChain, + onchain_part: CheckpointOnChain, + ) -> Self { Self { proofs: offchain_part.proofs, data: offchain_part.data, @@ -105,7 +112,7 @@ impl Checkpoint { return; } let merkle_tree = self.to_merkle_tree(); - let Some(root) = merkle_tree.root() else { + let Some(root) = merkle_tree.root() else { // Skip root update as this is not a master checkpoint return; }; @@ -144,7 +151,13 @@ impl Checkpoint { } } - pub fn init_data(&mut self, id: GameId, game_spec: GameSpec, versions: Versions, data: Vec) -> Result<(), Error> { + pub fn init_data( + &mut self, + id: GameId, + game_spec: GameSpec, + versions: Versions, + data: Vec, + ) -> Result<(), Error> { match self.data.entry(id) { std::collections::hash_map::Entry::Occupied(_) => { return Err(Error::CheckpointAlreadyExists); @@ -159,6 +172,8 @@ impl Checkpoint { game_spec, versions, event: None, + dispatch: None, + bridge_events: vec![], }; v.insert(versioned_data); self.update_root_and_proofs(); @@ -191,14 +206,42 @@ impl Checkpoint { } } - pub fn set_event_in_versioned_data(&mut self, id: GameId, event: Option) -> Result<(), Error> { + pub fn set_event_in_versioned_data( + &mut self, + id: GameId, + event: Option, + ) -> Result<(), Error> { if let Some(versioned_data) = self.data.get_mut(&id) { versioned_data.event = event; - println!("set event in versioned data: {:?} {:?}", id, versioned_data.event); Ok(()) } else { - println!("set event in versioned data no versioned data"); + Err(Error::MissingCheckpoint) + } + } + + pub fn set_dispatch_in_versioned_data( + &mut self, + id: GameId, + dispatch: Option, + ) -> Result<(), Error> { + if let Some(versioned_data) = self.data.get_mut(&id) { + versioned_data.dispatch = dispatch; + Ok(()) + } else { + Err(Error::MissingCheckpoint) + } + } + + pub fn set_bridge_in_versioned_data( + &mut self, + id: GameId, + bridge_events: Vec, + ) -> Result<(), Error> { + if let Some(versioned_data) = self.data.get_mut(&id) { + versioned_data.bridge_events = bridge_events; Ok(()) + } else { + Err(Error::MissingCheckpoint) } } diff --git a/core/src/context.rs b/core/src/context.rs index bab1d0af..174cdb65 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -97,6 +97,7 @@ impl Node { } #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct DispatchEvent { pub timeout: u64, pub event: Event, @@ -458,22 +459,15 @@ impl GameContext { pub fn remove_settle_lock(&mut self, game_id: GameId, versioned_data: VersionedData) { // remove from next_settle_lock - match self.next_settle_locks.entry(game_id) { - std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { - occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); - } - std::collections::hash_map::Entry::Vacant(vacant_entry) => { - vacant_entry.insert(SettleLock::Unlocked(versioned_data.clone())); - } - } + self.next_settle_locks + .insert(game_id, SettleLock::Unlocked(versioned_data)); + // clear emitbridgeevent. + let _ = self + .checkpoint_mut() + .set_bridge_in_versioned_data(game_id, vec![]); // remove from Checkpoints for checkpoint in self.pending_settle_details.iter_mut() { - match checkpoint.settle_locks.entry(game_id) { - std::collections::hash_map::Entry::Occupied(occupied_entry) => { - occupied_entry.remove(); - } - std::collections::hash_map::Entry::Vacant(_vacant_entry) => {} - } + checkpoint.settle_locks.remove(&game_id); } } @@ -1032,8 +1026,16 @@ impl GameContext { } let game_id = self.game_id(); + let dispatch = self.dispatch.clone(); + // todo: remove after debug + println!("apply_effect: {}, {:?}", game_id, event); + self.checkpoint_mut() .set_event_in_versioned_data(game_id, event)?; + self.checkpoint_mut() + .set_dispatch_in_versioned_data(game_id, dispatch)?; + self.checkpoint_mut() + .set_bridge_in_versioned_data(game_id, bridge_events.clone())?; if self.spec.game_id == 0 { for sub_game in launch_sub_games.iter().cloned() { @@ -1052,7 +1054,12 @@ impl GameContext { } } - println!("settle locks: {:?}", self.next_settle_locks); + // todo: remove after debug + println!( + "settle locks: game_id[{}], {:?}", + self.game_id(), + self.next_settle_locks + ); let settle_locks: Vec<_> = self.next_settle_locks.drain().collect(); let mut locked_settle_locks: HashMap = HashMap::new(); @@ -1335,7 +1342,10 @@ mod tests { assert_eq!(settle_details.previous_settle_version, 10); assert_eq!(settle_details.entry_lock, Some(EntryLock::Closed)); assert_eq!(settle_details.accept_deposits, vec![4, 5, 6]); - assert_eq!(settle_details.settle_locks, HashMap::from([(1, SettleLock::Locked(0))])); + assert_eq!( + settle_details.settle_locks, + HashMap::from([(1, SettleLock::Locked(0))]) + ); let evt_in_checkpoint = game_context .checkpoint() diff --git a/js/sdk-core/src/checkpoint.ts b/js/sdk-core/src/checkpoint.ts index 557f2d87..9018e9df 100644 --- a/js/sdk-core/src/checkpoint.ts +++ b/js/sdk-core/src/checkpoint.ts @@ -10,6 +10,7 @@ import { import { sha256 } from './encryptor' import { Fields } from './types' import { GameEvent } from './events' +import { EmitBridgeEvent } from './effect' export class Versions { @field('u64') @@ -64,6 +65,18 @@ export class GameSpec { } } +export class DispatchEvent { + @field('u64') + timeout!: bigint + + @field(enums(GameEvent)) + event!: GameEvent + + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + export class VersionedData { @field('usize') id!: number @@ -83,6 +96,12 @@ export class VersionedData { @field(option(enums(GameEvent))) event!: GameEvent | undefined + @field(option(enums(DispatchEvent))) + dispatch!: DispatchEvent | undefined + + @field(array(struct(EmitBridgeEvent))) + bridgeEvents!: EmitBridgeEvent[] + constructor(fields: any) { Object.assign(this, fields) } diff --git a/test/src/context_helpers.rs b/test/src/context_helpers.rs index f0839b9e..134ed146 100644 --- a/test/src/context_helpers.rs +++ b/test/src/context_helpers.rs @@ -181,6 +181,7 @@ impl TestContextBuilder { data: borsh::to_vec(checkpoint).expect("Failed to serialize checkpoint"), sha: vec![], game_spec: spec, + event: None }, ); diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 105a0d1e..2b455192 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -334,6 +334,8 @@ impl Component for EventLoop { if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { info!("BridgeEvent: Update checkpoint for sub game: {}", from); game_context.remove_settle_lock(from, checkpoint_state.clone()); + // todo: remove after debug + println!("recv bridge event: {} {:?}", from, checkpoint_state.event); if let Err(e) = game_context .pending_settle_details_mut() diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 5373833e..9d1e9a83 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -289,23 +289,49 @@ pub async fn recover_from_checkpoint( if client_mode == ClientMode::Transactor { game_context.dispatch_safe(Event::Ready, 0); + + for versioned_data in game_context.checkpoint().list_versioned_data() { + if !versioned_data.bridge_events.is_empty() { + send_bridge_event( + versioned_data.bridge_events.clone(), + game_context, + ports, + env, + ).await; + } + + if let Some(dispatch) = versioned_data.dispatch.as_ref() { + let server_event = EventFrame::SendServerEvent { + event: dispatch.event.clone(), + timestamp: dispatch.timeout, + }; + ports.send(server_event).await; + } + } } // Tell master game the subgame is successfully created. if game_mode == GameMode::Sub && client_mode == ClientMode::Transactor { let game_id = game_context.game_id(); - if let Some(versioned_data) = game_context.checkpoint().get_versioned_data(game_id) { - println!("recover from checkpoint {:?}", versioned_data); - if let Some(event) = &versioned_data.event { - let frame = EventFrame::SendBridgeEvent { - from: game_id, - dest: 0, - event: event.clone(), - checkpoint_state: versioned_data.clone(), - }; - ports.send(frame).await; - } + if let Some(_versioned_data) = game_context.checkpoint().get_versioned_data(game_id) { + // if let Some(event) = &versioned_data.event { + // let frame = EventFrame::SendBridgeEvent { + // from: game_id, + // dest: 0, + // event: event.clone(), + // checkpoint_state: versioned_data.clone(), + // }; + // ports.send(frame).await; + // } + + // let frame = EventFrame::SendBridgeEvent { + // from: game_id, + // dest: 0, + // event: Event::WaitingTimeout, + // checkpoint_state: versioned_data.clone(), + // }; + // ports.send(frame).await; // send_subgame_ready(versioned_data.clone(), game_context, game_context.init_account(), ports).await; } else { From 53a9f0eda9337df82ee592377ea4a3cb981b8cdf Mon Sep 17 00:00:00 2001 From: miles Date: Sat, 14 Jun 2025 03:19:12 +0800 Subject: [PATCH 10/23] chore: remove event in apply_effect --- core/src/checkpoint.rs | 18 +-------------- core/src/context.rs | 5 +--- js/sdk-core/src/checkpoint.ts | 3 --- test/src/handler_helpers.rs | 4 ++-- transactor/src/component/event_loop.rs | 2 -- .../src/component/event_loop/event_handler.rs | 23 +------------------ transactor/src/component/wrapped_handler.rs | 4 ++-- 7 files changed, 7 insertions(+), 52 deletions(-) diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs index 105892d5..ec5eb6f1 100644 --- a/core/src/checkpoint.rs +++ b/core/src/checkpoint.rs @@ -11,7 +11,7 @@ use rs_merkle::{algorithms::Sha256, proof_serializers::ReverseHashesOrder, Hashe use serde::{Deserialize, Serialize}; use crate::types::GameSpec; -use race_api::{effect::EmitBridgeEvent, event::Event, types::GameId}; +use race_api::{effect::EmitBridgeEvent, types::GameId}; /// Checkpoint represents the state snapshot of game. /// It is used as a submission to the blockchain. @@ -50,7 +50,6 @@ pub struct VersionedData { pub data: Vec, pub sha: Vec, pub game_spec: GameSpec, - pub event: Option, pub dispatch: Option, pub bridge_events: Vec, } @@ -80,7 +79,6 @@ impl Checkpoint { versions, data: root_data, sha: sha.into(), - event: None, dispatch: None, bridge_events: vec![], }, @@ -171,7 +169,6 @@ impl Checkpoint { sha: sha.into(), game_spec, versions, - event: None, dispatch: None, bridge_events: vec![], }; @@ -206,19 +203,6 @@ impl Checkpoint { } } - pub fn set_event_in_versioned_data( - &mut self, - id: GameId, - event: Option, - ) -> Result<(), Error> { - if let Some(versioned_data) = self.data.get_mut(&id) { - versioned_data.event = event; - Ok(()) - } else { - Err(Error::MissingCheckpoint) - } - } - pub fn set_dispatch_in_versioned_data( &mut self, id: GameId, diff --git a/core/src/context.rs b/core/src/context.rs index 174cdb65..a536a376 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -912,7 +912,7 @@ impl GameContext { } } - pub fn apply_effect(&mut self, effect: Effect, event: Option) -> Result { + pub fn apply_effect(&mut self, effect: Effect) -> Result { let Effect { action_timeout, wait_timeout, @@ -1028,10 +1028,7 @@ impl GameContext { let game_id = self.game_id(); let dispatch = self.dispatch.clone(); // todo: remove after debug - println!("apply_effect: {}, {:?}", game_id, event); - self.checkpoint_mut() - .set_event_in_versioned_data(game_id, event)?; self.checkpoint_mut() .set_dispatch_in_versioned_data(game_id, dispatch)?; self.checkpoint_mut() diff --git a/js/sdk-core/src/checkpoint.ts b/js/sdk-core/src/checkpoint.ts index 9018e9df..24471672 100644 --- a/js/sdk-core/src/checkpoint.ts +++ b/js/sdk-core/src/checkpoint.ts @@ -93,9 +93,6 @@ export class VersionedData { @field(struct(GameSpec)) spec!: GameSpec - @field(option(enums(GameEvent))) - event!: GameEvent | undefined - @field(option(enums(DispatchEvent))) dispatch!: DispatchEvent | undefined diff --git a/test/src/handler_helpers.rs b/test/src/handler_helpers.rs index 4dec9498..df256745 100644 --- a/test/src/handler_helpers.rs +++ b/test/src/handler_helpers.rs @@ -32,7 +32,7 @@ impl TestHandler { let init_account = new_context.init_account(); let effect = new_context.derive_effect(true); let handler = H::init_state(init_account)?; - let event_effects = new_context.apply_effect(effect, None)?; + let event_effects = new_context.apply_effect(effect)?; patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); Ok((Self { handler }, event_effects)) @@ -44,7 +44,7 @@ impl TestHandler { general_handle_event(&mut new_context, event, &encryptor)?; let mut effect = new_context.derive_effect(false); self.handler.handle_event(&mut effect, event.to_owned())?; - let event_effects = new_context.apply_effect(effect, Some(event.clone()))?; + let event_effects = new_context.apply_effect(effect)?; patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); Ok(event_effects) diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 2b455192..105a0d1e 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -334,8 +334,6 @@ impl Component for EventLoop { if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { info!("BridgeEvent: Update checkpoint for sub game: {}", from); game_context.remove_settle_lock(from, checkpoint_state.clone()); - // todo: remove after debug - println!("recv bridge event: {} {:?}", from, checkpoint_state.event); if let Err(e) = game_context .pending_settle_details_mut() diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 9d1e9a83..1ad09f4a 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -313,28 +313,7 @@ pub async fn recover_from_checkpoint( // Tell master game the subgame is successfully created. if game_mode == GameMode::Sub && client_mode == ClientMode::Transactor { let game_id = game_context.game_id(); - - if let Some(_versioned_data) = game_context.checkpoint().get_versioned_data(game_id) { - // if let Some(event) = &versioned_data.event { - // let frame = EventFrame::SendBridgeEvent { - // from: game_id, - // dest: 0, - // event: event.clone(), - // checkpoint_state: versioned_data.clone(), - // }; - // ports.send(frame).await; - // } - - // let frame = EventFrame::SendBridgeEvent { - // from: game_id, - // dest: 0, - // event: Event::WaitingTimeout, - // checkpoint_state: versioned_data.clone(), - // }; - // ports.send(frame).await; - - // send_subgame_ready(versioned_data.clone(), game_context, game_context.init_account(), ports).await; - } else { + if game_context.checkpoint().get_versioned_data(game_id).is_none() { ports.send(EventFrame::Shutdown).await; return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); } diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index cfc748f0..0db0d30f 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -138,7 +138,7 @@ impl WrappedHandler { if let Some(e) = effect.__take_error() { Err(e.into()) } else { - context.apply_effect(effect, None) + context.apply_effect(effect) } } @@ -213,7 +213,7 @@ impl WrappedHandler { error!("Effect: {:?}", effect_bs); Err(e.into()) } else { - context.apply_effect(effect, Some(event.clone())) + context.apply_effect(effect) } } From 144bbcb6fc05192c5a689f8a0f6c69856677420b Mon Sep 17 00:00:00 2001 From: miles Date: Sat, 14 Jun 2025 03:36:33 +0800 Subject: [PATCH 11/23] chore: remove some debug log --- core/src/context.rs | 8 -------- local-db/src/lib.rs | 2 +- transactor/src/component/event_loop/event_handler.rs | 2 -- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index a536a376..a66dfc61 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1027,7 +1027,6 @@ impl GameContext { let game_id = self.game_id(); let dispatch = self.dispatch.clone(); - // todo: remove after debug self.checkpoint_mut() .set_dispatch_in_versioned_data(game_id, dispatch)?; @@ -1051,13 +1050,6 @@ impl GameContext { } } - // todo: remove after debug - println!( - "settle locks: game_id[{}], {:?}", - self.game_id(), - self.next_settle_locks - ); - let settle_locks: Vec<_> = self.next_settle_locks.drain().collect(); let mut locked_settle_locks: HashMap = HashMap::new(); diff --git a/local-db/src/lib.rs b/local-db/src/lib.rs index 192cd813..d34f8a8b 100644 --- a/local-db/src/lib.rs +++ b/local-db/src/lib.rs @@ -20,7 +20,6 @@ pub struct LocalDbStorage { impl StorageT for LocalDbStorage { async fn save_checkpoint(&self, params: SaveCheckpointParams) -> Result<()> { let conn = self.conn.lock().await; - println!("save_checkpoint: {:?}", params.checkpoint); let checkpoint_bs = borsh::to_vec(¶ms.checkpoint).or(Err(Error::MalformedCheckpoint))?; let sha = digest(&checkpoint_bs); conn.execute( @@ -52,6 +51,7 @@ impl StorageT for LocalDbStorage { let checkpoint = CheckpointOffChain::try_from_slice(&checkpoint_bs) .map_err(|e| Error::StorageError(e.to_string()))?; + // todo: remove after debug println!("get_checkpoint: {:?}", checkpoint); Ok(Some(checkpoint)) diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 1ad09f4a..d53b6043 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -288,8 +288,6 @@ pub async fn recover_from_checkpoint( } if client_mode == ClientMode::Transactor { - game_context.dispatch_safe(Event::Ready, 0); - for versioned_data in game_context.checkpoint().list_versioned_data() { if !versioned_data.bridge_events.is_empty() { send_bridge_event( From aaa292d9b0c2271eb76b4258e3bd12495f92adf0 Mon Sep 17 00:00:00 2001 From: miles Date: Sat, 14 Jun 2025 04:15:37 +0800 Subject: [PATCH 12/23] test: add test for apply_effect, assert bridge_events and dispatch --- core/src/context.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index a66dfc61..e3b1a94e 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1286,17 +1286,10 @@ mod tests { balances: vec![PlayerBalance::new(1, 1000), PlayerBalance::new(2, 2000)], }; - // prepare event - let event = Event::Bridge { - dest_game_id: 0, - from_game_id: 1, - raw: vec![], - }; + let effect_bridge_events = effect.bridge_events.clone(); // Apply the effect - let event_effects = game_context - .apply_effect(effect, Some(event.clone())) - .unwrap(); + let event_effects = game_context.apply_effect(effect).unwrap(); assert_eq!(event_effects.launch_sub_games.len(), 1); assert_eq!(event_effects.bridge_events.len(), 1); @@ -1336,13 +1329,15 @@ mod tests { HashMap::from([(1, SettleLock::Locked(0))]) ); - let evt_in_checkpoint = game_context + let game_id = game_context.game_id(); + let bridge_events = game_context .checkpoint() - .get_versioned_data(game_context.game_id()) + .get_versioned_data(game_id) .unwrap() - .event + .bridge_events .clone(); - assert_eq!(evt_in_checkpoint, Some(event.clone())); + assert_eq!(bridge_events, effect_bridge_events); + assert_eq!(game_context.dispatch, None); } // #[test] From 55fcf07e4d65e6f3f762c18ad5fb251022935bad Mon Sep 17 00:00:00 2001 From: miles Date: Sun, 15 Jun 2025 06:46:48 +0800 Subject: [PATCH 13/23] test: recover from checkpoint wip --- core/src/context.rs | 2 + local-db/src/lib.rs | 2 +- .../src/component/event_loop/event_handler.rs | 58 ++++++++++++++----- transactor/src/component/submitter.rs | 2 +- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index e3b1a94e..9763c91d 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1081,6 +1081,8 @@ impl GameContext { self.pending_settle_details.push(settle_details); } + println!("apply_effect: {}, {}", is_checkpoint, self.pending_settle_details.len()); + return Ok(EventEffects { launch_sub_games, bridge_events, diff --git a/local-db/src/lib.rs b/local-db/src/lib.rs index d34f8a8b..3fa96b14 100644 --- a/local-db/src/lib.rs +++ b/local-db/src/lib.rs @@ -52,7 +52,7 @@ impl StorageT for LocalDbStorage { .map_err(|e| Error::StorageError(e.to_string()))?; // todo: remove after debug - println!("get_checkpoint: {:?}", checkpoint); + println!("get_checkpoint: version: {}, {:?}", params.settle_version, checkpoint); Ok(Some(checkpoint)) } else { diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index d53b6043..04e4c103 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -1,3 +1,5 @@ +use std::{thread, time}; + use super::misc::log_execution_context; use race_api::{ effect::{EmitBridgeEvent, Log, SubGame}, @@ -96,7 +98,12 @@ async fn send_bridge_event( env: &ComponentEnv, ) { for be in bridge_events { - info!("{} Send bridge event, dest: {}", env.log_prefix, be.dest); + info!( + "{} Send bridge event, dest: {}, from: {}", + env.log_prefix, + be.dest, + game_context.game_id() + ); let checkpoint_state = game_context .checkpoint() .get_versioned_data(game_context.game_id()); @@ -287,23 +294,38 @@ pub async fn recover_from_checkpoint( } } + if client_mode == ClientMode::Transactor && game_context.game_id() != 0 { + game_context.dispatch_safe(Event::Ready, 0); + } + + let time = time::Duration::from_secs(3); + thread::sleep(time); + if client_mode == ClientMode::Transactor { for versioned_data in game_context.checkpoint().list_versioned_data() { - if !versioned_data.bridge_events.is_empty() { - send_bridge_event( - versioned_data.bridge_events.clone(), - game_context, - ports, - env, - ).await; - } + if game_context.game_id() == versioned_data.id { + println!( + "recover_from_checkpoint: game_id[{}], {}", + versioned_data.id, + versioned_data.bridge_events.clone().len() + ); + if !versioned_data.bridge_events.is_empty() { + send_bridge_event( + versioned_data.bridge_events.clone(), + game_context, + ports, + env, + ) + .await; + } - if let Some(dispatch) = versioned_data.dispatch.as_ref() { - let server_event = EventFrame::SendServerEvent { - event: dispatch.event.clone(), - timestamp: dispatch.timeout, - }; - ports.send(server_event).await; + if let Some(dispatch) = versioned_data.dispatch.as_ref() { + let server_event = EventFrame::SendServerEvent { + event: dispatch.event.clone(), + timestamp: dispatch.timeout, + }; + ports.send(server_event).await; + } } } } @@ -311,7 +333,11 @@ pub async fn recover_from_checkpoint( // Tell master game the subgame is successfully created. if game_mode == GameMode::Sub && client_mode == ClientMode::Transactor { let game_id = game_context.game_id(); - if game_context.checkpoint().get_versioned_data(game_id).is_none() { + if game_context + .checkpoint() + .get_versioned_data(game_id) + .is_none() + { ports.send(EventFrame::Shutdown).await; return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); } diff --git a/transactor/src/component/submitter.rs b/transactor/src/component/submitter.rs index b0d850e3..e65f1e33 100644 --- a/transactor/src/component/submitter.rs +++ b/transactor/src/component/submitter.rs @@ -24,7 +24,7 @@ use super::ComponentEnv; const DEFAULT_SUBMITTER_SQUASH_TIME_WINDOW: u64 = 30; // The default for maximum number of transactions for one squash. -const DEFAULT_SUBMITTER_SQUASH_COMPLEXITY_LIMIT: usize = 50; +const DEFAULT_SUBMITTER_SQUASH_COMPLEXITY_LIMIT: usize = 1; // The default for size of transcation queue. const DEFAULT_SUBMITTER_TX_QUEUE_SIZE: usize = 100; From d38ca6c4d665d7dd664aa31e5a4656ce5dae8f78 Mon Sep 17 00:00:00 2001 From: miles Date: Wed, 18 Jun 2025 15:39:27 +0800 Subject: [PATCH 14/23] fix: recover from checkpoint --- core/src/context.rs | 2 - local-db/src/lib.rs | 4 -- transactor/src/component/event_bridge.rs | 28 ++++++++++ .../src/component/event_loop/event_handler.rs | 38 ++++---------- transactor/src/frame.rs | 51 +++++++++++++++---- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index 9763c91d..e3b1a94e 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1081,8 +1081,6 @@ impl GameContext { self.pending_settle_details.push(settle_details); } - println!("apply_effect: {}, {}", is_checkpoint, self.pending_settle_details.len()); - return Ok(EventEffects { launch_sub_games, bridge_events, diff --git a/local-db/src/lib.rs b/local-db/src/lib.rs index 3fa96b14..ec73fa8a 100644 --- a/local-db/src/lib.rs +++ b/local-db/src/lib.rs @@ -50,10 +50,6 @@ impl StorageT for LocalDbStorage { if let Some(checkpoint_bs) = checkpoint_bs { let checkpoint = CheckpointOffChain::try_from_slice(&checkpoint_bs) .map_err(|e| Error::StorageError(e.to_string()))?; - - // todo: remove after debug - println!("get_checkpoint: version: {}, {:?}", params.settle_version, checkpoint); - Ok(Some(checkpoint)) } else { Ok(None) diff --git a/transactor/src/component/event_bridge.rs b/transactor/src/component/event_bridge.rs index 2f92be46..8ff88326 100644 --- a/transactor/src/component/event_bridge.rs +++ b/transactor/src/component/event_bridge.rs @@ -148,6 +148,27 @@ impl Component for EventBridgeParent { ports.send(event_frame).await; } + + EventFrame::SubGameRecovered { game_id } => { + info!("{} Receives subgame recovered: {}", env.log_prefix, game_id); + // Remove the launching game's ID + launching_game_ids.retain(|id| *id != game_id); + // Send the pending events, for print log reason, not combine with SubGameReady + let mut i = 0; + while i < pending_events.len() { + if pending_events[i].0 == game_id { + let (_, event_frame) = pending_events.remove(i); + info!("{} Send pending event: {}", env.log_prefix, event_frame); + if let Err(e) = ctx.tx.send(event_frame) { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } else { + i += 1; + } + } + + ports.send(event_frame).await; + } _ => (), } } else { // Bridge parent receives event from event bus @@ -306,6 +327,13 @@ impl Component for EventBridgeChild { } } + EventFrame::SubGameRecovered { .. } => { + info!("{} Send SubGameRecovered to parent", env.log_prefix); + if let Err(e) = ctx.tx.send(event_frame).await { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + EventFrame::SendBridgeEvent { dest, .. } if dest != ctx.game_id => { info!("{} Sends event: {}", env.log_prefix, event_frame); if let Err(e) = ctx.tx.send(event_frame).await { diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 04e4c103..cb15872c 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -1,10 +1,9 @@ -use std::{thread, time}; - use super::misc::log_execution_context; use race_api::{ effect::{EmitBridgeEvent, Log, SubGame}, event::Event, prelude::InitAccount, + types::GameId, }; use race_core::{ checkpoint::{Checkpoint, VersionedData}, @@ -91,6 +90,12 @@ async fn send_subgame_ready( .await; } +async fn send_subgame_recovered(game_id: GameId, ports: &PipelinePorts) { + ports + .send(EventFrame::SubGameRecovered { game_id: game_id }) + .await; +} + async fn send_bridge_event( bridge_events: Vec, game_context: &GameContext, @@ -294,21 +299,13 @@ pub async fn recover_from_checkpoint( } } - if client_mode == ClientMode::Transactor && game_context.game_id() != 0 { - game_context.dispatch_safe(Event::Ready, 0); - } - - let time = time::Duration::from_secs(3); - thread::sleep(time); - if client_mode == ClientMode::Transactor { + if game_mode == GameMode::Sub { + send_subgame_recovered(game_context.game_id(), ports).await; + } + for versioned_data in game_context.checkpoint().list_versioned_data() { if game_context.game_id() == versioned_data.id { - println!( - "recover_from_checkpoint: game_id[{}], {}", - versioned_data.id, - versioned_data.bridge_events.clone().len() - ); if !versioned_data.bridge_events.is_empty() { send_bridge_event( versioned_data.bridge_events.clone(), @@ -330,19 +327,6 @@ pub async fn recover_from_checkpoint( } } - // Tell master game the subgame is successfully created. - if game_mode == GameMode::Sub && client_mode == ClientMode::Transactor { - let game_id = game_context.game_id(); - if game_context - .checkpoint() - .get_versioned_data(game_id) - .is_none() - { - ports.send(EventFrame::Shutdown).await; - return Some(CloseReason::Fault(Error::CheckpointNotFoundAfterInit)); - } - } - None } diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index be2933a3..2a929e4f 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -1,22 +1,30 @@ use race_api::{ - event::{Event, Message}, types::GameId + event::{Event, Message}, + types::GameId, }; use race_core::{ - checkpoint::{Checkpoint, VersionedData}, context::{GameContext, SubGameInit, SettleDetails}, types::{ClientMode, PlayerDeposit, PlayerJoin, ServerJoin, TxState, VoteType} + checkpoint::{Checkpoint, VersionedData}, + context::{GameContext, SettleDetails, SubGameInit}, + types::{ClientMode, PlayerDeposit, PlayerJoin, ServerJoin, TxState, VoteType}, }; use crate::component::BridgeToParent; #[derive(Debug)] pub enum SignalFrame { - StartGame { game_addr: String, mode: ClientMode }, + StartGame { + game_addr: String, + mode: ClientMode, + }, LaunchSubGame { sub_game_init: SubGameInit, bridge_to_parent: BridgeToParent, }, #[allow(unused)] Shutdown, - RemoveGame { game_addr: String }, + RemoveGame { + game_addr: String, + }, } #[derive(Debug, Clone)] @@ -122,10 +130,15 @@ pub enum EventFrame { init_data: Vec, }, + /// Subgames send this frame when start via recovering from checkpoint. + SubGameRecovered { + game_id: GameId, + }, + /// Reject a deposit RejectDeposits { reject_deposits: Vec, - } + }, } impl std::fmt::Display for EventFrame { @@ -135,7 +148,11 @@ impl std::fmt::Display for EventFrame { EventFrame::GameStart { access_version } => { write!(f, "GameStart, access_version = {}", access_version) } - EventFrame::InitState { access_version, settle_version, .. } => write!( + EventFrame::InitState { + access_version, + settle_version, + .. + } => write!( f, "InitState, access_version = {}, settle_version = {}", access_version, settle_version @@ -178,14 +195,30 @@ impl std::fmt::Display for EventFrame { write!(f, "RecvBridgeEvent: dest {}, event: {}", dest, event) } EventFrame::LaunchSubGame { sub_game_init } => { - write!(f, "LaunchSubGame: {}#{}", sub_game_init.spec.game_addr, sub_game_init.spec.game_id) + write!( + f, + "LaunchSubGame: {}#{}", + sub_game_init.spec.game_addr, sub_game_init.spec.game_id + ) } - EventFrame::SubSync { new_players, new_servers, .. } => { - write!(f, "SyncNodes: new_players: {}, new_servers: {}", new_players.len(), new_servers.len()) + EventFrame::SubSync { + new_players, + new_servers, + .. + } => { + write!( + f, + "SyncNodes: new_players: {}, new_servers: {}", + new_players.len(), + new_servers.len() + ) } EventFrame::SubGameReady { game_id, .. } => { write!(f, "SubGameReady, game_id: {}", game_id) } + EventFrame::SubGameRecovered { game_id } => { + write!(f, "SubGameRecovered, game_id: {}", game_id) + } EventFrame::RejectDeposits { reject_deposits } => { write!(f, "Reject deposits, {:?}", reject_deposits) } From 0a98e494028e4f54380d9af5a0c98860ad4cc888 Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Fri, 20 Jun 2025 01:29:39 +0800 Subject: [PATCH 15/23] refactor remove_settle_lock to handle_versioned_data --- api/src/effect.rs | 7 + core/src/checkpoint.rs | 15 +- core/src/context.rs | 175 ++++++++++++++++-- transactor/src/component/event_bridge.rs | 8 +- transactor/src/component/event_loop.rs | 67 ++----- .../src/component/event_loop/event_handler.rs | 4 +- transactor/src/frame.rs | 4 +- 7 files changed, 199 insertions(+), 81 deletions(-) diff --git a/api/src/effect.rs b/api/src/effect.rs index cd00766b..60ac3469 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -110,6 +110,13 @@ impl EmitBridgeEvent { raw: borsh::to_vec(&bridge_event)?, }) } + + pub fn new_empty(dest: GameId) -> Self { + Self { + dest, + raw: vec![] + } + } } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs index ec5eb6f1..0a89d3fc 100644 --- a/core/src/checkpoint.rs +++ b/core/src/checkpoint.rs @@ -54,6 +54,13 @@ pub struct VersionedData { pub bridge_events: Vec, } +impl VersionedData { + fn clear_future_events(&mut self) { + self.dispatch = None; + self.bridge_events.clear(); + } +} + impl Display for Checkpoint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = self @@ -229,6 +236,10 @@ impl Checkpoint { } } + pub fn clear_future_events(&mut self) { + self.data.values_mut().for_each(VersionedData::clear_future_events); + } + pub fn list_versioned_data(&self) -> Vec<&VersionedData> { self.data.values().collect() } @@ -244,14 +255,14 @@ impl Checkpoint { pub fn get_sha(&self, id: GameId) -> Option<[u8; 32]> { self.data .get(&id) - .map(|d| d.sha.clone().try_into().unwrap()) + .map(|d| d.sha.clone().try_into().expect("Failed to get SHA")) } pub fn to_merkle_tree(&self) -> MerkleTree { let mut leaves: Vec<[u8; 32]> = vec![]; let mut i = 0; while let Some(vd) = self.data.get(&i) { - leaves.push(vd.sha.clone().try_into().unwrap()); + leaves.push(vd.sha.clone().try_into().expect("Failed to build merkle tree")); i += 1; } MerkleTree::from_leaves(&leaves) diff --git a/core/src/context.rs b/core/src/context.rs index e3b1a94e..c4cab230 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -160,7 +160,7 @@ pub struct EventEffects { pub checkpoint: Option, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct SettleDetails { pub settles: Vec, pub transfer: Option, @@ -171,7 +171,31 @@ pub struct SettleDetails { pub previous_settle_version: u64, pub entry_lock: Option, pub accept_deposits: Vec, - pub settle_locks: HashMap, + pub settle_locks: HashMap, +} + +impl SettleDetails { + pub fn handle_versioned_data(&mut self, versioned_data: VersionedData) -> Result<()> { + self.settle_locks.remove(&versioned_data.id); + if self.checkpoint.data.contains_key(&versioned_data.id) { + self.checkpoint.update_versioned_data(versioned_data)?; + } else { + self.checkpoint.init_versioned_data(versioned_data)?; + } + Ok(()) + } + + pub fn print(&self) { + println!("Settles: {:?}", self.settles); + println!("Transfer: {:?}", self.transfer); + println!("Awards: {:?}", self.awards); + println!("Checkpoint:"); + for data in self.checkpoint.data.iter() { + println!("- Id: {}", data.1.id); + println!(" Dispatch: {:?}", data.1.dispatch); + println!(" Bridge Events: {:?}", data.1.bridge_events); + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -240,6 +264,7 @@ pub struct GameContext { pub(crate) accept_deposits: Vec, /// The pending settle details. They are here because the settle is blocked by the settle locks. pub(crate) pending_settle_details: Vec, + /// The locks for the under construction settle pub(crate) next_settle_locks: HashMap, } @@ -457,20 +482,6 @@ impl GameContext { } } - pub fn remove_settle_lock(&mut self, game_id: GameId, versioned_data: VersionedData) { - // remove from next_settle_lock - self.next_settle_locks - .insert(game_id, SettleLock::Unlocked(versioned_data)); - // clear emitbridgeevent. - let _ = self - .checkpoint_mut() - .set_bridge_in_versioned_data(game_id, vec![]); - // remove from Checkpoints - for checkpoint in self.pending_settle_details.iter_mut() { - checkpoint.settle_locks.remove(&game_id); - } - } - pub fn set_handler_state(&mut self, handler: &H) where H: GameHandler, @@ -823,6 +834,43 @@ impl GameContext { Ok(()) } + pub fn handle_versioned_data( + &mut self, + game_id: GameId, + mut versioned_data: VersionedData, + is_init: bool, + ) -> Result<()> { + match self.next_settle_locks.entry(game_id) { + // Update + std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { + match occupied_entry.get() { + SettleLock::Locked(_) => { + versioned_data.bridge_events = vec![]; + occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); + self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; + println!("locked"); + } + SettleLock::Unlocked(_) => { + return Err(Error::InvalidCheckpoint); + } + } + } + // Initialize + std::collections::hash_map::Entry::Vacant(_) => { + if is_init { + self.checkpoint_mut().init_versioned_data(versioned_data.clone())?; + } else { + self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; + self.pending_settle_details + .iter_mut() + .map(|sd| sd.handle_versioned_data(versioned_data.clone())) + .collect::>>()?; + } + } + } + Ok(()) + } + pub fn add_revealed_random( &mut self, random_id: RandomId, @@ -1051,12 +1099,12 @@ impl GameContext { } let settle_locks: Vec<_> = self.next_settle_locks.drain().collect(); - let mut locked_settle_locks: HashMap = HashMap::new(); + let mut locked_settle_locks: HashMap = HashMap::new(); for (game_id, settle_lock) in settle_locks { match settle_lock { - SettleLock::Locked(_) => { - locked_settle_locks.insert(game_id, settle_lock); + SettleLock::Locked(settle_version) => { + locked_settle_locks.insert(game_id, settle_version); } SettleLock::Unlocked(versioned_data) => { self.checkpoint_mut() @@ -1078,6 +1126,8 @@ impl GameContext { accept_deposits: self.accept_deposits.drain(..).collect(), settle_locks: locked_settle_locks, }; + settle_details.print(); + self.checkpoint.clear_future_events(); self.pending_settle_details.push(settle_details); } @@ -1222,6 +1272,91 @@ mod tests { use super::*; use race_api::effect::{EmitBridgeEvent, SubGame, Withdraw}; + + #[test] + fn given_effect_with_bridge_event_do_apply_effect() -> anyhow::Result<()> { + let mut ctx = GameContext::default(); + let mut ef = Effect::default(); + let mut cp = Checkpoint::new(0, GameSpec::default(), Versions::default(), vec![]); + cp.init_data(1, GameSpec::default(), Versions::default(), vec![])?; + ctx.checkpoint = cp; + ef.handler_state = Some(vec![1]); + ef.bridge_events = vec![EmitBridgeEvent::new_empty(1)]; + ef.is_checkpoint = true; + ctx.apply_effect(ef)?; + assert!(ctx.next_settle_locks.is_empty()); + assert_eq!(ctx.pending_settle_details.len(), 1); + assert_eq!(ctx.checkpoint().data.get(&0).unwrap().bridge_events.len(), 1); + + // A future call on handle_versioned_data should remove the lock for SettleDetails + let mut vd = VersionedData::default(); + vd.id = 1; + vd.versions.settle_version = 1; + ctx.handle_versioned_data(1, vd, false)?; + assert!(ctx.pending_settle_details[0].checkpoint.data.get(&1).unwrap().bridge_events.is_empty()); + assert_eq!(ctx.pending_settle_details[0].settle_locks, HashMap::default()); + Ok(()) + } + + #[test] + fn given_init_sub_checkpoint_do_handle_versioned_data() { + + } + + #[test] + fn given_sub_checkpoint_before_main_checkpoint_do_handle_versioned_data() -> anyhow::Result<()> { + let mut ctx = GameContext::default(); + let mut vd = VersionedData::default(); + let mut cp = Checkpoint::default(); + cp.data.insert(0, VersionedData::default()); + cp.data.insert(1, VersionedData::default()); + vd.id = 1; + vd.bridge_events = vec![EmitBridgeEvent::new_empty(0)]; + ctx.next_settle_locks.insert(1, SettleLock::Locked(0)); + ctx.checkpoint = cp; + ctx.handle_versioned_data(1, vd.clone(), false)?; + vd.bridge_events = vec![]; + assert_eq!( + ctx.next_settle_locks.get(&1), + Some(&SettleLock::Unlocked(vd)) + ); + assert_eq!(ctx.checkpoint().data.get(&1).unwrap().bridge_events, vec![]); + match ctx.next_settle_locks.get(&1) { + Some(SettleLock::Unlocked(vd)) => { + assert_eq!(vd.bridge_events, vec![]); + } + _ => panic!("not a settle lock"), + } + Ok(()) + } + + #[test] + fn given_sub_checkpoint_after_main_checkpoint_do_handle_versioned_data() { + let mut ctx = GameContext::default(); + let mut vd = VersionedData::default(); + let mut cp = Checkpoint::default(); + let mut sd = SettleDetails::default(); + sd.settle_locks.insert(1, 0); + + cp.data.insert(0, VersionedData::default()); + cp.data.insert(1, VersionedData::default()); + sd.checkpoint = cp.clone(); + let e = EmitBridgeEvent::new_empty(0); + vd.id = 1; + vd.bridge_events = vec![e.clone()]; + ctx.pending_settle_details_mut().push(sd); + ctx.checkpoint = cp; + ctx + .handle_versioned_data(1, vd.clone(), false) + .unwrap(); + assert_eq!( + ctx.next_settle_locks.get(&1), + None, + ); + assert_eq!(ctx.next_settle_locks.len(), 0); + assert_eq!(ctx.pending_settle_details[0].settle_locks.len(), 0); + } + #[test] fn test_apply_effect_with_all_fields() { // Setting up a GameContext @@ -1326,7 +1461,7 @@ mod tests { assert_eq!(settle_details.accept_deposits, vec![4, 5, 6]); assert_eq!( settle_details.settle_locks, - HashMap::from([(1, SettleLock::Locked(0))]) + HashMap::from([(1, 0)]) ); let game_id = game_context.game_id(); diff --git a/transactor/src/component/event_bridge.rs b/transactor/src/component/event_bridge.rs index 8ff88326..aea31028 100644 --- a/transactor/src/component/event_bridge.rs +++ b/transactor/src/component/event_bridge.rs @@ -113,7 +113,7 @@ impl Component for EventBridgeParent { event, // access_version, // settle_version, - checkpoint_state, + versioned_data, } => { info!("{} Receives event: {}", env.log_prefix, event); ports @@ -123,7 +123,7 @@ impl Component for EventBridgeParent { event, // access_version, // settle_version, - checkpoint_state, + versioned_data, }) .await; } @@ -300,7 +300,7 @@ impl Component for EventBridgeChild { event, // access_version, // settle_version, - checkpoint_state, + versioned_data, } if dest == ctx.game_id => { info!("{} Receives {}", env.log_prefix, event); ports @@ -310,7 +310,7 @@ impl Component for EventBridgeChild { event, // access_version, // settle_version, - checkpoint_state, + versioned_data, }) .await; } diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 105a0d1e..ffa037f3 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -3,7 +3,6 @@ use race_api::types::GameDeposit; use async_trait::async_trait; use race_api::event::Event; use race_core::context::GameContext; -use race_core::error::Error; use tracing::{error, info, warn}; use crate::component::common::{Component, PipelinePorts}; @@ -273,28 +272,12 @@ impl Component for EventLoop { } => { if ctx.game_mode == GameMode::Main && ctx.client_mode == ClientMode::Transactor { - info!("SubGameReady: Update checkpoint for sub game: {}", game_id); - game_context.remove_settle_lock(game_id, versioned_data.clone()); - if let Err(e) = game_context - .pending_settle_details_mut() - .iter_mut() - .map(|sd| sd.checkpoint.init_versioned_data(versioned_data.clone())) - .collect::, Error>>() - { - error!( - "{} Failed to init checkpoint data for pending settles: {:?}", - env.log_prefix, e - ); - ports.send(EventFrame::Shutdown).await; - } - - if let Err(e) = game_context - .checkpoint_mut() - .init_versioned_data(versioned_data) - { - error!("{} Failed to init checkpoint data: {:?}", env.log_prefix, e); + info!("SubGameReady: Update checkpoint for sub game: {}", game_id); + if let Err(e) = game_context.handle_versioned_data(game_id, versioned_data, true) { + error!("{} Failed in handling new sub game's versioned data: {:?}", env.log_prefix, e); ports.send(EventFrame::Shutdown).await; + return CloseReason::Fault(e); } let timestamp = current_timestamp(); let event = Event::SubGameReady { @@ -323,42 +306,13 @@ impl Component for EventLoop { event, dest, from, - checkpoint_state, + versioned_data, .. } => { // In the case of parent, update the child game' checkpoint value. let timestamp = current_timestamp(); - let settle_version = checkpoint_state.versions.settle_version; - - if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { - info!("BridgeEvent: Update checkpoint for sub game: {}", from); - game_context.remove_settle_lock(from, checkpoint_state.clone()); - - if let Err(e) = game_context - .pending_settle_details_mut() - .iter_mut() - .map(|sd| { - sd.checkpoint - .update_versioned_data(checkpoint_state.clone()) - }) - .collect::, Error>>() - { - error!( - "{} Failed to set checkpoint data for pending settles: {:?}", - env.log_prefix, e - ); - ports.send(EventFrame::Shutdown).await; - } - - if let Err(e) = game_context - .checkpoint_mut() - .update_versioned_data(checkpoint_state) - { - error!("{} Failed to set checkpoint data: {:?}", env.log_prefix, e); - ports.send(EventFrame::Shutdown).await; - } - } + let settle_version = versioned_data.versions.settle_version; if let Some(close_reason) = event_handler::handle_event( &mut handler, @@ -374,6 +328,15 @@ impl Component for EventLoop { { return close_reason; } + + if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { + info!("BridgeEvent: Update checkpoint for sub game: {}", from); + if let Err(e) = game_context.handle_versioned_data(from, versioned_data, false) { + error!("{} Failed in handling new sub game's versioned data: {:?}", env.log_prefix, e); + ports.send(EventFrame::Shutdown).await; + return CloseReason::Fault(e); + } + } } EventFrame::SendEvent { event, timestamp } => { if let Some(close_reason) = event_handler::handle_event( diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index cb15872c..2daa39b2 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -129,7 +129,7 @@ async fn send_bridge_event( }, // access_version: game_context.access_version(), // settle_version: game_context.settle_version(), - checkpoint_state: checkpoint_state.clone(), + versioned_data: checkpoint_state.clone(), }; ports.send(ef).await; } @@ -162,6 +162,8 @@ async fn do_send_settlements( game_context.settle_version(), ); + settle_details.print(); + ports .send(EventFrame::Settle { settle_details: Box::new(settle_details), diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index 2a929e4f..ef82ec58 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -96,7 +96,7 @@ pub enum EventFrame { event: Event, // access_version: u64, // settle_version: u64, - checkpoint_state: VersionedData, + versioned_data: VersionedData, }, /// Similar to `SendBridgeEvent`, but for receiver's event bus. RecvBridgeEvent { @@ -106,7 +106,7 @@ pub enum EventFrame { // #[allow(unused)] // access_version: u64, // settle_version: u64, - checkpoint_state: VersionedData, + versioned_data: VersionedData, }, /// Launch a subgame. From a0593b4a050a979327f79470936931026abf2fe5 Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Fri, 20 Jun 2025 22:24:37 +0800 Subject: [PATCH 16/23] Add HandlerT --- core/src/context.rs | 3 +- transactor/src/component.rs | 2 + transactor/src/component/event_loop.rs | 34 +++++++----- .../src/component/event_loop/event_handler.rs | 8 +-- transactor/src/component/handler.rs | 10 ++++ transactor/src/component/wrapped_handler.rs | 54 ++++++++++--------- transactor/src/handle/subgame.rs | 2 +- transactor/src/handle/transactor.rs | 2 +- transactor/src/handle/validator.rs | 2 +- 9 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 transactor/src/component/handler.rs diff --git a/core/src/context.rs b/core/src/context.rs index c4cab230..f86fbe4c 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -847,8 +847,7 @@ impl GameContext { SettleLock::Locked(_) => { versioned_data.bridge_events = vec![]; occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); - self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; - println!("locked"); + self.checkpoint_mut().update_versioned_data(versioned_data)?; } SettleLock::Unlocked(_) => { return Err(Error::InvalidCheckpoint); diff --git a/transactor/src/component.rs b/transactor/src/component.rs index 493342e7..9f9cdaa2 100644 --- a/transactor/src/component.rs +++ b/transactor/src/component.rs @@ -9,6 +9,7 @@ mod synchronizer; mod refunder; mod voter; mod wrapped_client; +mod handler; mod wrapped_handler; mod wrapped_transport; mod wrapped_storage; @@ -32,3 +33,4 @@ pub use wrapped_transport::WrappedTransport; pub use wrapped_storage::WrappedStorage; pub use event_bridge::{EventBridgeChild, EventBridgeParent, BridgeToParent}; pub use refunder::Refunder; +pub use handler::HandlerT; diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index ffa037f3..b4d82011 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -7,18 +7,18 @@ use tracing::{error, info, warn}; use crate::component::common::{Component, PipelinePorts}; use crate::component::event_bus::CloseReason; -use crate::component::wrapped_handler::WrappedHandler; use crate::frame::EventFrame; use crate::utils::current_timestamp; use race_core::types::{ClientMode, GameMode, GamePlayer}; +use super::handler::HandlerT; use super::ComponentEnv; mod event_handler; mod misc; pub struct EventLoopContext { - handler: WrappedHandler, + handler: Box, game_context: GameContext, client_mode: ClientMode, game_mode: GameMode, @@ -62,7 +62,7 @@ impl Component for EventLoop { if let Some(close_reason) = event_handler::init_state( access_version, settle_version, - &mut handler, + &mut *handler, &mut game_context, &ports, ctx.client_mode, @@ -93,7 +93,7 @@ impl Component for EventLoop { if ctx.client_mode == ClientMode::Transactor { let event = Event::GameStart; if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -204,7 +204,7 @@ impl Component for EventLoop { if !players.is_empty() { let event = Event::Join { players }; if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -222,7 +222,7 @@ impl Component for EventLoop { if !deposits.is_empty() { let event = Event::Deposit { deposits }; if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -243,7 +243,7 @@ impl Component for EventLoop { if let Ok(player_id) = game_context.addr_to_id(&player_addr) { let event = Event::Leave { player_id }; if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -286,7 +286,7 @@ impl Component for EventLoop { init_data, }; if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -315,7 +315,7 @@ impl Component for EventLoop { let settle_version = versioned_data.versions.settle_version; if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -340,7 +340,7 @@ impl Component for EventLoop { } EventFrame::SendEvent { event, timestamp } => { if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -361,7 +361,7 @@ impl Component for EventLoop { } if let Some(close_reason) = event_handler::handle_event( - &mut handler, + &mut *handler, &mut game_context, event, &ports, @@ -389,7 +389,7 @@ impl Component for EventLoop { impl EventLoop { pub fn init( - handler: WrappedHandler, + handler: Box, game_context: GameContext, client_mode: ClientMode, game_mode: GameMode, @@ -405,3 +405,13 @@ impl EventLoop { ) } } + + + +#[cfg(test)] +mod tests { + + use super::*; + + +} diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 2daa39b2..fbf69895 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -13,7 +13,7 @@ use race_core::{ }; use crate::{ - component::{common::PipelinePorts, CloseReason, ComponentEnv, WrappedHandler}, + component::{common::PipelinePorts, handler::HandlerT, CloseReason, ComponentEnv}, frame::EventFrame, }; use tracing::{debug, error, info, warn}; @@ -92,7 +92,7 @@ async fn send_subgame_ready( async fn send_subgame_recovered(game_id: GameId, ports: &PipelinePorts) { ports - .send(EventFrame::SubGameRecovered { game_id: game_id }) + .send(EventFrame::SubGameRecovered { game_id }) .await; } @@ -207,7 +207,7 @@ async fn launch_sub_game( pub async fn init_state( access_version: u64, settle_version: u64, - handler: &mut WrappedHandler, + handler: &mut dyn HandlerT, mut game_context: &mut GameContext, ports: &PipelinePorts, client_mode: ClientMode, @@ -333,7 +333,7 @@ pub async fn recover_from_checkpoint( } pub async fn handle_event( - handler: &mut WrappedHandler, + handler: &mut dyn HandlerT, game_context: &mut GameContext, event: Event, ports: &PipelinePorts, diff --git a/transactor/src/component/handler.rs b/transactor/src/component/handler.rs new file mode 100644 index 00000000..b06dd5a2 --- /dev/null +++ b/transactor/src/component/handler.rs @@ -0,0 +1,10 @@ +use race_api::prelude::InitAccount; +use race_core::context::{EventEffects, GameContext}; +use race_core::error::Result; +use race_api::event::Event; + +pub trait HandlerT: Send + Sync { + fn handle_event(&mut self, context: &mut GameContext, event: &Event) -> Result; + + fn init_state(&mut self, context: &mut GameContext, init_account: &InitAccount) -> Result; +} diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index 0db0d30f..68d7591e 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -16,6 +16,8 @@ use tracing::info; use tracing::log::error; use wasmer::{imports, Instance, Module, Store, TypedFunction}; +use super::HandlerT; + fn log_execution_context(effect_bs: &Vec, event_bs: &Vec) { info!("Execution context"); info!("===== Effect Bytes ====="); @@ -31,6 +33,33 @@ pub struct WrappedHandler { encryptor: Arc, } +impl HandlerT for WrappedHandler { + fn handle_event( + &mut self, + context: &mut GameContext, + event: &Event, + ) -> Result { + let mut new_context = context.clone(); + general_handle_event(&mut new_context, event, self.encryptor.as_ref())?; + let event_effects = self.custom_handle_event(&mut new_context, event)?; + swap(context, &mut new_context); + Ok(event_effects) + } + + // Initialize game state + fn init_state( + &mut self, + context: &mut GameContext, + init_account: &InitAccount, + ) -> Result { + let mut new_context = context.clone(); + new_context.set_timestamp(0); + let event_effects = self.custom_init_state(&mut new_context, init_account)?; + swap(context, &mut new_context); + Ok(event_effects) + } +} + impl WrappedHandler { /// Load WASM bundle by game address pub async fn load_by_bundle( @@ -216,31 +245,6 @@ impl WrappedHandler { context.apply_effect(effect) } } - - pub fn handle_event( - &mut self, - context: &mut GameContext, - event: &Event, - ) -> Result { - let mut new_context = context.clone(); - general_handle_event(&mut new_context, event, self.encryptor.as_ref())?; - let event_effects = self.custom_handle_event(&mut new_context, event)?; - swap(context, &mut new_context); - Ok(event_effects) - } - - // Initialize game state - pub fn init_state( - &mut self, - context: &mut GameContext, - init_account: &InitAccount, - ) -> Result { - let mut new_context = context.clone(); - new_context.set_timestamp(0); - let event_effects = self.custom_init_state(&mut new_context, init_account)?; - swap(context, &mut new_context); - Ok(event_effects) - } } #[cfg(test)] diff --git a/transactor/src/handle/subgame.rs b/transactor/src/handle/subgame.rs index c574b6df..e156c46c 100644 --- a/transactor/src/handle/subgame.rs +++ b/transactor/src/handle/subgame.rs @@ -47,7 +47,7 @@ impl SubGameHandle { let settle_version = game_context.settle_version(); let checkpoint = game_context.checkpoint().clone(); - let handler = WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?; + let handler = Box::new(WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?); let (broadcaster, broadcaster_ctx) = Broadcaster::init(addr.clone(), game_id); let mut broadcaster_handle = broadcaster.start(&addr, broadcaster_ctx); diff --git a/transactor/src/handle/transactor.rs b/transactor/src/handle/transactor.rs index 14f68377..7ee50d79 100644 --- a/transactor/src/handle/transactor.rs +++ b/transactor/src/handle/transactor.rs @@ -104,7 +104,7 @@ impl TransactorHandle { return Err(Error::GameBundleNotFound); }; - let handler = WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?; + let handler = Box::new(WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?); let event_bus = EventBus::new(game_account.addr.clone()); diff --git a/transactor/src/handle/validator.rs b/transactor/src/handle/validator.rs index c94c5b38..aaf7a9bf 100644 --- a/transactor/src/handle/validator.rs +++ b/transactor/src/handle/validator.rs @@ -78,7 +78,7 @@ impl ValidatorHandle { return Err(Error::GameBundleNotFound); }; - let handler = WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?; + let handler = Box::new(WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?); info!("Creating components"); let event_bus = EventBus::new(game_account.addr.clone()); From 400627dd77df70f8b5a973549ca903f07e950025 Mon Sep 17 00:00:00 2001 From: miles Date: Fri, 20 Jun 2025 23:27:20 +0800 Subject: [PATCH 17/23] fix: one step to solve restart transactor --- core/src/context.rs | 121 ++++++++++++++++--------- transactor/src/component/event_loop.rs | 34 ++++--- 2 files changed, 98 insertions(+), 57 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index f86fbe4c..320789d8 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -186,6 +186,8 @@ impl SettleDetails { } pub fn print(&self) { + println!("Version: {}", self.settle_version); + println!("Locks: {:?}", self.settle_locks); println!("Settles: {:?}", self.settles); println!("Transfer: {:?}", self.transfer); println!("Awards: {:?}", self.awards); @@ -837,36 +839,67 @@ impl GameContext { pub fn handle_versioned_data( &mut self, game_id: GameId, - mut versioned_data: VersionedData, + versioned_data: VersionedData, is_init: bool, ) -> Result<()> { - match self.next_settle_locks.entry(game_id) { - // Update - std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { - match occupied_entry.get() { - SettleLock::Locked(_) => { - versioned_data.bridge_events = vec![]; - occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); - self.checkpoint_mut().update_versioned_data(versioned_data)?; - } - SettleLock::Unlocked(_) => { - return Err(Error::InvalidCheckpoint); + // match self.next_settle_locks.entry(game_id) { + // // Update + // std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { + // match occupied_entry.get() { + // SettleLock::Locked(_) => { + // versioned_data.bridge_events = vec![]; + // occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); + // self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; + // println!("locked"); + // } + // SettleLock::Unlocked(_) => { + // return Err(Error::InvalidCheckpoint); + // } + // } + // } + // // Initialize + // std::collections::hash_map::Entry::Vacant(_) => { + // if is_init { + // self.checkpoint_mut().init_versioned_data(versioned_data.clone())?; + // } else { + // self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; + // self.pending_settle_details + // .iter_mut() + // .map(|sd| sd.handle_versioned_data(versioned_data.clone())) + // .collect::>>()?; + // } + // } + // } + + self.next_settle_locks + .insert(game_id, SettleLock::Unlocked(versioned_data.clone())); + + if is_init { + self.checkpoint_mut() + .init_versioned_data(versioned_data.clone())?; + } else { + self.checkpoint_mut() + .update_versioned_data(versioned_data.clone())?; + } + + let cur_id = self.game_id(); + // remove from Checkpoints + for settle_detail in self.pending_settle_details.iter_mut() { + settle_detail.settle_locks.remove(&game_id); + if settle_detail.settle_locks.is_empty() { + if let Some(master_cp) = settle_detail.checkpoint.get_versioned_data(0) { + for be in master_cp.bridge_events.clone() { + settle_detail + .checkpoint + .set_bridge_in_versioned_data(be.dest, vec![])?; } } } - // Initialize - std::collections::hash_map::Entry::Vacant(_) => { - if is_init { - self.checkpoint_mut().init_versioned_data(versioned_data.clone())?; - } else { - self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; - self.pending_settle_details - .iter_mut() - .map(|sd| sd.handle_versioned_data(versioned_data.clone())) - .collect::>>()?; - } + if cur_id == 0 { + settle_detail.print(); } } + Ok(()) } @@ -1125,8 +1158,6 @@ impl GameContext { accept_deposits: self.accept_deposits.drain(..).collect(), settle_locks: locked_settle_locks, }; - settle_details.print(); - self.checkpoint.clear_future_events(); self.pending_settle_details.push(settle_details); } @@ -1271,7 +1302,6 @@ mod tests { use super::*; use race_api::effect::{EmitBridgeEvent, SubGame, Withdraw}; - #[test] fn given_effect_with_bridge_event_do_apply_effect() -> anyhow::Result<()> { let mut ctx = GameContext::default(); @@ -1285,25 +1315,36 @@ mod tests { ctx.apply_effect(ef)?; assert!(ctx.next_settle_locks.is_empty()); assert_eq!(ctx.pending_settle_details.len(), 1); - assert_eq!(ctx.checkpoint().data.get(&0).unwrap().bridge_events.len(), 1); + assert_eq!( + ctx.checkpoint().data.get(&0).unwrap().bridge_events.len(), + 1 + ); // A future call on handle_versioned_data should remove the lock for SettleDetails let mut vd = VersionedData::default(); vd.id = 1; vd.versions.settle_version = 1; ctx.handle_versioned_data(1, vd, false)?; - assert!(ctx.pending_settle_details[0].checkpoint.data.get(&1).unwrap().bridge_events.is_empty()); - assert_eq!(ctx.pending_settle_details[0].settle_locks, HashMap::default()); + assert!(ctx.pending_settle_details[0] + .checkpoint + .data + .get(&1) + .unwrap() + .bridge_events + .is_empty()); + assert_eq!( + ctx.pending_settle_details[0].settle_locks, + HashMap::default() + ); Ok(()) } #[test] - fn given_init_sub_checkpoint_do_handle_versioned_data() { - - } + fn given_init_sub_checkpoint_do_handle_versioned_data() {} #[test] - fn given_sub_checkpoint_before_main_checkpoint_do_handle_versioned_data() -> anyhow::Result<()> { + fn given_sub_checkpoint_before_main_checkpoint_do_handle_versioned_data() -> anyhow::Result<()> + { let mut ctx = GameContext::default(); let mut vd = VersionedData::default(); let mut cp = Checkpoint::default(); @@ -1345,13 +1386,8 @@ mod tests { vd.bridge_events = vec![e.clone()]; ctx.pending_settle_details_mut().push(sd); ctx.checkpoint = cp; - ctx - .handle_versioned_data(1, vd.clone(), false) - .unwrap(); - assert_eq!( - ctx.next_settle_locks.get(&1), - None, - ); + ctx.handle_versioned_data(1, vd.clone(), false).unwrap(); + assert_eq!(ctx.next_settle_locks.get(&1), None,); assert_eq!(ctx.next_settle_locks.len(), 0); assert_eq!(ctx.pending_settle_details[0].settle_locks.len(), 0); } @@ -1458,10 +1494,7 @@ mod tests { assert_eq!(settle_details.previous_settle_version, 10); assert_eq!(settle_details.entry_lock, Some(EntryLock::Closed)); assert_eq!(settle_details.accept_deposits, vec![4, 5, 6]); - assert_eq!( - settle_details.settle_locks, - HashMap::from([(1, 0)]) - ); + assert_eq!(settle_details.settle_locks, HashMap::from([(1, 0)])); let game_id = game_context.game_id(); let bridge_events = game_context diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index b4d82011..027c2d8c 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -272,10 +272,14 @@ impl Component for EventLoop { } => { if ctx.game_mode == GameMode::Main && ctx.client_mode == ClientMode::Transactor { - info!("SubGameReady: Update checkpoint for sub game: {}", game_id); - if let Err(e) = game_context.handle_versioned_data(game_id, versioned_data, true) { - error!("{} Failed in handling new sub game's versioned data: {:?}", env.log_prefix, e); + if let Err(e) = + game_context.handle_versioned_data(game_id, versioned_data, true) + { + error!( + "{} Failed in handling new sub game's versioned data: {:?}", + env.log_prefix, e + ); ports.send(EventFrame::Shutdown).await; return CloseReason::Fault(e); } @@ -310,10 +314,23 @@ impl Component for EventLoop { .. } => { // In the case of parent, update the child game' checkpoint value. - let timestamp = current_timestamp(); let settle_version = versioned_data.versions.settle_version; + if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { + info!("BridgeEvent: Update checkpoint for sub game: {}", from); + if let Err(e) = + game_context.handle_versioned_data(from, versioned_data, false) + { + error!( + "{} Failed in handling new sub game's versioned data: {:?}", + env.log_prefix, e + ); + ports.send(EventFrame::Shutdown).await; + return CloseReason::Fault(e); + } + } + if let Some(close_reason) = event_handler::handle_event( &mut *handler, &mut game_context, @@ -328,15 +345,6 @@ impl Component for EventLoop { { return close_reason; } - - if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { - info!("BridgeEvent: Update checkpoint for sub game: {}", from); - if let Err(e) = game_context.handle_versioned_data(from, versioned_data, false) { - error!("{} Failed in handling new sub game's versioned data: {:?}", env.log_prefix, e); - ports.send(EventFrame::Shutdown).await; - return CloseReason::Fault(e); - } - } } EventFrame::SendEvent { event, timestamp } => { if let Some(close_reason) = event_handler::handle_event( From c209fcfe961e2bc26507d03b132f6f5d772ea5d1 Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Sat, 21 Jun 2025 04:37:03 +0800 Subject: [PATCH 18/23] Fix tests --- test/src/context_helpers.rs | 3 +- transactor/src/component/event_loop.rs | 51 +++++++++++++++++++++ transactor/src/component/wrapped_handler.rs | 6 +-- transactor/src/frame.rs | 5 -- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/test/src/context_helpers.rs b/test/src/context_helpers.rs index 134ed146..ed46a7a0 100644 --- a/test/src/context_helpers.rs +++ b/test/src/context_helpers.rs @@ -181,7 +181,8 @@ impl TestContextBuilder { data: borsh::to_vec(checkpoint).expect("Failed to serialize checkpoint"), sha: vec![], game_spec: spec, - event: None + dispatch: None, + bridge_events: vec![], }, ); diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 027c2d8c..f51d4ca3 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -419,7 +419,58 @@ impl EventLoop { #[cfg(test)] mod tests { + use borsh::{BorshDeserialize, BorshSerialize}; + use race_api::{event::BridgeEvent, prelude::InitAccount}; + use race_core::checkpoint::Checkpoint; + use race_core::{checkpoint::VersionedData, context::EventEffects}; + use race_core::error::Result; + use super::*; + #[derive(BorshSerialize, BorshDeserialize)] + struct EmptyBridgeEvent {} + + impl BridgeEvent for EmptyBridgeEvent {} + + struct TestHandlerForBridgeEvent {} + + impl HandlerT for TestHandlerForBridgeEvent { + fn init_state(&mut self, _context: &mut GameContext, _init_account: &InitAccount) -> Result { + Ok(EventEffects::default()) + } + + fn handle_event(&mut self, context: &mut GameContext, _event: &Event) -> Result { + let mut ef = context.derive_effect(false); + ef.checkpoint(); + ef.bridge_event(0, EmptyBridgeEvent {})?; + let ee = context.apply_effect(ef); + ee + } + } + #[tokio::test(flavor = "multi_thread", worker_threads = 10)] + async fn test_event_loop_receive_bridge_event() { + let handler = TestHandlerForBridgeEvent {}; + let game_context = GameContext::default(); + + let (event_loop, event_loop_ctx) = EventLoop::init( + Box::new(handler), + game_context, + ClientMode::Transactor, + GameMode::Main, + ); + let mut event_loop_handle = event_loop.start("fake addresses", event_loop_ctx); + let mut vd1 = VersionedData::default(); + vd1.id = 1; + event_loop_handle.send_unchecked(EventFrame::RecvBridgeEvent { + from: 1, + dest: 0, + event: Event::Bridge { dest_game_id: 0, from_game_id: 1, raw: vec![] }, + versioned_data: vd1, + }).await; + println!("Sent!"); + let recv = event_loop_handle.recv_unchecked().await; + println!("{recv:?}"); + assert!(false); + } } diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index 68d7591e..82d25e3c 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -296,7 +296,7 @@ mod tests { let mut hdlr = make_wrapped_handler(); let (game_account, _tx) = setup_game(); let mut ctx = GameContext::try_new(&game_account, None).unwrap(); - let init_account = ctx.init_account().unwrap(); + let init_account = ctx.init_account(); hdlr.init_state(&mut ctx, &init_account).unwrap(); assert_eq!( &vec![42u8, 0, 0, 0, 0, 0, 0, 0], @@ -311,7 +311,7 @@ mod tests { let (game_account, _tx) = setup_game(); let mut ctx = GameContext::try_new(&game_account, None).unwrap(); let event = Event::GameStart; - let init_account = ctx.init_account().unwrap(); + let init_account = ctx.init_account(); hdlr.init_state(&mut ctx, &init_account).unwrap(); println!("ctx: {:?}", ctx); hdlr.handle_event(&mut ctx, &event).unwrap(); @@ -329,7 +329,7 @@ mod tests { let (game_account, _tx) = setup_game(); let mut ctx = GameContext::try_new(&game_account, None).unwrap(); let event = Event::custom(0, &MinimalEvent::Increment(1)); - let init_account = ctx.init_account().unwrap(); + let init_account = ctx.init_account(); hdlr.init_state(&mut ctx, &init_account).unwrap(); println!("ctx: {:?}", ctx); hdlr.handle_event(&mut ctx, &event).unwrap(); diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index ef82ec58..e303dfb1 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -94,8 +94,6 @@ pub enum EventFrame { from: GameId, dest: GameId, event: Event, - // access_version: u64, - // settle_version: u64, versioned_data: VersionedData, }, /// Similar to `SendBridgeEvent`, but for receiver's event bus. @@ -103,9 +101,6 @@ pub enum EventFrame { from: GameId, dest: GameId, event: Event, - // #[allow(unused)] - // access_version: u64, - // settle_version: u64, versioned_data: VersionedData, }, From 56aa836c5767dc1badbb30fe64a7eb42450d9c14 Mon Sep 17 00:00:00 2001 From: miles Date: Sat, 21 Jun 2025 23:13:59 +0800 Subject: [PATCH 19/23] feat: add subgame shutdown --- core/src/context.rs | 1 - transactor/src/component/event_bridge.rs | 58 ++++++++++----- transactor/src/component/event_loop.rs | 70 +++++++++++++++---- .../src/component/event_loop/event_handler.rs | 15 +++- transactor/src/frame.rs | 9 +++ 5 files changed, 118 insertions(+), 35 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index 320789d8..20a4a4a5 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -883,7 +883,6 @@ impl GameContext { } let cur_id = self.game_id(); - // remove from Checkpoints for settle_detail in self.pending_settle_details.iter_mut() { settle_detail.settle_locks.remove(&game_id); if settle_detail.settle_locks.is_empty() { diff --git a/transactor/src/component/event_bridge.rs b/transactor/src/component/event_bridge.rs index aea31028..e4e92a8f 100644 --- a/transactor/src/component/event_bridge.rs +++ b/transactor/src/component/event_bridge.rs @@ -1,8 +1,8 @@ use crate::frame::{EventFrame, SignalFrame}; use async_trait::async_trait; -use tokio::sync::{broadcast, mpsc}; -use tracing::{info, warn, log::error}; use race_api::types::GameId; +use tokio::sync::{broadcast, mpsc}; +use tracing::{info, log::error, warn}; use super::{common::PipelinePorts, CloseReason, Component, ComponentEnv}; @@ -45,9 +45,7 @@ impl EventBridgeParent { let (mpsc_tx, mpsc_rx) = mpsc::channel(10); let (bc_tx, _bc_rx) = broadcast::channel(10); ( - Self { - bc: bc_tx.clone(), - }, + Self { bc: bc_tx.clone() }, EventBridgeParentContext { tx: bc_tx, rx: mpsc_rx, @@ -96,7 +94,6 @@ impl Component for EventBridgeParent { mut ctx: EventBridgeParentContext, env: ComponentEnv, ) -> CloseReason { - // We save the pending events here. let mut pending_events: Vec<(GameId, EventFrame)> = Vec::with_capacity(10); @@ -105,14 +102,13 @@ impl Component for EventBridgeParent { while let Some((from_bridge, event_frame)) = Self::read_event(&mut ports, &mut ctx.rx).await { - if from_bridge { // Bridge parent receives event from bridge child + if from_bridge { + // Bridge parent receives event from bridge child match event_frame { EventFrame::SendBridgeEvent { from, dest, event, - // access_version, - // settle_version, versioned_data, } => { info!("{} Receives event: {}", env.log_prefix, event); @@ -121,8 +117,6 @@ impl Component for EventBridgeParent { from, dest, event, - // access_version, - // settle_version, versioned_data, }) .await; @@ -169,9 +163,16 @@ impl Component for EventBridgeParent { ports.send(event_frame).await; } + + EventFrame::SubGameShutdown { game_id, .. } => { + info!("{} Receives subgame shutdown: {}", env.log_prefix, game_id); + ports.send(event_frame).await; + } + _ => (), } - } else { // Bridge parent receives event from event bus + } else { + // Bridge parent receives event from event bus match event_frame { EventFrame::LaunchSubGame { sub_game_init } => { let game_id = sub_game_init.spec.game_id; @@ -209,10 +210,19 @@ impl Component for EventBridgeParent { } } } - EventFrame::Sync { new_players, new_servers, transactor_addr, access_version, .. } => { + EventFrame::Sync { + new_players, + new_servers, + transactor_addr, + access_version, + .. + } => { if ctx.tx.receiver_count() > 0 { let sub_sync = EventFrame::SubSync { - new_players, new_servers, transactor_addr, access_version + new_players, + new_servers, + transactor_addr, + access_version, }; info!("{} Broadcast sync: {}", env.log_prefix, sub_sync); if let Err(e) = ctx.tx.send(sub_sync) { @@ -230,8 +240,10 @@ impl Component for EventBridgeParent { } impl EventBridgeChild { - - pub fn init(game_id: GameId, bridge_to_parent: BridgeToParent) -> (EventBridgeChild, EventBridgeChildContext) { + pub fn init( + game_id: GameId, + bridge_to_parent: BridgeToParent, + ) -> (EventBridgeChild, EventBridgeChildContext) { ( EventBridgeChild { game_id: game_id.clone(), @@ -283,7 +295,8 @@ impl Component for EventBridgeChild { ) -> CloseReason { while let Some((from_bridge, event_frame)) = Self::read_event(&mut ports, &mut ctx.rx).await { - if from_bridge { // Bridge child receives event from event parent + if from_bridge { + // Bridge child receives event from event parent match event_frame { EventFrame::Shutdown => { info!("{} Stopped", env.log_prefix); @@ -316,7 +329,8 @@ impl Component for EventBridgeChild { } _ => {} } - } else { // Bridge child receives event from event bus + } else { + // Bridge child receives event from event bus match event_frame { EventFrame::Shutdown => break, @@ -334,12 +348,20 @@ impl Component for EventBridgeChild { } } + EventFrame::SubGameShutdown { .. } => { + info!("{} Send SubGameShutdown to parent", env.log_prefix); + if let Err(e) = ctx.tx.send(event_frame).await { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + EventFrame::SendBridgeEvent { dest, .. } if dest != ctx.game_id => { info!("{} Sends event: {}", env.log_prefix, event_frame); if let Err(e) = ctx.tx.send(event_frame).await { error!("{} Failed to send: {}", env.log_prefix, e); } } + _ => continue, } } diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index f51d4ca3..4db0136a 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -306,6 +306,31 @@ impl Component for EventLoop { } } + EventFrame::SubGameShutdown { + game_id, + versioned_data, + } => { + if ctx.game_mode == GameMode::Main + && ctx.client_mode == ClientMode::Transactor + && game_context.game_id() == 0 + { + info!( + "SubGameShutdown: Update checkpoint for sub game: {}", + game_id + ); + if let Err(e) = + game_context.handle_versioned_data(game_id, versioned_data, false) + { + error!( + "{} SubGameShutdown: Failed in handling new sub game's versioned data: {:?}", + env.log_prefix, e + ); + ports.send(EventFrame::Shutdown).await; + return CloseReason::Fault(e); + } + } + } + EventFrame::RecvBridgeEvent { event, dest, @@ -385,6 +410,14 @@ impl Component for EventLoop { } EventFrame::Shutdown => { info!("{} Stopped", env.log_prefix); + + let game_id = game_context.game_id(); + if game_id != 0 { + if let Some(vd) = game_context.checkpoint().get_versioned_data(game_id) { + event_handler::send_subgame_shutdown(game_id, vd, &ports).await; + } + } + return CloseReason::Complete; } _ => (), @@ -414,16 +447,13 @@ impl EventLoop { } } - - #[cfg(test)] mod tests { use borsh::{BorshDeserialize, BorshSerialize}; use race_api::{event::BridgeEvent, prelude::InitAccount}; - use race_core::checkpoint::Checkpoint; - use race_core::{checkpoint::VersionedData, context::EventEffects}; use race_core::error::Result; + use race_core::{checkpoint::VersionedData, context::EventEffects}; use super::*; @@ -434,12 +464,20 @@ mod tests { struct TestHandlerForBridgeEvent {} - impl HandlerT for TestHandlerForBridgeEvent { - fn init_state(&mut self, _context: &mut GameContext, _init_account: &InitAccount) -> Result { + impl HandlerT for TestHandlerForBridgeEvent { + fn init_state( + &mut self, + _context: &mut GameContext, + _init_account: &InitAccount, + ) -> Result { Ok(EventEffects::default()) } - fn handle_event(&mut self, context: &mut GameContext, _event: &Event) -> Result { + fn handle_event( + &mut self, + context: &mut GameContext, + _event: &Event, + ) -> Result { let mut ef = context.derive_effect(false); ef.checkpoint(); ef.bridge_event(0, EmptyBridgeEvent {})?; @@ -462,12 +500,18 @@ mod tests { let mut event_loop_handle = event_loop.start("fake addresses", event_loop_ctx); let mut vd1 = VersionedData::default(); vd1.id = 1; - event_loop_handle.send_unchecked(EventFrame::RecvBridgeEvent { - from: 1, - dest: 0, - event: Event::Bridge { dest_game_id: 0, from_game_id: 1, raw: vec![] }, - versioned_data: vd1, - }).await; + event_loop_handle + .send_unchecked(EventFrame::RecvBridgeEvent { + from: 1, + dest: 0, + event: Event::Bridge { + dest_game_id: 0, + from_game_id: 1, + raw: vec![], + }, + versioned_data: vd1, + }) + .await; println!("Sent!"); let recv = event_loop_handle.recv_unchecked().await; println!("{recv:?}"); diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index fbf69895..aa05629e 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -91,8 +91,19 @@ async fn send_subgame_ready( } async fn send_subgame_recovered(game_id: GameId, ports: &PipelinePorts) { + ports.send(EventFrame::SubGameRecovered { game_id }).await; +} + +pub async fn send_subgame_shutdown( + game_id: GameId, + versioned_data: &VersionedData, + ports: &PipelinePorts, +) { ports - .send(EventFrame::SubGameRecovered { game_id }) + .send(EventFrame::SubGameShutdown { + game_id, + versioned_data: versioned_data.clone(), + }) .await; } @@ -127,8 +138,6 @@ async fn send_bridge_event( from_game_id: game_context.game_id(), raw: be.raw, }, - // access_version: game_context.access_version(), - // settle_version: game_context.settle_version(), versioned_data: checkpoint_state.clone(), }; ports.send(ef).await; diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index e303dfb1..63bd7ace 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -130,6 +130,12 @@ pub enum EventFrame { game_id: GameId, }, + /// Subgame send this frame when it will be shutdown. + SubGameShutdown { + game_id: GameId, + versioned_data: VersionedData, + }, + /// Reject a deposit RejectDeposits { reject_deposits: Vec, @@ -214,6 +220,9 @@ impl std::fmt::Display for EventFrame { EventFrame::SubGameRecovered { game_id } => { write!(f, "SubGameRecovered, game_id: {}", game_id) } + EventFrame::SubGameShutdown { game_id, .. } => { + write!(f, "SubGameShutdown, game_id: {}", game_id) + } EventFrame::RejectDeposits { reject_deposits } => { write!(f, "Reject deposits, {:?}", reject_deposits) } From 902031d019db69d2a8c03649bbf1e13b7629b76a Mon Sep 17 00:00:00 2001 From: miles Date: Sun, 22 Jun 2025 02:37:37 +0800 Subject: [PATCH 20/23] fix: lack subgame shutdown handle --- core/src/context.rs | 2 ++ transactor/src/component/event_loop.rs | 8 -------- transactor/src/component/event_loop/event_handler.rs | 8 ++++++++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index 20a4a4a5..43f07e12 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -155,6 +155,7 @@ pub struct EventEffects { pub launch_sub_games: Vec, pub bridge_events: Vec, pub start_game: bool, + pub stop_game: bool, pub logs: Vec, pub reject_deposits: Vec, pub checkpoint: Option, @@ -1164,6 +1165,7 @@ impl GameContext { launch_sub_games, bridge_events, start_game, + stop_game, logs: effect.logs, reject_deposits, checkpoint: (is_checkpoint || is_init).then(|| self.checkpoint.clone()), diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 4db0136a..e0e61884 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -410,14 +410,6 @@ impl Component for EventLoop { } EventFrame::Shutdown => { info!("{} Stopped", env.log_prefix); - - let game_id = game_context.game_id(); - if game_id != 0 { - if let Some(vd) = game_context.checkpoint().get_versioned_data(game_id) { - event_handler::send_subgame_shutdown(game_id, vd, &ports).await; - } - } - return CloseReason::Complete; } _ => (), diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index aa05629e..741d90a7 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -364,6 +364,7 @@ pub async fn handle_event( launch_sub_games, bridge_events, start_game, + stop_game, logs, reject_deposits, checkpoint, @@ -386,6 +387,13 @@ pub async fn handle_event( send_start_game(&game_context, ports).await; } + if client_mode == ClientMode::Transactor && stop_game { + let game_id = game_context.game_id(); + if let Some(vd) = game_context.checkpoint().get_versioned_data(game_id) { + send_subgame_shutdown(game_id, vd, ports).await; + } + } + // Launch sub games if game_mode == GameMode::Main { launch_sub_game(launch_sub_games, game_context, ports, env).await; From b44e607a3056362a3aa3f8197d02b9293110a4a1 Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Mon, 23 Jun 2025 10:51:48 +0800 Subject: [PATCH 21/23] Remove Ready event --- api/src/engine.rs | 2 +- api/src/event.rs | 5 ----- core/src/engine.rs | 2 -- proc-macro/src/lib.rs | 2 +- transactor/src/component/event_loop/event_handler.rs | 7 +------ 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/api/src/engine.rs b/api/src/engine.rs index b13cb6e6..edb9dfe6 100644 --- a/api/src/engine.rs +++ b/api/src/engine.rs @@ -4,7 +4,7 @@ use crate::{effect::Effect, error::HandleResult, event::Event, init_account::Ini pub trait GameHandler: Sized + BorshSerialize + BorshDeserialize { /// Initialize handler state with on-chain game account data. - fn init_state(init_account: InitAccount) -> HandleResult; + fn init_state(effect: &mut Effect, init_account: InitAccount) -> HandleResult; /// Handle event. fn handle_event(&mut self, effect: &mut Effect, event: Event) -> HandleResult<()>; diff --git a/api/src/event.rs b/api/src/event.rs index 929ae495..bb069cfb 100644 --- a/api/src/event.rs +++ b/api/src/event.rs @@ -30,10 +30,6 @@ pub enum Event { raw: Vec, }, - /// A event sent by system, the first event sent by transactor - /// when game is loaded. - Ready, - /// Transactor shares its secert to specific player. /// The `secret_data` is encrypted with the receiver's public key. ShareSecrets { @@ -159,7 +155,6 @@ impl std::fmt::Display for Event { dest_game_id, from_game_id, raw[0] ) } - Event::Ready => write!(f, "Ready"), Event::ShareSecrets { sender, shares } => { let repr = shares .iter() diff --git a/core/src/engine.rs b/core/src/engine.rs index 3c55773c..59b8ea73 100644 --- a/core/src/engine.rs +++ b/core/src/engine.rs @@ -11,8 +11,6 @@ pub fn general_handle_event( ) -> Result<(), Error> { // General event handling match event { - Event::Ready => Ok(()), - Event::ShareSecrets { sender, shares } => { let addr = context.id_to_addr(*sender)?; context.add_shared_secrets(addr, shares.clone())?; diff --git a/proc-macro/src/lib.rs b/proc-macro/src/lib.rs index 17ee4443..fd2ddcc5 100644 --- a/proc-macro/src/lib.rs +++ b/proc-macro/src/lib.rs @@ -95,7 +95,7 @@ pub fn game_handler(_metadata: TokenStream, input: TokenStream) -> TokenStream { } else { return 2 }; - match #s_idt::init_state(init_account) { + match #s_idt::init_state(&mut effect, init_account) { Ok(handler) => effect.__set_handler_result(handler), Err(e) => effect.__set_error(e), } diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 741d90a7..1853e82c 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -219,7 +219,7 @@ pub async fn init_state( handler: &mut dyn HandlerT, mut game_context: &mut GameContext, ports: &PipelinePorts, - client_mode: ClientMode, + _client_mode: ClientMode, game_mode: GameMode, env: &ComponentEnv, ) -> Option { @@ -255,11 +255,6 @@ pub async fn init_state( do_send_settlements(game_context, ports, env).await; - // Dispatch the initial Ready event if running in Transactor mode. - if client_mode == ClientMode::Transactor { - game_context.dispatch_safe(Event::Ready, 0); - } - // Tell master game the subgame is successfully created. if game_mode == GameMode::Sub { let game_id = game_context.game_id(); From be6a93b3f73256a7b8e3bfb9f81a5f281760c6f7 Mon Sep 17 00:00:00 2001 From: miles Date: Mon, 23 Jun 2025 11:35:06 +0800 Subject: [PATCH 22/23] fix: can not restart before subgame start, due to dispatch event is None --- core/src/context.rs | 4 +-- .../src/component/event_loop/event_handler.rs | 35 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index 43f07e12..b9f6379c 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -698,9 +698,9 @@ impl GameContext { } /// Dispatch an event if there's none - pub fn dispatch_safe(&mut self, event: Event, timeout: u64) { + pub fn set_dispatch(&mut self, event: Option) { if self.dispatch.is_none() { - self.dispatch = Some(DispatchEvent::new(event, timeout + self.timestamp)); + self.dispatch = event; } } diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 1853e82c..003ad0f1 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -310,26 +310,23 @@ pub async fn recover_from_checkpoint( send_subgame_recovered(game_context.game_id(), ports).await; } - for versioned_data in game_context.checkpoint().list_versioned_data() { - if game_context.game_id() == versioned_data.id { - if !versioned_data.bridge_events.is_empty() { - send_bridge_event( - versioned_data.bridge_events.clone(), - game_context, - ports, - env, - ) - .await; - } - - if let Some(dispatch) = versioned_data.dispatch.as_ref() { - let server_event = EventFrame::SendServerEvent { - event: dispatch.event.clone(), - timestamp: dispatch.timeout, - }; - ports.send(server_event).await; - } + if let Some(versioned_data) = game_context + .checkpoint() + .list_versioned_data() + .iter() + .find(|vd| vd.id == game_context.game_id()) + { + if !versioned_data.bridge_events.is_empty() { + send_bridge_event( + versioned_data.bridge_events.clone(), + game_context, + ports, + env, + ) + .await; } + + game_context.set_dispatch(versioned_data.dispatch.clone()); } } From 2f8ebf71688f6ed1258c08398009778add2773ac Mon Sep 17 00:00:00 2001 From: miles Date: Mon, 23 Jun 2025 17:34:09 +0800 Subject: [PATCH 23/23] fix: wrong settle details pushed into pending vec --- api/src/effect.rs | 1 + api/src/init_account.rs | 3 + core/src/checkpoint.rs | 19 ++- core/src/context.rs | 126 ++++++------------ dev/scripts/misc.sh | 2 +- .../src/component/event_loop/event_handler.rs | 14 +- 6 files changed, 75 insertions(+), 90 deletions(-) diff --git a/api/src/effect.rs b/api/src/effect.rs index 60ac3469..501a6a0d 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -60,6 +60,7 @@ pub struct ActionTimeout { } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SubGame { pub id: GameId, pub bundle_addr: String, diff --git a/api/src/init_account.rs b/api/src/init_account.rs index 14bfdd5c..9e887b01 100644 --- a/api/src/init_account.rs +++ b/api/src/init_account.rs @@ -1,9 +1,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use crate::error::HandleError; /// A set of arguments for game handler initialization. #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct InitAccount { pub max_players: u16, pub data: Vec, diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs index 0a89d3fc..e2f767ba 100644 --- a/core/src/checkpoint.rs +++ b/core/src/checkpoint.rs @@ -11,7 +11,7 @@ use rs_merkle::{algorithms::Sha256, proof_serializers::ReverseHashesOrder, Hashe use serde::{Deserialize, Serialize}; use crate::types::GameSpec; -use race_api::{effect::EmitBridgeEvent, types::GameId}; +use race_api::{effect::{EmitBridgeEvent, SubGame}, types::GameId}; /// Checkpoint represents the state snapshot of game. /// It is used as a submission to the blockchain. @@ -23,6 +23,7 @@ pub struct Checkpoint { pub access_version: u64, pub data: HashMap, pub proofs: HashMap>, + pub launch_subgames: Vec, } #[derive(Default, Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -40,6 +41,7 @@ pub struct CheckpointOnChain { pub struct CheckpointOffChain { pub data: HashMap, pub proofs: HashMap>, + pub launch_subgames: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -91,6 +93,7 @@ impl Checkpoint { }, )]), proofs: HashMap::new(), + launch_subgames: vec![], }; ret.update_root_and_proofs(); ret @@ -105,6 +108,7 @@ impl Checkpoint { data: offchain_part.data, access_version: onchain_part.access_version, root: onchain_part.root, + launch_subgames: offchain_part.launch_subgames, } } @@ -236,6 +240,18 @@ impl Checkpoint { } } + pub fn append_launch_subgames(&mut self, subgame: SubGame) { + self.launch_subgames.push(subgame); + } + + pub fn delete_launch_subgames(&mut self, id: GameId) { + self.launch_subgames.retain(|subgame| subgame.id != id); + } + + pub fn get_launch_subgames(&self) -> Vec { + self.launch_subgames.clone() + } + pub fn clear_future_events(&mut self) { self.data.values_mut().for_each(VersionedData::clear_future_events); } @@ -284,6 +300,7 @@ impl Checkpoint { CheckpointOffChain { proofs: self.proofs.clone(), data: self.data.clone(), + launch_subgames: self.launch_subgames.clone(), } } diff --git a/core/src/context.rs b/core/src/context.rs index b9f6379c..8ab26c90 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -186,7 +186,8 @@ impl SettleDetails { Ok(()) } - pub fn print(&self) { + pub fn print(&self, prefix: String) { + println!("-------- {} --------", prefix); println!("Version: {}", self.settle_version); println!("Locks: {:?}", self.settle_locks); println!("Settles: {:?}", self.settles); @@ -201,12 +202,6 @@ impl SettleDetails { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SettleLock { - Locked(u64), - Unlocked(VersionedData), -} - /// The context for public data. /// /// This information is not transmitted over the network, instead it's @@ -268,7 +263,7 @@ pub struct GameContext { /// The pending settle details. They are here because the settle is blocked by the settle locks. pub(crate) pending_settle_details: Vec, /// The locks for the under construction settle - pub(crate) next_settle_locks: HashMap, + pub(crate) next_settle_locks: HashMap, } impl GameContext { @@ -843,41 +838,16 @@ impl GameContext { versioned_data: VersionedData, is_init: bool, ) -> Result<()> { - // match self.next_settle_locks.entry(game_id) { - // // Update - // std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { - // match occupied_entry.get() { - // SettleLock::Locked(_) => { - // versioned_data.bridge_events = vec![]; - // occupied_entry.insert(SettleLock::Unlocked(versioned_data.clone())); - // self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; - // println!("locked"); - // } - // SettleLock::Unlocked(_) => { - // return Err(Error::InvalidCheckpoint); - // } - // } - // } - // // Initialize - // std::collections::hash_map::Entry::Vacant(_) => { - // if is_init { - // self.checkpoint_mut().init_versioned_data(versioned_data.clone())?; - // } else { - // self.checkpoint_mut().update_versioned_data(versioned_data.clone())?; - // self.pending_settle_details - // .iter_mut() - // .map(|sd| sd.handle_versioned_data(versioned_data.clone())) - // .collect::>>()?; - // } - // } - // } - - self.next_settle_locks - .insert(game_id, SettleLock::Unlocked(versioned_data.clone())); + if let Some(&old_version) = self.next_settle_locks.get(&game_id) { + if old_version < versioned_data.versions.settle_version { + self.next_settle_locks.remove(&game_id); + } + } if is_init { self.checkpoint_mut() .init_versioned_data(versioned_data.clone())?; + self.checkpoint_mut().delete_launch_subgames(game_id); } else { self.checkpoint_mut() .update_versioned_data(versioned_data.clone())?; @@ -885,7 +855,12 @@ impl GameContext { let cur_id = self.game_id(); for settle_detail in self.pending_settle_details.iter_mut() { - settle_detail.settle_locks.remove(&game_id); + if let Some(&old_version) = settle_detail.settle_locks.get(&game_id) { + if old_version < versioned_data.versions.settle_version { + settle_detail.settle_locks.remove(&game_id); + } + } + if settle_detail.settle_locks.is_empty() { if let Some(master_cp) = settle_detail.checkpoint.get_versioned_data(0) { for be in master_cp.bridge_events.clone() { @@ -896,7 +871,7 @@ impl GameContext { } } if cur_id == 0 { - settle_detail.print(); + settle_detail.print("handle_versioned_data".to_string()); } } @@ -1070,8 +1045,14 @@ impl GameContext { if let Some(state) = handler_state { self.set_handler_state_raw(state.clone()); + let mut settles = vec![]; if is_checkpoint { + let settles_map = build_settles_map(&withdraws, &ejects, &self.balances, &balances); + self.balances = balances; + settles = settles_map.into_values().collect(); + settles.sort_by_key(|s| s.player_id); + self.random_states.clear(); self.decision_states.clear(); self.bump_settle_version()?; @@ -1092,59 +1073,18 @@ impl GameContext { self.set_game_status(GameStatus::Idle); } + let game_id = self.game_id(); + let dispatch = self.dispatch.clone(); // Append SubGame to context for sub_game in launch_sub_games.iter().cloned() { - self.sub_games.push(sub_game); - } - - let mut settles = vec![]; - if is_checkpoint { - let settles_map = build_settles_map(&withdraws, &ejects, &self.balances, &balances); - self.balances = balances; - settles = settles_map.into_values().collect(); - settles.sort_by_key(|s| s.player_id); + self.sub_games.push(sub_game.clone()); + self.checkpoint_mut().append_launch_subgames(sub_game); } - - let game_id = self.game_id(); - let dispatch = self.dispatch.clone(); - self.checkpoint_mut() .set_dispatch_in_versioned_data(game_id, dispatch)?; self.checkpoint_mut() .set_bridge_in_versioned_data(game_id, bridge_events.clone())?; - if self.spec.game_id == 0 { - for sub_game in launch_sub_games.iter().cloned() { - self.next_settle_locks - .insert(sub_game.id, SettleLock::Locked(0)); - } - - for evt in bridge_events.clone() { - let settle_version = self - .checkpoint - .get_versioned_data(evt.dest) - .map(|d| d.versions.settle_version) - .unwrap_or(0); - self.next_settle_locks - .insert(evt.dest, SettleLock::Locked(settle_version)); - } - } - - let settle_locks: Vec<_> = self.next_settle_locks.drain().collect(); - let mut locked_settle_locks: HashMap = HashMap::new(); - - for (game_id, settle_lock) in settle_locks { - match settle_lock { - SettleLock::Locked(settle_version) => { - locked_settle_locks.insert(game_id, settle_version); - } - SettleLock::Unlocked(versioned_data) => { - self.checkpoint_mut() - .update_versioned_data(versioned_data)?; - } - } - } - if is_checkpoint || is_init { let settle_details = SettleDetails { settles, @@ -1156,11 +1096,23 @@ impl GameContext { previous_settle_version, entry_lock, accept_deposits: self.accept_deposits.drain(..).collect(), - settle_locks: locked_settle_locks, + settle_locks: self.next_settle_locks.clone(), }; self.pending_settle_details.push(settle_details); } + if self.spec.game_id == 0 { + // Add lock into next_settle_locks + for evt in bridge_events.clone() { + let settle_version = self + .checkpoint + .get_versioned_data(evt.dest) + .map(|d| d.versions.settle_version) + .unwrap_or(0); + self.next_settle_locks.insert(evt.dest, settle_version); + } + } + return Ok(EventEffects { launch_sub_games, bridge_events, diff --git a/dev/scripts/misc.sh b/dev/scripts/misc.sh index 4df10bb0..421f1d56 100644 --- a/dev/scripts/misc.sh +++ b/dev/scripts/misc.sh @@ -3,7 +3,7 @@ BORSH="$ROOT/../race-sdk/packages/borsh" # Make a JSON specification for tourney game # TITLE TOKEN START_TIME function make_tourney { - local ENTRY_CLOSE_TIME=$(expr $3 + 300000) + local ENTRY_CLOSE_TIME=$(expr $3 + 50000) local TICKET=100000000 local TABLE_SIZE=$4 local START_CHIPS=100000 diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 003ad0f1..b725cf2c 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -171,7 +171,9 @@ async fn do_send_settlements( game_context.settle_version(), ); - settle_details.print(); + if game_context.game_id() == 0 { + settle_details.print("do_send_settlements".to_string()); + } ports .send(EventFrame::Settle { @@ -310,6 +312,16 @@ pub async fn recover_from_checkpoint( send_subgame_recovered(game_context.game_id(), ports).await; } + if game_mode == GameMode::Main { + launch_sub_game( + game_context.checkpoint().get_launch_subgames(), + game_context, + ports, + env, + ) + .await; + } + if let Some(versioned_data) = game_context .checkpoint() .list_versioned_data()