diff --git a/src/bank.rs b/src/bank.rs index 744cc32717..c94ce092e1 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -27,6 +27,7 @@ use std::time::Instant; use storage_program::StorageProgram; use system_program::SystemProgram; use system_transaction::SystemTransaction; +use tictactoe_dashboard_program::TicTacToeDashboardProgram; use tictactoe_program::TicTacToeProgram; use timing::{duration_as_us, timestamp}; use transaction::Transaction; @@ -433,6 +434,12 @@ impl Bank { { return Err(BankError::ProgramRuntimeError(program_index as u8)); } + } else if TicTacToeDashboardProgram::check_id(&tx_program_id) { + if TicTacToeDashboardProgram::process_transaction(&tx, program_index, program_accounts) + .is_err() + { + return Err(BankError::ProgramRuntimeError(program_index as u8)); + } } else if self.loaded_contract(tx_program_id, tx, program_index, program_accounts) { } else { return Err(BankError::UnknownContractId(program_index as u8)); diff --git a/src/lib.rs b/src/lib.rs index f97239fc19..2e81616a4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,7 @@ pub mod streamer; pub mod system_program; pub mod system_transaction; pub mod thin_client; +pub mod tictactoe_dashboard_program; pub mod tictactoe_program; pub mod timing; pub mod tpu; diff --git a/src/tictactoe_dashboard_program.rs b/src/tictactoe_dashboard_program.rs new file mode 100644 index 0000000000..d5adf0b6fb --- /dev/null +++ b/src/tictactoe_dashboard_program.rs @@ -0,0 +1,169 @@ +//! tic-tac-toe dashboard program + +use serde_cbor; +use solana_program_interface::account::Account; +use solana_program_interface::pubkey::Pubkey; +use tictactoe_program::{Error, Game, Result, State, TicTacToeProgram}; +use transaction::Transaction; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct TicTacToeDashboardProgram { + pending: Pubkey, // Latest pending game + completed: Vec, // Last N completed games (0 is the latest) + total: usize, // Total number of completed games +} + +pub const TICTACTOE_DASHBOARD_PROGRAM_ID: [u8; 32] = [ + 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +impl TicTacToeDashboardProgram { + fn deserialize(input: &[u8]) -> Result { + if input.len() < 2 { + Err(Error::InvalidUserdata)?; + } + let len = input[0] as usize + (0xFF * input[1] as usize); + if len == 0 { + Ok(TicTacToeDashboardProgram::default()) + } else if input.len() < len + 2 { + Err(Error::InvalidUserdata) + } else { + serde_cbor::from_slice(&input[2..(2 + len)]).map_err(|err| { + error!("Unable to deserialize game: {:?}", err); + Error::InvalidUserdata + }) + } + } + + fn update( + self: &mut TicTacToeDashboardProgram, + game_pubkey: &Pubkey, + game: &Game, + ) -> Result<()> { + match game.state { + State::Waiting => { + self.pending = *game_pubkey; + } + State::XMove | State::OMove => { + // Nothing to do. In progress games are not managed by the dashboard + } + State::XWon | State::OWon | State::Draw => { + if !self.completed.iter().any(|pubkey| pubkey == game_pubkey) { + // TODO: Once the PoH high is exposed to programs, it could be used to ensure + // that old games are not being re-added and causing |total| to increment + // incorrectly. + self.total += 1; + self.completed.insert(0, *game_pubkey); + + // Only track the last N completed games to + // avoid overrunning Account userdata + if self.completed.len() > 5 { + self.completed.pop(); + } + } + } + }; + + Ok(()) + } + + fn serialize(self: &TicTacToeDashboardProgram, output: &mut [u8]) -> Result<()> { + let self_serialized = serde_cbor::to_vec(self).unwrap(); + + if output.len() < 2 + self_serialized.len() { + warn!( + "{} bytes required to serialize but only have {} bytes", + self_serialized.len() + 2, + output.len() + ); + return Err(Error::UserdataTooSmall); + } + + assert!(self_serialized.len() <= 0xFFFF); + output[0] = (self_serialized.len() & 0xFF) as u8; + output[1] = (self_serialized.len() >> 8) as u8; + output[2..(2 + self_serialized.len())].clone_from_slice(&self_serialized); + Ok(()) + } + + pub fn check_id(program_id: &Pubkey) -> bool { + program_id.as_ref() == TICTACTOE_DASHBOARD_PROGRAM_ID + } + + pub fn id() -> Pubkey { + Pubkey::new(&TICTACTOE_DASHBOARD_PROGRAM_ID) + } + + pub fn process_transaction( + tx: &Transaction, + pix: usize, + accounts: &mut [&mut Account], + ) -> Result<()> { + info!("process_transaction: {:?}", tx); + // accounts[0] doesn't matter, anybody can cause a dashboard update + // accounts[1] must be a Dashboard account + // accounts[2] must be a Game account + if accounts.len() != 3 { + error!("Expected 3 accounts"); + Err(Error::InvalidArguments)?; + } + if !Self::check_id(&accounts[1].program_id) { + error!("accounts[1] is not a TICTACTOE_DASHBOARD_PROGRAM_ID"); + Err(Error::InvalidArguments)?; + } + if accounts[1].userdata.is_empty() { + error!("accounts[1] userdata is empty"); + Err(Error::InvalidArguments)?; + } + + let mut dashboard = Self::deserialize(&accounts[1].userdata)?; + + if !TicTacToeProgram::check_id(&accounts[2].program_id) { + error!("accounts[2] is not a TICTACTOE_PROGRAM_ID"); + Err(Error::InvalidArguments)?; + } + let ttt = TicTacToeProgram::deserialize(&accounts[2].userdata)?; + + match ttt.game { + None => Err(Error::NoGame), + Some(game) => dashboard.update(tx.key(pix, 2).unwrap(), &game), + }?; + + dashboard.serialize(&mut accounts[1].userdata)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn serde() { + let mut dashboard1 = TicTacToeDashboardProgram::default(); + dashboard1.total = 123; + + let mut userdata = vec![0xff; 256]; + dashboard1.serialize(&mut userdata).unwrap(); + + let dashboard2 = TicTacToeDashboardProgram::deserialize(&userdata).unwrap(); + + assert_eq!(dashboard1, dashboard2); + } + + #[test] + pub fn serde_userdata_too_small() { + let dashboard = TicTacToeDashboardProgram::default(); + let mut userdata = vec![0xff; 1]; + assert_eq!( + dashboard.serialize(&mut userdata), + Err(Error::UserdataTooSmall) + ); + + let err = TicTacToeDashboardProgram::deserialize(&userdata); + assert!(err.is_err()); + assert_eq!(err.err().unwrap(), Error::InvalidUserdata); + } + + // TODO: add tests for business logic +} diff --git a/src/tictactoe_program.rs b/src/tictactoe_program.rs index 15e7685314..6654f2b1f0 100644 --- a/src/tictactoe_program.rs +++ b/src/tictactoe_program.rs @@ -24,7 +24,7 @@ impl std::fmt::Display for Error { } impl std::error::Error for Error {} -type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] enum BoardItem { @@ -55,7 +55,7 @@ impl Default for State { } #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] -struct Game { +pub struct Game { player_x: Pubkey, player_o: Option, pub state: State, @@ -164,7 +164,7 @@ enum Command { #[derive(Debug, Default, Serialize, Deserialize)] pub struct TicTacToeProgram { - game: Option, + pub game: Option, } pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [ @@ -172,7 +172,7 @@ pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [ ]; impl TicTacToeProgram { - fn deserialize(input: &[u8]) -> Result { + pub fn deserialize(input: &[u8]) -> Result { let len = input[0] as usize; if len == 0 {