diff --git a/src/channel_handler/mod.rs b/src/channel_handler/mod.rs index aa7c52fd..c5be53f1 100644 --- a/src/channel_handler/mod.rs +++ b/src/channel_handler/mod.rs @@ -17,8 +17,8 @@ use clvmr::allocator::NodePtr; use crate::channel_handler::game_handler::TheirTurnResult; use crate::channel_handler::types::{ - AcceptTransactionState, CachedPotatoRegenerateLastHop, ChannelCoin, ChannelCoinInfo, - ChannelCoinSpendInfo, ChannelCoinSpentResult, ChannelHandlerEnv, ChannelHandlerInitiationData, + CachedPotatoRegenerateLastHop, ChannelCoin, ChannelCoinInfo, ChannelCoinSpendInfo, + ChannelCoinSpentResult, ChannelHandlerEnv, ChannelHandlerInitiationData, ChannelHandlerInitiationResult, ChannelHandlerPrivateKeys, ChannelHandlerUnrollSpendInfo, CoinDataForReward, CoinSpentAccept, CoinSpentDisposition, CoinSpentInformation, CoinSpentMoveUp, CoinSpentResult, DispositionResult, GameStartInfo, HandshakeResult, LiveGame, @@ -1485,15 +1485,41 @@ impl ChannelHandler { let initial_potato = self.is_initial_potato(); debug!( - "{} ALIGN GAME STATES: initiated {} my state {} coin state {}", - initial_potato, - self.initiated_on_chain, - self.current_state_number, - self.unroll.coin.state_number, + "{initial_potato} ALIGN GAME STATES: initiated {} my state {} coin state {}", + self.initiated_on_chain, self.current_state_number, self.unroll.coin.state_number, + ); + + debug!( + "{initial_potato} cached state {:?}", + self.cached_last_action ); + debug!("{initial_potato} #game coins {}", coins.len()); let mover_puzzle_hash = private_to_public_key(&self.referee_private_key()); for game_coin in coins.iter() { + if let Some(CachedPotatoRegenerateLastHop::PotatoAccept(cached)) = + &self.cached_last_action + { + if *game_coin == cached.puzzle_hash { + let coin_id = CoinString::from_parts( + &unroll_coin.to_coin_id(), + &game_coin.clone(), + &cached.live_game.get_amount(), + ); + debug!("{initial_potato} set coin for accept"); + res.insert( + coin_id, + OnChainGameState { + game_id: cached.live_game.game_id.clone(), + puzzle_hash: game_coin.clone(), + our_turn: cached.live_game.is_my_turn(), + accept: None, + }, + ); + continue; + } + } + for live_game in self.live_games.iter_mut() { debug!( "live game id {:?} try to use coin {game_coin:?}", @@ -1504,6 +1530,7 @@ impl ChannelHandler { game_coin, self.current_state_number, )?; + if let Some((_my_turn, rewind_state)) = rewind_target { debug!("{} rewind target state was {rewind_state}", initial_potato); debug!("mover puzzle hash is {:?}", mover_puzzle_hash); @@ -1523,14 +1550,14 @@ impl ChannelHandler { game_id: live_game.game_id.clone(), puzzle_hash: game_coin.clone(), our_turn: live_game.is_my_turn(), - accept: AcceptTransactionState::Waiting, + accept: None, }, ); } } } - assert_eq!(res.is_empty(), self.live_games.is_empty()); + // assert_eq!(res.is_empty(), coins.is_empty()); Ok(res) } @@ -1702,11 +1729,47 @@ impl ChannelHandler { let mut cla = None; swap(&mut cla, &mut self.cached_last_action); match cla { - Some(CachedPotatoRegenerateLastHop::PotatoCreatedGame(_, _, _)) => { - todo!(); + Some(CachedPotatoRegenerateLastHop::PotatoCreatedGame( + ids, + my_contrib, + their_contrib, + )) => { + // Can't restart games on chain so rewind. + self.my_allocated_balance -= my_contrib; + self.their_allocated_balance -= their_contrib; + let remove_ids: Vec = self + .live_games + .iter() + .enumerate() + .filter_map(|(i, g)| { + if ids.iter().any(|i| g.game_id == *i) { + Some(i) + } else { + None + } + }) + .collect(); + for id in remove_ids.into_iter() { + self.live_games.remove(id); + } + Ok(None) } - Some(CachedPotatoRegenerateLastHop::PotatoAccept(_)) => { - todo!(); + Some(CachedPotatoRegenerateLastHop::PotatoAccept(mut accept)) => { + debug!("{} redo move is an accept", self.is_initial_potato()); + let transaction = accept + .live_game + .get_transaction_for_timeout(env.allocator, coin)?; + + debug!("{} redo accept data {accept:?}", self.is_initial_potato()); + let outcome_puzzle_hash = accept.live_game.outcome_puzzle_hash(env.allocator)?; + Ok(transaction.map(|t| { + GameAction::RedoAccept( + accept.live_game.game_id.clone(), + coin.clone(), + outcome_puzzle_hash, + Box::new(t), + ) + })) } Some(CachedPotatoRegenerateLastHop::PotatoMoveHappening(move_data)) => { let game_idx = self.get_game_by_id(&move_data.game_id)?; @@ -1841,11 +1904,14 @@ impl ChannelHandler { game_id: &GameID, coin: &CoinString, ) -> Result, Error> { - let game_idx = self.get_game_by_id(game_id)?; - let tx = self.live_games[game_idx].get_transaction_for_timeout(env.allocator, coin)?; - // Game is done one way or another. - self.live_games.remove(game_idx); - Ok(tx) + if let Ok(game_idx) = self.get_game_by_id(game_id) { + let tx = self.live_games[game_idx].get_transaction_for_timeout(env.allocator, coin)?; + // Game is done one way or another. + self.live_games.remove(game_idx); + Ok(tx) + } else { + Ok(None) + } } // the vanilla coin we get and each reward coin are all sent to the referee diff --git a/src/channel_handler/types.rs b/src/channel_handler/types.rs index c963b878..a1121823 100644 --- a/src/channel_handler/types.rs +++ b/src/channel_handler/types.rs @@ -902,7 +902,7 @@ pub struct OnChainGameState { pub game_id: GameID, pub puzzle_hash: PuzzleHash, pub our_turn: bool, - pub accept: AcceptTransactionState, + pub accept: Option, } impl LiveGame { diff --git a/src/peer_container.rs b/src/peer_container.rs index 4beaa86b..1ac84f39 100644 --- a/src/peer_container.rs +++ b/src/peer_container.rs @@ -646,7 +646,7 @@ impl SynchronousGameCradle { let msg = if let Some(msg) = self.state.outbound_messages.pop_back() { msg } else { - todo!(); + return Err(Error::StrErr("no message to replace".to_string())); }; let doc = bson::Document::from_reader(&mut msg.as_slice()).into_gen()?; diff --git a/src/potato_handler/mod.rs b/src/potato_handler/mod.rs index a27db59d..1f58e76f 100644 --- a/src/potato_handler/mod.rs +++ b/src/potato_handler/mod.rs @@ -609,6 +609,9 @@ impl PotatoHandler { Some(GameAction::RedoMove(_game_id, _coin, _new_ph, _transaction)) => { Err(Error::StrErr("redo move when not on chain".to_string())) } + Some(GameAction::RedoAccept(_, _, _, _)) => { + Err(Error::StrErr("redo accept when not on chain".to_string())) + } Some(GameAction::Accept(game_id)) => { let (sigs, amount) = { let ch = self.channel_handler_mut()?; @@ -1466,7 +1469,9 @@ impl PotatoHandler { self.game_action_queue.push_back(action); if !matches!(self.have_potato, PotatoState::Present) { - self.request_potato(penv)?; + if matches!(self.have_potato, PotatoState::Absent) { + self.request_potato(penv)?; + } return Ok(()); } @@ -1596,7 +1601,6 @@ impl PotatoHandler { def.game_id, player_ch.game_is_my_turn(&def.game_id) ); - assert_eq!(player_ch.game_is_my_turn(&def.game_id), Some(def.our_turn)); } // Register each coin that corresponds to a game. @@ -1704,7 +1708,9 @@ impl { + if on_chain.shut_down(penv, conditions.clone())? { + self.channel_handler = Some(on_chain.into_channel_handler()); + self.handshake_state = HandshakeState::Completed; + } else { + self.handshake_state = HandshakeState::OnChain(on_chain); + } + return Ok(()); + } + x => { + self.handshake_state = x; } - return Ok(()); } if !matches!(self.handshake_state, HandshakeState::Finished(_)) { diff --git a/src/potato_handler/on_chain.rs b/src/potato_handler/on_chain.rs index 3d7c68b8..fdc8832f 100644 --- a/src/potato_handler/on_chain.rs +++ b/src/potato_handler/on_chain.rs @@ -1,3 +1,4 @@ +use std::borrow::Borrow; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; @@ -6,9 +7,7 @@ use rand::Rng; use log::debug; -use crate::channel_handler::types::{ - AcceptTransactionState, CoinSpentInformation, OnChainGameState, ReadableMove, -}; +use crate::channel_handler::types::{CoinSpentInformation, OnChainGameState, ReadableMove}; use crate::channel_handler::ChannelHandler; use crate::common::types::{ Amount, CoinCondition, CoinSpend, CoinString, Error, GameID, Hash, IntoErr, Program, @@ -18,7 +17,7 @@ use crate::potato_handler::types::{ BootstrapTowardWallet, GameAction, PacketSender, PeerEnv, PotatoHandlerImpl, PotatoState, ToLocalUI, WalletSpendInterface, }; -use crate::referee::TheirTurnCoinSpentResult; +use crate::referee::{RefereeOnChainTransaction, TheirTurnCoinSpentResult}; use crate::shutdown::ShutdownConditions; pub struct OnChainPotatoHandler { @@ -62,6 +61,10 @@ impl PotatoHandlerImpl for OnChainPotatoHandler { &mut self.player_ch } + fn into_channel_handler(self) -> ChannelHandler { + self.player_ch + } + fn check_game_coin_spent<'a, G, R: Rng + 'a>( &mut self, penv: &mut dyn PeerEnv<'a, G, R>, @@ -226,24 +229,39 @@ impl PotatoHandlerImpl for OnChainPotatoHandler { let (env, system_interface) = penv.env(); debug!("{initial_potato} timeout coin {coin_id:?}, do accept"); - let result_transaction = - self.player_ch - .accept_or_timeout_game_on_chain(env, &game_def.game_id, coin_id)?; + if let Some(tx) = &game_def.accept { + debug!("{initial_potato} accept tx {tx:?}"); + self.have_potato = PotatoState::Present; - self.have_potato = PotatoState::Present; - if let Some(tx) = result_transaction { - debug!("{initial_potato} accept: have transaction {tx:?}"); - self.have_potato = PotatoState::Absent; system_interface.spend_transaction_and_add_fee(&SpendBundle { - name: Some(format!("{initial_potato} accept transaction")), + name: Some("redo move".to_string()), spends: vec![CoinSpend { coin: coin_id.clone(), bundle: tx.bundle.clone(), }], })?; } else { - debug!("{initial_potato} Accepted game when our share was zero"); - debug!("when action queue is {:?}", self.game_action_queue); + let result_transaction = self.player_ch.accept_or_timeout_game_on_chain( + env, + &game_def.game_id, + coin_id, + )?; + + self.have_potato = PotatoState::Present; + if let Some(tx) = result_transaction { + debug!("{initial_potato} accept: have transaction {tx:?}"); + self.have_potato = PotatoState::Absent; + system_interface.spend_transaction_and_add_fee(&SpendBundle { + name: Some(format!("{initial_potato} accept transaction")), + spends: vec![CoinSpend { + coin: coin_id.clone(), + bundle: tx.bundle.clone(), + }], + })?; + } else { + debug!("{initial_potato} Accepted game when our share was zero"); + debug!("when action queue is {:?}", self.game_action_queue); + } } // XXX Have a notification for this. @@ -405,6 +423,22 @@ impl PotatoHandlerImpl for OnChainPotatoHandler { )?; Ok(()) } + GameAction::RedoAccept(_game_id, coin, _new_ph, tx) => { + let (_env, system_interface) = penv.env(); + // Wait for timeout. + if let Some(def) = self.game_map.get_mut(&coin) { + self.have_potato = PotatoState::Absent; + debug!("{initial_potato} redo accept: register for timeout {coin:?}"); + let tx_borrow: &RefereeOnChainTransaction = tx.borrow(); + def.accept = Some(tx_borrow.clone()); + system_interface.register_coin( + &coin, + &self.channel_timeout, + Some("redo accept wait"), + )?; + } + Ok(()) + } GameAction::Accept(game_id) => { let current_coin = get_current_coin(&game_id)?; let my_turn = self.player_ch.game_is_my_turn(&game_id); @@ -413,7 +447,7 @@ impl PotatoHandlerImpl for OnChainPotatoHandler { ); if let Some(coin_def) = self.game_map.get_mut(¤t_coin) { - coin_def.accept = AcceptTransactionState::Waiting; + coin_def.accept = None; } Ok(()) diff --git a/src/potato_handler/types.rs b/src/potato_handler/types.rs index 016654ce..104331e1 100644 --- a/src/potato_handler/types.rs +++ b/src/potato_handler/types.rs @@ -416,6 +416,12 @@ pub enum GameAction { PuzzleHash, Box, ), + RedoAccept( + GameID, + CoinString, + PuzzleHash, + Box, + ), Accept(GameID), Shutdown(Rc), } @@ -427,6 +433,9 @@ impl std::fmt::Debug for GameAction { GameAction::RedoMove(gi, cs, ph, rt) => { write!(formatter, "RedoMove({gi:?},{cs:?},{ph:?},{rt:?})") } + GameAction::RedoAccept(gi, cs, ph, rt) => { + write!(formatter, "RedoAccept({gi:?},{cs:?},{ph:?},{rt:?})") + } GameAction::Accept(gi) => write!(formatter, "Accept({gi:?})"), GameAction::Shutdown(_) => write!(formatter, "Shutdown(..)"), } @@ -449,6 +458,8 @@ pub trait PotatoHandlerImpl { fn channel_handler_mut(&mut self) -> &mut ChannelHandler; + fn into_channel_handler(self) -> ChannelHandler; + fn check_game_coin_spent<'a, G, R: Rng + 'a>( &mut self, penv: &mut dyn PeerEnv<'a, G, R>, diff --git a/src/tests/peer/potato_handler_sim.rs b/src/tests/peer/potato_handler_sim.rs index be51cbe8..479f5434 100644 --- a/src/tests/peer/potato_handler_sim.rs +++ b/src/tests/peer/potato_handler_sim.rs @@ -968,9 +968,9 @@ fn run_calpoker_container_with_action_list_with_success_predicate( entropy, )?; } - GameAction::GoOnChain(_who) => { + GameAction::GoOnChain(who) => { debug!("go on chain"); - todo!(); + local_uis[*who].go_on_chain = true; } GameAction::FakeMove(who, readable, move_data) => { // This is a fake move. We give that move to the given target channel @@ -1176,3 +1176,59 @@ fn sim_test_with_peer_container_piss_off_peer_complete() { &outcome, ); } + +#[test] +fn sim_test_with_peer_container_piss_off_peer_after_start_complete() { + let mut allocator = AllocEncoder::new(); + + let moves = vec![ + GameAction::GoOnChain(1), + GameAction::Shutdown(0, Rc::new(BasicShutdownConditions)), + GameAction::Shutdown(1, Rc::new(BasicShutdownConditions)), + ]; + + let outcome = + run_calpoker_container_with_action_list(&mut allocator, &moves).expect("should finish"); + + let p1_ph = outcome.identities[0].puzzle_hash.clone(); + let p2_ph = outcome.identities[1].puzzle_hash.clone(); + let p1_coins = outcome.simulator.get_my_coins(&p1_ph).expect("should work"); + let p2_coins = outcome.simulator.get_my_coins(&p2_ph).expect("should work"); + let p1_balance: u64 = p1_coins + .iter() + .map(|c| c.to_parts().map(|(_, _, amt)| amt.to_u64()).unwrap_or(0)) + .sum(); + let p2_balance: u64 = p2_coins + .iter() + .map(|c| c.to_parts().map(|(_, _, amt)| amt.to_u64()).unwrap_or(0)) + .sum(); + + assert_eq!(p2_balance, p1_balance + 200); +} + +#[test] +fn sim_test_with_peer_container_piss_off_peer_after_accept_complete() { + let mut allocator = AllocEncoder::new(); + + let mut moves = test_moves_1(&mut allocator).to_vec(); + moves.push(GameAction::Accept(0)); + moves.push(GameAction::GoOnChain(1)); + moves.push(GameAction::Shutdown(0, Rc::new(BasicShutdownConditions))); + moves.push(GameAction::Shutdown(1, Rc::new(BasicShutdownConditions))); + let outcome = + run_calpoker_container_with_action_list(&mut allocator, &moves).expect("should finish"); + + let p0_view_of_cards = &outcome.local_uis[0].opponent_moves[0]; + let p1_view_of_cards = &outcome.local_uis[1].opponent_moves[1]; + let alice_outcome_move = &outcome.local_uis[0].opponent_moves[1]; + let bob_outcome_move = &outcome.local_uis[1].opponent_moves[2]; + + check_calpoker_economic_result( + &mut allocator, + p0_view_of_cards, + p1_view_of_cards, + alice_outcome_move, + bob_outcome_move, + &outcome, + ); +}