Compare commits

...

21 Commits
v0.23 ... v0.9

Author SHA1 Message Date
68ec814bf3 Rename buildkite-snap to buildkite-secondary 2018-11-05 08:48:58 -08:00
d7f283a00a Add version for path dependencies 2018-10-24 20:29:20 -07:00
609889cc58 Wildcard early OOM deb package revision (#1554) (#1568) 2018-10-22 10:59:22 -07:00
83fa02ee44 Add ERC20-like Token program 2018-10-09 11:44:46 -07:00
d8fcb22000 Purge BudgetTransaction from banking_stage 2018-10-09 11:44:46 -07:00
6b1c90a8b5 Demote log messages 2018-10-09 11:44:46 -07:00
67d72e709f Demote 'not enough peers in crdt table' log message 2018-10-02 21:55:28 -07:00
f0d2870e0f Run a fullnode+drone automatically when the container starts up 2018-10-02 18:09:56 -07:00
02c47b48da Publish minimal Solana docker images to dockerhub 2018-10-02 16:28:54 -07:00
d30a39cd38 Remove SNAP_ prefix 2018-10-02 16:28:54 -07:00
9e57d0467e Return all instances 2018-10-01 07:51:12 -07:00
fb57d13c03 Correctly deserialize large userdata 2018-09-29 19:26:26 -07:00
41f6e27bba Ignore keep alive for completed games 2018-09-29 19:26:26 -07:00
aff5649b39 Add tic-tac-toe dashboard program 2018-09-28 19:13:09 -07:00
9fd6ffe83f s/grid/board/g 2018-09-28 19:13:09 -07:00
ff805361a9 Simplify game setup messaging 2018-09-28 19:13:09 -07:00
30d1b0b4bf Add KeepAlive message so players can detect abandoned games 2018-09-28 19:13:09 -07:00
5233cf1ca6 Update for new solana-jsonrpc 2018-09-28 18:10:15 -07:00
d2754fd702 Create new wallet on each run of wallet-sanity 2018-09-28 07:40:17 -07:00
ce9a0ae215 Specify zone 2018-09-28 07:35:33 -07:00
067adcdfa8 Include -z when deleting network 2018-09-27 21:27:38 -07:00
25 changed files with 788 additions and 117 deletions

View File

@ -105,16 +105,16 @@ serde_cbor = "0.9.0"
serde_derive = "1.0.27"
serde_json = "1.0.10"
socket2 = "0.3.8"
solana_program_interface = { path = "common" }
solana_program_interface = { path = "common", version="0.1.0" }
sys-info = "0.5.6"
tokio = "0.1"
tokio-codec = "0.1"
untrusted = "0.6.2"
[dev-dependencies]
noop = { path = "programs/noop" }
print = { path = "programs/print" }
move_funds = { path = "programs/move_funds" }
noop = { path = "programs/noop", version="0.1.0" }
print = { path = "programs/print", version="0.1.0" }
move_funds = { path = "programs/move_funds", version="0.1.0" }
[[bench]]
name = "bank"

View File

@ -0,0 +1,7 @@
steps:
- command: "ci/snap.sh"
timeout_in_minutes: 40
name: "snap [public]"
- command: "ci/docker-solana/build.sh"
timeout_in_minutes: 20
name: "docker-solana"

View File

@ -1,4 +0,0 @@
steps:
- command: "ci/snap.sh"
timeout_in_minutes: 40
name: "snap [public]"

View File

@ -39,7 +39,7 @@ steps:
- command: "ci/publish-crate.sh"
timeout_in_minutes: 20
name: "publish crate [public]"
- trigger: "solana-snap"
- trigger: "solana-secondary"
branches: "!pull/*"
async: true
build:

1
ci/docker-solana/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cargo-install/

View File

@ -0,0 +1,13 @@
FROM debian:stretch
# JSON RPC port
EXPOSE 8899/tcp
# Install libssl
RUN apt update && \
apt-get install -y libssl-dev && \
rm -rf /var/lib/apt/lists/*
COPY usr/bin /usr/bin/
ENTRYPOINT [ "/usr/bin/solana-entrypoint.sh" ]
CMD [""]

View File

@ -0,0 +1,17 @@
## Minimal Solana Docker image
This image is automatically updated by CI
https://hub.docker.com/r/solanalabs/solana/
### Usage:
Run the latest beta image:
```bash
$ docker run --rm -p 8899:8899 solanalabs/solana:beta
```
Run the latest edge image:
```bash
$ docker run --rm -p 8899:8899 solanalabs/solana:edge
```
Port *8899* is the JSON RPC port, which is used by clients to communicate with the network.

38
ci/docker-solana/build.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/bash -ex
cd "$(dirname "$0")"
eval "$(../channel-info.sh)"
if [[ $BUILDKITE_BRANCH = "$STABLE_CHANNEL" ]]; then
CHANNEL=stable
elif [[ $BUILDKITE_BRANCH = "$EDGE_CHANNEL" ]]; then
CHANNEL=edge
elif [[ $BUILDKITE_BRANCH = "$BETA_CHANNEL" ]]; then
CHANNEL=beta
fi
if [[ -z $CHANNEL ]]; then
echo Unable to determine channel to publish into, exiting.
exit 0
fi
rm -rf usr/
../docker-run.sh solanalabs/rust:1.29.1 \
cargo install --path . --root ci/docker-solana/usr
cp -f entrypoint.sh usr/bin/solana-entrypoint.sh
docker build -t solanalabs/solana:$CHANNEL .
maybeEcho=
if [[ -z $CI ]]; then
echo "Not CI, skipping |docker push|"
maybeEcho="echo"
else
(
set +x
if [[ -n $DOCKER_PASSWORD && -n $DOCKER_USERNAME ]]; then
echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_USERNAME" --password-stdin
fi
)
fi
$maybeEcho docker push solanalabs/solana:$CHANNEL

23
ci/docker-solana/entrypoint.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash -ex
export RUST_LOG=solana=info
export RUST_BACKTRACE=1
solana-keygen -o /config/leader-keypair.json
solana-keygen -o /config/drone-keypair.json
solana-genesis --tokens=1000000000 --ledger /ledger < /config/drone-keypair.json
solana-fullnode-config --keypair=/config/leader-keypair.json -l > /config/leader-config.json
solana-drone --keypair /config/drone-keypair.json --network 127.0.0.1:8001 &
drone=$!
solana-fullnode --identity /config/leader-config.json --ledger /ledger/ &
fullnode=$!
abort() {
kill "$drone" "$fullnode"
}
trap abort SIGINT SIGTERM
wait "$fullnode"
kill "$drone" "$fullnode"

View File

@ -10,14 +10,14 @@ fi
eval "$(ci/channel-info.sh)"
if [[ $BUILDKITE_BRANCH = "$STABLE_CHANNEL" ]]; then
SNAP_CHANNEL=stable
CHANNEL=stable
elif [[ $BUILDKITE_BRANCH = "$EDGE_CHANNEL" ]]; then
SNAP_CHANNEL=edge
CHANNEL=edge
elif [[ $BUILDKITE_BRANCH = "$BETA_CHANNEL" ]]; then
SNAP_CHANNEL=beta
CHANNEL=beta
fi
if [[ -z $SNAP_CHANNEL ]]; then
if [[ -z $CHANNEL ]]; then
echo Unable to determine channel to publish into, exiting.
exit 0
fi
@ -51,11 +51,11 @@ if [[ ! -x /usr/bin/multilog ]]; then
sudo apt-get install -y daemontools
fi
echo --- build: $SNAP_CHANNEL channel
echo --- build: $CHANNEL channel
snapcraft
source ci/upload_ci_artifact.sh
upload_ci_artifact solana_*.snap
echo --- publish: $SNAP_CHANNEL channel
$DRYRUN snapcraft push solana_*.snap --release $SNAP_CHANNEL
echo --- publish: $CHANNEL channel
$DRYRUN snapcraft push solana_*.snap --release $CHANNEL

View File

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

View File

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

View File

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

View File

@ -31,11 +31,7 @@ __cloud_FindInstances() {
declare name zone publicIp privateIp status
while read -r name publicIp privateIp status; do
if [[ $status != RUNNING ]]; then
echo "Warning: $name is not RUNNING, ignoring it."
continue
fi
printf "%-30s | publicIp=%-16s privateIp=%s\n" "$name" "$publicIp" "$privateIp"
printf "%-30s | publicIp=%-16s privateIp=%s staus=%s\n" "$name" "$publicIp" "$privateIp" "$status"
instances+=("$name:$publicIp:$privateIp")
done < <(gcloud compute instances list \

View File

@ -13,8 +13,8 @@ sysctl -w kernel.sysrq=$(( $(cat /proc/sys/kernel/sysrq) | 64 ))
if command -v earlyoom; then
systemctl status earlyoom
else
wget http://ftp.us.debian.org/debian/pool/main/e/earlyoom/earlyoom_1.1-2_amd64.deb
apt install --quiet --yes ./earlyoom_1.1-2_amd64.deb
wget -r -l1 -np http://ftp.us.debian.org/debian/pool/main/e/earlyoom/ -A 'earlyoom_1.1-*_amd64.deb' -e robots=off -nd
apt install --quiet --yes ./earlyoom_1.1-*_amd64.deb
cat > earlyoom <<OOM
# use the kernel OOM killer, trigger at 20% available RAM,

View File

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

View File

@ -28,8 +28,10 @@ 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 token_program::TokenProgram;
use transaction::Transaction;
use window::WINDOW_SIZE;
@ -405,6 +407,14 @@ impl Bank {
if TicTacToeProgram::process_transaction(&tx, accounts).is_err() {
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 TokenProgram::check_id(&tx.program_id) {
if TokenProgram::process_transaction(&tx, accounts).is_err() {
return Err(BankError::ProgramRuntimeError);
}
} else if self.loaded_contract(&tx, accounts) {
} else {
return Err(BankError::UnknownContractId);

View File

@ -4,7 +4,6 @@
use bank::Bank;
use bincode::deserialize;
use budget_transaction::BudgetTransaction;
use counter::Counter;
use entry::Entry;
use log::Level;
@ -224,7 +223,7 @@ impl BankingStage {
.zip(vers)
.filter_map(|(tx, ver)| match tx {
None => None,
Some((tx, _addr)) => if tx.verify_plan() && ver != 0 {
Some((tx, _addr)) => if ver != 0 {
Some(tx)
} else {
None

View File

@ -526,7 +526,7 @@ impl Crdt {
received_index: u64,
) -> Result<()> {
if broadcast_table.is_empty() {
warn!("{}:not enough peers in crdt table", me.id);
debug!("{}:not enough peers in crdt table", me.id);
inc_new_counter_info!("crdt-broadcast-not_enough_peers_error", 1);
Err(CrdtError::NoPeers)?;
}

View File

@ -60,8 +60,10 @@ 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 token_program;
pub mod tpu;
pub mod transaction;
pub mod tvu;

View File

@ -43,7 +43,7 @@ impl JsonRpcService {
io.extend_with(rpc.to_delegate());
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(),
transactions_addr,
drone_addr,

View File

@ -0,0 +1,168 @@
//! 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 + (0x100 * 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: {:?}", 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 = 1234567890;
dashboard1.pending = Pubkey::new(&[100; 32]);
for i in 1..5 {
dashboard1.completed.push(Pubkey::new(&[100 + i; 32]));
}
let mut userdata = vec![0xff; 512];
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

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

404
src/token_program.rs Normal file
View File

@ -0,0 +1,404 @@
//! ERC20-like Token program
use bincode;
use solana_program_interface::account::Account;
use solana_program_interface::pubkey::Pubkey;
use std;
use transaction::Transaction;
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidArgument,
InsufficentFunds,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "error")
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct TokenInfo {
/**
* Total supply of tokens
*/
supply: u64,
/**
* Number of base 10 digits to the right of the decimal place in the total supply
*/
decimals: u8,
/**
* Descriptive name of this token
*/
name: String,
/**
* Symbol for this token
*/
symbol: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct TokenAccountInfo {
/**
* The kind of token this account holds
*/
token: Pubkey,
/**
* Owner of this account
*/
owner: Pubkey,
/**
* Amount of tokens this account holds
*/
amount: u64,
/**
* The source account for the tokens.
*
* If `source` is None, `amount` belongs to this account.
* If `source` is Option<>, `amount` represents an allowance of tokens that
* may be transferred from the `source` account.
*/
source: Option<Pubkey>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
enum Command {
NewToken(TokenInfo),
NewTokenAccount(),
Transfer(u64),
Approve(u64),
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum TokenProgram {
Unallocated,
Token(TokenInfo),
Account(TokenAccountInfo),
Invalid,
}
impl Default for TokenProgram {
fn default() -> TokenProgram {
TokenProgram::Unallocated
}
}
pub const TOKEN_PROGRAM_ID: [u8; 32] = [
5, 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 TokenProgram {
fn to_invalid_args(err: std::boxed::Box<bincode::ErrorKind>) -> Error {
warn!("invalid argument: {:?}", err);
Error::InvalidArgument
}
fn deserialize(input: &[u8]) -> Result<TokenProgram> {
if input.len() < 1 {
Err(Error::InvalidArgument)?;
}
match input[0] {
0 => Ok(TokenProgram::Unallocated),
1 => Ok(TokenProgram::Token(
bincode::deserialize(&input[1..]).map_err(Self::to_invalid_args)?,
)),
2 => Ok(TokenProgram::Account(
bincode::deserialize(&input[1..]).map_err(Self::to_invalid_args)?,
)),
_ => Err(Error::InvalidArgument),
}
}
fn serialize(self: &TokenProgram, output: &mut [u8]) -> Result<()> {
if output.len() == 0 {
warn!("serialize fail: ouput.len is 0");
Err(Error::InvalidArgument)?;
}
match self {
TokenProgram::Unallocated | TokenProgram::Invalid => Err(Error::InvalidArgument),
TokenProgram::Token(token_info) => {
output[0] = 1;
let writer = std::io::BufWriter::new(&mut output[1..]);
bincode::serialize_into(writer, &token_info).map_err(Self::to_invalid_args)
}
TokenProgram::Account(account_info) => {
output[0] = 2;
let writer = std::io::BufWriter::new(&mut output[1..]);
bincode::serialize_into(writer, &account_info).map_err(Self::to_invalid_args)
}
}
}
pub fn check_id(program_id: &Pubkey) -> bool {
program_id.as_ref() == TOKEN_PROGRAM_ID
}
pub fn id() -> Pubkey {
Pubkey::new(&TOKEN_PROGRAM_ID)
}
pub fn process_transaction(tx: &Transaction, accounts: &mut [Account]) -> Result<()> {
let command =
bincode::deserialize::<Command>(&tx.userdata).map_err(Self::to_invalid_args)?;
info!("process_transaction: command={:?}", command);
let input_program_accounts: Vec<TokenProgram> = accounts
.iter()
.map(|account| {
if Self::check_id(&account.program_id) {
Self::deserialize(&account.userdata)
.map_err(|err| {
info!("deserialize failed: {:?}", err);
TokenProgram::Invalid
}).unwrap()
} else {
TokenProgram::Invalid
}
}).collect();
//let mut output_program_accounts: Vec<(usize, TokenProgram)> = vec![];
let mut output_program_accounts: Vec<(_, _)> = vec![];
match command {
Command::NewToken(token_info) => {
if accounts.len() != 2 {
error!("Expected 2 accounts");
Err(Error::InvalidArgument)?;
}
if let TokenProgram::Account(dest_account) = &input_program_accounts[1] {
if tx.keys[0] != dest_account.token {
info!("account 1 token mismatch");
Err(Error::InvalidArgument)?;
}
if dest_account.source.is_some() {
info!("account 1 is a delegate and cannot accept tokens");
Err(Error::InvalidArgument)?;
}
let mut output_dest_account = dest_account.clone();
output_dest_account.amount = token_info.supply;
output_program_accounts.push((1, TokenProgram::Account(output_dest_account)));
} else {
info!("account 1 invalid");
Err(Error::InvalidArgument)?;
}
if input_program_accounts[0] != TokenProgram::Unallocated {
info!("account 0 not available");
Err(Error::InvalidArgument)?;
}
output_program_accounts.push((0, TokenProgram::Token(token_info)));
}
Command::NewTokenAccount() => {
// key 0 - Destination new token account
// key 1 - Owner of the account
// key 2 - Token this account is associated with
// key 3 - Source account that this account is a delegate for (optional)
if accounts.len() < 3 {
error!("Expected 3 accounts");
Err(Error::InvalidArgument)?;
}
if input_program_accounts[0] != TokenProgram::Unallocated {
info!("account 0 is already allocated");
Err(Error::InvalidArgument)?;
}
let mut token_account_info = TokenAccountInfo {
token: tx.keys[2],
owner: tx.keys[1],
amount: 0,
source: None,
};
if accounts.len() >= 4 {
token_account_info.source = Some(tx.keys[3]);
}
output_program_accounts.push((0, TokenProgram::Account(token_account_info)));
}
Command::Transfer(amount) => {
if accounts.len() < 3 {
error!("Expected 3 accounts");
Err(Error::InvalidArgument)?;
}
if let (
TokenProgram::Account(source_account),
TokenProgram::Account(dest_account),
) = (&input_program_accounts[1], &input_program_accounts[2])
{
if source_account.token != dest_account.token {
info!("account 1/2 token mismatch");
Err(Error::InvalidArgument)?;
}
if dest_account.source.is_some() {
info!("account 2 is a delegate and cannot accept tokens");
Err(Error::InvalidArgument)?;
}
if source_account.owner != tx.keys[0] {
info!("owner of account 1 not present");
Err(Error::InvalidArgument)?;
}
if source_account.amount < amount {
Err(Error::InsufficentFunds)?;
}
let mut output_source_account = source_account.clone();
output_source_account.amount -= amount;
output_program_accounts.push((1, TokenProgram::Account(output_source_account)));
if source_account.source.is_some() {
if accounts.len() != 4 {
error!("Expected 4 accounts");
Err(Error::InvalidArgument)?;
}
let delegate_account = source_account;
if let TokenProgram::Account(source_account) = &input_program_accounts[3] {
if source_account.token != delegate_account.token {
info!("account 1/3 token mismatch");
Err(Error::InvalidArgument)?;
}
if delegate_account.source != Some(tx.keys[3]) {
info!("Account 1 is not a delegate of account 3");
Err(Error::InvalidArgument)?;
}
if source_account.amount < amount {
Err(Error::InsufficentFunds)?;
}
let mut output_source_account = source_account.clone();
output_source_account.amount -= amount;
output_program_accounts
.push((3, TokenProgram::Account(output_source_account)));
} else {
info!("account 3 is an invalid account");
Err(Error::InvalidArgument)?;
}
}
let mut output_dest_account = dest_account.clone();
output_dest_account.amount += amount;
output_program_accounts.push((2, TokenProgram::Account(output_dest_account)));
} else {
info!("account 1 and/or 2 are invalid accounts");
Err(Error::InvalidArgument)?;
}
}
Command::Approve(amount) => {
if accounts.len() != 3 {
error!("Expected 3 accounts");
Err(Error::InvalidArgument)?;
}
if let (
TokenProgram::Account(source_account),
TokenProgram::Account(delegate_account),
) = (&input_program_accounts[1], &input_program_accounts[2])
{
if source_account.token != delegate_account.token {
info!("account 1/2 token mismatch");
Err(Error::InvalidArgument)?;
}
if source_account.owner != tx.keys[0] {
info!("owner of account 1 not present");
Err(Error::InvalidArgument)?;
}
if source_account.source.is_some() {
info!("account 1 is a delegate");
Err(Error::InvalidArgument)?;
}
if delegate_account.source != Some(tx.keys[1]) {
info!("account 2 is not a delegate of account 1");
Err(Error::InvalidArgument)?;
}
let mut output_delegate_account = delegate_account.clone();
output_delegate_account.amount = amount;
output_program_accounts
.push((2, TokenProgram::Account(output_delegate_account)));
} else {
info!("account 1 and/or 2 are invalid accounts");
Err(Error::InvalidArgument)?;
}
}
}
for (index, program_account) in output_program_accounts.iter() {
info!(
"output_program_account: index={} userdata={:?}",
index, program_account
);
Self::serialize(program_account, &mut accounts[*index].userdata)?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
pub fn serde() {
assert_eq!(TokenProgram::deserialize(&[0]), Ok(TokenProgram::default()));
let mut userdata = vec![0; 256];
let account = TokenProgram::Account(TokenAccountInfo {
token: Pubkey::new(&[1; 32]),
owner: Pubkey::new(&[2; 32]),
amount: 123,
source: None,
});
assert!(account.serialize(&mut userdata).is_ok());
assert_eq!(TokenProgram::deserialize(&userdata), Ok(account));
let account = TokenProgram::Token(TokenInfo {
supply: 12345,
decimals: 2,
name: "A test token".to_string(),
symbol: "TEST".to_string(),
});
assert!(account.serialize(&mut userdata).is_ok());
assert_eq!(TokenProgram::deserialize(&userdata), Ok(account));
}
#[test]
pub fn serde_expect_fail() {
let mut userdata = vec![0; 256];
// Certain TokenProgram's may not be serialized
let account = TokenProgram::default();
assert_eq!(account, TokenProgram::Unallocated);
assert!(account.serialize(&mut userdata).is_err());
assert!(account.serialize(&mut userdata).is_err());
let account = TokenProgram::Invalid;
assert!(account.serialize(&mut userdata).is_err());
// Bad deserialize userdata
assert!(TokenProgram::deserialize(&[]).is_err());
assert!(TokenProgram::deserialize(&[1]).is_err());
assert!(TokenProgram::deserialize(&[1, 2]).is_err());
assert!(TokenProgram::deserialize(&[2, 2]).is_err());
assert!(TokenProgram::deserialize(&[3]).is_err());
}
// Note: business logic tests are located in the @solana/web3.js test suite
}

View File

@ -124,7 +124,7 @@ impl WriteStage {
}
inc_new_counter_info!("write_stage-entries_received", num_new_entries);
info!("write_stage entries: {}", num_new_entries);
debug!("write_stage entries: {}", num_new_entries);
let mut entries_send_total = 0;
let mut crdt_votes_total = 0;
@ -165,7 +165,7 @@ impl WriteStage {
"write_stage-time_ms",
duration_as_ms(&now.elapsed()) as usize
);
info!("done write_stage txs: {} time {} ms txs/s: {} entries_send_total: {} crdt_votes_total: {}",
debug!("done write_stage txs: {} time {} ms txs/s: {} entries_send_total: {} crdt_votes_total: {}",
num_txs, duration_as_ms(&start.elapsed()),
num_txs as f32 / duration_as_s(&start.elapsed()),
entries_send_total,