Skip to content

Commit

Permalink
Support immutable objects
Browse files Browse the repository at this point in the history
  • Loading branch information
lxfind committed Jan 11, 2022
1 parent bf5a732 commit d239626
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 91 deletions.
5 changes: 2 additions & 3 deletions fastpay_core/src/unit_tests/authority_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,10 +409,9 @@ async fn test_handle_move_order() {
MAX_GAS,
&sender_key,
);

// 36 is for bytecode execution, 24 is for object creation.
// 34 is for bytecode execution, 24 is for object creation.
// If the number changes, we want to verify that the change is intended.
let gas_cost = 36 + 24;
let gas_cost = 34 + 24;
let res = send_and_confirm_order(&mut authority_state, order)
.await
.unwrap();
Expand Down
57 changes: 26 additions & 31 deletions fastx_programmability/adapter/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,6 @@ pub fn generate_module_ids(
Ok(rewriter.into_inner())
}

/// Check if this is a special event type emitted when there is a transfer between fastX addresses
pub fn is_transfer_event(e: &Event) -> bool {
// TODO: hack that leverages implementation of Transfer::transfer_internal native function
!e.0.is_empty()
}

type Event = (Vec<u8>, u64, TypeTag, Vec<u8>);

/// Update `state_view` with the effects of successfully executing a transaction:
Expand Down Expand Up @@ -244,37 +238,38 @@ fn process_successful_execution<
state_view.write_object(obj);
}
// process events to identify transfers, freezes
// TODO(https://github.com/MystenLabs/fastnft/issues/96): implement freeze and immutable objects
for e in events {
if is_transfer_event(&e) {
let (recipient_bytes, _seq_num, type_, event_bytes) = e;
match type_ {
TypeTag::Struct(s_type) => {
// special transfer event. process by saving object under given authenticator
let contents = event_bytes;
// unwrap safe due to size enforcement in Move code for `Authenticator`
let recipient = FastPayAddress::try_from(recipient_bytes.borrow()).unwrap();
let mut move_obj = MoveObject::new(s_type, contents);
let _old_object = by_value_objects.remove(&move_obj.id());
let (recipient, should_freeze, type_, event_bytes) = e;
debug_assert!(!recipient.is_empty() && should_freeze < 2);
match type_ {
TypeTag::Struct(s_type) => {
let contents = event_bytes;
let should_freeze = should_freeze != 0;
// unwrap safe due to size enforcement in Move code for `Authenticator
let recipient = FastPayAddress::try_from(recipient.borrow()).unwrap();
let mut move_obj = MoveObject::new(s_type, contents);
let old_object = by_value_objects.remove(&move_obj.id());

#[cfg(debug_assertions)]
{
check_transferred_object_invariants(&move_obj, &_old_object)
}
#[cfg(debug_assertions)]
{
check_transferred_object_invariants(&move_obj, &old_object)
}

// increment the object version. note that if the transferred object was
// freshly created, this means that its version will now be 1.
// thus, all objects in the global object pool have version > 0
move_obj.increment_version()?;
let obj = Object::new_move(move_obj, recipient, ctx.digest());
// increment the object version. note that if the transferred object was
// freshly created, this means that its version will now be 1.
// thus, all objects in the global object pool have version > 0
move_obj.increment_version()?;
if should_freeze {
move_obj.freeze();
}
let obj = Object::new_move(move_obj, recipient, ctx.digest());
if old_object.is_none() {
// Charge extra gas based on object size if we are creating a new object.
gas_used += calculate_object_creation_cost(&obj);
state_view.write_object(obj);
}
_ => unreachable!("Only structs can be transferred"),
state_view.write_object(obj);
}
} else {
// the fastX framework doesn't support user-generated events yet, so shouldn't hit this
unimplemented!("Processing user events")
_ => unreachable!("Only structs can be transferred"),
}
}
if gas_used > gas_budget {
Expand Down
68 changes: 66 additions & 2 deletions fastx_programmability/adapter/src/unit_tests/adapter_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ fn test_object_basics() {
let native_functions = genesis.native_functions.clone();
let mut storage = InMemoryStorage::new(genesis.objects.clone());

// 0. Create a gas object for gas payment. Note that we won't really use it because we won't be providing a gas budget.
// 0. Create a gas object for gas payment.
let gas_object = Object::with_id_owner_for_testing(
ObjectID::random(),
base_types::FastPayAddress::default(),
Expand Down Expand Up @@ -425,7 +425,7 @@ fn test_move_call_insufficient_gas() {
&native_functions,
"create",
gas_object,
25, // This budget is not enough to execute all bytecode.
20, // This budget is not enough to execute all bytecode.
Vec::new(),
pure_args,
);
Expand Down Expand Up @@ -470,4 +470,68 @@ fn test_publish_module_insufficient_gas() {
.contains("Gas balance is 30, not enough to pay 58"));
}

#[test]
fn test_transfer_and_freeze() {
let addr1 = base_types::get_key_pair().0;
let addr2 = base_types::get_key_pair().0;

let genesis = genesis::GENESIS.lock().unwrap();
let native_functions = genesis.native_functions.clone();
let mut storage = InMemoryStorage::new(genesis.objects.clone());

// 0. Create a gas object for gas payment.
let gas_object = Object::with_id_owner_for_testing(
ObjectID::random(),
base_types::FastPayAddress::default(),
);
storage.write_object(gas_object.clone());
storage.flush();

// 1. Create obj1 owned by addr1
// ObjectBasics::create expects integer value and recipient address
let pure_args = vec![
10u64.to_le_bytes().to_vec(),
bcs::to_bytes(&addr1.to_vec()).unwrap(),
];
call(
&mut storage,
&native_functions,
"create",
gas_object.clone(),
MAX_GAS,
Vec::new(),
pure_args,
)
.unwrap();

let created = storage.created();
let id1 = created
.keys()
.cloned()
.collect::<Vec<ObjectID>>()
.pop()
.unwrap();
storage.flush();
let obj1 = storage.read_object(&id1).unwrap();
assert!(!obj1.is_read_only());

// 2. Call transfer_and_freeze.
let pure_args = vec![bcs::to_bytes(&addr2.to_vec()).unwrap()];
call(
&mut storage,
&native_functions,
"transfer_and_freeze",
gas_object,
MAX_GAS,
vec![obj1],
pure_args,
)
.unwrap();
assert_eq!(storage.updated().len(), 2);
storage.flush();
let obj1 = storage.read_object(&id1).unwrap();
assert!(obj1.is_read_only());
assert_eq!(obj1.owner, addr2);
}

// TODO(https://github.com/MystenLabs/fastnft/issues/92): tests that exercise all the error codes of the adapter
23 changes: 0 additions & 23 deletions fastx_programmability/framework/sources/Immutable.move

This file was deleted.

5 changes: 4 additions & 1 deletion fastx_programmability/framework/sources/ObjectBasics.move
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ module FastX::ObjectBasics {
Transfer::transfer(o, Address::new(recipient))
}

public fun transfer_and_freeze(o: Object, recipient: vector<u8>, _ctx: &mut TxContext) {
Transfer::transfer_and_freeze(o, Address::new(recipient))
}

// test that reading o2 and updating o1 works
public fun update(o1: &mut Object, o2: &Object, _ctx: &mut TxContext) {
o1.value = o2.value
Expand All @@ -43,5 +47,4 @@ module FastX::ObjectBasics {
let Wrapper { id: _, o } = w;
Transfer::transfer(o, TxContext::get_signer_address(ctx))
}

}
17 changes: 12 additions & 5 deletions fastx_programmability/framework/sources/Transfer.move
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ module FastX::Transfer {
/// Transfer ownership of `obj` to `recipient`. `obj` must have the
/// `key` attribute, which (in turn) ensures that `obj` has a globally
/// unique ID.
// TODO: add bytecode verifier pass to ensure that `T` is a struct declared
// in the calling module. This will allow modules to define custom transfer
// logic for their structs that cannot be subverted by other modules
public fun transfer<T: key>(obj: T, recipient: Address) {
// TODO: emit event
transfer_internal(obj, Address::into_bytes(recipient))
transfer_internal(obj, Address::into_bytes(recipient), false)
}

native fun transfer_internal<T: key>(obj: T, recipient: vector<u8>);
/// Transfer ownership of `obj` to `recipient` and then freeze
/// `obj`. After freezing `obj` becomes immutable and can no
/// longer be transfered or mutated.
/// If you just want to freeze an object, you can set the `recipient`
/// to the current owner of `obj` and it will only be frozen without
/// being transfered.
public fun transfer_and_freeze<T: key>(obj: T, recipient: Address) {
transfer_internal(obj, Address::into_bytes(recipient), true)
}

native fun transfer_internal<T: key>(obj: T, recipient: vector<u8>, should_freeze: bool);

/*/// Transfer ownership of `obj` to another object `id`. Afterward, `obj`
/// can only be used in a transaction that also includes the object with
Expand Down
18 changes: 8 additions & 10 deletions fastx_programmability/framework/src/natives/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

use move_binary_format::errors::PartialVMResult;
use move_core_types::gas_schedule::GasAlgebra;
use move_vm_runtime::native_functions::NativeContext;
use move_vm_types::{
gas_schedule::NativeCostIndex,
Expand All @@ -27,20 +26,19 @@ pub fn transfer_internal(
mut args: VecDeque<Value>,
) -> PartialVMResult<NativeResult> {
debug_assert!(ty_args.len() == 1);
debug_assert!(args.len() == 2);
debug_assert!(args.len() == 3);

let ty = ty_args.pop().unwrap();
let should_freeze = pop_arg!(args, bool);
let recipient = pop_arg!(args, Vec<u8>);
let transferred_obj = args.pop_back().unwrap();

// Charge by size of transferred object
let cost = native_gas(
context.cost_table(),
NativeCostIndex::EMIT_EVENT,
transferred_obj.size().get() as usize,
);
let seq_num = 0;
if !context.save_event(recipient, seq_num, ty, transferred_obj)? {
// Charge a constant native gas cost here, since
// we will charge it properly when processing
// all the events in adapter.
// TODO: adjust native_gas cost size base.
let cost = native_gas(context.cost_table(), NativeCostIndex::EMIT_EVENT, 1);
if !context.save_event(recipient, should_freeze as u64, ty, transferred_obj)? {
return Ok(NativeResult::err(cost, 0));
}

Expand Down
4 changes: 2 additions & 2 deletions fastx_types/src/base_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ pub struct TransactionDigest([u8; TRANSACTION_DIGEST_LENGTH]);
pub struct ObjectDigest([u8; 32]); // We use SHA3-256 hence 32 bytes here

// TODO: migrate TxContext type + these constants to a separate file
/// 0x242C70E260BADD483440B4E3DAD63E9D
/// 0x81D51F48E5DFC02DBC8F6003517274F7
pub const TX_CONTEXT_ADDRESS: AccountAddress = AccountAddress::new([
0x24, 0x2C, 0x70, 0xE2, 0x60, 0xBA, 0xDD, 0x48, 0x34, 0x40, 0xB4, 0xE3, 0xDA, 0xD6, 0x3E, 0x9D,
0x81, 0xD5, 0x1F, 0x48, 0xE5, 0xDF, 0xC0, 0x2D, 0xBC, 0x8F, 0x60, 0x03, 0x51, 0x72, 0x74, 0xF7,
]);
pub const TX_CONTEXT_MODULE_NAME: &IdentStr = ident_str!("TxContext");
pub const TX_CONTEXT_STRUCT_NAME: &IdentStr = TX_CONTEXT_MODULE_NAME;
Expand Down
2 changes: 2 additions & 0 deletions fastx_types/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ pub enum FastPayError {
TypeError { error: String },
#[error("Execution aborted: {error:?}.")]
AbortedExecution { error: String },
#[error("Invalid move event: {error:?}.")]
InvalidMoveEvent { error: String },

// Gas related errors
#[error("Gas budget set higher than max: {error:?}.")]
Expand Down
34 changes: 20 additions & 14 deletions fastx_types/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
pub struct MoveObject {
pub type_: StructTag,
contents: Vec<u8>,
read_only: bool,
}

/// Byte encoding of a 64 byte unsigned integer in BCS
Expand All @@ -31,7 +32,11 @@ const VERSION_END_INDEX: usize = 24;

impl MoveObject {
pub fn new(type_: StructTag, contents: Vec<u8>) -> Self {
Self { type_, contents }
Self {
type_,
contents,
read_only: false,
}
}

pub fn id(&self) -> ObjectID {
Expand Down Expand Up @@ -100,6 +105,14 @@ impl MoveObject {
pub fn into_contents(self) -> Vec<u8> {
self.contents
}

pub fn is_read_only(&self) -> bool {
self.read_only
}

pub fn freeze(&mut self) {
self.read_only = true;
}
}

#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize)]
Expand All @@ -113,14 +126,6 @@ pub enum Data {
}

impl Data {
pub fn is_read_only(&self) -> bool {
use Data::*;
match self {
Move(_) => false,
Module { .. } => true,
}
}

pub fn try_as_move(&self) -> Option<&MoveObject> {
use Data::*;
match self {
Expand Down Expand Up @@ -195,7 +200,10 @@ impl Object {
}

pub fn is_read_only(&self) -> bool {
self.data.is_read_only()
match &self.data {
Data::Move(m) => m.is_read_only(),
Data::Module(_) => true,
}
}

pub fn to_object_reference(&self) -> ObjectRef {
Expand Down Expand Up @@ -234,10 +242,7 @@ impl Object {
/// Change the owner of `self` to `new_owner`
pub fn transfer(&mut self, new_owner: FastPayAddress) -> Result<(), FastPayError> {
// TODO: these should be raised FastPayError's instead of panic's
assert!(
!self.data.is_read_only(),
"Cannot transfer an immutable object"
);
assert!(!self.is_read_only(), "Cannot transfer an immutable object");
match &mut self.data {
Data::Move(m) => {
assert!(
Expand All @@ -262,6 +267,7 @@ impl Object {
let data = Data::Move(MoveObject {
type_: GasCoin::type_(),
contents: GasCoin::new(id, version, gas).to_bcs_bytes(),
read_only: false,
});
Self {
owner,
Expand Down

0 comments on commit d239626

Please sign in to comment.