Compare commits

...

8 Commits

9 changed files with 238 additions and 88 deletions

View File

@ -14,7 +14,6 @@ _() {
_ cargo fmt -- --check _ cargo fmt -- --check
_ cargo build --verbose _ cargo build --verbose
_ cargo test --verbose _ cargo test --verbose
_ cargo clippy -- --deny=warnings
echo --- ci/localnet-sanity.sh echo --- ci/localnet-sanity.sh
( (

View File

@ -24,7 +24,7 @@ usage: $0 [name] [zone] [options...]
Deploys a CD testnet Deploys a CD testnet
name - name of the network name - name of the network
zone - GCE to deploy the network into zone - zone to deploy the network into
options: options:
-s edge|beta|stable - Deploy the specified Snap release channel -s edge|beta|stable - Deploy the specified Snap release channel
@ -116,7 +116,7 @@ fi
set -x set -x
echo --- gce.sh delete echo --- gce.sh delete
time net/gce.sh delete -p "$netName" time net/gce.sh delete -z "$zone" -p "$netName"
if $delete; then if $delete; then
exit 0 exit 0
fi fi

View File

@ -9,11 +9,12 @@ usage() {
echo "Error: $*" echo "Error: $*"
fi fi
cat <<EOF cat <<EOF
usage: $0 [name] usage: $0 [name] [zone]
Sanity check a CD testnet Sanity check a CD testnet
name - name of the network name - name of the network
zone - zone of the network
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
metrics metrics
@ -22,11 +23,13 @@ EOF
} }
netName=$1 netName=$1
zone=$2
[[ -n $netName ]] || usage "" [[ -n $netName ]] || usage ""
[[ -n $zone ]] || usage "Zone not specified"
set -x set -x
echo --- gce.sh config echo --- gce.sh config
net/gce.sh config -p "$netName" net/gce.sh config -p "$netName" -z "$zone"
net/init-metrics.sh -e net/init-metrics.sh -e
echo --- net.sh sanity echo --- net.sh sanity
net/net.sh sanity \ net/net.sh sanity \

View File

@ -33,6 +33,7 @@ pay_and_confirm() {
$solana_wallet "${entrypoint[@]}" confirm "$signature" $solana_wallet "${entrypoint[@]}" confirm "$signature"
} }
$solana_keygen
$solana_wallet "${entrypoint[@]}" address $solana_wallet "${entrypoint[@]}" address
check_balance_output "No account found" "Your balance is: 0" check_balance_output "No account found" "Your balance is: 0"
$solana_wallet "${entrypoint[@]}" airdrop 60 $solana_wallet "${entrypoint[@]}" airdrop 60

View File

@ -28,6 +28,7 @@ use std::time::Instant;
use storage_program::StorageProgram; use storage_program::StorageProgram;
use system_program::SystemProgram; use system_program::SystemProgram;
use system_transaction::SystemTransaction; use system_transaction::SystemTransaction;
use tictactoe_dashboard_program::TicTacToeDashboardProgram;
use tictactoe_program::TicTacToeProgram; use tictactoe_program::TicTacToeProgram;
use timing::{duration_as_us, timestamp}; use timing::{duration_as_us, timestamp};
use transaction::Transaction; use transaction::Transaction;
@ -405,6 +406,10 @@ impl Bank {
if TicTacToeProgram::process_transaction(&tx, accounts).is_err() { if TicTacToeProgram::process_transaction(&tx, accounts).is_err() {
return Err(BankError::ProgramRuntimeError); return Err(BankError::ProgramRuntimeError);
} }
} else if TicTacToeDashboardProgram::check_id(&tx.program_id) {
if TicTacToeDashboardProgram::process_transaction(&tx, accounts).is_err() {
return Err(BankError::ProgramRuntimeError);
}
} else if self.loaded_contract(&tx, accounts) { } else if self.loaded_contract(&tx, accounts) {
} else { } else {
return Err(BankError::UnknownContractId); return Err(BankError::UnknownContractId);

View File

@ -60,6 +60,7 @@ pub mod streamer;
pub mod system_program; pub mod system_program;
pub mod system_transaction; pub mod system_transaction;
pub mod thin_client; pub mod thin_client;
pub mod tictactoe_dashboard_program;
pub mod tictactoe_program; pub mod tictactoe_program;
pub mod timing; pub mod timing;
pub mod tpu; pub mod tpu;

View File

@ -43,7 +43,7 @@ impl JsonRpcService {
io.extend_with(rpc.to_delegate()); io.extend_with(rpc.to_delegate());
let server = let server =
ServerBuilder::with_meta_extractor(io, move |_req: &hyper::Request| Meta { ServerBuilder::with_meta_extractor(io, move |_req: &hyper::Request<hyper::Body>| Meta {
request_processor: request_processor.clone(), request_processor: request_processor.clone(),
transactions_addr, transactions_addr,
drone_addr, drone_addr,

View File

@ -0,0 +1,165 @@
//! 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<Pubkey>, // 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<TicTacToeDashboardProgram> {
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, accounts: &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.keys[2], &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
}

View File

@ -24,25 +24,24 @@ impl std::fmt::Display for Error {
} }
impl std::error::Error for Error {} impl std::error::Error for Error {}
type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
enum GridItem { enum BoardItem {
Free, F, // Free
X, X,
O, O,
} }
impl Default for GridItem { impl Default for BoardItem {
fn default() -> GridItem { fn default() -> BoardItem {
GridItem::Free BoardItem::F
} }
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
enum State { pub enum State {
WaitingForO, Waiting,
ORequestPending,
XMove, XMove,
OMove, OMove,
XWon, XWon,
@ -51,131 +50,121 @@ enum State {
} }
impl Default for State { impl Default for State {
fn default() -> State { fn default() -> State {
State::WaitingForO State::Waiting
} }
} }
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] #[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
struct Game { pub struct Game {
player_x: Pubkey, player_x: Pubkey,
player_o: Option<Pubkey>, player_o: Option<Pubkey>,
state: State, pub state: State,
grid: [GridItem; 9], board: [BoardItem; 9],
keep_alive: [i64; 2],
} }
impl Game { impl Game {
pub fn create(player_x: &Pubkey) -> Game { pub fn create(player_x: &Pubkey) -> Game {
let mut game = Game::default(); let mut game = Game::default();
game.player_x = *player_x; game.player_x = *player_x;
assert_eq!(game.state, State::WaitingForO); assert_eq!(game.state, State::Waiting);
game game
} }
#[cfg(test)] #[cfg(test)]
pub fn new(player_x: Pubkey, player_o: Pubkey) -> Game { pub fn new(player_x: Pubkey, player_o: Pubkey) -> Game {
let mut game = Game::create(&player_x); let mut game = Game::create(&player_x);
game.join(&player_o).unwrap(); game.join(player_o, 0).unwrap();
game.accept().unwrap();
game game
} }
pub fn join(self: &mut Game, player_o: &Pubkey) -> Result<()> { pub fn join(self: &mut Game, player_o: Pubkey, timestamp: i64) -> Result<()> {
if self.state == State::WaitingForO { if self.state == State::Waiting {
self.player_o = Some(*player_o); self.player_o = Some(player_o);
self.state = State::ORequestPending;
Ok(())
} else {
Err(Error::NotYourTurn)
}
}
pub fn accept(self: &mut Game) -> Result<()> {
if self.state == State::ORequestPending {
assert!(self.player_o.is_some());
self.state = State::XMove; self.state = State::XMove;
self.keep_alive[1] = timestamp;
Ok(()) Ok(())
} else { } else {
Err(Error::NotYourTurn) Err(Error::GameInProgress)
} }
} }
pub fn reject(self: &mut Game) -> Result<()> { fn same(x_or_o: BoardItem, triple: &[BoardItem]) -> bool {
if self.state == State::ORequestPending {
assert!(self.player_o.is_some());
self.player_o = None;
self.state = State::WaitingForO;
Ok(())
} else {
Err(Error::NotYourTurn)
}
}
fn same(x_or_o: GridItem, triple: &[GridItem]) -> bool {
triple.iter().all(|&i| i == x_or_o) triple.iter().all(|&i| i == x_or_o)
} }
pub fn next_move(self: &mut Game, player: Pubkey, x: usize, y: usize) -> Result<()> { pub fn next_move(self: &mut Game, player: Pubkey, x: usize, y: usize) -> Result<()> {
let grid_index = y * 3 + x; let board_index = y * 3 + x;
if grid_index >= self.grid.len() || self.grid[grid_index] != GridItem::Free { if board_index >= self.board.len() || self.board[board_index] != BoardItem::F {
return Err(Error::InvalidMove); Err(Error::InvalidMove)?;
} }
let (x_or_o, won_state) = match self.state { let (x_or_o, won_state) = match self.state {
State::XMove => { State::XMove => {
if player != self.player_x { if player != self.player_x {
return Err(Error::PlayerNotFound)?; return Err(Error::PlayerNotFound);
} }
self.state = State::OMove; self.state = State::OMove;
(GridItem::X, State::XWon) (BoardItem::X, State::XWon)
} }
State::OMove => { State::OMove => {
if player != self.player_o.unwrap() { if player != self.player_o.unwrap() {
return Err(Error::PlayerNotFound)?; return Err(Error::PlayerNotFound);
} }
self.state = State::XMove; self.state = State::XMove;
(GridItem::O, State::OWon) (BoardItem::O, State::OWon)
} }
_ => { _ => {
return Err(Error::NotYourTurn)?; return Err(Error::NotYourTurn);
} }
}; };
self.grid[grid_index] = x_or_o; self.board[board_index] = x_or_o;
let winner = let winner =
// Check rows // Check rows
Game::same(x_or_o, &self.grid[0..3]) Game::same(x_or_o, &self.board[0..3])
|| Game::same(x_or_o, &self.grid[3..6]) || Game::same(x_or_o, &self.board[3..6])
|| Game::same(x_or_o, &self.grid[6..9]) || Game::same(x_or_o, &self.board[6..9])
// Check columns // Check columns
|| Game::same(x_or_o, &[self.grid[0], self.grid[3], self.grid[6]]) || Game::same(x_or_o, &[self.board[0], self.board[3], self.board[6]])
|| Game::same(x_or_o, &[self.grid[1], self.grid[4], self.grid[7]]) || Game::same(x_or_o, &[self.board[1], self.board[4], self.board[7]])
|| Game::same(x_or_o, &[self.grid[2], self.grid[5], self.grid[8]]) || Game::same(x_or_o, &[self.board[2], self.board[5], self.board[8]])
// Check both diagonals // Check both diagonals
|| Game::same(x_or_o, &[self.grid[0], self.grid[4], self.grid[8]]) || Game::same(x_or_o, &[self.board[0], self.board[4], self.board[8]])
|| Game::same(x_or_o, &[self.grid[2], self.grid[4], self.grid[6]]); || Game::same(x_or_o, &[self.board[2], self.board[4], self.board[6]]);
if winner { if winner {
self.state = won_state; self.state = won_state;
} else if self.grid.iter().all(|&p| p != GridItem::Free) { } else if self.board.iter().all(|&p| p != BoardItem::F) {
self.state = State::Draw; self.state = State::Draw;
} }
Ok(()) Ok(())
} }
pub fn keep_alive(self: &mut Game, player: Pubkey, timestamp: i64) -> Result<()> {
if player == self.player_x {
self.keep_alive[0] = timestamp;
} else if Some(player) == self.player_o {
self.keep_alive[1] = timestamp;
} else {
Err(Error::PlayerNotFound)?;
}
Ok(())
}
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
enum Command { enum Command {
Init, // player X initializes a new game Init, // player X initializes a new game
Join, // player O wants to join Join(i64), // player O wants to join (seconds since UNIX epoch)
Accept, // player X accepts the Join request KeepAlive(i64), // player X/O keep alive (seconds since UNIX epoch)
Reject, // player X rejects the Join request Move(u8, u8), // player X/O mark board position (x, y)
Move(u8, u8), // player X/O mark board position (x, y)
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct TicTacToeProgram { pub struct TicTacToeProgram {
game: Option<Game>, pub game: Option<Game>,
} }
pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [ pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
@ -183,7 +172,7 @@ pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
]; ];
impl TicTacToeProgram { impl TicTacToeProgram {
fn deserialize(input: &[u8]) -> Result<TicTacToeProgram> { pub fn deserialize(input: &[u8]) -> Result<TicTacToeProgram> {
let len = input[0] as usize; let len = input[0] as usize;
if len == 0 { if len == 0 {
@ -214,22 +203,9 @@ impl TicTacToeProgram {
if let Some(ref mut game) = self.game { if let Some(ref mut game) = self.game {
match cmd { match cmd {
Command::Join => game.join(player), Command::Join(timestamp) => game.join(*player, *timestamp),
Command::Accept => {
if *player == game.player_x {
game.accept()
} else {
Err(Error::PlayerNotFound)
}
}
Command::Reject => {
if *player == game.player_x {
game.reject()
} else {
Err(Error::PlayerNotFound)
}
}
Command::Move(x, y) => game.next_move(*player, *x as usize, *y as usize), Command::Move(x, y) => game.next_move(*player, *x as usize, *y as usize),
Command::KeepAlive(timestamp) => game.keep_alive(*player, *timestamp),
Command::Init => panic!("Unreachable"), Command::Init => panic!("Unreachable"),
} }
} else { } else {