Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Move] Add shared mutable object support #722

Merged
merged 1 commit into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions sui_programmability/adapter/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub fn new_move_vm(natives: NativeFunctionTable) -> Result<Arc<MoveVM>, SuiError
/// Execute `module::function<type_args>(object_args ++ pure_args)` as a call from `sender` with the given `gas_budget`.
/// Execution will read from/write to the store in `state_view`.
/// IMPORTANT NOTES on the return value:
/// The return value indicates whether a system error has occured (i.e. issues with the sui system, not with user transaction).
/// The return value indicates whether a system error has occurred (i.e. issues with the sui system, not with user transaction).
/// As long as there are no system issues we return Ok(ExecutionStatus).
/// ExecutionStatus indicates the execution result. If execution failed, we wrap both the gas used and the error
/// into ExecutionStatus::Failure.
Expand Down Expand Up @@ -221,7 +221,7 @@ fn execute_internal<
// Cap total_gas by gas_budget in the fail case.
return ExecutionStatus::new_failure(cmp::min(total_gas, gas_budget), err);
}
// gas_budget should be enough to pay not only the VM excution cost,
// gas_budget should be enough to pay not only the VM execution cost,
// but also the cost to process all events, such as transfers.
if total_gas > gas_budget {
ExecutionStatus::new_failure(
Expand Down Expand Up @@ -568,6 +568,7 @@ fn process_successful_execution<
deleted_ids.insert(*id.object_id(), id.version());
Ok(())
}
EventType::ShareObject => Err(SuiError::UnsupportedSharedObjectError),
EventType::User => {
match type_ {
TypeTag::Struct(s) => state_view.log_event(Event::new(s, event_bytes)),
Expand Down Expand Up @@ -662,7 +663,7 @@ fn handle_transfer<
if let Owner::ObjectOwner(new_owner) = recipient {
// Below we check whether the transfer introduced any circular ownership.
// We know that for any mutable object, all its ancenstors (if it was owned by another object)
// must be in the input as well. Prior to this we have recored the original ownership mapping
// must be in the input as well. Prior to this we have recorded the original ownership mapping
// in object_owner_map. For any new transfer, we trace the new owner through the ownership
// chain to see if a cycle is detected.
// TODO: Set a constant upper bound to the depth of the new ownership chain.
Expand All @@ -687,7 +688,7 @@ fn handle_transfer<
fn check_transferred_object_invariants(new_object: &MoveObject, old_object: &Option<Object>) {
if let Some(o) = old_object {
// check consistency between the transferred object `new_object` and the tx input `o`
// specificially, the object id, type, and version should be unchanged
// specifically, the object id, type, and version should be unchanged
let m = o.data.try_as_move().unwrap();
debug_assert_eq!(m.id(), new_object.id());
debug_assert_eq!(m.version(), new_object.version());
Expand Down
20 changes: 18 additions & 2 deletions sui_programmability/examples/games/sources/TicTacToe.move
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
// This is an implementation of the TicTacToe game.
// The game object (which includes gameboard) is owned by a game admin.
// Since players don't have ownership over the game object, they cannot
// mutate the gameboard directly. In order for each plaer to place
// a marker, they must first show their intention of placing a marker
// by creating a marker object with the placement information and send
// the marker to the admin. The admin needs to run a centralized cervice
// that monitors the marker placement events and respond do them.
// Upon receiving an event, the admin will attempt the place the new
// marker on the gameboard. This means that every marker placement operation
// always take two transactions, one by the player, and one by the admin.
// It also means that we need to trust the centralized service for liveness,
// i.e. the service is willing to make progress in the game.
// TicTacToeV2 shows a simpler way to implement this using shared objects,
// providing different trade-offs: using shared object is more expensive,
// however it eliminates the need of a centralized service.
module Games::TicTacToe {
use Std::Option::{Self, Option};
use Std::Vector;
Expand Down Expand Up @@ -212,7 +228,7 @@ module Games::TicTacToe {
check_for_winner(game, 2, 0, 1, 1, 0, 2);

// Check if we have a draw
if (game.cur_turn == 9) {
if (game.game_status != IN_PROGRESS && game.cur_turn == 9) {
game.game_status = DRAW;
};
}
Expand Down Expand Up @@ -263,4 +279,4 @@ module Games::TicTacToe {
public fun mark_col(mark: &Mark): u64 {
mark.col
}
}
}
171 changes: 171 additions & 0 deletions sui_programmability/examples/games/sources/TicTacToeV2.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// This is a rewrite of TicTacToe using a completely different approach.
// In TicTacToe, since the game object is owned by the admin, the players was not
// able to directly mutate the gameboard. Hence each marker placement takes
// two transactions.
// In this implementation, we make the game object a shared mutable object.
// Both players have access and can mutate the game object, and hence they
// can place markers directly in one transaction.
// In general, using shared mutable object has an extra cost due to the fact
// that Sui need to sequence the operations that mutate the shared object from
// different transactions. In this case however, since it is expected for players
// to take turns to place the marker, there won't be a significant overhead in practice.
// As we can see, by using shared mutable object, the implementation is much
// simpler than the other implementation.
module Games::TicTacToeV2 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm wondering about (and this is philosophical/for the future): if we want to support both "modes" of tic tac toe, what is the best way to engineer this?

  1. Fully separate modules [current approach]
  2. Separate modules that share a common library
  3. Single module with some _async functions that can only be called in single-owner mode, some _sync functions that can only be called in shared mode, and (hopefully) a large number of common functions

My instinct is that (2) is probably a cleaner approach than (1) (should lead to less code/test/Move prover spec duplication), but (3) may be a bridge too far. But at some point, we should try them all and see how it shakes out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was somewhat surprising that these two approaches don't share much code at all. The primary reason is that the data storage model changed from storing objects as data into storing primitive data directly. So every function changed because of that. The only things that are still sharable are probably the constants.

use Std::Vector;

use Sui::ID::{Self, ID, VersionedID};
use Sui::Event;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

// Game status
const IN_PROGRESS: u8 = 0;
const X_WIN: u8 = 1;
const O_WIN: u8 = 2;
const DRAW: u8 = 3;

// Mark type
const MARK_EMPTY: u8 = 0;
const MARK_X: u8 = 1;
const MARK_O: u8 = 2;

// Error codes
/// Trying to place a mark when it's not your turn.
const EINVALID_TURN: u64 = 0;
/// Trying to place a mark when the game has already ended.
const EGAME_ENDED: u64 = 1;
/// Trying to place a mark in an invalid location, i.e. row/column out of bound.
const EINVALID_LOCATION: u64 = 2;
/// The cell to place a new mark at is already oocupied.
const ECELL_OCCUPIED: u64 = 3;

struct TicTacToe has key {
id: VersionedID,
gameboard: vector<vector<u8>>,
cur_turn: u8,
game_status: u8,
x_address: address,
o_address: address,
}

struct Trophy has key {
id: VersionedID,
}

struct GameEndEvent has copy, drop {
// The Object ID of the game object
game_id: ID,
}

/// `x_address` and `o_address` are the account address of the two players.
public fun create_game(x_address: address, o_address: address, ctx: &mut TxContext) {
// TODO: Validate sender address, only GameAdmin can create games.

let id = TxContext::new_id(ctx);
let gameboard = vector[
vector[MARK_EMPTY, MARK_EMPTY, MARK_EMPTY],
vector[MARK_EMPTY, MARK_EMPTY, MARK_EMPTY],
vector[MARK_EMPTY, MARK_EMPTY, MARK_EMPTY],
];
let game = TicTacToe {
id,
gameboard,
// X always go first.
cur_turn: MARK_X,
game_status: IN_PROGRESS,
x_address: x_address,
o_address: o_address,
};
// Make the game a shared object so that both players can mutate it.
Transfer::share_object(game);
}

public fun place_mark(game: &mut TicTacToe, row: u8, col: u8, ctx: &mut TxContext) {
assert!(row < 3 && col < 3, EINVALID_LOCATION);
assert!(game.game_status == IN_PROGRESS, EGAME_ENDED);
let addr = get_cur_turn_address(game);
assert!(addr == TxContext::sender(ctx), EINVALID_TURN);

let cell = Vector::borrow_mut(Vector::borrow_mut(&mut game.gameboard, (row as u64)), (col as u64));
assert!(*cell == MARK_EMPTY, ECELL_OCCUPIED);

*cell = game.cur_turn;
update_winner(game);
game.cur_turn = if (game.cur_turn == MARK_X) MARK_O else MARK_X;

if (game.game_status != IN_PROGRESS) {
// Notify the server that the game ended so that it can delete the game.
Event::emit(GameEndEvent { game_id: *ID::inner(&game.id) });
if (game.game_status == X_WIN) {
Transfer::transfer( Trophy { id: TxContext::new_id(ctx) }, *&game.x_address);
} else if (game.game_status == O_WIN) {
Transfer::transfer( Trophy { id: TxContext::new_id(ctx) }, *&game.o_address);
}
}
}

public fun delete_game(game: TicTacToe, _ctx: &mut TxContext) {
let TicTacToe { id, gameboard: _, cur_turn: _, game_status: _, x_address: _, o_address: _ } = game;
ID::delete(id);
}

public fun delete_trophy(trophy: Trophy, _ctx: &mut TxContext) {
let Trophy { id } = trophy;
ID::delete(id);
}

public fun get_status(game: &TicTacToe): u8 {
game.game_status
}

fun get_cur_turn_address(game: &TicTacToe): address {
if (game.cur_turn == MARK_X) {
*&game.x_address
} else {
*&game.o_address
}
}

fun get_cell(game: &TicTacToe, row: u64, col: u64): u8 {
*Vector::borrow(Vector::borrow(&game.gameboard, row), col)
}

fun update_winner(game: &mut TicTacToe) {
// Check all rows
check_for_winner(game, 0, 0, 0, 1, 0, 2);
check_for_winner(game, 1, 0, 1, 1, 1, 2);
check_for_winner(game, 2, 0, 2, 1, 2, 2);

// Check all columns
check_for_winner(game, 0, 0, 1, 0, 2, 0);
check_for_winner(game, 0, 1, 1, 1, 2, 1);
check_for_winner(game, 0, 2, 1, 2, 2, 2);

// Check diagonals
check_for_winner(game, 0, 0, 1, 1, 2, 2);
check_for_winner(game, 2, 0, 1, 1, 0, 2);

// Check if we have a draw
if (game.game_status == IN_PROGRESS && game.cur_turn == 9) {
game.game_status = DRAW;
};
}

fun check_for_winner(game: &mut TicTacToe, row1: u64, col1: u64, row2: u64, col2: u64, row3: u64, col3: u64) {
if (game.game_status != IN_PROGRESS) {
return
};
let result = get_winner_if_all_equal(game, row1, col1, row2, col2, row3, col3);
if (result != MARK_EMPTY) {
game.game_status = if (result == MARK_X) X_WIN else O_WIN;
};
}

fun get_winner_if_all_equal(game: &TicTacToe, row1: u64, col1: u64, row2: u64, col2: u64, row3: u64, col3: u64): u8 {
let cell1 = get_cell(game, row1, col1);
let cell2 = get_cell(game, row2, col2);
let cell3 = get_cell(game, row3, col3);
if (cell1 == cell2 && cell1 == cell3) cell1 else MARK_EMPTY
}
}
93 changes: 93 additions & 0 deletions sui_programmability/examples/games/tests/TicTacToeV2Tests.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#[test_only]
module Games::TicTacToeV2Tests {
use Sui::TestScenario::{Self, Scenario};
use Games::TicTacToeV2::{Self, TicTacToe, Trophy};

const SEND_MARK_FAILED: u64 = 0;
const UNEXPECTED_WINNER: u64 = 1;
const MARK_PLACEMENT_FAILED: u64 = 2;
const IN_PROGRESS: u8 = 0;
const X_WIN: u8 = 1;

#[test]
fun play_tictactoe() {
let player_x = @0x0;
let player_o = @0x1;

// Anyone can create a game, because the game object will be eventually shared.
let scenario = &mut TestScenario::begin(&player_x);
TicTacToeV2::create_game(copy player_x, copy player_o, TestScenario::ctx(scenario));
// Player1 places an X in (1, 1).
place_mark(1, 1, &player_x, scenario);
/*
Current game board:
_|_|_
_|X|_
| |
*/

// Player2 places an O in (0, 0).
place_mark(0, 0, &player_o, scenario);
/*
Current game board:
O|_|_
_|X|_
| |
*/

// Player1 places an X in (0, 2).
place_mark(0, 2, &player_x, scenario);
/*
Current game board:
O|_|X
_|X|_
| |
*/

// Player2 places an O in (1, 0).
let status = place_mark(1, 0, &player_o, scenario);
assert!(status == IN_PROGRESS, 1);
/*
Current game board:
O|_|X
O|X|_
| |
*/

// Opportunity for Player1! Player1 places an X in (2, 0).
status = place_mark(2, 0, &player_x, scenario);
/*
Current game board:
O|_|X
O|X|_
X| |
*/

// Check that X has won!
assert!(status == X_WIN, 2);
TestScenario::next_tx(scenario, &player_x);
{
let trophy = TestScenario::remove_object<Trophy>(scenario);
TestScenario::return_object(scenario, trophy)
}
}

fun place_mark(
row: u8,
col: u8,
player: &address,
scenario: &mut Scenario,
): u8 {
// The gameboard is now a shared mutable object.
// Any player can place a mark on it directly.
TestScenario::next_tx(scenario, player);
let status;
{
let game = TestScenario::remove_object<TicTacToe>(scenario);
TicTacToeV2::place_mark(&mut game, row, col, TestScenario::ctx(scenario));
status = TicTacToeV2::get_status(&game);
TestScenario::return_object(scenario, game);
};
status
}
}
15 changes: 13 additions & 2 deletions sui_programmability/framework/sources/Transfer.move
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module Sui::Transfer {

/// Represents a reference to a child object, whose type is T.
/// This is used to track ownership between objects.
/// Whenver an object is transferred to another object (and hence owned by object),
/// Whenever an object is transferred to another object (and hence owned by object),
/// a ChildRef is created. A ChildRef cannot be dropped. When a child object is
/// transferred to a new parent object, the original ChildRef is dropped but a new
/// one will be created. The only way to fully destroy a ChildRef is to transfer the
Expand Down Expand Up @@ -78,8 +78,19 @@ module Sui::Transfer {
}

/// Freeze `obj`. After freezing `obj` becomes immutable and can no
/// longer be transfered or mutated.
/// longer be transferred or mutated.
public native fun freeze_object<T: key>(obj: T);

/// Turn the given object into a mutable shared object that everyone
/// can access and mutate. This is irreversible, i.e. once an object
/// is shared, it will stay shared forever.
/// Shared mutable object is not yet fully supported in Sui, which is being
/// actively worked on and should be supported very soon.
/// https://github.com/MystenLabs/sui/issues/633
/// https://github.com/MystenLabs/sui/issues/681
/// This API is exposed to demonstrate how we may be able to use it to program
/// Move contracts that use shared mutable objects.
public native fun share_object<T: key>(obj: T);

native fun transfer_internal<T: key>(obj: T, recipient: address, to_object: bool);
}
2 changes: 2 additions & 0 deletions sui_programmability/framework/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub enum EventType {
TransferToObject,
/// System event: freeze object
FreezeObject,
/// System event: turn an object into a shared mutable object
ShareObject,
/// System event: an object ID is deleted. This does not necessarily
/// mean an object is being deleted. However whenever an object is being
/// deleted, the object ID must be deleted and this event will be
Expand Down
1 change: 1 addition & 0 deletions sui_programmability/framework/src/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub fn all_natives(
),
("Transfer", "transfer_internal", transfer::transfer_internal),
("Transfer", "freeze_object", transfer::freeze_object),
("Transfer", "share_object", transfer::share_object),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we allow shared objects to be transferred?

("TxContext", "fresh_id", tx_context::fresh_id),
(
"TxContext",
Expand Down
Loading