use battleware_types::{
execution::{
Account, Creature, Event, Instruction, Key, Leaderboard, Outcome, Output, Transaction,
Value, LOBBY_EXPIRY, MAX_BATTLE_ROUNDS, MAX_LOBBY_SIZE, MOVE_EXPIRY, TOTAL_MOVES,
},
Seed,
};
use bytes::{Buf, BufMut};
use commonware_codec::{Encode, EncodeSize, Error, Read, ReadExt, Write};
use commonware_consensus::threshold_simplex::types::View;
use commonware_cryptography::{
bls12381::{
primitives::variant::{MinSig, Variant},
tle::{decrypt, Ciphertext},
},
ed25519::PublicKey,
sha256::{Digest, Sha256},
Hasher,
};
#[cfg(feature = "parallel")]
use commonware_runtime::ThreadPool;
use commonware_runtime::{Clock, Metrics, Spawner, Storage};
use commonware_storage::{adb::any::variable::Any, translator::Translator};
use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
#[cfg(feature = "parallel")]
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
future::Future,
};
mod elo;
mod fixed;
pub mod state_transition;
#[cfg(any(test, feature = "mocks"))]
pub mod mocks;
pub type Adb<E, T> = Any<E, Digest, Value, Sha256, T>;
pub trait State {
fn get(&self, key: &Key) -> impl Future<Output = Option<Value>>;
fn insert(&mut self, key: Key, value: Value) -> impl Future<Output = ()>;
fn delete(&mut self, key: &Key) -> impl Future<Output = ()>;
fn apply(&mut self, changes: Vec<(Key, Status)>) -> impl Future<Output = ()> {
async {
for (key, status) in changes {
match status {
Status::Update(value) => self.insert(key, value).await,
Status::Delete => self.delete(&key).await,
}
}
}
}
}
impl<E: Spawner + Metrics + Clock + Storage, T: Translator> State for Adb<E, T> {
async fn get(&self, key: &Key) -> Option<Value> {
let key = Sha256::hash(&key.encode());
self.get(&key).await.unwrap()
}
async fn insert(&mut self, key: Key, value: Value) {
let key = Sha256::hash(&key.encode());
self.update(key, value).await.unwrap();
}
async fn delete(&mut self, key: &Key) {
let key = Sha256::hash(&key.encode());
self.delete(key).await.unwrap();
}
}
#[derive(Default)]
pub struct Memory {
state: HashMap<Key, Value>,
}
impl State for Memory {
async fn get(&self, key: &Key) -> Option<Value> {
self.state.get(key).cloned()
}
async fn insert(&mut self, key: Key, value: Value) {
self.state.insert(key, value);
}
async fn delete(&mut self, key: &Key) {
self.state.remove(key);
}
}
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Status {
Update(Value),
Delete,
}
impl Write for Status {
fn write(&self, writer: &mut impl BufMut) {
match self {
Status::Update(value) => {
0u8.write(writer);
value.write(writer);
}
Status::Delete => 1u8.write(writer),
}
}
}
impl Read for Status {
type Cfg = ();
fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
let kind = u8::read(reader)?;
match kind {
0 => Ok(Status::Update(Value::read(reader)?)),
1 => Ok(Status::Delete),
_ => Err(Error::InvalidEnum(kind)),
}
}
}
impl EncodeSize for Status {
fn encode_size(&self) -> usize {
1 + match self {
Status::Update(value) => value.encode_size(),
Status::Delete => 0,
}
}
}
pub async fn nonce<S: State>(state: &S, public: &PublicKey) -> u64 {
let account =
if let Some(Value::Account(account)) = state.get(&Key::Account(public.clone())).await {
account
} else {
Account::default()
};
account.nonce
}
pub struct Noncer<'a, S: State> {
state: &'a S,
pending: BTreeMap<Key, Status>,
}
impl<'a, S: State> Noncer<'a, S> {
pub fn new(state: &'a S) -> Self {
Self {
state,
pending: BTreeMap::new(),
}
}
pub async fn prepare(&mut self, transaction: &Transaction) -> bool {
let mut account = if let Some(Value::Account(account)) =
self.get(&Key::Account(transaction.public.clone())).await
{
account
} else {
Account::default()
};
if account.nonce != transaction.nonce {
return false;
}
account.nonce += 1;
self.insert(
Key::Account(transaction.public.clone()),
Value::Account(account),
)
.await;
true
}
}
impl<'a, S: State> State for Noncer<'a, S> {
async fn get(&self, key: &Key) -> Option<Value> {
match self.pending.get(key) {
Some(Status::Update(value)) => Some(value.clone()),
Some(Status::Delete) => None,
None => self.state.get(key).await,
}
}
async fn insert(&mut self, key: Key, value: Value) {
self.pending.insert(key, Status::Update(value));
}
async fn delete(&mut self, key: &Key) {
self.pending.insert(key.clone(), Status::Delete);
}
}
#[derive(Hash, Eq, PartialEq)]
#[allow(clippy::large_enum_variant)]
enum Task {
Seed(Seed),
Decrypt(Seed, Ciphertext<MinSig>),
}
enum TaskResult {
Seed(bool),
Decrypt([u8; 32]),
}
pub struct Layer<'a, S: State> {
state: &'a S,
pending: BTreeMap<Key, Status>,
master: <MinSig as Variant>::Public,
namespace: Vec<u8>,
seed: Seed,
precomputations: HashMap<Task, TaskResult>,
}
impl<'a, S: State> Layer<'a, S> {
pub fn new(
state: &'a S,
master: <MinSig as Variant>::Public,
namespace: &[u8],
seed: Seed,
) -> Self {
let mut verified_seeds = HashSet::new();
verified_seeds.insert(seed.clone());
Self {
state,
pending: BTreeMap::new(),
master,
namespace: namespace.to_vec(),
seed,
precomputations: HashMap::new(),
}
}
fn insert(&mut self, key: Key, value: Value) {
self.pending.insert(key, Status::Update(value));
}
fn delete(&mut self, key: Key) {
self.pending.insert(key, Status::Delete);
}
pub fn view(&self) -> View {
self.seed.view
}
async fn prepare(&mut self, transaction: &Transaction) -> bool {
let mut account = if let Some(Value::Account(account)) =
self.get(&Key::Account(transaction.public.clone())).await
{
account
} else {
Account::default()
};
if account.nonce != transaction.nonce {
return false;
}
account.nonce += 1;
self.insert(
Key::Account(transaction.public.clone()),
Value::Account(account),
);
true
}
async fn extract(&mut self, transaction: &Transaction) -> Vec<Task> {
match &transaction.instruction {
Instruction::Generate => vec![],
Instruction::Match => vec![],
Instruction::Move(_) => vec![],
Instruction::Settle(signature) => {
let Some(Value::Account(account)) =
self.get(&Key::Account(transaction.public.clone())).await
else {
return vec![];
};
let Some(battle) = account.battle else {
return vec![];
};
let Some(Value::Battle {
expiry,
player_a_pending,
player_b_pending,
..
}) = self.get(&Key::Battle(battle)).await
else {
return vec![];
};
if expiry > self.seed.view {
return vec![];
}
let seed = Seed::new(expiry, *signature);
let mut ops = vec![Task::Seed(seed.clone())];
if let Some(pending) = player_a_pending {
ops.push(Task::Decrypt(seed.clone(), pending));
}
if let Some(pending) = player_b_pending {
ops.push(Task::Decrypt(seed, pending));
}
ops
}
}
}
async fn apply(&mut self, transaction: &Transaction) -> Vec<Event> {
let Some(Value::Account(mut account)) =
self.get(&Key::Account(transaction.public.clone())).await
else {
panic!("Account should exist");
};
let mut events = vec![];
match &transaction.instruction {
Instruction::Generate => {
if account.battle.is_some() {
return events;
}
let creature = Creature::new(
transaction.public.clone(),
transaction.nonce,
self.seed.signature,
);
account.creature = Some(creature.clone());
self.insert(
Key::Account(transaction.public.clone()),
Value::Account(account),
);
events.push(Event::Generated {
account: transaction.public.clone(),
creature,
});
}
Instruction::Match => {
if account.creature.is_none() {
return events;
}
if account.battle.is_some() {
return events;
}
let Some(Value::Lobby {
expiry,
mut players,
}) = self.get(&Key::Lobby).await
else {
let mut players = BTreeSet::new();
players.insert(transaction.public.clone());
let lobby = Value::Lobby {
expiry: self
.seed
.view
.checked_add(LOBBY_EXPIRY)
.expect("view overflow"),
players,
};
self.insert(Key::Lobby, lobby);
return events;
};
players.insert(transaction.public.clone());
if expiry < self.seed.view || players.len() >= MAX_LOBBY_SIZE {
let mut players = players.iter().collect::<Vec<_>>();
let seed = Sha256::hash(self.seed.encode().as_ref());
let mut rng = StdRng::from_seed(seed.as_ref().try_into().unwrap());
players.shuffle(&mut rng);
let iter = players.chunks_exact(2).map(|chunk| (chunk[0], chunk[1]));
let mut hasher = Sha256::new();
for (player_a, player_b) in iter {
hasher.update(self.seed.encode().as_ref());
hasher.update(player_a.as_ref());
hasher.update(player_b.as_ref());
let key = hasher.finalize();
let Some(Value::Account(mut account_a)) =
self.get(&Key::Account(player_a.clone())).await
else {
panic!("Player A should have an account");
};
let player_a_creature = account_a.creature.as_ref().unwrap().clone();
let player_a_stats = account_a.stats.clone();
let player_a_health = player_a_creature.health();
account_a.battle = Some(key);
self.insert(Key::Account(player_a.clone()), Value::Account(account_a));
let Some(Value::Account(mut account_b)) =
self.get(&Key::Account(player_b.clone())).await
else {
panic!("Player B should have an account");
};
let player_b_creature = account_b.creature.as_ref().unwrap().clone();
let player_b_stats = account_b.stats.clone();
let player_b_health = player_b_creature.health();
account_b.battle = Some(key);
self.insert(Key::Account(player_b.clone()), Value::Account(account_b));
let expiry = self
.seed
.view
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let value = Value::Battle {
expiry,
round: 0,
player_a: (*player_a).clone(),
player_a_max_health: player_a_health,
player_a_health,
player_a_pending: None,
player_a_move_counts: [0; TOTAL_MOVES],
player_b: (*player_b).clone(),
player_b_max_health: player_b_health,
player_b_health,
player_b_pending: None,
player_b_move_counts: [0; TOTAL_MOVES],
};
self.insert(Key::Battle(key), value);
events.push(Event::Matched {
battle: key,
expiry,
player_a: (*player_a).clone(),
player_a_creature,
player_a_stats,
player_b: (*player_b).clone(),
player_b_creature,
player_b_stats,
});
}
let remainder = players.chunks_exact(2).remainder();
let mut new_players = BTreeSet::new();
if !remainder.is_empty() {
for player in remainder {
new_players.insert((*player).clone());
}
}
let lobby = Value::Lobby {
expiry: self
.seed
.view
.checked_add(LOBBY_EXPIRY)
.expect("view overflow"),
players: new_players,
};
self.insert(Key::Lobby, lobby);
} else {
let lobby = Value::Lobby { expiry, players };
self.insert(Key::Lobby, lobby);
}
}
Instruction::Move(encrypted_move) => {
let Some(battle) = account.battle else {
return events;
};
let Some(Value::Battle {
expiry,
round,
player_a,
player_a_max_health,
player_a_health,
mut player_a_pending,
player_a_move_counts,
player_b,
player_b_max_health,
player_b_health,
mut player_b_pending,
player_b_move_counts,
}) = self.get(&Key::Battle(battle)).await
else {
panic!("Battle should exist");
};
if expiry < self.seed.view {
return events;
}
if player_a == transaction.public && player_a_pending.is_none() {
player_a_pending = Some(encrypted_move.clone());
} else if player_b == transaction.public && player_b_pending.is_none() {
player_b_pending = Some(encrypted_move.clone());
} else {
return events;
}
events.push(Event::Locked {
battle,
round,
locker: transaction.public.clone(),
observer: if player_a == transaction.public {
player_b.clone()
} else {
player_a.clone()
},
ciphertext: encrypted_move.clone(),
});
let value = Value::Battle {
expiry,
round,
player_a,
player_a_max_health,
player_a_health,
player_a_pending,
player_a_move_counts,
player_b,
player_b_max_health,
player_b_health,
player_b_pending,
player_b_move_counts,
};
self.insert(Key::Battle(battle), value);
}
Instruction::Settle(signature) => {
let Some(battle) = account.battle else {
return events; };
let Some(Value::Battle {
expiry,
mut round,
player_a,
player_a_max_health,
mut player_a_health,
player_a_pending,
mut player_a_move_counts,
player_b,
player_b_max_health,
mut player_b_health,
player_b_pending,
mut player_b_move_counts,
}) = self.get(&Key::Battle(battle)).await
else {
panic!("Battle should exist");
};
if expiry > self.seed.view {
return events;
}
let seed = Seed::new(expiry, *signature);
if seed != self.seed {
match self.precomputations.get(&Task::Seed(seed.clone())) {
Some(TaskResult::Seed(result)) => {
if !result {
return events;
}
}
None => {
if !seed.verify(&self.namespace, &self.master) {
return events; }
}
_ => unreachable!(),
}
}
let mut player_a_move = if let Some(player_a_pending) = player_a_pending {
match self
.precomputations
.get(&Task::Decrypt(seed.clone(), player_a_pending.clone()))
{
Some(TaskResult::Decrypt(result)) => result[0],
None => {
let raw: [u8; 32] = decrypt::<MinSig>(signature, &player_a_pending)
.map(|block| block.as_ref().try_into().unwrap())
.unwrap_or_else(|| [0; 32]);
raw[0]
}
_ => unreachable!(),
}
} else {
0
};
if player_a_move >= TOTAL_MOVES as u8 {
player_a_move = 0;
}
let Some(Value::Account(account)) = self.get(&Key::Account(player_a.clone())).await
else {
panic!("Player A should have an account");
};
let player_a_creature = account.creature.as_ref().unwrap();
let player_a_limits = player_a_creature.get_move_usage_limits();
if player_a_move_counts[player_a_move as usize]
>= player_a_limits[player_a_move as usize]
{
player_a_move = 0;
}
let (player_a_defense, player_a_power) =
player_a_creature.action(player_a_move, self.seed.signature);
let mut player_b_move = if let Some(player_b_pending) = player_b_pending {
match self
.precomputations
.get(&Task::Decrypt(seed, player_b_pending.clone()))
{
Some(TaskResult::Decrypt(result)) => result[0],
None => {
let raw: [u8; 32] = decrypt::<MinSig>(signature, &player_b_pending)
.map(|block| block.as_ref().try_into().unwrap())
.unwrap_or_else(|| [0; 32]);
raw[0]
}
_ => unreachable!(),
}
} else {
0
};
if player_b_move >= TOTAL_MOVES as u8 {
player_b_move = 0;
}
let Some(Value::Account(account)) = self.get(&Key::Account(player_b.clone())).await
else {
panic!("Player B should have an account");
};
let player_b_creature = account.creature.as_ref().unwrap();
let player_b_limits = player_b_creature.get_move_usage_limits();
if player_b_move_counts[player_b_move as usize]
>= player_b_limits[player_b_move as usize]
{
player_b_move = 0;
}
let (player_b_defense, player_b_power) =
player_b_creature.action(player_b_move, self.seed.signature);
let mut player_a_effective_health = player_a_health as i16;
let mut player_b_effective_health = player_b_health as i16;
if player_a_defense && !player_b_defense {
player_a_health = player_a_health
.saturating_add(player_a_power)
.min(player_a_max_health);
player_a_effective_health = (player_a_health as i16) - (player_b_power as i16);
player_a_health = player_a_health.saturating_sub(player_b_power);
} else if !player_a_defense && player_b_defense {
player_b_health = player_b_health
.saturating_add(player_b_power)
.min(player_b_max_health);
player_b_effective_health = (player_b_health as i16) - (player_a_power as i16);
player_b_health = player_b_health.saturating_sub(player_a_power);
} else if player_a_defense && player_b_defense {
player_a_health = player_a_health
.saturating_add(player_a_power)
.min(player_a_max_health);
player_b_health = player_b_health
.saturating_add(player_b_power)
.min(player_b_max_health);
player_a_effective_health = player_a_health as i16;
player_b_effective_health = player_b_health as i16;
} else {
player_a_effective_health = (player_a_health as i16) - (player_b_power as i16);
player_b_effective_health = (player_b_health as i16) - (player_a_power as i16);
player_a_health = player_a_health.saturating_sub(player_b_power);
player_b_health = player_b_health.saturating_sub(player_a_power);
}
player_a_move_counts[player_a_move as usize] =
player_a_move_counts[player_a_move as usize].saturating_add(1);
player_b_move_counts[player_b_move as usize] =
player_b_move_counts[player_b_move as usize].saturating_add(1);
round += 1;
let next_expiry = self
.seed
.view
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
events.push(Event::Moved {
battle,
round,
expiry: next_expiry,
player_a: player_a.clone(),
player_a_health,
player_a_move,
player_a_move_counts,
player_a_power,
player_b: player_b.clone(),
player_b_health,
player_b_move,
player_b_move_counts,
player_b_power,
});
if player_a_health == 0 && player_b_health > 0 {
self.delete(Key::Battle(battle));
let Some(Value::Account(mut account_a)) =
self.get(&Key::Account(player_a.clone())).await
else {
panic!("Player A should have an account");
};
let old_account_a_stats = account_a.stats.clone();
let Some(Value::Account(mut account_b)) =
self.get(&Key::Account(player_b.clone())).await
else {
panic!("Player B should have an account");
};
let old_account_b_stats = account_b.stats.clone();
account_a.stats.losses = account_a.stats.losses.saturating_add(1);
account_a.battle = None;
account_b.stats.wins = account_b.stats.wins.saturating_add(1);
account_b.battle = None;
let max_health_a = account_a.creature.as_ref().unwrap().health();
let max_health_b = account_b.creature.as_ref().unwrap().health();
let (new_elo_a, new_elo_b) = elo::update(
account_a.stats.elo,
player_a_effective_health,
max_health_a,
account_b.stats.elo,
player_b_effective_health,
max_health_b,
);
account_a.stats.elo = new_elo_a;
let new_account_a_stats = account_a.stats.clone();
account_b.stats.elo = new_elo_b;
let new_account_b_stats = account_b.stats.clone();
self.insert(Key::Account(player_a.clone()), Value::Account(account_a));
self.insert(Key::Account(player_b.clone()), Value::Account(account_b));
let mut leaderboard = match self.get(&Key::Leaderboard).await {
Some(Value::Leaderboard(lb)) => lb,
_ => Leaderboard::default(),
};
leaderboard.update(player_a.clone(), new_account_a_stats.clone());
leaderboard.update(player_b.clone(), new_account_b_stats.clone());
self.insert(Key::Leaderboard, Value::Leaderboard(leaderboard.clone()));
events.push(Event::Settled {
battle,
round,
player_a,
player_a_old: old_account_a_stats,
player_a_new: new_account_a_stats,
player_b,
player_b_old: old_account_b_stats,
player_b_new: new_account_b_stats,
outcome: Outcome::PlayerB,
leaderboard,
});
} else if player_b_health == 0 && player_a_health > 0 {
self.delete(Key::Battle(battle));
let Some(Value::Account(mut account_a)) =
self.get(&Key::Account(player_a.clone())).await
else {
panic!("Player A should have an account");
};
let old_account_a_stats = account_a.stats.clone();
let Some(Value::Account(mut account_b)) =
self.get(&Key::Account(player_b.clone())).await
else {
panic!("Player B should have an account");
};
let old_account_b_stats = account_b.stats.clone();
account_b.stats.losses = account_b.stats.losses.saturating_add(1);
account_b.battle = None;
account_a.stats.wins = account_a.stats.wins.saturating_add(1);
account_a.battle = None;
let max_health_a = account_a.creature.as_ref().unwrap().health();
let max_health_b = account_b.creature.as_ref().unwrap().health();
let (new_elo_a, new_elo_b) = elo::update(
account_a.stats.elo,
player_a_effective_health,
max_health_a,
account_b.stats.elo,
player_b_effective_health,
max_health_b,
);
account_a.stats.elo = new_elo_a;
let new_account_a_stats = account_a.stats.clone();
account_b.stats.elo = new_elo_b;
let new_account_b_stats = account_b.stats.clone();
self.insert(Key::Account(player_a.clone()), Value::Account(account_a));
self.insert(Key::Account(player_b.clone()), Value::Account(account_b));
let mut leaderboard = match self.get(&Key::Leaderboard).await {
Some(Value::Leaderboard(lb)) => lb,
_ => Leaderboard::default(),
};
leaderboard.update(player_a.clone(), new_account_a_stats.clone());
leaderboard.update(player_b.clone(), new_account_b_stats.clone());
self.insert(Key::Leaderboard, Value::Leaderboard(leaderboard.clone()));
events.push(Event::Settled {
battle,
round,
player_a,
player_a_old: old_account_a_stats,
player_a_new: new_account_a_stats,
player_b,
player_b_old: old_account_b_stats,
player_b_new: new_account_b_stats,
outcome: Outcome::PlayerA,
leaderboard,
});
} else if round >= MAX_BATTLE_ROUNDS
|| (player_a_health == 0 && player_b_health == 0)
{
self.delete(Key::Battle(battle));
let Some(Value::Account(mut account_a)) =
self.get(&Key::Account(player_a.clone())).await
else {
panic!("Player A should have an account");
};
let old_account_a_stats = account_a.stats.clone();
let Some(Value::Account(mut account_b)) =
self.get(&Key::Account(player_b.clone())).await
else {
panic!("Player B should have an account");
};
let old_account_b_stats = account_b.stats.clone();
account_a.stats.draws = account_a.stats.draws.saturating_add(1);
account_a.battle = None;
account_b.stats.draws = account_b.stats.draws.saturating_add(1);
account_b.battle = None;
let max_health_a = account_a.creature.as_ref().unwrap().health();
let max_health_b = account_b.creature.as_ref().unwrap().health();
let (new_elo_a, new_elo_b) = elo::update(
account_a.stats.elo,
player_a_effective_health,
max_health_a,
account_b.stats.elo,
player_b_effective_health,
max_health_b,
);
account_a.stats.elo = new_elo_a;
let new_account_a_stats = account_a.stats.clone();
account_b.stats.elo = new_elo_b;
let new_account_b_stats = account_b.stats.clone();
self.insert(Key::Account(player_a.clone()), Value::Account(account_a));
self.insert(Key::Account(player_b.clone()), Value::Account(account_b));
let mut leaderboard = match self.get(&Key::Leaderboard).await {
Some(Value::Leaderboard(lb)) => lb,
_ => Leaderboard::default(),
};
leaderboard.update(player_a.clone(), new_account_a_stats.clone());
leaderboard.update(player_b.clone(), new_account_b_stats.clone());
self.insert(Key::Leaderboard, Value::Leaderboard(leaderboard.clone()));
events.push(Event::Settled {
battle,
round,
player_a,
player_a_old: old_account_a_stats,
player_a_new: new_account_a_stats,
player_b,
player_b_old: old_account_b_stats,
player_b_new: new_account_b_stats,
outcome: Outcome::Draw,
leaderboard,
});
} else {
self.insert(
Key::Battle(battle),
Value::Battle {
expiry: next_expiry,
round,
player_a,
player_a_max_health,
player_a_health,
player_a_pending: None,
player_a_move_counts,
player_b,
player_b_max_health,
player_b_health,
player_b_pending: None,
player_b_move_counts,
},
);
}
}
}
events
}
pub async fn execute(
&mut self,
#[cfg(feature = "parallel")] pool: ThreadPool,
transactions: Vec<Transaction>,
) -> (Vec<Output>, BTreeMap<PublicKey, u64>) {
let mut processed_nonces = BTreeMap::new();
let mut seed_ops = HashSet::new();
let mut decrypt_ops = HashSet::new();
let mut valid_transactions = Vec::new();
for tx in transactions {
if !self.prepare(&tx).await {
continue;
}
processed_nonces.insert(tx.public.clone(), tx.nonce.saturating_add(1));
let ops = self.extract(&tx).await;
for op in ops {
match op {
Task::Seed(_) => seed_ops.insert(op),
Task::Decrypt(_, _) => decrypt_ops.insert(op),
};
}
valid_transactions.push(tx);
}
macro_rules! process_ops {
($iter:ident) => {{
let mut results: HashMap<Task, TaskResult> = seed_ops
.$iter()
.map(|op| match op {
Task::Seed(ref seed) => {
if self.seed == *seed {
return (op, TaskResult::Seed(true));
}
let result = seed.verify(&self.namespace, &self.master);
(op, TaskResult::Seed(result))
}
_ => unreachable!(),
})
.collect();
let decrypt_results: HashMap<Task, TaskResult> = decrypt_ops
.$iter()
.flat_map(|op| match op {
Task::Decrypt(ref seed, ref ciphertext) => {
if !matches!(
results.get(&Task::Seed(seed.clone())).unwrap(), TaskResult::Seed(true)
) {
return None;
}
let result = decrypt::<MinSig>(&seed.signature, ciphertext)
.map(|block| block.as_ref().try_into().unwrap())
.unwrap_or_else(|| [0; 32]);
Some((op, TaskResult::Decrypt(result)))
}
_ => unreachable!(),
})
.collect();
results.extend(decrypt_results);
results
}};
}
#[cfg(feature = "parallel")]
let precomputations = pool.install(|| process_ops!(into_par_iter));
#[cfg(not(feature = "parallel"))]
let precomputations = process_ops!(into_iter);
self.precomputations = precomputations;
let mut events = Vec::new();
for tx in valid_transactions {
events.extend(self.apply(&tx).await.into_iter().map(Output::Event));
events.push(Output::Transaction(tx));
}
(events, processed_nonces)
}
pub fn commit(self) -> Vec<(Key, Status)> {
self.pending.into_iter().collect()
}
}
impl<'a, S: State> State for Layer<'a, S> {
async fn get(&self, key: &Key) -> Option<Value> {
match self.pending.get(key) {
Some(Status::Update(value)) => Some(value.clone()),
Some(Status::Delete) => None,
None => self.state.get(key).await,
}
}
async fn insert(&mut self, key: Key, value: Value) {
self.pending.insert(key, Status::Update(value));
}
async fn delete(&mut self, key: &Key) {
self.pending.insert(key.clone(), Status::Delete);
}
}
#[cfg(test)]
mod tests {
use super::*;
use commonware_cryptography::bls12381::tle::{encrypt, Block, Ciphertext};
use commonware_cryptography::{
bls12381::primitives::{ops, variant::MinSig},
ed25519, PrivateKeyExt, Signer,
};
use commonware_runtime::deterministic::Runner;
use commonware_runtime::Runner as _;
use rand::{rngs::StdRng, SeedableRng};
use std::collections::HashMap;
const TEST_NAMESPACE: &[u8] = b"test-namespace";
struct MockState {
data: HashMap<Key, Value>,
}
impl MockState {
fn new() -> Self {
Self {
data: HashMap::new(),
}
}
}
impl State for MockState {
async fn get(&self, key: &Key) -> Option<Value> {
self.data.get(key).cloned()
}
async fn insert(&mut self, key: Key, value: Value) {
self.data.insert(key, value);
}
async fn delete(&mut self, key: &Key) {
self.data.remove(key);
}
}
fn create_network_keypair() -> (
commonware_cryptography::bls12381::primitives::group::Private,
<MinSig as commonware_cryptography::bls12381::primitives::variant::Variant>::Public,
) {
let mut rng = StdRng::seed_from_u64(0);
ops::keypair::<_, MinSig>(&mut rng)
}
fn create_seed(
network_secret: &commonware_cryptography::bls12381::primitives::group::Private,
view: u64,
) -> Seed {
use commonware_consensus::threshold_simplex::types::{seed_namespace, view_message};
let seed_namespace = seed_namespace(TEST_NAMESPACE);
let message = view_message(view);
Seed::new(
view,
ops::sign_message::<MinSig>(network_secret, Some(&seed_namespace), &message),
)
}
fn create_test_move_ciphertext(
master_public: <MinSig as commonware_cryptography::bls12381::primitives::variant::Variant>::Public,
next_expiry: u64,
move_data: u8,
) -> Ciphertext<MinSig> {
use commonware_consensus::threshold_simplex::types::{seed_namespace, view_message};
let seed_namespace = seed_namespace(TEST_NAMESPACE);
let view_msg = view_message(next_expiry);
let mut message = [0u8; 32];
message[0] = move_data;
let mut rng = StdRng::seed_from_u64(42); encrypt::<_, MinSig>(
&mut rng,
master_public,
(Some(&seed_namespace), &view_msg),
&Block::new(message),
)
}
#[allow(dead_code)]
fn decrypt_test_move(
network_secret: &commonware_cryptography::bls12381::primitives::group::Private,
next_expiry: u64,
ciphertext: &Ciphertext<MinSig>,
) -> Option<u8> {
use commonware_cryptography::bls12381::tle::decrypt;
let seed = create_seed(network_secret, next_expiry);
decrypt::<MinSig>(&seed.signature, ciphertext).map(|block| block.as_ref()[0])
}
fn create_test_actor(seed: u64) -> (ed25519::PrivateKey, ed25519::PublicKey) {
let private = ed25519::PrivateKey::from_seed(seed);
let public = private.public_key();
(private, public)
}
#[test]
fn test_invalid_nonce_dropped() {
let executor = Runner::default();
executor.start(|_| async move {
let state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer, _) = create_test_actor(1);
let tx = Transaction::sign(&signer, 1, Instruction::Generate);
assert!(!layer.prepare(&tx).await);
let tx = Transaction::sign(&signer, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let tx = Transaction::sign(&signer, 0, Instruction::Generate);
assert!(!layer.prepare(&tx).await);
let tx = Transaction::sign(&signer, 1, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let _ = layer.commit();
});
}
#[test]
fn test_must_have_creature_before_battle() {
let executor = Runner::default();
executor.start(|_| async move {
let state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer, _) = create_test_actor(1);
let tx = Transaction::sign(&signer, 0, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.is_empty());
let tx = Transaction::sign(&signer, 1, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Event::Generated { .. }));
let tx = Transaction::sign(&signer, 2, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.is_empty());
let _ = layer.commit();
});
}
#[test]
fn test_cannot_enter_multiple_concurrent_battles() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Event::Matched { .. }));
let tx = Transaction::sign(&signer_a, 2, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.is_empty());
let _ = layer.commit();
});
}
#[test]
fn test_can_only_swap_creatures_when_not_in_battle() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Event::Generated { .. }));
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 2, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Event::Matched { .. }));
let tx = Transaction::sign(&signer_a, 3, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.is_empty());
let _ = layer.commit();
});
}
#[test]
fn test_cannot_send_multiple_moves_in_single_round() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move1 = create_test_move_ciphertext(master_public, battle_expiry, 1);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move1.clone()));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let move2 = create_test_move_ciphertext(master_public, battle_expiry, 2);
let tx = Transaction::sign(&signer_a, 3, Instruction::Move(move2));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.is_empty());
let _ = layer.commit();
});
}
#[test]
fn test_one_player_can_win_if_other_doesnt_play() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 2);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(&signer_a, 3, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Moved { .. })));
let _ = layer.commit();
});
}
#[test]
fn test_scores_update_correctly_on_game_over() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, actor_b) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let account_a_initial = layer.get(&Key::Account(actor_a.clone())).await.unwrap();
let account_b_initial = layer.get(&Key::Account(actor_b.clone())).await.unwrap();
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
let _battle_digest = if let Event::Matched { battle, .. } = &events[0] {
*battle
} else {
panic!("Expected matched event");
};
let mut round = 0;
loop {
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 3);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 3);
let tx = Transaction::sign(&signer_a, 2 + round * 2, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 2 + round, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(
&signer_a,
3 + round * 2,
Instruction::Settle(signature.signature),
);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
if let Some(settled_event) =
events.iter().find(|e| matches!(e, Event::Settled { .. }))
{
let account_a_final = layer.get(&Key::Account(actor_a.clone())).await.unwrap();
let account_b_final = layer.get(&Key::Account(actor_b.clone())).await.unwrap();
if let (
Value::Account(mut acc_a_init),
Value::Account(mut acc_a_final),
Value::Account(mut acc_b_init),
Value::Account(mut acc_b_final),
) = (
account_a_initial.clone(),
account_a_final,
account_b_initial.clone(),
account_b_final,
) {
assert!(
acc_a_final.stats.wins > acc_a_init.stats.wins
|| acc_a_final.stats.losses > acc_a_init.stats.losses
|| acc_a_final.stats.draws > acc_a_init.stats.draws
);
assert!(
acc_b_final.stats.wins > acc_b_init.stats.wins
|| acc_b_final.stats.losses > acc_b_init.stats.losses
|| acc_b_final.stats.draws > acc_b_init.stats.draws
);
assert!(
acc_a_final.stats.elo != acc_a_init.stats.elo
|| acc_b_final.stats.elo != acc_b_init.stats.elo
);
assert!(acc_a_final.battle.is_none());
assert!(acc_b_final.battle.is_none());
if let Event::Settled {
player_a,
player_a_old,
player_a_new,
player_b_old,
player_b_new,
..
} = settled_event
{
if player_a != &signer_a.public_key() {
let acc_temp_init = acc_a_init;
let acc_temp_final = acc_a_final;
acc_a_init = acc_b_init;
acc_a_final = acc_b_final;
acc_b_init = acc_temp_init;
acc_b_final = acc_temp_final;
}
assert_eq!(
player_a_old.elo, acc_a_init.stats.elo,
"Player A old Elo should match initial Elo"
);
assert_eq!(
player_b_old.elo, acc_b_init.stats.elo,
"Player B old Elo should match initial Elo"
);
assert_eq!(
player_a_new.elo, acc_a_final.stats.elo,
"Player A new Elo should match final Elo"
);
assert_eq!(
player_b_new.elo, acc_b_final.stats.elo,
"Player B new Elo should match final Elo"
);
assert_ne!(
player_a_old.elo, player_a_new.elo,
"Player A Elo should have changed"
);
assert_ne!(
player_b_old.elo, player_b_new.elo,
"Player B Elo should have changed"
);
}
}
break;
}
round += 1;
if round > 10 {
panic!("Game should have ended by now");
}
}
let _ = layer.commit();
});
}
#[test]
fn test_invalid_signature_does_nothing() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 1);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 2);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 2, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let wrong_signature =
ops::sign_message::<MinSig>(&network_secret, Some(b"test"), b"wrong");
let tx = Transaction::sign(&signer_a, 3, Instruction::Settle(wrong_signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.is_empty());
let battle_key = layer.get(&Key::Account(actor_a.clone())).await.unwrap();
if let Value::Account(account) = battle_key {
assert!(account.battle.is_some());
}
let _ = layer.commit();
});
}
#[test]
fn test_decryption_failure_defaults_to_no_move() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
let (_, actual_player_a, _) = match &events[0] {
Event::Matched {
battle,
expiry: _,
player_a,
player_a_creature: _,
player_a_stats: _,
player_b,
player_b_creature: _,
player_b_stats: _,
} => (*battle, player_a.clone(), player_b.clone()),
_ => panic!("Expected Matched event"),
};
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let (bad_move_signer, bad_move_nonce, good_move_signer, good_move_nonce) =
if actual_player_a == signer_a.public_key() {
(&signer_a, 2, &signer_b, 2)
} else {
(&signer_b, 2, &signer_a, 2)
};
let bad_move = create_test_move_ciphertext(master_public, 9999, 3); let good_move = create_test_move_ciphertext(master_public, battle_expiry, 2);
let tx =
Transaction::sign(bad_move_signer, bad_move_nonce, Instruction::Move(bad_move));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(
good_move_signer,
good_move_nonce,
Instruction::Move(good_move),
);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(&signer_a, 3, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let move_event = events
.iter()
.find(|e| matches!(e, Event::Moved { .. }))
.unwrap();
if let Event::Moved {
player_a_move,
player_b_move,
..
} = move_event
{
assert_eq!(*player_a_move, 0);
assert_eq!(*player_b_move, 2);
}
let _ = layer.commit();
});
}
#[test]
fn test_move_usage_limits_respected() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let Some(Event::Matched { player_a, .. }) = events.first() else {
panic!("Expected Matched event");
};
let mut nonce_a = 2;
let mut nonce_b = 2;
let is_signer_a_player_a = signer_a.public_key() == *player_a;
let Some(Value::Account(account_player_a)) =
layer.get(&Key::Account(player_a.clone())).await
else {
panic!("Player A account should exist");
};
let creature_player_a = account_player_a.creature.as_ref().unwrap();
let move_limits = creature_player_a.get_move_usage_limits();
let mut strongest_move = 0;
let mut min_limit = u8::MAX;
for (i, &limit) in move_limits.iter().enumerate().skip(1) {
if limit < min_limit {
min_limit = limit;
strongest_move = i as u8;
}
}
let battle_key = account_player_a.battle.unwrap();
for _ in 0..min_limit {
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let (move_signer_a, move_signer_b) = if is_signer_a_player_a {
(
create_test_move_ciphertext(master_public, battle_expiry, strongest_move),
create_test_move_ciphertext(master_public, battle_expiry, 1),
)
} else {
(
create_test_move_ciphertext(master_public, battle_expiry, 1),
create_test_move_ciphertext(master_public, battle_expiry, strongest_move),
)
};
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(move_signer_a));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(move_signer_b));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
nonce_b += 1;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
nonce_a += 1;
let move_event = events
.iter()
.find(|e| matches!(e, Event::Moved { .. }))
.unwrap();
if let Event::Moved {
player_a_move,
player_b_move,
..
} = move_event
{
assert_eq!(*player_a_move, strongest_move);
assert_eq!(*player_b_move, 1);
}
if layer.get(&Key::Battle(battle_key)).await.is_none() {
break;
}
}
if let Some(Value::Battle { .. }) = layer.get(&Key::Battle(battle_key)).await {
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let (move_signer_a, move_signer_b) = if is_signer_a_player_a {
(
create_test_move_ciphertext(master_public, battle_expiry, strongest_move),
create_test_move_ciphertext(master_public, battle_expiry, 1),
)
} else {
(
create_test_move_ciphertext(master_public, battle_expiry, 1),
create_test_move_ciphertext(master_public, battle_expiry, strongest_move),
)
};
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(move_signer_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(move_signer_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let move_event = events
.iter()
.find(|e| matches!(e, Event::Moved { .. }))
.unwrap();
if let Event::Moved {
player_a_move,
player_b_move,
..
} = move_event
{
assert_eq!(*player_a_move, 0);
assert_eq!(*player_b_move, 1);
}
}
let _ = layer.commit();
});
}
#[test]
fn test_move_usage_counts_persist_across_rounds() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let Some(Event::Matched { player_a, .. }) = events.first() else {
panic!("Expected Matched event");
};
let Some(Value::Account(account)) = layer.get(&Key::Account(actor_a.clone())).await
else {
panic!("Account should exist");
};
let battle_key = account.battle.unwrap();
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 3);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 1);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
let tx = Transaction::sign(&signer_b, 2, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(&signer_a, 3, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let battle_ended = events.iter().any(|e| matches!(e, Event::Settled { .. }));
if battle_ended {
let _ = layer.commit();
return;
}
let Some(Value::Battle {
player_a_move_counts,
player_b_move_counts,
..
}) = layer.get(&Key::Battle(battle_key)).await
else {
panic!("Battle should exist");
};
if signer_a.public_key() == *player_a {
assert_eq!(
player_a_move_counts[3], 1,
"Move 2 should have been used once"
);
} else {
assert_eq!(
player_b_move_counts[3], 1,
"Move 2 should have been used once"
);
}
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 4);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 1);
let tx = Transaction::sign(&signer_a, 4, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 3, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(&signer_a, 5, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
if let Some(Value::Battle {
player_a_move_counts,
player_b_move_counts,
..
}) = layer.get(&Key::Battle(battle_key)).await
{
if signer_a.public_key() == *player_a {
assert_eq!(player_a_move_counts[3], 1, "Move 2 count should persist");
assert_eq!(
player_a_move_counts[4], 1,
"Move 3 should have been used once"
);
} else {
assert_eq!(player_b_move_counts[3], 1, "Move 2 count should persist");
assert_eq!(
player_b_move_counts[4], 1,
"Move 3 should have been used once"
);
}
}
let _ = layer.commit();
});
}
#[test]
fn test_creature_strength_method() {
let executor = Runner::default();
executor.start(|_| async move {
let (network_secret, _) = create_network_keypair();
let (_, actor) = create_test_actor(1);
let seed = create_seed(&network_secret, 1);
let creature = Creature::new(actor, 0, seed.signature);
assert_eq!(creature.health(), creature.traits[0]);
});
}
#[test]
fn test_creature_get_move_strengths() {
let executor = Runner::default();
executor.start(|_| async move {
let (network_secret, _) = create_network_keypair();
let (_, actor) = create_test_actor(1);
let seed = create_seed(&network_secret, 1);
let creature = Creature::new(actor, 0, seed.signature);
let move_strengths = creature.get_move_strengths();
assert_eq!(move_strengths[0], 0); assert_eq!(move_strengths[1], creature.traits[1]); assert_eq!(move_strengths[2], creature.traits[2]); assert_eq!(move_strengths[3], creature.traits[3]); assert_eq!(move_strengths[4], creature.traits[4]); });
}
#[test]
fn test_creature_actions() {
let executor = Runner::default();
executor.start(|_| async move {
let (network_secret, _) = create_network_keypair();
let (_, actor) = create_test_actor(1);
let seed = create_seed(&network_secret, 1);
let creature = Creature::new(actor, 0, seed.signature);
assert_eq!(creature.action(0, seed.signature), (false, 0));
assert_eq!(
creature.action(TOTAL_MOVES as u8, seed.signature),
(false, 0)
);
assert_eq!(creature.action(u8::MAX, seed.signature), (false, 0));
});
}
#[test]
fn test_creature_action_minimum_effectiveness() {
let executor = Runner::default();
executor.start(|_| async move {
let (network_secret, _) = create_network_keypair();
let (_, actor) = create_test_actor(1);
let seed = create_seed(&network_secret, 1);
let creature = Creature::new(actor, 0, seed.signature);
for i in 0..100 {
let (sk, _) = create_network_keypair();
let test_seed = ops::sign_message::<MinSig>(&sk, Some(b"test"), &[i; 32]);
for move_idx in 1..TOTAL_MOVES as u8 {
let (is_defense, power) = creature.action(move_idx, test_seed);
let max_power = creature.traits[move_idx as usize];
let min_expected = max_power / 2;
assert!(power >= min_expected);
assert!(power <= max_power);
assert_eq!(is_defense, move_idx == 1);
}
}
});
}
#[test]
fn test_generate_multiple_times() {
let executor = Runner::default();
executor.start(|_| async move {
let state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer, _) = create_test_actor(1);
let tx = Transaction::sign(&signer, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Event::Generated { .. }));
let tx = Transaction::sign(&signer, 1, Instruction::Generate);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Event::Generated { .. }));
let _ = layer.commit();
});
}
#[test]
fn test_match_with_empty_lobby() {
let executor = Runner::default();
executor.start(|_| async move {
let state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer, actor) = create_test_actor(1);
let tx = Transaction::sign(&signer, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 0);
if let Some(Value::Lobby { players, .. }) = layer.get(&Key::Lobby).await {
assert!(players.contains(&actor));
} else {
panic!("Lobby should exist");
}
let _ = layer.commit();
});
}
#[test]
fn test_move_with_no_battle() {
let executor = Runner::default();
executor.start(|_| async move {
let state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer, _) = create_test_actor(1);
let tx = Transaction::sign(&signer, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let encrypted_move = create_test_move_ciphertext(master_public, 100, 1);
let tx = Transaction::sign(&signer, 1, Instruction::Move(encrypted_move));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 0);
let _ = layer.commit();
});
}
#[test]
fn test_settle_with_no_battle() {
let executor = Runner::default();
executor.start(|_| async move {
let state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer, _) = create_test_actor(1);
let tx = Transaction::sign(&signer, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let signature = create_seed(&network_secret, 100);
let tx = Transaction::sign(&signer, 1, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 0);
let _ = layer.commit();
});
}
#[test]
fn test_move_when_turn_expired() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 202);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let encrypted_move = create_test_move_ciphertext(master_public, 102 + MOVE_EXPIRY, 1);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(encrypted_move));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 0);
let _ = layer.commit();
});
}
#[test]
fn test_settle_before_turn_expired() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 1);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 2);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 2, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(&signer_a, 3, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 0);
let _ = layer.commit();
});
}
#[test]
fn test_double_move_submission() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a1 = create_test_move_ciphertext(master_public, battle_expiry, 1);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move_a1));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let move_a2 = create_test_move_ciphertext(master_public, battle_expiry, 2);
let tx = Transaction::sign(&signer_a, 3, Instruction::Move(move_a2));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 0);
let _ = layer.commit();
});
}
#[test]
fn test_battle_with_all_health_scenarios() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, actor_b) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let Some(Value::Account(account_a)) = layer.get(&Key::Account(actor_a.clone())).await
else {
panic!("Account A should exist");
};
let Some(Value::Account(account_b)) = layer.get(&Key::Account(actor_b.clone())).await
else {
panic!("Account B should exist");
};
let _max_health_a = account_a.creature.as_ref().unwrap().health();
let _max_health_b = account_b.creature.as_ref().unwrap().health();
let mut nonce_a = 2;
let mut nonce_b = 2;
let mut round = 0;
loop {
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 2);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 3);
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_b += 1;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
nonce_a += 1;
let settled = events.iter().any(|e| matches!(e, Event::Settled { .. }));
if settled {
let Some(Value::Account(final_a)) =
layer.get(&Key::Account(actor_a.clone())).await
else {
panic!("Account A should exist");
};
let Some(Value::Account(final_b)) =
layer.get(&Key::Account(actor_b.clone())).await
else {
panic!("Account B should exist");
};
assert_ne!(
final_a.stats.elo, account_a.stats.elo,
"ELO should have changed for player A"
);
assert_ne!(
final_b.stats.elo, account_b.stats.elo,
"ELO should have changed for player B"
);
let total_games_a =
final_a.stats.wins + final_a.stats.losses + final_a.stats.draws;
let total_games_b =
final_b.stats.wins + final_b.stats.losses + final_b.stats.draws;
assert_eq!(total_games_a, 1, "Player A should have exactly 1 game");
assert_eq!(total_games_b, 1, "Player B should have exactly 1 game");
break;
}
round += 1;
if round > 100 {
panic!("Battle should have ended by now");
}
}
let _ = layer.commit();
});
}
#[test]
fn test_player_a_defends_player_b_attacks() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let Some(Event::Matched { player_a, .. }) = events.first() else {
panic!("Expected Matched event");
};
let battle_expiry = layer
.view()
.checked_add(MOVE_EXPIRY)
.expect("view overflow");
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 1);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 3);
let tx = Transaction::sign(&signer_a, 2, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 2, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, battle_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx = Transaction::sign(&signer_a, 3, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let move_event = events
.iter()
.find(|e| matches!(e, Event::Moved { .. }))
.unwrap();
if let Event::Moved {
player_a_power,
player_b_power,
..
} = move_event
{
if signer_a.public_key() == *player_a {
assert!(*player_a_power > 0, "Defense should have positive impact");
assert!(*player_b_power > 0, "Attack should have negative impact");
} else {
assert!(*player_b_power > 0, "Attack should have positive impact");
assert!(*player_a_power > 0, "Defense should have negative impact");
}
}
let _ = layer.commit();
});
}
#[test]
fn test_battle_times_out_after_max_rounds() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, actor_b) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let seed = create_seed(&network_secret, 2);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let view = 2 + LOBBY_EXPIRY + 1;
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let Some(Event::Matched { battle, .. }) =
events.iter().find(|e| matches!(e, Event::Matched { .. }))
else {
panic!("Battle should be matched");
};
let changes = layer.commit();
state.apply(changes).await;
let mut nonce_a = 2;
let mut nonce_b = 2;
let mut view = view + 1;
for round in 0..MAX_BATTLE_ROUNDS {
let Some(Value::Battle { expiry, .. }) = state.get(&Key::Battle(*battle)).await
else {
panic!("Battle should exist");
};
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let move_a = create_test_move_ciphertext(master_public, expiry, 0);
let move_b = create_test_move_ciphertext(master_public, expiry, 0);
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
nonce_b += 1;
let changes = layer.commit();
state.apply(changes).await;
view = expiry + 1;
let new_seed = create_seed(&network_secret, view);
let new_layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
layer = new_layer;
let expiry_seed = create_seed(&network_secret, expiry);
let tx = Transaction::sign(
&signer_a,
nonce_a,
Instruction::Settle(expiry_seed.signature),
);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
nonce_a += 1;
if let Some(Event::Settled { round, outcome, .. }) =
events.iter().find(|e| matches!(e, Event::Settled { .. }))
{
assert_eq!(*round, MAX_BATTLE_ROUNDS);
assert_eq!(*outcome, Outcome::Draw);
let Some(Value::Account(account_a_final)) =
layer.get(&Key::Account(actor_a.clone())).await
else {
panic!("Account A should exist");
};
let Some(Value::Account(account_b_final)) =
layer.get(&Key::Account(actor_b.clone())).await
else {
panic!("Account B should exist");
};
assert_eq!(account_a_final.stats.draws, 1);
assert_eq!(account_b_final.stats.draws, 1);
assert!(account_a_final.battle.is_none());
assert!(account_b_final.battle.is_none());
return; }
if round < MAX_BATTLE_ROUNDS - 1 {
assert!(
events.iter().any(|e| matches!(e, Event::Moved { .. })),
"Round {round}: Expected Moved event but got {events:?}",
);
}
let changes = layer.commit();
state.apply(changes).await;
view += 1;
}
panic!("Battle should have ended in a draw after MAX_BATTLE_ROUNDS");
});
}
#[test]
fn test_out_of_range_moves_handled_as_no_move() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let Some(Value::Account(account_a)) = state.get(&Key::Account(actor_a.clone())).await
else {
panic!("Account A should exist");
};
let battle_key = account_a.battle.unwrap();
let Some(Value::Battle { player_a, .. }) = state.get(&Key::Battle(battle_key)).await
else {
panic!("Battle should exist");
};
let Some(Value::Battle { expiry, .. }) = state.get(&Key::Battle(battle_key)).await
else {
panic!("Battle should exist");
};
let out_of_range_moves = vec![
5, 10, 100, 255, ];
let mut next_expiry = expiry;
let mut nonce_a = 2;
let mut nonce_b = 2;
for out_of_range_move in out_of_range_moves {
let new_seed = create_seed(&network_secret, next_expiry);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let move_out_of_range =
create_test_move_ciphertext(master_public, next_expiry, out_of_range_move);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Move(move_out_of_range));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
nonce_a += 1;
let move_valid = create_test_move_ciphertext(master_public, next_expiry, 1);
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(move_valid));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert!(events.iter().any(|e| matches!(e, Event::Locked { .. })));
nonce_b += 1;
let changes = layer.commit();
state.apply(changes).await;
let settle_seed = create_seed(&network_secret, next_expiry + 1);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, settle_seed);
let signature = create_seed(&network_secret, next_expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
nonce_a += 1;
let Some(Event::Moved {
expiry,
player_a_move,
player_b_move,
..
}) = events.first()
else {
panic!("Expected Moved event");
};
if signer_a.public_key() == player_a {
assert_eq!(*player_a_move, 0);
assert_eq!(*player_b_move, 1);
} else {
assert_eq!(*player_a_move, 1);
assert_eq!(*player_b_move, 0);
}
next_expiry = *expiry;
let Some(Value::Account(account)) = layer.get(&Key::Account(actor_a.clone())).await
else {
panic!("Account should exist");
};
assert!(account.battle.is_some(), "Battle should still be ongoing");
let changes = layer.commit();
state.apply(changes).await;
}
});
}
#[test]
fn test_prefer_result_over_draw_when_player_killed_in_last_round() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, actor_a) = create_test_actor(1);
let (signer_b, actor_b) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let (battle, actual_player_a, _actual_player_b) = if let Event::Matched {
battle,
player_a,
player_b,
..
} = &events[0]
{
(*battle, player_a.clone(), player_b.clone())
} else {
panic!("Expected Matched event");
};
let is_signer_a_player_a = actual_player_a == actor_a;
let changes = layer.commit();
state.apply(changes).await;
let Some(Value::Battle {
expiry,
round,
player_a,
player_a_max_health,
player_a_pending,
player_a_move_counts,
player_b,
player_b_max_health,
player_b_pending,
player_b_move_counts,
..
}) = state.get(&Key::Battle(battle)).await
else {
panic!("Battle should exist");
};
state
.insert(
Key::Battle(battle),
Value::Battle {
expiry,
round,
player_a: player_a.clone(),
player_a_max_health,
player_a_health: 1,
player_a_pending,
player_a_move_counts,
player_b: player_b.clone(),
player_b_max_health,
player_b_health: 1,
player_b_pending,
player_b_move_counts,
},
)
.await;
let mut nonce_a = 2;
let mut nonce_b = 2;
let mut view = 103;
for _ in 0..(MAX_BATTLE_ROUNDS - 1) {
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let Some(Value::Battle { expiry, .. }) = layer.get(&Key::Battle(battle)).await
else {
panic!("Battle should exist");
};
let battle_expiry = expiry;
let move_a = create_test_move_ciphertext(master_public, battle_expiry, 0);
let move_b = create_test_move_ciphertext(master_public, battle_expiry, 0);
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_b += 1;
let changes = layer.commit();
state.apply(changes).await;
view = battle_expiry + 1;
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
nonce_a += 1;
assert!(events.iter().any(|e| matches!(e, Event::Moved { .. })));
assert!(!events.iter().any(|e| matches!(e, Event::Settled { .. })));
let changes = layer.commit();
state.apply(changes).await;
view += 1;
}
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let Some(Value::Battle { expiry, .. }) = layer.get(&Key::Battle(battle)).await else {
panic!("Battle should exist");
};
if is_signer_a_player_a {
let attack_move = create_test_move_ciphertext(master_public, expiry, 2); let no_move = create_test_move_ciphertext(master_public, expiry, 0);
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(attack_move));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(no_move));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
} else {
let no_move = create_test_move_ciphertext(master_public, expiry, 0); let attack_move = create_test_move_ciphertext(master_public, expiry, 2);
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(no_move));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(attack_move));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
}
let changes = layer.commit();
state.apply(changes).await;
view = expiry + 1;
let new_seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let signature = create_seed(&network_secret, expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
let settled_event = events.iter().find(|e| matches!(e, Event::Settled { .. }));
assert!(settled_event.is_some(), "Battle should have been settled");
if let Some(Event::Settled { outcome, round, .. }) = settled_event {
assert_eq!(
*round, MAX_BATTLE_ROUNDS,
"Battle should end at MAX_BATTLE_ROUNDS"
);
assert_eq!(
*outcome,
Outcome::PlayerA,
"Player A should win by killing Player B in the last round"
);
}
let final_account_a =
if let Some(Value::Account(acc)) = layer.get(&Key::Account(actor_a)).await {
acc
} else {
panic!("Account A not found after battle");
};
let final_account_b =
if let Some(Value::Account(acc)) = layer.get(&Key::Account(actor_b)).await {
acc
} else {
panic!("Account B not found after battle");
};
if is_signer_a_player_a {
assert_eq!(
final_account_a.stats.wins, 1,
"Signer A (as Player A) should have won"
);
assert_eq!(final_account_a.stats.draws, 0);
assert_eq!(
final_account_b.stats.losses, 1,
"Signer B (as Player B) should have lost"
);
assert_eq!(final_account_b.stats.draws, 0);
} else {
assert_eq!(
final_account_b.stats.wins, 1,
"Signer B (as Player A) should have won"
);
assert_eq!(final_account_b.stats.draws, 0);
assert_eq!(
final_account_a.stats.losses, 1,
"Signer A (as Player B) should have lost"
);
assert_eq!(final_account_a.stats.draws, 0);
}
let _ = layer.commit();
});
}
#[test]
fn test_defense_moves_never_exceed_max_health() {
let executor = Runner::default();
executor.start(|_| async move {
let mut state = MockState::new();
let (network_secret, master_public) = create_network_keypair();
let seed = create_seed(&network_secret, 1);
let mut layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let (signer_a, _) = create_test_actor(1);
let (signer_b, _) = create_test_actor(2);
let tx = Transaction::sign(&signer_a, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_b, 0, Instruction::Generate);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let tx = Transaction::sign(&signer_a, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
let changes = layer.commit();
state.apply(changes).await;
let new_seed = create_seed(&network_secret, 102);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, new_seed);
let tx = Transaction::sign(&signer_b, 1, Instruction::Match);
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
assert_eq!(events.len(), 1);
let Some(Event::Matched { battle, .. }) = events.first() else {
panic!("Expected Matched event");
};
let mut nonce_a = 2;
let mut nonce_b = 2;
let mut view = 103;
let changes = layer.commit();
state.apply(changes).await;
for round in 0..5 {
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let Some(Value::Battle { expiry, .. }) = layer.get(&Key::Battle(*battle)).await
else {
panic!("Battle should exist");
};
let battle_expiry = expiry;
let defense_move_a = create_test_move_ciphertext(master_public, battle_expiry, 1);
let defense_move_b = create_test_move_ciphertext(master_public, battle_expiry, 1);
let tx = Transaction::sign(&signer_a, nonce_a, Instruction::Move(defense_move_a));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_a += 1;
let tx = Transaction::sign(&signer_b, nonce_b, Instruction::Move(defense_move_b));
assert!(layer.prepare(&tx).await);
layer.apply(&tx).await;
nonce_b += 1;
let changes = layer.commit();
state.apply(changes).await;
view = battle_expiry + 1;
let seed = create_seed(&network_secret, view);
layer = Layer::new(&state, master_public, TEST_NAMESPACE, seed);
let signature = create_seed(&network_secret, battle_expiry);
let tx =
Transaction::sign(&signer_a, nonce_a, Instruction::Settle(signature.signature));
assert!(layer.prepare(&tx).await);
let events = layer.apply(&tx).await;
nonce_a += 1;
assert!(
events.iter().any(|e| matches!(e, Event::Moved { .. })),
"Should have a Moved event"
);
if let Some(Value::Battle {
player_a_health,
player_a_max_health,
player_b_health,
player_b_max_health,
..
}) = layer.get(&Key::Battle(*battle)).await
{
assert!(player_a_health <= player_a_max_health);
assert!(player_b_health <= player_b_max_health);
} else {
panic!("Battle should exist after round {round}");
}
let changes = layer.commit();
state.apply(changes).await;
view += 1;
}
});
}
}