diff --git a/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_dof.exp b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_dof.exp new file mode 100644 index 0000000000000..7d7f4e25ec137 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_dof.exp @@ -0,0 +1,112 @@ +processed 24 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-61: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 10548800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 63-63: +created: object(2,0), object(2,1), object(2,2), object(2,3) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7273200, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3 'view-object'. lines 65-65: +Owner: Object ID: ( fake(2,2) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,1)}} + +task 4 'view-object'. lines 67-67: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 5 'view-object'. lines 69-69: +Owner: Account Address ( fake(2,3) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 6 'view-object'. lines 71-71: +Owner: Account Address ( A ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 0u64} + +task 7 'run'. lines 73-73: +created: object(7,0) +mutated: object(0,0), object(2,2), object(2,3) +gas summary: computation_cost: 1000000, storage_cost: 5996400, storage_rebate: 3506184, non_refundable_storage_fee: 35416 + +task 8 'view-object'. lines 75-77: +Owner: Object ID: ( fake(2,2) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,1)}} + +task 9 'view-object'. lines 78-78: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 10 'view-object'. lines 80-80: +Owner: Object ID: ( fake(7,0) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 11 'view-object'. lines 82-82: +Owner: Account Address ( A ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 0u64} + +task 12 'programmable'. lines 84-85: +mutated: object(0,0), object(2,1), object(2,2), object(2,3) +gas summary: computation_cost: 1000000, storage_cost: 4818400, storage_rebate: 4770216, non_refundable_storage_fee: 48184 + +task 13 'view-object'. lines 87-89: +Owner: Object ID: ( fake(2,2) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,1)}} + +task 14 'view-object'. lines 90-90: +Owner: Object ID: ( fake(2,0) ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 3u64} + +task 15 'view-object'. lines 92-92: +Owner: Object ID: ( fake(7,0) ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 2u64} + +task 16 'view-object'. lines 94-94: +Owner: Account Address ( A ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 1u64} + +task 17 'programmable'. lines 96-99: +mutated: object(0,0), object(2,3) +deleted: object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 2264800, storage_rebate: 5936436, non_refundable_storage_fee: 59964 + +task 18 'programmable'. lines 101-102: +mutated: object(_), object(2,3) +gas summary: computation_cost: 500000, storage_cost: 2264800, storage_rebate: 1264032, non_refundable_storage_fee: 12768 + +task 19 'programmable'. lines 104-105: +mutated: object(_), object(2,3) +gas summary: computation_cost: 500000, storage_cost: 2264800, storage_rebate: 1264032, non_refundable_storage_fee: 12768 + +task 20 'programmable'. lines 107-110: +mutated: object(_), object(2,3) +gas summary: computation_cost: 500000, storage_cost: 2264800, storage_rebate: 1264032, non_refundable_storage_fee: 12768 + +task 21 'programmable'. lines 112-113: +Error: Transaction Effects Status: MoveAbort(MoveLocation { module: ModuleId { address: tto, name: Identifier("M1") }, function: 4, instruction: 10, function_name: Some("check") }, 0) in command 0 +Execution Error: MoveAbort(MoveLocation { module: ModuleId { address: tto, name: Identifier("M1") }, function: 4, instruction: 10, function_name: Some("check") }, 0) in command 0 + +task 22 'programmable'. lines 115-116: +Error: Transaction Effects Status: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("dynamic_field") }, function: 11, instruction: 0, function_name: Some("borrow_child_object") }, 1) in command 0 +Execution Error: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("dynamic_field") }, function: 11, instruction: 0, function_name: Some("borrow_child_object") }, 1) in command 0 + +task 23 'programmable'. lines 118-119: +Error: Transaction Effects Status: MoveAbort(MoveLocation { module: ModuleId { address: tto, name: Identifier("M1") }, function: 4, instruction: 10, function_name: Some("check") }, 0) in command 0 +Execution Error: MoveAbort(MoveLocation { module: ModuleId { address: tto, name: Identifier("M1") }, function: 4, instruction: 10, function_name: Some("check") }, 0) in command 0 diff --git a/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_dof.move b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_dof.move new file mode 100644 index 0000000000000..5b2ee39eb2a02 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_dof.move @@ -0,0 +1,119 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use std::option::{Self, Option}; + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_object_field as dof; + + const KEY: u64 = 0; + + struct A has key, store { + id: UID, + value: u64, + } + + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx), value: 0 }; + let a_address = object::id_address(&a); + let b = A { id: object::new(ctx), value: 0 }; + dof::add(&mut b.id, KEY, A { id: object::new(ctx), value: 0 }); + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receive(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + dof::add(&mut parent.id, KEY, b); + } + + public fun set(grand: &mut A, v1: u64, v2: u64, v3: u64) { + grand.value = v1; + let parent: &mut A = dof::borrow_mut(&mut grand.id, KEY); + parent.value = v2; + let child: &mut A = dof::borrow_mut(&mut parent.id, KEY); + child.value = v3; + } + + public fun remove(grand: &mut A) { + let parent: &mut A = dof::borrow_mut(&mut grand.id, KEY); + let A { id, value: _ } = dof::remove(&mut parent.id, KEY); + object::delete(id); + } + + public fun check(grand: &A, v1: u64, v2: u64, v3: Option) { + assert!(grand.value == v1, 0); + let parent: &A = dof::borrow(&grand.id, KEY); + assert!(parent.value == v2, 0); + if (option::is_some(&v3)) { + let child: &A = dof::borrow(&parent.id, KEY); + assert!(&child.value == option::borrow(&v3), 0); + } else { + assert!(!dof::exists_(&parent.id, KEY), 0); + } + } +} + +//# run tto::M1::start --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# run tto::M1::receive --args object(2,3) receiving(2,2) --sender A + +//# view-object 2,0 + +// The grand parent +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# programmable --sender A --inputs object(2,3) 1 2 3 +//> tto::M1::set(Input(0), Input(1), Input(2), Input(3)) + +//# view-object 2,0 + +// The grand parent +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# programmable --sender A --inputs object(2,3) +//> tto::M1::remove(Input(0)) + +// dev-inspect with 'check' and correct values + +//# programmable --sender A --inputs object(2,3)@3 0 0 vector[0] --dev-inspect +//> tto::M1::check(Input(0), Input(1), Input(2), Input(3)) + +//# programmable --sender A --inputs object(2,3)@4 1 2 vector[3] --dev-inspect +//> tto::M1::check(Input(0), Input(1), Input(2), Input(3)) + +//# programmable --sender A --inputs object(2,3)@5 1 2 vector[] --dev-inspect +//> tto::M1::check(Input(0), Input(1), Input(2), Input(3)) + +// dev-inspect with 'check' and _incorrect_ values + +//# programmable --sender A --inputs object(2,3)@4 0 0 vector[0] --dev-inspect +//> tto::M1::check(Input(0), Input(1), Input(2), Input(3)) + +//# programmable --sender A --inputs object(2,3)@5 1 2 vector[3] --dev-inspect +//> tto::M1::check(Input(0), Input(1), Input(2), Input(3)) + +//# programmable --sender A --inputs object(2,3)@3 1 2 vector[] --dev-inspect +//> tto::M1::check(Input(0), Input(1), Input(2), Input(3)) diff --git a/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_split_changes_dof.exp b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_split_changes_dof.exp new file mode 100644 index 0000000000000..48f6fa17b7fa0 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_split_changes_dof.exp @@ -0,0 +1,83 @@ +processed 17 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-54: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 9606400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 56-56: +created: object(2,0), object(2,1), object(2,2), object(2,3), object(2,4), object(2,5) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 11004800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3 'view-object'. lines 58-58: +Owner: Object ID: ( fake(2,5) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,2)}} + +task 4 'view-object'. lines 60-60: +Owner: Object ID: ( fake(2,3) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,4)}} + +task 5 'view-object'. lines 62-62: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 6 'view-object'. lines 64-64: +Owner: Account Address ( A ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 0u64} + +task 7 'view-object'. lines 66-66: +Owner: Object ID: ( fake(2,1) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,4)}}, value: 0u64} + +task 8 'view-object'. lines 68-68: +Owner: Account Address ( A ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,5)}}, value: 0u64} + +task 9 'run'. lines 70-70: +mutated: object(0,0), object(2,3), object(2,4), object(2,5) +gas summary: computation_cost: 1000000, storage_cost: 4818400, storage_rebate: 4770216, non_refundable_storage_fee: 48184 + +task 10 'view-object'. lines 72-72: +Owner: Object ID: ( fake(2,5) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,2)}} + +task 11 'view-object'. lines 74-74: +Owner: Object ID: ( fake(2,3) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,4)}} + +task 12 'view-object'. lines 76-76: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 13 'view-object'. lines 78-78: +Owner: Account Address ( fake(2,5) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 40u64} + +task 14 'view-object'. lines 80-80: +Owner: Object ID: ( fake(2,1) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,4)}}, value: 40u64} + +task 15 'view-object'. lines 82-82: +Owner: Account Address ( A ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,5)}}, value: 0u64} + +task 16 'run'. lines 84-84: +created: object(16,0) +mutated: object(0,0), object(2,3), object(2,5) +gas summary: computation_cost: 1000000, storage_cost: 5996400, storage_rebate: 3506184, non_refundable_storage_fee: 35416 diff --git a/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_split_changes_dof.move b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_split_changes_dof.move new file mode 100644 index 0000000000000..5c1b690430f7f --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/mvcc/receive_object_split_changes_dof.move @@ -0,0 +1,84 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + // use std::option::{Self, Option}; + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_object_field as dof; + + const KEY: u64 = 0; + const BKEY: u64 = 1; + + struct A has key, store { + id: UID, + value: u64, + } + + public fun start(ctx: &mut TxContext) { + let a_parent = A { id: object::new(ctx), value: 0 }; + let a_child = A { id: object::new(ctx), value: 0 }; + + let b_parent = A { id: object::new(ctx), value: 0 }; + let b_child = A { id: object::new(ctx), value: 0 }; + dof::add(&mut a_parent.id, KEY, a_child); + dof::add(&mut b_parent.id, KEY, b_child); + transfer::public_transfer(a_parent, tx_context::sender(ctx)); + transfer::public_transfer(b_parent, tx_context::sender(ctx)); + } + + public entry fun receive(a_parent: &mut A, x: Receiving,apv: u64, acv: u64, bpv: u64, bcv: u64) { + let b_parent = transfer::receive(&mut a_parent.id, x); + dof::add(&mut a_parent.id, BKEY, b_parent); + let b_parent: &A = dof::borrow(&a_parent.id, BKEY); + let b_child: &A = dof::borrow(&b_parent.id, KEY); + let a_child: &A = dof::borrow(&a_parent.id, KEY); + assert!(a_parent.value == apv, 0); + assert!(a_child.value == acv, 1); + assert!(b_parent.value == bpv, 2); + assert!(b_child.value == bcv, 3); + } + + public entry fun mutate(b_parent: A, a_parent: &A) { + let b_child: &mut A = dof::borrow_mut(&mut b_parent.id, KEY); + b_parent.value = 40; + b_child.value = 40; + let a_address = object::id_address(a_parent); + transfer::public_transfer(b_parent, a_address); + } + +} + +//# run tto::M1::start --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# view-object 2,4 + +//# view-object 2,5 + +//# run tto::M1::mutate --args object(2,3) object(2,5) --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# view-object 2,4 + +//# view-object 2,5 + +//# run tto::M1::receive --args object(2,5) receiving(2,3) 0 0 40 40 --sender A diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/basic_receive.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/basic_receive.exp new file mode 100644 index 0000000000000..0153880414069 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/basic_receive.exp @@ -0,0 +1,39 @@ +processed 9 tasks + +task 1 'publish'. lines 6-32: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6923600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 34-34: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 36-36: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 38-40: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 41-41: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 6 'view-object'. lines 43-43: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 7 'view-object'. lines 45-47: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 8 'run'. lines 48-48: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/basic_receive.move b/crates/sui-adapter-transactional-tests/tests/receive_object/basic_receive.move new file mode 100644 index 0000000000000..d165fd0e517a5 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/basic_receive.move @@ -0,0 +1,48 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Can receive the object +//# run tto::M1::receiver --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +// Cannot receive the object again at the old version +//# run tto::M1::receiver --args object(2,0) receiving(2,1)@3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/drop_receiving.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/drop_receiving.exp new file mode 100644 index 0000000000000..3e2ba9f3edb78 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/drop_receiving.exp @@ -0,0 +1,23 @@ +processed 6 tasks + +task 1 'publish'. lines 6-36: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7144000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 38-38: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'programmable'. lines 40-43: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 4 'programmable'. lines 44-48: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 + +task 5 'programmable'. lines 49-50: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/drop_receiving.move b/crates/sui-adapter-transactional-tests/tests/receive_object/drop_receiving.move new file mode 100644 index 0000000000000..d9dd3af6deab3 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/drop_receiving.move @@ -0,0 +1,50 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } + + public entry fun nop(_parent: &mut A) { } + public entry fun nop_with_receiver(_parent: &mut A, _x: Receiving) { } +} + +//# run tto::M1::start + +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::send_back(Input(0), Input(1)) + +// Include the receiving argument, but don't use it at the PTB level +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::nop(Input(0)) + +// Include the receiving argument, but don't use it at the Move level. The +// receiving object should not be mutated by this. +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::nop_with_receiver(Input(0), Input(1)) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/duplicate_receive_argument.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/duplicate_receive_argument.exp new file mode 100644 index 0000000000000..9f37f4a8a751a --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/duplicate_receive_argument.exp @@ -0,0 +1,21 @@ +processed 6 tasks + +task 1 'publish'. lines 6-33: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6756400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 35-35: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'programmable'. lines 37-40: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 4 'programmable'. lines 41-44: +Error: Error checking transaction input objects: DuplicateObjectRefInput + +task 5 'programmable'. lines 45-46: +Error: Error checking transaction input objects: IncorrectUserSignature { error: "Object 0x6b7c4000d7f46a0cf15aeab3134157cbdecc6d25aa828d74c8bd36f91314a17e is owned by account address 0xacc3895100e62eddf620872c5c762488afac12c37c4e1141ca0a88052738a9e1, but given owner/signer address is 0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/duplicate_receive_argument.move b/crates/sui-adapter-transactional-tests/tests/receive_object/duplicate_receive_argument.move new file mode 100644 index 0000000000000..e12bbb883a184 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/duplicate_receive_argument.move @@ -0,0 +1,46 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } +} + +//# run tto::M1::start + +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::send_back(Input(0), Input(1)) + +// Duplicate object ref in input +//# programmable --inputs object(2,0) receiving(2,1) receiving(2,1) +//> tto::M1::send_back(Input(0), Input(1)) + +// Invalid signature for the receiving object since we try to use it as a normal input +//# programmable --inputs object(2,1) receiving(2,1) +//> tto::M1::send_back(Input(0), Input(1)) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_immut_then_reuse.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_immut_then_reuse.exp new file mode 100644 index 0000000000000..32a319256ac81 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_immut_then_reuse.exp @@ -0,0 +1,25 @@ +processed 6 tasks + +task 1 'publish'. lines 6-34: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7174400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 36-36: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 38-38: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 40-42: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'programmable'. lines 43-45: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_immut_then_reuse.move b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_immut_then_reuse.move new file mode 100644 index 0000000000000..c44c52a48bb48 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_immut_then_reuse.move @@ -0,0 +1,46 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun pass_through(_x: &Receiving) { } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Pass through the receiving object by immut ref, and then try to reuse the input receiving object argument -- should succeed. +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::pass_through(Input(1)); +//> tto::M1::receiver(Input(0), Input(1)); + diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_mut_then_reuse.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_mut_then_reuse.exp new file mode 100644 index 0000000000000..32a319256ac81 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_mut_then_reuse.exp @@ -0,0 +1,25 @@ +processed 6 tasks + +task 1 'publish'. lines 6-34: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7174400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 36-36: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 38-38: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 40-42: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'programmable'. lines 43-45: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_mut_then_reuse.move b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_mut_then_reuse.move new file mode 100644 index 0000000000000..287e811f56b07 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_receiver_mut_then_reuse.move @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun pass_through(_x: &mut Receiving) { } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Pass through the receiving object by mut ref, and then try to reuse the input receiving object argument -- should succeed. +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::pass_through(Input(1)); +//> tto::M1::receiver(Input(0), Input(1)); + + diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/pass_through_then_receive.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_through_then_receive.exp new file mode 100644 index 0000000000000..e16b665a7ac36 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_through_then_receive.exp @@ -0,0 +1,25 @@ +processed 6 tasks + +task 1 'publish'. lines 6-34: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7182000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 36-36: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 38-38: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 40-43: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'programmable'. lines 44-46: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/pass_through_then_receive.move b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_through_then_receive.move new file mode 100644 index 0000000000000..efcce4e2506e8 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/pass_through_then_receive.move @@ -0,0 +1,46 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun pass_through(x: Receiving): Receiving { x } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Pass a receiving object through a move function, and then use the returned +// Receiving argument in a subsequent command. +//# programmable --inputs object(2,0) receiving(2,1) +//> 0: tto::M1::pass_through(Input(1)); +//> tto::M1::receiver(Input(0), Result(0)); diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_add_dof_and_mutate.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_add_dof_and_mutate.exp new file mode 100644 index 0000000000000..610c4fa266538 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_add_dof_and_mutate.exp @@ -0,0 +1,54 @@ +processed 11 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-38: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 7835600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 40-40: +created: object(2,0), object(2,1) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 3541600, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 42-42: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, value: 0u64} + +task 4 'view-object'. lines 44-44: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 5 'run'. lines 46-46: +created: object(5,0), object(5,1), object(5,2) +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 9728000, storage_rebate: 3506184, non_refundable_storage_fee: 35416 + +task 6 'view-object'. lines 48-48: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, value: 0u64} + +task 7 'view-object'. lines 50-50: +Owner: Object ID: ( fake(5,0) ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 100u64} + +task 8 'view-object'. lines 52-52: +Owner: Object ID: ( fake(2,0) ) +Version: 4 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(5,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,1)}} + +task 9 'view-object'. lines 54-54: +Owner: Object ID: ( fake(2,1) ) +Version: 4 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(5,1)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(5,2)}} + +task 10 'view-object'. lines 56-56: +Owner: Object ID: ( fake(5,1) ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(5,2)}}, value: 100u64} diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_add_dof_and_mutate.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_add_dof_and_mutate.move new file mode 100644 index 0000000000000..fd9c8221aa358 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_add_dof_and_mutate.move @@ -0,0 +1,56 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_object_field as dof; + + const KEY: u64 = 0; + + struct A has key, store { + id: UID, + value: u64, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx), value: 0 }; + let a_address = object::id_address(&a); + let b = A { id: object::new(ctx), value: 0 }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receive(parent: &mut A, x: Receiving, ctx: &mut TxContext) { + let b = transfer::receive(&mut parent.id, x); + let x = A { id: object::new(ctx), value: 0 }; + dof::add(&mut b.id, KEY, x); + dof::add(&mut parent.id, KEY, b); + let x: &mut A = dof::borrow_mut(&mut parent.id, KEY); + let y: &mut A = dof::borrow_mut(&mut x.id, KEY); + x.value = 100; + y.value = 100; + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::receive --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 5,0 + +//# view-object 5,1 + +//# view-object 5,2 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_deleted.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_deleted.exp new file mode 100644 index 0000000000000..135e2a9b66f81 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_deleted.exp @@ -0,0 +1,38 @@ +processed 9 tasks + +task 1 'publish'. lines 6-32: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6726000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 34-34: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 36-36: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 38-40: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 41-41: +mutated: object(0,0), object(2,0) +deleted: object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 6 'view-object'. lines 43-43: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 7 'view-object'. lines 45-47: +No object at id 2,1 + +task 8 'run'. lines 48-48: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_deleted.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_deleted.move new file mode 100644 index 0000000000000..94621b906d2bc --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_deleted.move @@ -0,0 +1,48 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun deleter(parent: &mut A, x: Receiving) { + let B { id } = transfer::receive(&mut parent.id, x); + object::delete(id); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Receive and delete the received object +//# run tto::M1::deleter --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +// Try and receive the same object again -- should fail +//# run tto::M1::deleter --args object(2,0) receiving(2,1)@3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_send_back.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_send_back.exp new file mode 100644 index 0000000000000..53cf09db57b13 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_send_back.exp @@ -0,0 +1,39 @@ +processed 9 tasks + +task 1 'publish'. lines 6-33: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6756400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 35-35: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 37-37: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 39-41: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 42-42: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 6 'view-object'. lines 44-44: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 7 'view-object'. lines 46-48: +Owner: Account Address ( fake(2,0) ) +Version: 4 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 8 'run'. lines 49-49: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_send_back.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_send_back.move new file mode 100644 index 0000000000000..bb41a9737eb36 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_send_back.move @@ -0,0 +1,49 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Receive the object and send it back to the object we received it from. +//# run tto::M1::send_back --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +// Make sure that we cannot receive it again at the old version number +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_wrap.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_wrap.exp new file mode 100644 index 0000000000000..4b438019a3cc2 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_wrap.exp @@ -0,0 +1,39 @@ +processed 9 tasks + +task 1 'publish'. lines 6-41: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7569600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 43-43: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 45-45: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 47-49: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 50-50: +created: object(5,0) +mutated: object(0,0), object(2,0) +wrapped: object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3708800, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 6 'view-object'. lines 52-52: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 7 'view-object'. lines 54-56: +No object at id 2,1 + +task 8 'run'. lines 57-57: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_wrap.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_wrap.move new file mode 100644 index 0000000000000..8ea505b1413cc --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_and_wrap.move @@ -0,0 +1,58 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct Wrapper has key, store { + id: UID, + elem: B + } + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun wrapper(parent: &mut A, x: Receiving, ctx: &mut TxContext) { + let b = transfer::receive(&mut parent.id, x); + let wrapper = Wrapper { + id: object::new(ctx), + elem: b + }; + transfer::public_transfer(wrapper, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Receive wrap and then transfer the wrapped object +//# run tto::M1::wrapper --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +// Try an receive at the old version -- should fail +//# run tto::M1::wrapper --args object(2,0) receiving(2,1)@3 + diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_ref.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_ref.exp new file mode 100644 index 0000000000000..bf42f1474b0f4 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_ref.exp @@ -0,0 +1,73 @@ +processed 18 tasks + +task 1 'publish'. lines 6-41: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 9872400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 43-43: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 45-45: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 47-47: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 49-49: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 + +task 6 'run'. lines 51-51: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 + +task 7 'run'. lines 53-53: +Error: Transaction Effects Status: Invalid public Move function signature. Unsupported return type for return value 0 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: InvalidPublicFunctionReturnType { idx: 0 }, source: None, command: Some(0) } } + +task 8 'run'. lines 55-55: +Error: Transaction Effects Status: Invalid public Move function signature. Unsupported return type for return value 0 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: InvalidPublicFunctionReturnType { idx: 0 }, source: None, command: Some(0) } } + +task 9 'programmable'. lines 57-58: +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10 'programmable'. lines 60-61: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 11 'programmable'. lines 63-64: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 12 'programmable'. lines 66-67: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 13 'programmable'. lines 69-70: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 14 'programmable'. lines 72-73: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 15 'programmable'. lines 75-76: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 16 'programmable'. lines 78-79: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } + +task 17 'programmable'. lines 81-82: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_ref.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_ref.move new file mode 100644 index 0000000000000..15ee828d0403e --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_ref.move @@ -0,0 +1,82 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public fun call_immut_ref(_parent: &mut A, _x: &Receiving) { } + public fun call_mut_ref(_parent: &mut A, _x: &mut Receiving) { } + public fun call_mut_ref_ret(_parent: &mut A, x: &mut Receiving): &mut Receiving { x } + public fun call_mut_ref_immut_ret(_parent: &mut A, x: &mut Receiving): &Receiving { x } + public fun immut_immut_ref(_x: &Receiving, _y: &Receiving) { } + public fun immut_mut_ref(_x: &Receiving, _y: &mut Receiving) { } + public fun mut_immut_ref(_x: &mut Receiving, _y: &Receiving) { } + public fun mut_mut_ref(_x: &mut Receiving, _y: &mut Receiving) { } + public fun take_mut_ref(_x: Receiving, _y: &mut Receiving) { } + public fun take_immut_ref(_x: Receiving, _y: &Receiving) { } + public fun immut_ref_take(_x: &Receiving, _y: Receiving) { } + public fun mut_ref_take(_x: &mut Receiving, _y: Receiving) { } + public fun double_take(_x: Receiving, _y: Receiving) { } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::call_mut_ref --args object(2,0) receiving(2,1) + +//# run tto::M1::call_immut_ref --args object(2,0) receiving(2,1) + +//# run tto::M1::call_mut_ref_ret --args object(2,0) receiving(2,1) + +//# run tto::M1::call_mut_ref_immut_ret --args object(2,0) receiving(2,1) + +//# programmable --inputs receiving(2,1) +//> tto::M1::immut_immut_ref(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::immut_mut_ref(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::mut_immut_ref(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::mut_mut_ref(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::take_mut_ref(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::take_immut_ref(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::immut_ref_take(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::mut_ref_take(Input(0), Input(0)) + +//# programmable --inputs receiving(2,1) +//> tto::M1::double_take(Input(0), Input(0)) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_value_flow_through.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_value_flow_through.exp new file mode 100644 index 0000000000000..ca9fc81c8ca16 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_value_flow_through.exp @@ -0,0 +1,29 @@ +processed 7 tasks + +task 1 'publish'. lines 6-30: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6437200, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 32-32: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 34-34: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 36-38: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 39-41: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 + +task 6 'run'. lines 42-42: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_value_flow_through.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_value_flow_through.move new file mode 100644 index 0000000000000..11fa69c05cc33 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_by_value_flow_through.move @@ -0,0 +1,42 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public fun flow(_parent: &mut A, x: Receiving): Receiving { x } + public fun drop(_parent: &mut A, _x: Receiving) { } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Flow through but don't use the receiving argument. 2,0 should be updated but 2,1 should not. +//# run tto::M1::flow --args object(2,0) receiving(2,1) + +// Drop the receiving argument. 2,0 should be updated but 2,1 should not. +//# run tto::M1::drop --args object(2,0) receiving(2,1) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_dof_and_mutate.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_dof_and_mutate.exp new file mode 100644 index 0000000000000..5c051c5a257ad --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_dof_and_mutate.exp @@ -0,0 +1,59 @@ +processed 12 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-36: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 7828000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 38-38: +created: object(2,0), object(2,1), object(2,2), object(2,3) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7273200, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3 'view-object'. lines 40-40: +Owner: Object ID: ( fake(2,2) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,3)}} + +task 4 'view-object'. lines 42-42: +Owner: Account Address ( A ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 5 'view-object'. lines 44-44: +Owner: Account Address ( fake(2,1) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 6 'view-object'. lines 46-46: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 0u64} + +task 7 'run'. lines 48-48: +created: object(7,0) +mutated: object(0,0), object(2,1), object(2,2) +gas summary: computation_cost: 1000000, storage_cost: 5996400, storage_rebate: 3506184, non_refundable_storage_fee: 35416 + +task 8 'view-object'. lines 50-50: +Owner: Object ID: ( fake(2,2) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,3)}} + +task 9 'view-object'. lines 52-52: +Owner: Account Address ( A ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 10 'view-object'. lines 54-54: +Owner: Object ID: ( fake(7,0) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 100u64} + +task 11 'view-object'. lines 56-56: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 0u64} diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_dof_and_mutate.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_dof_and_mutate.move new file mode 100644 index 0000000000000..55a023d59c4cc --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_dof_and_mutate.move @@ -0,0 +1,56 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_object_field as dof; + + const KEY: u64 = 0; + + struct A has key, store { + id: UID, + value: u64, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx), value: 0 }; + let a_address = object::id_address(&a); + let b = A { id: object::new(ctx), value: 0 }; + dof::add(&mut b.id, KEY, A { id: object::new(ctx), value: 0 }); + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receive(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + dof::add(&mut parent.id, KEY, b); + let _: &A = dof::borrow(&parent.id, KEY); + let x: &mut A = dof::borrow_mut(&mut parent.id, KEY); + x.value = 100; + } +} + +//# run tto::M1::start --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# run tto::M1::receive --args object(2,1) receiving(2,2) --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_param_ty.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_param_ty.exp new file mode 100644 index 0000000000000..3de80d6774c85 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_param_ty.exp @@ -0,0 +1,60 @@ +processed 15 tasks + +task 1 'publish'. lines 6-43: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 8519600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 45-45: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 47-47: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 49-49: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 51-51: +Error: Transaction Effects Status: Invalid command argument at 0. The type of the value does not match the expected type +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: TypeMismatch }, source: None, command: Some(0) } } + +task 6 'run'. lines 53-53: +Error: Transaction Effects Status: Invalid command argument at 0. The type of the value does not match the expected type +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: TypeMismatch }, source: None, command: Some(0) } } + +task 7 'run'. lines 55-55: +Error: Transaction Effects Status: Invalid command argument at 0. The type of the value does not match the expected type +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: TypeMismatch }, source: None, command: Some(0) } } + +task 8 'run'. lines 57-57: +Error: Transaction Effects Status: Invalid command argument at 0. The type of the value does not match the expected type +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: TypeMismatch }, source: None, command: Some(0) } } + +task 9 'run'. lines 59-59: +Error: Transaction Effects Status: Invalid command argument at 0. The type of the value does not match the expected type +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: TypeMismatch }, source: None, command: Some(0) } } + +task 10 'run'. lines 61-61: +Error: Error checking transaction input objects: IncorrectUserSignature { error: "Object 0xd38c9db0c1935b185ec59bacb9e6ce2e736579cedabb791fce9a8165cef6c319 is owned by account address 0xa0027b0c13d66e2958feb9c56af104ea20ddfcb46a3ea93b9ba5d3c2d9b94d42, but given owner/signer address is 0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" } + +task 11 'run'. lines 63-63: +Error: Transaction Effects Status: Invalid command argument at 0. The type of the value does not match the expected type +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: TypeMismatch }, source: None, command: Some(0) } } + +task 12 'run'. lines 65-65: +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13 'run'. lines 67-67: +Error: Transaction Effects Status: Invalid command argument at 0. The argument cannot be instantiated from raw bytes +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: InvalidUsageOfPureArg }, source: Some("Non-primitive argument at index 0. If it is an object, it must be populated by an object"), command: Some(0) } } + +task 14 'run'. lines 69-69: +Error: Transaction Effects Status: Invalid command argument at 0. The argument cannot be instantiated from raw bytes +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 0, kind: InvalidUsageOfPureArg }, source: Some("Non-primitive argument at index 0. If it is an object, it must be populated by an object"), command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_param_ty.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_param_ty.move new file mode 100644 index 0000000000000..9c93c35ff6c4f --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_param_ty.move @@ -0,0 +1,69 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID, ID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + struct Fake has drop { } + + struct FakeSameLayout has drop { + id: ID, + version: u64, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(_x: u64) { } + public fun receiver2(_x: Fake) { } + public fun receiver3(_x: &Fake) { } + + public fun receiver4(_x: FakeSameLayout) { } + public fun receiver5(_x: &FakeSameLayout) { } + + public fun receiver6(_x: Receiving) { } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::receiver --args receiving(2,1) + +//# run tto::M1::receiver2 --args receiving(2,1) + +//# run tto::M1::receiver3 --args receiving(2,1) + +//# run tto::M1::receiver4 --args receiving(2,1) + +//# run tto::M1::receiver5 --args receiving(2,1) + +//# run tto::M1::receiver6 --args object(2,1) + +//# run tto::M1::receiver6 --args object(2,0) + +//# run tto::M1::receiver6 --args receiving(2,0) + +//# run tto::M1::receiver6 --args 0 + +//# run tto::M1::receiver6 --args vector[0,0,0,0,0,0,0,0,0,0] diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_type.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_type.exp new file mode 100644 index 0000000000000..4f4a7306b4ad7 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_type.exp @@ -0,0 +1,25 @@ +processed 6 tasks + +task 1 'publish'. lines 6-32: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6923600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 34-34: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 36-36: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 38-40: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 41-41: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 2 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 2), source: Some(VMError { major_status: ABORTED, sub_status: Some(2), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_type.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_type.move new file mode 100644 index 0000000000000..38cb5d5d8031a --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_invalid_type.move @@ -0,0 +1,41 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// TYPE_MISMATCH of the receiving object +//# run tto::M1::receiver --args object(2,0) receiving(2,1) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_multiple_times_in_row.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_multiple_times_in_row.exp new file mode 100644 index 0000000000000..2ecbcddd577a1 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_multiple_times_in_row.exp @@ -0,0 +1,59 @@ +processed 13 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-38: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 7007200, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 40-40: +created: object(2,0), object(2,1) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'run'. lines 42-42: +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4 'view-object'. lines 44-44: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 5 'view-object'. lines 46-48: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 6 'run'. lines 49-49: +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 7 'view-object'. lines 51-51: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 8 'view-object'. lines 53-55: +Owner: Account Address ( fake(2,0) ) +Version: 4 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 9 'run'. lines 56-58: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } + +task 10 'run'. lines 59-61: +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 11 'run'. lines 62-64: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } + +task 12 'run'. lines 65-65: +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_multiple_times_in_row.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_multiple_times_in_row.move new file mode 100644 index 0000000000000..024118e57fc69 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_multiple_times_in_row.move @@ -0,0 +1,65 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public fun middle(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } +} + +//# run tto::M1::start + +//# run tto::M1::middle --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +// Receive the object and then send it +//# run tto::M1::send_back --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +// Can no longer receive that object at the previous version number +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@3 + +// Can receive the object at the new version number +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@4 + +// Cannot try and receive the object with an invalid owner even if it has the right type +//# run tto::M1::send_back --summarize --args object(3,0) receiving(2,1)@6 --sender A + +// Can still receive and send back so state is all good still, and version number hasn't been incremented for the receiving object due to the failed tx above. +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@6 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_object_owner.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_object_owner.exp new file mode 100644 index 0000000000000..4c3b59c4a78f7 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_object_owner.exp @@ -0,0 +1,35 @@ +processed 8 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-30: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 6634800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 32-32: +created: object(2,0), object(2,1), object(2,2) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 5996400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3 'view-object'. lines 34-34: +Owner: Object ID: ( fake(2,2) ) +Version: 2 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,1)}} + +task 4 'view-object'. lines 36-36: +Owner: Object ID: ( fake(2,0) ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 5 'view-object'. lines 38-40: +Owner: Account Address ( A ) +Version: 2 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 6 'run'. lines 41-43: +Error: Error checking transaction input objects: ObjectNotFound { object_id: 0xc8a6ab1286d28dc445a92bd8852fbbf41a97d17f0ff8fe74f44d5ca31bc5ac6e, version: Some(SequenceNumber(2)) } + +task 7 'run'. lines 44-44: +Error: Error checking transaction input objects: ObjectNotFound { object_id: 0xc8182c524f27fa3e1e79ef9f3e478d5b27637f94e5c7664eee82881e2381ed93, version: Some(SequenceNumber(2)) } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/receive_object_owner.move b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_object_owner.move new file mode 100644 index 0000000000000..b0ea7ffd334b2 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/receive_object_owner.move @@ -0,0 +1,44 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_object_field as dof; + + const KEY: u64 = 0; + + struct A has key, store { + id: UID, + value: u64, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx), value: 0 }; + dof::add(&mut a.id, KEY, A { id: object::new(ctx), value: 0 }); + transfer::public_transfer(a, tx_context::sender(ctx)); + } + + public entry fun receive(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + dof::add(&mut parent.id, KEY, b); + } +} + +//# run tto::M1::start --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +// Try to receive an object with an object +//# run tto::M1::receive --args object(2,2) receiving(2,1) --sender A + +// Try to receive another object with an object owner +//# run tto::M1::receive --args object(2,2) receiving(2,0) --sender A diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/basic_receive.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/basic_receive.exp new file mode 100644 index 0000000000000..9afb85550faaf --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/basic_receive.exp @@ -0,0 +1,39 @@ +processed 9 tasks + +task 1 'publish'. lines 6-32: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6969200, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 34-34: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 36-36: +Owner: Shared +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 38-38: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 40-40: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 6 'view-object'. lines 42-42: +Owner: Shared +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 7 'view-object'. lines 44-44: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 8 'run'. lines 46-46: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Cannot return execution error with shared objects. Debug of error: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3) at command Some(0) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/basic_receive.move b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/basic_receive.move new file mode 100644 index 0000000000000..7d4e47d4e9237 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/basic_receive.move @@ -0,0 +1,46 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::TxContext; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_share_object(a); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::receiver --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::receiver --args object(2,0) receiving(2,1)@3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/drop_receiving.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/drop_receiving.exp new file mode 100644 index 0000000000000..305fe29335f95 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/drop_receiving.exp @@ -0,0 +1,23 @@ +processed 6 tasks + +task 1 'publish'. lines 6-35: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7182000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 37-37: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'programmable'. lines 39-42: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 4 'programmable'. lines 43-47: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 + +task 5 'programmable'. lines 48-49: +mutated: object(0,0), object(2,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 2181960, non_refundable_storage_fee: 22040 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/drop_receiving.move b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/drop_receiving.move new file mode 100644 index 0000000000000..353c4d3d48b39 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/drop_receiving.move @@ -0,0 +1,49 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::TxContext; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_share_object(a); + transfer::public_transfer(b, a_address); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } + + public entry fun nop(_parent: &mut A) { } + public entry fun nop_with_receiver(_parent: &mut A, _x: Receiving) { } +} + +//# run tto::M1::start + +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::send_back(Input(0), Input(1)) + +// Include the receiving argument, but don't use it at the PTB level +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::nop(Input(0)) + +// Include the receiving argument, but don't use it at the Move level. The +// receiving object should not be mutated by this. +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::nop_with_receiver(Input(0), Input(1)) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_dof_and_mutate.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_dof_and_mutate.exp new file mode 100644 index 0000000000000..4609e158c07e5 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_dof_and_mutate.exp @@ -0,0 +1,59 @@ +processed 12 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-36: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 7881200, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 38-38: +created: object(2,0), object(2,1), object(2,2), object(2,3) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 7273200, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 40-40: +Owner: Object ID: ( fake(2,3) ) +Version: 3 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,2)}} + +task 4 'view-object'. lines 42-42: +Owner: Shared +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 5 'view-object'. lines 44-44: +Owner: Object ID: ( fake(2,0) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 6 'view-object'. lines 46-46: +Owner: Account Address ( fake(2,1) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 0u64} + +task 7 'run'. lines 48-48: +created: object(7,0) +mutated: object(0,1), object(2,1), object(2,3) +gas summary: computation_cost: 1000000, storage_cost: 5996400, storage_rebate: 3506184, non_refundable_storage_fee: 35416 + +task 8 'view-object'. lines 50-50: +Owner: Object ID: ( fake(2,3) ) +Version: 3 +Contents: sui::dynamic_field::Field, sui::object::ID> {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}, name: sui::dynamic_object_field::Wrapper {name: 0u64}, value: sui::object::ID {bytes: fake(2,2)}} + +task 9 'view-object'. lines 52-52: +Owner: Shared +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}, value: 0u64} + +task 10 'view-object'. lines 54-54: +Owner: Object ID: ( fake(2,0) ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}, value: 0u64} + +task 11 'view-object'. lines 56-56: +Owner: Object ID: ( fake(7,0) ) +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,3)}}, value: 100u64} diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_dof_and_mutate.move b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_dof_and_mutate.move new file mode 100644 index 0000000000000..1ab1c5301dab7 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_dof_and_mutate.move @@ -0,0 +1,56 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::TxContext; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_object_field as dof; + + const KEY: u64 = 0; + + struct A has key, store { + id: UID, + value: u64, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx), value: 0 }; + let a_address = object::id_address(&a); + let b = A { id: object::new(ctx), value: 0 }; + dof::add(&mut b.id, KEY, A { id: object::new(ctx), value: 0 }); + transfer::public_share_object(a); + transfer::public_transfer(b, a_address); + } + + public entry fun receive(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + dof::add(&mut parent.id, KEY, b); + let _: &A = dof::borrow(&parent.id, KEY); + let x: &mut A = dof::borrow_mut(&mut parent.id, KEY); + x.value = 100; + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 + +//# run tto::M1::receive --args object(2,1) receiving(2,3) + +//# view-object 2,0 + +//# view-object 2,1 + +//# view-object 2,2 + +//# view-object 2,3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_multiple_times_in_row.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_multiple_times_in_row.exp new file mode 100644 index 0000000000000..76af55f82f548 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_multiple_times_in_row.exp @@ -0,0 +1,59 @@ +processed 13 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 6-39: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 7182000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 41-41: +created: object(2,0), object(2,1) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'run'. lines 43-43: +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2204000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4 'view-object'. lines 45-45: +Owner: Shared +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 5 'view-object'. lines 47-49: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 6 'run'. lines 50-50: +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 7 'view-object'. lines 52-52: +Owner: Shared +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 8 'view-object'. lines 54-56: +Owner: Account Address ( fake(2,0) ) +Version: 4 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 9 'run'. lines 57-59: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Cannot return execution error with shared objects. Debug of error: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3) at command Some(0) + +task 10 'run'. lines 60-62: +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 11 'run'. lines 63-65: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3), source: Some(VMError { major_status: ABORTED, sub_status: Some(3), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(10), 0)] }), command: Some(0) } } + +task 12 'run'. lines 66-66: +mutated: object(0,1), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_multiple_times_in_row.move b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_multiple_times_in_row.move new file mode 100644 index 0000000000000..ffe72183d3c1e --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/receive_multiple_times_in_row.move @@ -0,0 +1,66 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 --accounts A + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_share_object(a); + transfer::public_transfer(b, a_address); + } + + + public fun middle(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } +} + +//# run tto::M1::start + +//# run tto::M1::middle --sender A + +//# view-object 2,0 + +//# view-object 2,1 + +// Can receive the object and then send it +//# run tto::M1::send_back --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +// Can no longer receive that object at the previous version number +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@3 + +// Can receive the object at the new version number +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@4 + +// Cannot try and receive the object with an invalid owner even if it has the right type +//# run tto::M1::send_back --summarize --args object(3,0) receiving(2,1)@6 --sender A + +// Can run still receive and send back so state is all good still, and version number hasn't been incremented for the object +//# run tto::M1::send_back --args object(2,0) receiving(2,1)@6 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/transfer_then_share.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/transfer_then_share.exp new file mode 100644 index 0000000000000..06eedacd5252b --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/transfer_then_share.exp @@ -0,0 +1,39 @@ +processed 9 tasks + +task 1 'publish'. lines 6-33: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 6916000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 35-35: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 37-37: +Owner: Shared +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 39-39: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'run'. lines 41-41: +mutated: object(0,0), object(2,0), object(2,1) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 3385800, non_refundable_storage_fee: 34200 + +task 6 'view-object'. lines 43-43: +Owner: Shared +Version: 4 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 7 'view-object'. lines 45-45: +Owner: Account Address ( _ ) +Version: 4 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 8 'run'. lines 47-47: +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::receive_impl (function index 10) at offset 0, Abort Code: 3 +Cannot return execution error with shared objects. Debug of error: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 10, instruction: 0, function_name: Some("receive_impl") }, 3) at command Some(0) diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/transfer_then_share.move b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/transfer_then_share.move new file mode 100644 index 0000000000000..1bf62f2a5cf47 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/shared_parent/transfer_then_share.move @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::TxContext; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + // transfer to 'a' first, then share it + transfer::public_transfer(b, a_address); + transfer::public_share_object(a); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::receiver --args object(2,0) receiving(2,1) + +//# view-object 2,0 + +//# view-object 2,1 + +//# run tto::M1::receiver --args object(2,0) receiving(2,1)@3 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/take_receiver_then_try_to_reuse.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/take_receiver_then_try_to_reuse.exp new file mode 100644 index 0000000000000..b20a9a66e11f4 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/take_receiver_then_try_to_reuse.exp @@ -0,0 +1,25 @@ +processed 6 tasks + +task 1 'publish'. lines 6-34: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 7182000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 36-36: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 3 'view-object'. lines 38-38: +Owner: Account Address ( _ ) +Version: 3 +Contents: tto::M1::A {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,0)}}} + +task 4 'view-object'. lines 40-42: +Owner: Account Address ( fake(2,0) ) +Version: 3 +Contents: tto::M1::B {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,1)}}} + +task 5 'programmable'. lines 43-45: +Error: Transaction Effects Status: Invalid command argument at 1. Invalid usage of value. Mutably borrowed values require unique usage. Immutably borrowed values cannot be taken or borrowed mutably. Taken values cannot be used again. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: CommandArgumentError { arg_idx: 1, kind: InvalidValueUsage }, source: None, command: Some(1) } } diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/take_receiver_then_try_to_reuse.move b/crates/sui-adapter-transactional-tests/tests/receive_object/take_receiver_then_try_to_reuse.move new file mode 100644 index 0000000000000..9d067cbf65a4a --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/take_receiver_then_try_to_reuse.move @@ -0,0 +1,45 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun pass_through(x: Receiving): Receiving { x } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } +} + +//# run tto::M1::start + +//# view-object 2,0 + +//# view-object 2,1 + +// Pass through the receiving object and then try to reuse the input receiving object argument -- should fail. +//# programmable --inputs object(2,0) receiving(2,1) +//> tto::M1::pass_through(Input(1)); +//> tto::M1::receiver(Input(0), Input(1)); diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/transfer_object_cyclic.exp b/crates/sui-adapter-transactional-tests/tests/receive_object/transfer_object_cyclic.exp new file mode 100644 index 0000000000000..5ed9270a42889 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/transfer_object_cyclic.exp @@ -0,0 +1,11 @@ +processed 3 tasks + +task 1 'publish'. lines 6-28: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 5852000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 30-30: +created: object(2,0), object(2,1) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 3420000, storage_rebate: 978120, non_refundable_storage_fee: 9880 diff --git a/crates/sui-adapter-transactional-tests/tests/receive_object/transfer_object_cyclic.move b/crates/sui-adapter-transactional-tests/tests/receive_object/transfer_object_cyclic.move new file mode 100644 index 0000000000000..8a471302502aa --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/receive_object/transfer_object_cyclic.move @@ -0,0 +1,30 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --addresses tto=0x0 + +//# publish +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::TxContext; + use sui::transfer::{Self}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + let b_address = object::id_address(&b); + transfer::public_transfer(a, b_address); + transfer::public_transfer(b, a_address); + } +} + +//# run tto::M1::start diff --git a/crates/sui-adapter-transactional-tests/tests/shared/upgrade.exp b/crates/sui-adapter-transactional-tests/tests/shared/upgrade.exp index 82e295bb5bfd8..76a6fb7909c80 100644 --- a/crates/sui-adapter-transactional-tests/tests/shared/upgrade.exp +++ b/crates/sui-adapter-transactional-tests/tests/shared/upgrade.exp @@ -19,13 +19,13 @@ Version: 2 Contents: t::m::Obj {id: sui::object::UID {id: sui::object::ID {bytes: fake(2,2)}}} task 4 'run'. lines 46-46: -Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::share_object_impl (function index 7) at offset 0, Abort Code: 0 -Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 7, instruction: 0, function_name: Some("share_object_impl") }, 0), source: Some(VMError { major_status: ABORTED, sub_status: Some(0), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(7), 0)] }), command: Some(0) } } +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::share_object_impl (function index 8) at offset 0, Abort Code: 0 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 8, instruction: 0, function_name: Some("share_object_impl") }, 0), source: Some(VMError { major_status: ABORTED, sub_status: Some(0), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(8), 0)] }), command: Some(0) } } task 5 'run'. lines 48-48: -Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::share_object_impl (function index 7) at offset 0, Abort Code: 0 -Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 7, instruction: 0, function_name: Some("share_object_impl") }, 0), source: Some(VMError { major_status: ABORTED, sub_status: Some(0), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(7), 0)] }), command: Some(0) } } +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::share_object_impl (function index 8) at offset 0, Abort Code: 0 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 8, instruction: 0, function_name: Some("share_object_impl") }, 0), source: Some(VMError { major_status: ABORTED, sub_status: Some(0), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(8), 0)] }), command: Some(0) } } task 6 'run'. lines 50-50: -Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::share_object_impl (function index 7) at offset 0, Abort Code: 0 -Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 7, instruction: 0, function_name: Some("share_object_impl") }, 0), source: Some(VMError { major_status: ABORTED, sub_status: Some(0), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(7), 0)] }), command: Some(0) } } +Error: Transaction Effects Status: Move Runtime Abort. Location: sui::transfer::share_object_impl (function index 8) at offset 0, Abort Code: 0 +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: MoveAbort(MoveLocation { module: ModuleId { address: sui, name: Identifier("transfer") }, function: 8, instruction: 0, function_name: Some("share_object_impl") }, 0), source: Some(VMError { major_status: ABORTED, sub_status: Some(0), message: None, exec_state: None, location: Module(ModuleId { address: sui, name: Identifier("transfer") }), indices: [], offsets: [(FunctionDefinitionIndex(8), 0)] }), command: Some(0) } } diff --git a/crates/sui-benchmark/src/lib.rs b/crates/sui-benchmark/src/lib.rs index 6ca4345bbf212..115726a53134a 100644 --- a/crates/sui-benchmark/src/lib.rs +++ b/crates/sui-benchmark/src/lib.rs @@ -857,6 +857,9 @@ impl From for BenchMoveCallArg { initial_shared_version, mutable, } => BenchMoveCallArg::Shared((id, initial_shared_version, mutable)), + ObjectArg::Receiving(_) => { + unimplemented!("Receiving is not supported for benchmarks") + } }, } } diff --git a/crates/sui-config/src/transaction_deny_config.rs b/crates/sui-config/src/transaction_deny_config.rs index 9bc84fede54c5..ec039a3a367e6 100644 --- a/crates/sui-config/src/transaction_deny_config.rs +++ b/crates/sui-config/src/transaction_deny_config.rs @@ -58,6 +58,10 @@ pub struct TransactionDenyConfig { #[serde(skip)] address_deny_set: OnceCell>, + + /// Whether receiving objects transferred to other objects is allowed + #[serde(default)] + receiving_objects_disabled: bool, // TODO: We could consider add a deny list for types that we want to disable public transfer. // TODO: We could also consider disable more types of commands, such as transfer, split and etc. } @@ -93,6 +97,10 @@ impl TransactionDenyConfig { pub fn user_transaction_disabled(&self) -> bool { self.user_transaction_disabled } + + pub fn receiving_objects_disabled(&self) -> bool { + self.receiving_objects_disabled + } } #[derive(Default)] @@ -129,6 +137,11 @@ impl TransactionDenyConfigBuilder { self } + pub fn disable_receiving_objects(mut self) -> Self { + self.config.receiving_objects_disabled = true; + self + } + pub fn add_denied_object(mut self, id: ObjectID) -> Self { self.config.object_deny_list.push(id); self diff --git a/crates/sui-core/src/authority.rs b/crates/sui-core/src/authority.rs index 635f1b6755f2c..cb0e59a90b485 100644 --- a/crates/sui-core/src/authority.rs +++ b/crates/sui-core/src/authority.rs @@ -41,6 +41,7 @@ use std::{ }; use sui_config::node::StateDebugDumpConfig; use sui_config::NodeConfig; +use sui_types::execution::LoadedRuntimeObjectMetadata; use tap::{TapFallible, TapOptional}; use tokio::sync::mpsc::unbounded_channel; use tokio::sync::oneshot; @@ -105,7 +106,7 @@ use sui_types::messages_grpc::{ }; use sui_types::metrics::{BytecodeVerifierMetrics, LimitsMetrics}; use sui_types::object::{MoveObject, Owner, PastObjectRead, OBJECT_START_VERSION}; -use sui_types::storage::{ObjectKey, ObjectStore, WriteKind}; +use sui_types::storage::{MarkerKind, ObjectKey, ObjectStore, WriteKind}; use sui_types::sui_system_state::epoch_start_sui_system_state::EpochStartSystemStateTrait; use sui_types::sui_system_state::SuiSystemStateTrait; use sui_types::sui_system_state::{get_sui_system_state, SuiSystemState}; @@ -926,6 +927,41 @@ impl AuthorityState { .expect("notify_read_effects should return exactly 1 element")) } + fn receiving_object_transaction_dependencies( + &self, + epoch_store: &AuthorityPerEpochStore, + receiving_objects: &[ObjectRef], + ) -> SuiResult> { + let mut dependencies = Vec::new(); + for (oid, version, _) in receiving_objects.iter() { + // First try and get the object normally. + let previous_transaction = match self.database.get_object_by_key(oid, *version)? { + Some(object) => object.previous_transaction, + // If the object has already been pruned, we must be receiving it in the epoch it + // was deleted (otherwise it would have been rejected at signing before this). + // Because of this the transaction digest that we need for the dependency will be + // in the per-epoch marker table so get it from there. This should not fail, but if + // it does for some reason we will return an error that the the object is not found. + None => { + let object_key = ( + epoch_store.epoch(), + ObjectKey(*oid, *version), + MarkerKind::Received, + ); + let Some(tx_digest) = self.database.perpetual_tables.object_per_epoch_marker_table.get(&object_key)? else { + return Err(SuiError::from(UserInputError::ObjectNotFound { + object_id: *oid, + version: Some(*version), + })); + }; + tx_digest + } + }; + dependencies.push(previous_transaction); + } + Ok(dependencies) + } + async fn check_owned_locks(&self, owned_object_refs: &[ObjectRef]) -> SuiResult { self.database .check_owned_object_locks_exist(owned_object_refs) @@ -1093,7 +1129,16 @@ impl AuthorityState { let output_keys: Vec<_> = inner_temporary_store .written .iter() - .map(|(_, ((id, seq, _), obj, _))| InputKey(*id, (!obj.is_package()).then_some(*seq))) + .map(|(_, ((id, seq, _), obj, _))| { + if obj.is_package() { + InputKey::Package { id: *id } + } else { + InputKey::VersionedObject { + id: *id, + version: *seq, + } + } + }) .collect(); self.commit_certificate(inner_temporary_store, certificate, effects, epoch_store) @@ -1178,10 +1223,21 @@ impl AuthorityState { let tx_digest = *certificate.digest(); let protocol_config = epoch_store.protocol_config(); let shared_object_refs = input_objects.filter_shared_objects(); - let transaction_dependencies = input_objects.transaction_dependencies(); + let receiving_objects = certificate + .data() + .intent_message() + .value + .receiving_objects()?; + let mut transaction_dependencies = input_objects.transaction_dependencies(); + // Register each receiving object's previous transaction as a dependency regardless of + // whether or not we will actually receive it in the transaction. + let receiving_object_dependencies = + self.receiving_object_transaction_dependencies(epoch_store, &receiving_objects)?; + transaction_dependencies.extend(receiving_object_dependencies); let temporary_store = TemporaryStore::new( self.database.clone(), input_objects, + receiving_objects, tx_digest, protocol_config, ); @@ -1494,7 +1550,7 @@ impl AuthorityState { tx_coins: Option, written: &WrittenObjects, module_resolver: &impl GetModule, - loaded_child_objects: &BTreeMap, + loaded_child_objects: &BTreeMap, ) -> SuiResult { let changes = self .process_object_index(effects, written, module_resolver) @@ -1754,7 +1810,7 @@ impl AuthorityState { tx_coins, written, &module_resolver, - &inner_temporary_store.loaded_child_objects, + &inner_temporary_store.loaded_runtime_objects, ) .await .tap_ok(|_| self.metrics.post_processing_total_tx_indexed.inc()) @@ -4299,8 +4355,8 @@ impl NodeStateDump { // Record all loaded child objects // Child objects which are read but not mutated are not tracked anywhere else let mut loaded_child_objects = Vec::new(); - for (id, ver) in &inner_temporary_store.loaded_child_objects { - if let Some(w) = authority_store.get_object_by_key(id, *ver)? { + for (id, meta) in &inner_temporary_store.loaded_runtime_objects { + if let Some(w) = authority_store.get_object_by_key(id, meta.version)? { loaded_child_objects.push(w) } } diff --git a/crates/sui-core/src/authority/authority_store.rs b/crates/sui-core/src/authority/authority_store.rs index 929880b33a657..e8ac4afb947b7 100644 --- a/crates/sui-core/src/authority/authority_store.rs +++ b/crates/sui-core/src/authority/authority_store.rs @@ -23,7 +23,8 @@ use sui_types::message_envelope::Message; use sui_types::messages_checkpoint::ECMHLiveObjectSetDigest; use sui_types::object::Owner; use sui_types::storage::{ - get_module_by_id, BackingPackageStore, ChildObjectResolver, DeleteKind, ObjectKey, ObjectStore, + get_module_by_id, BackingPackageStore, ChildObjectResolver, DeleteKind, MarkerKind, ObjectKey, + ObjectStore, }; use sui_types::sui_system_state::get_sui_system_state; use sui_types::{base_types::SequenceNumber, fp_bail, fp_ensure, storage::ParentSync}; @@ -540,6 +541,66 @@ impl AuthorityStore { Ok(result) } + pub fn have_received_object_at_version( + &self, + object_id: &ObjectID, + version: VersionNumber, + epoch_id: EpochId, + ) -> Result { + let object_key = ( + epoch_id, + ObjectKey(*object_id, version), + MarkerKind::Received, + ); + Ok(self + .perpetual_tables + .object_per_epoch_marker_table + .get(&object_key)? + .is_some()) + } + + pub fn check_receiving_objects( + &self, + receiving_objects: &[ObjectRef], + input_objects_len: usize, + epoch_store: &AuthorityPerEpochStore, + ) -> Result<(), SuiError> { + let protocol_config = epoch_store.protocol_config(); + // Count receiving objects towards the input object limit as they are passed in the PTB + // args and they will (most likely) incur an object load at runtime. + fp_ensure!( + receiving_objects.len() + input_objects_len + <= protocol_config.max_input_objects() as usize, + UserInputError::SizeLimitExceeded { + limit: "maximum input and receiving objects in a transaction".to_string(), + value: protocol_config.max_input_objects().to_string() + } + .into() + ); + + // Since we're at signing we check that every object reference that we are receiving is the + // most recent version of that object. If it's been received at the version specified we + // let it through to allow the transaction to run and fail to unlock any other objects in + // the transaction. Otherwise, we return an error. + for (object_id, version, _) in receiving_objects { + fp_ensure!( + self.get_object(object_id)? + .is_some_and(|x| x.owner.is_address_owned() && x.version() == *version) + || self.have_received_object_at_version( + object_id, + *version, + epoch_store.epoch() + )?, + UserInputError::ObjectNotFound { + object_id: *object_id, + version: Some(*version), + } + .into() + ); + } + Ok(()) + } + pub fn check_input_objects( &self, objects: &[InputObjectKind], @@ -608,43 +669,61 @@ impl AuthorityStore { } else { LockMode::ReadOnly }; - (InputKey(*id, Some(*version)), lock_mode) + (InputKey::VersionedObject{ id: *id, version: *version}, lock_mode) } // TODO: use ReadOnly lock? - InputObjectKind::MovePackage(id) => (InputKey(*id, None), LockMode::Default), + InputObjectKind::MovePackage(id) => (InputKey::Package { id: *id }, LockMode::Default), // Cannot use ReadOnly lock because we do not know if the object is immutable. - InputObjectKind::ImmOrOwnedMoveObject(objref) => (InputKey(objref.0, Some(objref.1)), LockMode::Default), + InputObjectKind::ImmOrOwnedMoveObject(objref) => (InputKey::VersionedObject {id: objref.0, version: objref.1}, LockMode::Default), } }) .collect() } /// Checks if the input object identified by the InputKey exists, with support for non-system - /// packages i.e. when version is None. - pub fn multi_input_objects_exist( + /// packages i.e. when version is None. If the input object doesn't exist and it's a receiving + /// object, we also check if the object exists in the object marker table and view it as + /// existing if it is in the table. + pub fn multi_input_objects_available( &self, keys: impl Iterator + Clone, + epoch_store: &AuthorityPerEpochStore, ) -> Result, SuiError> { - let (keys_with_version, keys_without_version): (Vec<_>, Vec<_>) = - keys.enumerate().partition(|(_, key)| key.1.is_some()); + let (keys_with_version, keys_without_version): (Vec<_>, Vec<_>) = keys + .enumerate() + .partition(|(_, key)| key.version().is_some()); - let versioned_results = keys_with_version.iter().map(|(idx, _)| *idx).zip( + let mut versioned_results = vec![]; + for ((idx, input_key), has_key) in keys_with_version.iter().zip( self.perpetual_tables .objects - .multi_get( + .multi_contains_keys( keys_with_version .iter() - .map(|(_, k)| ObjectKey(k.0, k.1.unwrap())), + .map(|(_, k)| ObjectKey(k.id(), k.version().unwrap())), )? - .into_iter() - .map(|o| o.is_some()), - ); + .into_iter(), + ) { + if has_key { + versioned_results.push((*idx, true)) + } else { + // For any objects that are unable to be fetched, lookup and determine if + // the object exists in the object marker table as well. If so we will then mark it as + // "available" to let it progress through. + let has_received = self.have_received_object_at_version( + &input_key.id(), + input_key.version().unwrap(), + epoch_store.epoch(), + )?; + versioned_results.push((*idx, has_received)); + } + } let unversioned_results = keys_without_version.into_iter().map(|(idx, key)| { ( idx, match self - .get_latest_object_ref_or_tombstone(key.0) + .get_latest_object_ref_or_tombstone(key.id()) .expect("read cannot fail") { None => false, @@ -654,6 +733,7 @@ impl AuthorityStore { }); let mut results = versioned_results + .into_iter() .chain(unversioned_results) .collect::>(); results.sort_by_key(|(idx, _)| *idx); @@ -1027,8 +1107,8 @@ impl AuthorityStore { &self, write_batch: &mut DBBatch, inner_temporary_store: InnerTemporaryStore, - _transaction: &VerifiedTransaction, - _epoch_id: EpochId, + transaction: &VerifiedTransaction, + epoch_id: EpochId, ) -> SuiResult { let InnerTemporaryStore { objects, @@ -1037,13 +1117,45 @@ impl AuthorityStore { deleted, events, max_binary_format_version: _, - loaded_child_objects: _, + loaded_runtime_objects, no_extraneous_module_bytes: _, runtime_packages_loaded_from_db: _, } = inner_temporary_store; trace!(written =? written.values().map(|((obj_id, ver, _), _, _)| (obj_id, ver)).collect::>(), "batch_update_objects: temp store written"); + // Get the list of objects that could be received in this transaction. + let possible_to_receive = transaction + .transaction_data() + .receiving_objects() + .unwrap_or(vec![]); + // Use this to get the actual set of objects that have been received -- any received + // object will show up as a write or delete. + let received_objects: Vec<_> = possible_to_receive + .into_iter() + .filter(|obj_ref| { + written.get(&obj_ref.0).is_some() || deleted.get(&obj_ref.0).is_some() + }) + .collect(); + + // Insert each received object into the received objects marker table + write_batch.insert_batch( + &self.perpetual_tables.object_per_epoch_marker_table, + received_objects.iter().map(|(object_id, version, _)| { + ( + ( + epoch_id, + ObjectKey(*object_id, *version), + MarkerKind::Received, + ), + loaded_runtime_objects + .get(object_id) + .expect("received objects must always be in the loaded runtime objects") + .previous_transaction, + ) + }), + )?; + let owned_inputs: Vec<_> = active_inputs .iter() .filter(|(id, _, _)| objects.get(id).unwrap().is_address_owned()) @@ -1136,7 +1248,12 @@ impl AuthorityStore { self.check_owned_object_locks_exist(&owned_inputs)?; self.initialize_locks_impl(write_batch, &new_locks_to_init, false)?; - self.delete_locks(write_batch, &owned_inputs) + self.delete_locks(write_batch, &owned_inputs)?; + + // Make sure to delete the locks for any received objects. + // Any objects that occur as a `Receiving` argument but have not been received will not + // have their locks touched. + self.delete_locks(write_batch, &received_objects) } /// Acquires a lock for a transaction on the given objects if they have all been initialized previously @@ -1829,7 +1946,6 @@ impl AuthorityStore { } } - #[cfg(msim)] pub fn remove_all_versions_of_object(&self, object_id: ObjectID) { let entries: Vec<_> = self .perpetual_tables @@ -1895,6 +2011,35 @@ impl ChildObjectResolver for AuthorityStore { } Ok(Some(child_object)) } + + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult> { + let Some(recv_object) = self.get_object_by_key(receiving_object_id, receive_object_at_version)? else { + return Ok(None) + }; + + // Check for: + // * Invalid access -- treat as the object does not exist. Or; + // * If we've already received the object at the version -- then treat it as though it doesn't exist. + // These two cases must remain indisguishable to the caller otherwise we risk forks in + // transaction replay due to possible reordering of transactions during replay. + if recv_object.owner != Owner::AddressOwner((*owner).into()) + || self.have_received_object_at_version( + receiving_object_id, + receive_object_at_version, + epoch_id, + )? + { + return Ok(None); + } + + Ok(Some(recv_object)) + } } impl ParentSync for AuthorityStore { @@ -2026,14 +2171,41 @@ impl From for LockDetailsWrapper { /// A potential input to a transaction. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct InputKey(pub ObjectID, pub Option); +pub enum InputKey { + VersionedObject { + id: ObjectID, + version: SequenceNumber, + }, + Package { + id: ObjectID, + }, +} + +impl InputKey { + pub fn id(&self) -> ObjectID { + match self { + InputKey::VersionedObject { id, .. } => *id, + InputKey::Package { id } => *id, + } + } + + pub fn version(&self) -> Option { + match self { + InputKey::VersionedObject { version, .. } => Some(*version), + InputKey::Package { .. } => None, + } + } +} impl From<&Object> for InputKey { fn from(obj: &Object) -> Self { if obj.is_package() { - InputKey(obj.id(), None) + InputKey::Package { id: obj.id() } } else { - InputKey(obj.id(), Some(obj.version())) + InputKey::VersionedObject { + id: obj.id(), + version: obj.version(), + } } } } diff --git a/crates/sui-core/src/authority/authority_store_tables.rs b/crates/sui-core/src/authority/authority_store_tables.rs index 5633ef2ae8ea9..dd5c8dcee2ddd 100644 --- a/crates/sui-core/src/authority/authority_store_tables.rs +++ b/crates/sui-core/src/authority/authority_store_tables.rs @@ -125,7 +125,8 @@ pub struct AuthorityPerpetualTables { /// objects (since they are not locked by the transaction manager) and for tracking shared /// objects that have been deleted. This table is meant to be pruned per-epoch, and all /// previous epochs other than the current epoch may be pruned safely. - pub(crate) object_per_epoch_marker_table: DBMap<(EpochId, ObjectKey, MarkerKind), ()>, + pub(crate) object_per_epoch_marker_table: + DBMap<(EpochId, ObjectKey, MarkerKind), TransactionDigest>, } impl AuthorityPerpetualTables { diff --git a/crates/sui-core/src/authority/authority_test_utils.rs b/crates/sui-core/src/authority/authority_test_utils.rs index ddeca9c434e1e..78b34979f6ce4 100644 --- a/crates/sui-core/src/authority/authority_test_utils.rs +++ b/crates/sui-core/src/authority/authority_test_utils.rs @@ -47,19 +47,10 @@ pub async fn send_and_confirm_transaction_( Ok((txn, effects)) } -pub async fn send_and_confirm_transaction_with_execution_error( +pub async fn certify_transaction( authority: &AuthorityState, - fullnode: Option<&AuthorityState>, transaction: Transaction, - with_shared: bool, // transaction includes shared objects -) -> Result< - ( - CertifiedTransaction, - SignedTransactionEffects, - Option, - ), - SuiError, -> { +) -> Result { // Make the initial request let epoch_store = authority.load_epoch_store_one_call_per_task(); let transaction = authority.verify_transaction(transaction).unwrap(); @@ -71,12 +62,27 @@ pub async fn send_and_confirm_transaction_with_execution_error( // Collect signatures from a quorum of authorities let committee = authority.clone_committee_for_testing(); - let certificate = - CertifiedTransaction::new(transaction.into_message(), vec![vote.clone()], &committee) - .unwrap() - .verify_authenticated(&committee, &Default::default()) - .unwrap(); + let certificate = CertifiedTransaction::new(transaction.into_message(), vec![vote], &committee) + .unwrap() + .verify_authenticated(&committee, &Default::default()) + .unwrap(); + Ok(certificate) +} +pub async fn execute_certificate_with_execution_error( + authority: &AuthorityState, + fullnode: Option<&AuthorityState>, + certificate: VerifiedCertificate, + with_shared: bool, // transaction includes shared objects +) -> Result< + ( + CertifiedTransaction, + SignedTransactionEffects, + Option, + ), + SuiError, +> { + let epoch_store = authority.load_epoch_store_one_call_per_task(); // We also check the incremental effects of the transaction on the live object set against StateAccumulator // for testing and regression detection. // We must do this before sending to consensus, otherwise consensus may already @@ -117,6 +123,23 @@ pub async fn send_and_confirm_transaction_with_execution_error( )) } +pub async fn send_and_confirm_transaction_with_execution_error( + authority: &AuthorityState, + fullnode: Option<&AuthorityState>, + transaction: Transaction, + with_shared: bool, // transaction includes shared objects +) -> Result< + ( + CertifiedTransaction, + SignedTransactionEffects, + Option, + ), + SuiError, +> { + let certificate = certify_transaction(authority, transaction).await?; + execute_certificate_with_execution_error(authority, fullnode, certificate, with_shared).await +} + pub async fn init_state_validator_with_fullnode() -> (Arc, Arc) { use sui_types::crypto::get_authority_key_pair; diff --git a/crates/sui-core/src/lib.rs b/crates/sui-core/src/lib.rs index c67885a414ef3..2e30bfa6a422a 100644 --- a/crates/sui-core/src/lib.rs +++ b/crates/sui-core/src/lib.rs @@ -48,6 +48,9 @@ mod move_package_upgrade_tests; mod pay_sui_tests; pub mod test_authority_clients; #[cfg(test)] +#[path = "unit_tests/transfer_to_object_tests.rs"] +mod transfer_to_object_tests; +#[cfg(test)] #[path = "unit_tests/type_param_tests.rs"] mod type_param_tests; diff --git a/crates/sui-core/src/transaction_input_checker.rs b/crates/sui-core/src/transaction_input_checker.rs index d569b75b5a97f..44e60cd749694 100644 --- a/crates/sui-core/src/transaction_input_checker.rs +++ b/crates/sui-core/src/transaction_input_checker.rs @@ -62,9 +62,11 @@ mod checked { transaction.check_version_supported(epoch_store.protocol_config())?; transaction.validity_check(epoch_store.protocol_config())?; let input_objects = transaction.input_objects()?; + let receiving_objects = transaction.receiving_objects()?; transaction_signing_filter::check_transaction_for_signing( transaction, &input_objects, + &receiving_objects, transaction_deny_config, store, )?; @@ -80,6 +82,7 @@ mod checked { let gas_status = get_gas_status(&objects, transaction.gas(), epoch_store, transaction).await?; let input_objects = check_objects(transaction, input_objects, objects)?; + store.check_receiving_objects(&receiving_objects, input_objects.len(), epoch_store)?; Ok((gas_status, input_objects)) } @@ -97,6 +100,7 @@ mod checked { epoch_store.protocol_config(), metrics, )?; + let receiving_objects = transaction.receiving_objects()?; let mut input_objects = transaction.input_objects()?; let mut objects = store.check_input_objects(&input_objects, epoch_store.protocol_config())?; @@ -108,6 +112,7 @@ mod checked { let gas_status = get_gas_status(&objects, &[gas_object_ref], epoch_store, transaction).await?; let input_objects = check_objects(transaction, input_objects, objects)?; + store.check_receiving_objects(&receiving_objects, input_objects.len(), epoch_store)?; Ok((gas_status, input_objects)) } @@ -172,6 +177,7 @@ mod checked { ); let tx_data = &cert.data().intent_message().value; + let receiving_objects = tx_data.receiving_objects()?; let input_object_kinds = tx_data.input_objects()?; let input_object_data = if tx_data.is_change_epoch_tx() { // When changing the epoch, we update a the system object, which is shared, without going @@ -183,6 +189,7 @@ mod checked { let gas_status = get_gas_status(&input_object_data, tx_data.gas(), epoch_store, tx_data).await?; let input_objects = check_objects(tx_data, input_object_kinds, input_object_data)?; + store.check_receiving_objects(&receiving_objects, input_objects.len(), epoch_store)?; Ok((gas_status, input_objects)) } diff --git a/crates/sui-core/src/transaction_manager.rs b/crates/sui-core/src/transaction_manager.rs index 52aa4e1767853..65ee5b363d497 100644 --- a/crates/sui-core/src/transaction_manager.rs +++ b/crates/sui-core/src/transaction_manager.rs @@ -140,15 +140,15 @@ impl CacheInner { } fn insert(&mut self, object: &InputKey) { - if let Some(version) = object.1 { + if let Some(version) = object.version() { if let Some((previous_id, previous_version)) = - self.versioned_cache.push(object.0, version) + self.versioned_cache.push(object.id(), version) { - if previous_id == object.0 && previous_version > version { + if previous_id == object.id() && previous_version > version { // do not allow highest known version to decrease // This should not be possible unless bugs are introduced elsewhere in this // module. - self.versioned_cache.put(object.0, previous_version); + self.versioned_cache.put(object.id(), previous_version); } else { self.metrics .transaction_manager_object_cache_evictions @@ -158,11 +158,11 @@ impl CacheInner { self.metrics .transaction_manager_object_cache_size .set(self.versioned_cache.len() as i64); - } else if let Some((previous_id, _)) = self.unversioned_cache.push(object.0, ()) { + } else if let Some((previous_id, _)) = self.unversioned_cache.push(object.id(), ()) { // lru_cache will does not check if the value being evicted is the same as the value // being inserted, so we do need to check if the id is different before counting this // as an eviction. - if previous_id != object.0 { + if previous_id != object.id() { self.metrics .transaction_manager_package_cache_evictions .inc(); @@ -176,8 +176,8 @@ impl CacheInner { // Returns Some(true/false) for a definitive result. Returns None if the caller must defer to // the db. fn is_object_available(&mut self, object: &InputKey) -> Option { - if let Some(version) = object.1 { - if let Some(current) = self.versioned_cache.get(&object.0) { + if let Some(version) = object.version() { + if let Some(current) = self.versioned_cache.get(&object.id()) { self.metrics.transaction_manager_object_cache_hits.inc(); Some(*current >= version) } else { @@ -186,7 +186,7 @@ impl CacheInner { } } else { self.unversioned_cache - .get(&object.0) + .get(&object.id()) .tap_some(|_| self.metrics.transaction_manager_package_cache_hits.inc()) .tap_none(|| self.metrics.transaction_manager_package_cache_misses.inc()) .map(|_| true) @@ -305,15 +305,18 @@ impl Inner { return ready_certificates; } - let input_count = self.input_objects.get_mut(&input_key.0).unwrap_or_else(|| { - panic!( - "# of transactions waiting on object {:?} cannot be 0", - input_key.0 - ) - }); + let input_count = self + .input_objects + .get_mut(&input_key.id()) + .unwrap_or_else(|| { + panic!( + "# of transactions waiting on object {:?} cannot be 0", + input_key.id() + ) + }); *input_count -= digests.len(); if *input_count == 0 { - self.input_objects.remove(&input_key.0); + self.input_objects.remove(&input_key.id()); } for digest in digests { @@ -461,17 +464,34 @@ impl TransactionManager { .value .input_objects() .expect("input_objects() cannot fail"); - let input_object_locks = self.authority_store.get_input_object_locks( + let mut input_object_locks = self.authority_store.get_input_object_locks( &digest, &input_object_kinds, epoch_store, ); + if input_object_kinds.len() != input_object_locks.len() { error!("Duplicated input objects: {:?}", input_object_kinds); } + + let receiving_object_entries = cert + .data() + .intent_message() + .value + .receiving_objects() + .expect("receiving_objects() cannot fail"); + for entry in receiving_object_entries { + let key = InputKey::VersionedObject { + id: entry.0, + version: entry.1, + }; + input_object_locks.insert(key, LockMode::Default); + } + for key in input_object_locks.keys() { object_availability.insert(*key, None); } + (cert, fx_digest, input_object_locks) }) .collect(); @@ -497,7 +517,7 @@ impl TransactionManager { // So missing objects' availability are checked again after releasing the TM lock. let cache_miss_availibility = self .authority_store - .multi_input_objects_exist(input_object_cache_misses.iter().cloned()) + .multi_input_objects_available(input_object_cache_misses.iter().cloned(), epoch_store) .expect("Checking object existence cannot fail!") .into_iter() .zip(input_object_cache_misses.into_iter()); @@ -511,7 +531,7 @@ impl TransactionManager { let _scope = monitored_scope("TransactionManager::enqueue::wlock"); for (available, key) in cache_miss_availibility { - if available && key.1.is_none() { + if available && key.version().is_none() { // Mutable objects obtained from cache_miss_availability usually will not be read // again, so we do not want to evict other objects in order to insert them into the // cache. However, packages will likely be read often, so we do want to insert them @@ -635,7 +655,7 @@ impl TransactionManager { } if acquire { pending_cert.acquiring_locks.insert(key, lock_mode); - let input_count = inner.input_objects.entry(key.0).or_default(); + let input_count = inner.input_objects.entry(key.id()).or_default(); *input_count += 1; } else { pending_cert.acquired_locks.insert(key, lock_mode); @@ -795,15 +815,11 @@ impl TransactionManager { let cert = pending_certificate.certificate; let expected_effects_digest = pending_certificate.expected_effects_digest; trace!(tx_digest = ?cert.digest(), "certificate ready"); + let tx_data = &cert.data().intent_message().value; // Record as an executing certificate. assert_eq!( pending_certificate.acquired_locks.len(), - cert.data() - .intent_message() - .value - .input_objects() - .unwrap() - .len() + tx_data.input_objects().unwrap().len() + tx_data.receiving_objects().unwrap().len(), ); assert!(inner .executing_certificates @@ -915,7 +931,7 @@ mod test { // insert 10 unique unversioned objects for i in 0..10 { let object = ObjectID::new([i; 32]); - let input_key = InputKey(object, None); + let input_key = InputKey::Package { id: object }; assert_eq!(cache.is_object_available(&input_key), None); cache.insert(&input_key); assert_eq!(cache.is_object_available(&input_key), Some(true)); @@ -924,14 +940,17 @@ mod test { // first 5 have been evicted for i in 0..5 { let object = ObjectID::new([i; 32]); - let input_key = InputKey(object, None); + let input_key = InputKey::Package { id: object }; assert_eq!(cache.is_object_available(&input_key), None); } // insert 10 unique versioned objects for i in 0..10 { let object = ObjectID::new([i; 32]); - let input_key = InputKey(object, Some((i as u64).into())); + let input_key = InputKey::VersionedObject { + id: object, + version: (i as u64).into(), + }; assert_eq!(cache.is_object_available(&input_key), None); cache.insert(&input_key); assert_eq!(cache.is_object_available(&input_key), Some(true)); @@ -940,26 +959,38 @@ mod test { // first 5 versioned objects have been evicted for i in 0..5 { let object = ObjectID::new([i; 32]); - let input_key = InputKey(object, Some((i as u64).into())); + let input_key = InputKey::VersionedObject { + id: object, + version: (i as u64).into(), + }; assert_eq!(cache.is_object_available(&input_key), None); } // but versioned objects do not cause evictions of unversioned objects for i in 5..10 { let object = ObjectID::new([i; 32]); - let input_key = InputKey(object, None); + let input_key = InputKey::Package { id: object }; assert_eq!(cache.is_object_available(&input_key), Some(true)); } // object 9 is available at version 9 let object = ObjectID::new([9; 32]); - let input_key = InputKey(object, Some(9.into())); + let input_key = InputKey::VersionedObject { + id: object, + version: 9.into(), + }; assert_eq!(cache.is_object_available(&input_key), Some(true)); // but not at version 10 - let input_key = InputKey(object, Some(10.into())); + let input_key = InputKey::VersionedObject { + id: object, + version: 10.into(), + }; assert_eq!(cache.is_object_available(&input_key), Some(false)); // it is available at version 8 (this case can be used by readonly shared objects) - let input_key = InputKey(object, Some(8.into())); + let input_key = InputKey::VersionedObject { + id: object, + version: 8.into(), + }; assert_eq!(cache.is_object_available(&input_key), Some(true)); } } diff --git a/crates/sui-core/src/transaction_signing_filter.rs b/crates/sui-core/src/transaction_signing_filter.rs index 3b7bec33d0d1a..8ec58509b4471 100644 --- a/crates/sui-core/src/transaction_signing_filter.rs +++ b/crates/sui-core/src/transaction_signing_filter.rs @@ -3,6 +3,7 @@ use sui_config::transaction_deny_config::TransactionDenyConfig; use sui_types::{ + base_types::ObjectRef, error::{SuiError, SuiResult, UserInputError}, storage::BackingPackageStore, transaction::{Command, InputObjectKind, TransactionData, TransactionDataAPI}, @@ -29,6 +30,7 @@ macro_rules! deny_if_true { pub fn check_transaction_for_signing( tx_data: &TransactionData, input_objects: &[InputObjectKind], + receiving_objects: &[ObjectRef], filter_config: &TransactionDenyConfig, package_store: &impl BackingPackageStore, ) -> SuiResult { @@ -40,6 +42,25 @@ pub fn check_transaction_for_signing( check_package_dependencies(filter_config, tx_data, package_store)?; + check_receiving_objects(filter_config, receiving_objects)?; + + Ok(()) +} + +fn check_receiving_objects( + filter_config: &TransactionDenyConfig, + receiving_objects: &[ObjectRef], +) -> SuiResult { + deny_if_true!( + filter_config.receiving_objects_disabled() && !receiving_objects.is_empty(), + "Receiving objects is temporarily disabled".to_string() + ); + for (id, _, _) in receiving_objects { + deny_if_true!( + filter_config.get_object_deny_set().contains(id), + format!("Access to object {:?} is temporarily disabled", id) + ); + } Ok(()) } diff --git a/crates/sui-core/src/unit_tests/authority_tests.rs b/crates/sui-core/src/unit_tests/authority_tests.rs index 78c1fcfafab8e..63e6127497fbb 100644 --- a/crates/sui-core/src/unit_tests/authority_tests.rs +++ b/crates/sui-core/src/unit_tests/authority_tests.rs @@ -4236,6 +4236,23 @@ pub async fn execute_programmable_transaction_with_shared( .await } +pub async fn build_programmable_transaction( + authority: &AuthorityState, + gas_object_id: &ObjectID, + sender: &SuiAddress, + sender_key: &AccountKeyPair, + pt: ProgrammableTransaction, + gas_unit: u64, +) -> SuiResult { + let rgp = authority.reference_gas_price_for_testing().unwrap(); + let gas_object = authority.get_object(gas_object_id).await.unwrap(); + let gas_object_ref = gas_object.unwrap().compute_object_reference(); + let data = + TransactionData::new_programmable(*sender, vec![gas_object_ref], pt, rgp * gas_unit, rgp); + + Ok(to_sender_signed_transaction(data, sender_key)) +} + async fn execute_programmable_transaction_( authority: &AuthorityState, fullnode: Option<&AuthorityState>, diff --git a/crates/sui-core/src/unit_tests/data/entry_point_types/Move.lock b/crates/sui-core/src/unit_tests/data/entry_point_types/Move.lock index 02050fcae1551..e1e2d4f6736a8 100644 --- a/crates/sui-core/src/unit_tests/data/entry_point_types/Move.lock +++ b/crates/sui-core/src/unit_tests/data/entry_point_types/Move.lock @@ -2,6 +2,8 @@ [move] version = 0 +manifest_digest = "767739C9147CAC379CE17A273955EE03EF28351CE7D26C3C286A02AAA14FB302" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" dependencies = [ { name = "Sui" }, diff --git a/crates/sui-core/src/unit_tests/data/entry_point_vector/Move.lock b/crates/sui-core/src/unit_tests/data/entry_point_vector/Move.lock index 02050fcae1551..fb52bc64b3cdb 100644 --- a/crates/sui-core/src/unit_tests/data/entry_point_vector/Move.lock +++ b/crates/sui-core/src/unit_tests/data/entry_point_vector/Move.lock @@ -2,6 +2,8 @@ [move] version = 0 +manifest_digest = "5EDC4495EB1E648D2099207F79D616639179085FA517FC2CE705753805534B04" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" dependencies = [ { name = "Sui" }, diff --git a/crates/sui-core/src/unit_tests/data/tto/Move.toml b/crates/sui-core/src/unit_tests/data/tto/Move.toml new file mode 100644 index 0000000000000..162ee2126f44c --- /dev/null +++ b/crates/sui-core/src/unit_tests/data/tto/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "tto" +version = "0.0.1" + +[dependencies] +Sui = { local = "../../../../../sui-framework/packages/sui-framework" } + +[addresses] +tto = "0x0" diff --git a/crates/sui-core/src/unit_tests/data/tto/sources/tto1.move b/crates/sui-core/src/unit_tests/data/tto/sources/tto1.move new file mode 100644 index 0000000000000..92f5d6bd272f3 --- /dev/null +++ b/crates/sui-core/src/unit_tests/data/tto/sources/tto1.move @@ -0,0 +1,54 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module tto::M1 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + struct C has key { + id: UID, + wrapped: B, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } + + public entry fun deleter(parent: &mut A, x: Receiving) { + let B { id } = transfer::receive(&mut parent.id, x); + object::delete(id); + } + + public entry fun wrapper(parent: &mut A, x: Receiving, ctx: &mut TxContext) { + let b = transfer::receive(&mut parent.id, x); + let c = C { id: object::new(ctx), wrapped: b }; + transfer::transfer(c, @tto); + } + + public fun call_immut_ref(_parent: &mut A, _x: &Receiving) { } + public fun call_mut_ref(_parent: &mut A, _x: &mut Receiving) { } +} diff --git a/crates/sui-core/src/unit_tests/data/tto/sources/tto2.move b/crates/sui-core/src/unit_tests/data/tto/sources/tto2.move new file mode 100644 index 0000000000000..b29eac1de571b --- /dev/null +++ b/crates/sui-core/src/unit_tests/data/tto/sources/tto2.move @@ -0,0 +1,50 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module tto::M2 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + use sui::dynamic_field as df; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + struct C has key { + id: UID, + wrapped: B, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + let c = C { id: object::new(ctx), wrapped: b }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::transfer(c, a_address); + } + + public entry fun unwrap_receiver(parent: &mut A, x: Receiving) { + let C { id, wrapped } = transfer::receive(&mut parent.id, x); + transfer::public_transfer(wrapped, @0x0); + object::delete(id); + } + + public entry fun unwrap_deleter(parent: &mut A, x: Receiving) { + let C { id, wrapped: B { id: idb } } = transfer::receive(&mut parent.id, x); + object::delete(id); + object::delete(idb); + } + + public entry fun unwrap_add_dyn(parent: &mut A, x: Receiving) { + let C { id, wrapped } = transfer::receive(&mut parent.id, x); + object::delete(id); + df::add(&mut parent.id, 0, wrapped); + } +} + diff --git a/crates/sui-core/src/unit_tests/data/tto/sources/tto3.move b/crates/sui-core/src/unit_tests/data/tto/sources/tto3.move new file mode 100644 index 0000000000000..60b564cf96b50 --- /dev/null +++ b/crates/sui-core/src/unit_tests/data/tto/sources/tto3.move @@ -0,0 +1,46 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module tto::M3 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let fake_a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(fake_a, tx_context::sender(ctx)); + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } + + public entry fun simple(_parent: &mut A) { } + + + public entry fun send_back(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + let parent_address = object::id_address(parent); + transfer::public_transfer(b, parent_address); + } + + public entry fun deleter(parent: &mut A, x: Receiving) { + let B { id } = transfer::receive(&mut parent.id, x); + object::delete(id); + } +} + diff --git a/crates/sui-core/src/unit_tests/data/tto/sources/tto4.move b/crates/sui-core/src/unit_tests/data/tto/sources/tto4.move new file mode 100644 index 0000000000000..ba6a676d920d4 --- /dev/null +++ b/crates/sui-core/src/unit_tests/data/tto/sources/tto4.move @@ -0,0 +1,49 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module tto::M4 { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start1(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + } + + public fun start2(ctx: &mut TxContext) { + let b = B { id: object::new(ctx) }; + transfer::public_transfer(b, tx_context::sender(ctx)); + } + + public fun transfer(addr: address, b: B) { + transfer::public_transfer(b, addr); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } + + public entry fun deleter(parent: &mut A, x: Receiving) { + let B { id } = transfer::receive(&mut parent.id, x); + object::delete(id); + } + + public entry fun nop(_parent: &mut A, _x: Receiving) { } + + public entry fun aborter(_parent: &mut A, _x: Receiving) { abort 0 } + + public entry fun receive_abort(parent: &mut A, x: Receiving) { + let _b = transfer::receive(&mut parent.id, x); + abort 0 + } +} diff --git a/crates/sui-core/src/unit_tests/move_package_upgrade_tests.rs b/crates/sui-core/src/unit_tests/move_package_upgrade_tests.rs index 881d8c7f41555..8201b3e421ae2 100644 --- a/crates/sui-core/src/unit_tests/move_package_upgrade_tests.rs +++ b/crates/sui-core/src/unit_tests/move_package_upgrade_tests.rs @@ -28,6 +28,7 @@ use crate::authority::{ move_integration_tests::build_and_publish_test_package_with_upgrade_cap, AuthorityState, }; +#[macro_export] macro_rules! move_call { {$builder:expr, ($addr:expr)::$module_name:ident::$func:ident($($args:expr),* $(,)?)} => { $builder.programmable_move_call( @@ -108,10 +109,6 @@ struct UpgradeStateRunner { impl UpgradeStateRunner { pub async fn new(base_package_name: &str) -> Self { telemetry_subscribers::init_for_testing(); - let _dont_remove = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { - config.set_package_upgrades_for_testing(true); - config - }); let (sender, sender_key): (_, AccountKeyPair) = get_key_pair(); let gas_object_id = ObjectID::random(); let gas_object = Object::with_id_owner_for_testing(gas_object_id, sender); diff --git a/crates/sui-core/src/unit_tests/transaction_manager_tests.rs b/crates/sui-core/src/unit_tests/transaction_manager_tests.rs index 447c8d82a7b3d..bddf27e1519d5 100644 --- a/crates/sui-core/src/unit_tests/transaction_manager_tests.rs +++ b/crates/sui-core/src/unit_tests/transaction_manager_tests.rs @@ -64,7 +64,10 @@ fn make_transaction(gas_object: Object, input: Vec) -> VerifiedExecutab fn get_input_keys(objects: &[Object]) -> Vec { objects .iter() - .map(|object| InputKey(object.id(), Some(object.version()))) + .map(|object| InputKey::VersionedObject { + id: object.id(), + version: object.version(), + }) .collect() } @@ -251,7 +254,10 @@ async fn transaction_manager_read_lock() { // Notify TM about availability of the shared object. transaction_manager.objects_available( - vec![InputKey(shared_object.id(), Some(shared_version))], + vec![InputKey::VersionedObject { + id: shared_object.id(), + version: shared_version, + }], &state.epoch_store_for_testing(), ); @@ -284,3 +290,188 @@ async fn transaction_manager_read_lock() { transaction_manager.notify_commit(tx_2.digest(), vec![], &state.epoch_store_for_testing()); transaction_manager.check_empty_for_testing(); } + +#[tokio::test(flavor = "current_thread", start_paused = true)] +async fn transaction_manager_receiving_notify_commit() { + telemetry_subscribers::init_for_testing(); + // Initialize an authority state. + let (owner, _keypair) = deterministic_random_account_key(); + let gas_objects: Vec = (0..10) + .map(|_| { + let gas_object_id = ObjectID::random(); + Object::with_id_owner_for_testing(gas_object_id, owner) + }) + .collect(); + let state = init_state_with_objects(gas_objects.clone()).await; + + // Create a new transaction manager instead of reusing the authority's, to examine + // transaction_manager output from rx_ready_certificates. + let (transaction_manager, mut rx_ready_certificates) = make_transaction_manager(&state); + // TM should output no transaction. + assert!(rx_ready_certificates.try_recv().is_err()); + // TM should be empty at the beginning. + transaction_manager.check_empty_for_testing(); + + let obj_id = ObjectID::random(); + let object_arguments: Vec<_> = (0..10) + .map(|i| { + let object = Object::with_id_owner_version_for_testing(obj_id, i.into(), owner); + // Every other transaction receives the object, and we create a run of multiple receives in + // a row at the beginning to test that the TM doesn't get stuck in either configuration of: + // ImmOrOwnedObject => Receiving, + // Receiving => Receiving + // Receiving => ImmOrOwnedObject + // ImmOrOwnedObject => ImmOrOwnedObject is already tested as the default case on mainnet. + let object_arg = if i % 2 == 0 || i == 3 { + ObjectArg::Receiving(object.compute_object_reference()) + } else { + ObjectArg::ImmOrOwnedObject(object.compute_object_reference()) + }; + let txn = make_transaction(gas_objects[0].clone(), vec![CallArg::Object(object_arg)]); + (object, txn) + }) + .collect(); + + for (i, (_, txn)) in object_arguments.iter().enumerate() { + // TM should output no transaction yet since waiting on receiving object or + // ImmOrOwnedObject input. + transaction_manager + .enqueue(vec![txn.clone()], &state.epoch_store_for_testing()) + .unwrap(); + sleep(Duration::from_secs(1)).await; + assert!(rx_ready_certificates.try_recv().is_err()); + assert_eq!(transaction_manager.inflight_queue_len(), i + 1); + } + + // Start things off by notifying TM that the receiving object 0 is available. + transaction_manager.objects_available( + get_input_keys(&vec![object_arguments[0].0.clone()]), + &state.epoch_store_for_testing(), + ); + + // Now start to unravel the rest of the transactions by notifying that each subsequent + // transaction has been processed. + for (i, (object, txn)) in object_arguments.iter().enumerate() { + // TM should output the transaction eventually now that the receiving object has become + // available. + rx_ready_certificates.recv().await.unwrap(); + + // Only one transaction at a time should become available though. So if we try to get + // another one it should fail. + sleep(Duration::from_secs(1)).await; + assert!(rx_ready_certificates.try_recv().is_err()); + + // Notify the TM that the transaction has been processed, and that it has written the + // object at the next version. + transaction_manager.notify_commit( + txn.digest(), + vec![InputKey::VersionedObject { + id: object.id(), + version: object.version().next(), + }], + &state.epoch_store_for_testing(), + ); + + // TM should now output another transaction to run since it the next version of that object + // has become available. + assert_eq!( + transaction_manager.inflight_queue_len(), + object_arguments.len() - i - 1 + ); + } + + // After everything TM should be empty. + transaction_manager.check_empty_for_testing(); +} + +#[tokio::test(flavor = "current_thread", start_paused = true)] +async fn transaction_manager_receiving_object_ready_notifications() { + telemetry_subscribers::init_for_testing(); + // Initialize an authority state. + let (owner, _keypair) = deterministic_random_account_key(); + let gas_objects: Vec = (0..10) + .map(|_| { + let gas_object_id = ObjectID::random(); + Object::with_id_owner_for_testing(gas_object_id, owner) + }) + .collect(); + let state = init_state_with_objects(gas_objects.clone()).await; + + // Create a new transaction manager instead of reusing the authority's, to examine + // transaction_manager output from rx_ready_certificates. + let (transaction_manager, mut rx_ready_certificates) = make_transaction_manager(&state); + // TM should output no transaction. + assert!(rx_ready_certificates.try_recv().is_err()); + // TM should be empty at the beginning. + transaction_manager.check_empty_for_testing(); + + let obj_id = ObjectID::random(); + let receiving_object_new0 = Object::with_id_owner_version_for_testing(obj_id, 0.into(), owner); + let receiving_object_new1 = Object::with_id_owner_version_for_testing(obj_id, 1.into(), owner); + let receiving_object_arg0 = + ObjectArg::Receiving(receiving_object_new0.compute_object_reference()); + let receive_object_transaction0 = make_transaction( + gas_objects[0].clone(), + vec![CallArg::Object(receiving_object_arg0)], + ); + + let receiving_object_arg1 = + ObjectArg::Receiving(receiving_object_new1.compute_object_reference()); + let receive_object_transaction1 = make_transaction( + gas_objects[0].clone(), + vec![CallArg::Object(receiving_object_arg1)], + ); + + // TM should output no transaction yet since waiting on receiving object. + transaction_manager + .enqueue( + vec![receive_object_transaction0.clone()], + &state.epoch_store_for_testing(), + ) + .unwrap(); + sleep(Duration::from_secs(1)).await; + assert!(rx_ready_certificates.try_recv().is_err()); + assert_eq!(transaction_manager.inflight_queue_len(), 1); + + // TM should output no transaction yet since waiting on receiving object. + transaction_manager + .enqueue( + vec![receive_object_transaction1.clone()], + &state.epoch_store_for_testing(), + ) + .unwrap(); + sleep(Duration::from_secs(1)).await; + assert!(rx_ready_certificates.try_recv().is_err()); + assert_eq!(transaction_manager.inflight_queue_len(), 2); + + // Duplicate enqueue of receiving object is allowed. + transaction_manager + .enqueue( + vec![receive_object_transaction0.clone()], + &state.epoch_store_for_testing(), + ) + .unwrap(); + sleep(Duration::from_secs(1)).await; + assert!(rx_ready_certificates.try_recv().is_err()); + assert_eq!(transaction_manager.inflight_queue_len(), 2); + + // Notify TM that the receiving object 0 is available. + transaction_manager.objects_available( + get_input_keys(&vec![receiving_object_new0.clone()]), + &state.epoch_store_for_testing(), + ); + + // TM should output the transaction eventually now that the receiving object has become + // available. + rx_ready_certificates.recv().await.unwrap(); + + // Notify TM that the receiving object 0 is available. + transaction_manager.objects_available( + get_input_keys(&vec![receiving_object_new1.clone()]), + &state.epoch_store_for_testing(), + ); + + // TM should output the transaction eventually now that the receiving object has become + // available. + rx_ready_certificates.recv().await.unwrap(); +} diff --git a/crates/sui-core/src/unit_tests/transfer_to_object_tests.rs b/crates/sui-core/src/unit_tests/transfer_to_object_tests.rs new file mode 100644 index 0000000000000..48d1b7a84e206 --- /dev/null +++ b/crates/sui-core/src/unit_tests/transfer_to_object_tests.rs @@ -0,0 +1,1199 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::HashSet, sync::Arc}; + +use sui_types::{ + base_types::{ObjectID, ObjectRef, SuiAddress}, + crypto::{get_key_pair, AccountKeyPair}, + effects::{TransactionEffects, TransactionEffectsAPI}, + execution_status::{ExecutionFailureStatus, ExecutionStatus}, + object::{Object, Owner}, + programmable_transaction_builder::ProgrammableTransactionBuilder, + transaction::{ + ObjectArg, ProgrammableTransaction, VerifiedCertificate, TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + }, +}; + +use crate::{ + authority::{ + authority_test_utils::{certify_transaction, execute_certificate_with_execution_error}, + authority_tests::{build_programmable_transaction, execute_programmable_transaction}, + move_integration_tests::build_and_publish_test_package_with_upgrade_cap, + test_authority_builder::TestAuthorityBuilder, + AuthorityState, + }, + move_call, +}; +use move_core_types::ident_str; + +// The primary use for these tests is to make sure the generated effect sets match what we expect +// when receiving an object, and if we then perform different types of operations on the received +// object (e.g., deleting, wrapping, unwrapping, adding as a dynamic field, etc.) and various +// combinations of that. Some of these tests also check and validate locking behavior around +// receiving object arguments as well. + +pub struct TestRunner { + pub sender: SuiAddress, + pub sender_key: AccountKeyPair, + pub gas_object_ids: Vec, + pub authority_state: Arc, + pub package: ObjectRef, + pub upgrade_cap: ObjectRef, +} + +impl TestRunner { + pub async fn new_with_objects(base_package_name: &str, num: usize) -> Self { + telemetry_subscribers::init_for_testing(); + let (sender, sender_key): (_, AccountKeyPair) = get_key_pair(); + + let authority_state = TestAuthorityBuilder::new().build().await; + let mut gas_object_ids = vec![]; + for _ in 0..num { + let gas_object_id = ObjectID::random(); + let gas_object = Object::with_id_owner_for_testing(gas_object_id, sender); + authority_state.insert_genesis_object(gas_object).await; + gas_object_ids.push(gas_object_id); + } + + let (package, upgrade_cap) = build_and_publish_test_package_with_upgrade_cap( + &authority_state, + &sender, + &sender_key, + &gas_object_ids[0], + base_package_name, + /* with_unpublished_deps */ false, + ) + .await; + + Self { + sender, + sender_key, + gas_object_ids, + authority_state, + package, + upgrade_cap, + } + } + + pub async fn new(base_package_name: &str) -> Self { + Self::new_with_objects(base_package_name, 1).await + } + + pub async fn run_with_gas_object( + &mut self, + pt: ProgrammableTransaction, + idx: usize, + ) -> TransactionEffects { + let effects = execute_programmable_transaction( + &self.authority_state, + &self.gas_object_ids[idx], + &self.sender, + &self.sender_key, + pt, + TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + ) + .await + .unwrap(); + + let TransactionEffects::V1(fx) = &effects; + + if let Some(updated_cap) = fx + .mutated + .iter() + .find_map(|(cap, _)| (cap.0 == self.upgrade_cap.0).then_some(cap)) + { + self.upgrade_cap = *updated_cap; + } + + effects + } + + pub async fn run(&mut self, pt: ProgrammableTransaction) -> TransactionEffects { + self.run_with_gas_object(pt, 0).await + } + + pub async fn lock_and_verify_transaction( + &mut self, + pt: ProgrammableTransaction, + account_id: usize, + ) -> VerifiedCertificate { + let transaction = build_programmable_transaction( + &self.authority_state, + &self.gas_object_ids[account_id], + &self.sender, + &self.sender_key, + pt, + TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + ) + .await + .unwrap(); + certify_transaction(&self.authority_state, transaction) + .await + .unwrap() + } + + pub async fn execute_certificate(&mut self, ct: VerifiedCertificate) -> TransactionEffects { + execute_certificate_with_execution_error(&self.authority_state, None, ct, false) + .await + .unwrap() + .1 + .into_data() + } +} + +fn get_parent_and_child( + created: &[(ObjectRef, Owner)], +) -> (&(ObjectRef, Owner), &(ObjectRef, Owner)) { + // make sure there is an object with an `AddressOwner` who matches the object ID of another + // object. + let created_addrs: HashSet<_> = created.iter().map(|((i, _, _), _)| i).collect(); + let (child, parent_id) = created + .iter() + .find_map(|child @ (_, owner)| match owner { + Owner::AddressOwner(j) if created_addrs.contains(&ObjectID::from(*j)) => { + Some((child, (*j).into())) + } + _ => None, + }) + .unwrap(); + let parent = created + .iter() + .find(|((id, _, _), _)| *id == parent_id) + .unwrap(); + (parent, child) +} + +#[tokio::test] +async fn test_tto_transfer() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M1::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + let transfer_digest = effects.transaction_digest; + + // No receive the sent object + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M1::receiver(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + assert!(effects.dependencies.contains(&transfer_digest)); + + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == child.0 .0 { + // Child should be sent to 0x0 + assert_eq!(owner, &Owner::AddressOwner(SuiAddress::ZERO)); + // It's version should be bumped as well + assert!(obj_ref.1 > child.0 .1); + } + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_unused_receiver() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M1::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + + // If the receiving argument is not used it should not be modified! + assert!(!effects + .modified_at_versions + .iter() + .any(|(i, _)| i == &child.0 .0)); + // Since the parent was not used but it was an input object, it should be modified + assert!(effects + .modified_at_versions + .iter() + .any(|(i, _)| i == &parent.0 .0)); + + // Make sure parent exists in mutated, and the version is bumped. + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_pass_receiving_by_refs() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M1::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M1::call_immut_ref(parent, child) + }; + move_call! { + builder, + (runner.package.0)::M1::call_mut_ref(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + + // If the receiving argument is not used it should not be modified! + assert!(!effects + .modified_at_versions + .iter() + .any(|(i, _)| i == &child.0 .0)); + // Since the parent was not used but it was an input object, it should be modified + assert!(effects + .modified_at_versions + .iter() + .any(|(i, _)| i == &parent.0 .0)); + + // Make sure parent exists in mutated, and the version is bumped. + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_delete() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M1::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M1::deleter(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + // Deleted should be non-empty + assert_eq!(effects.deleted.len(), 1); + // Deleted should contain the child object + assert_eq!(effects.deleted[0].0, child.0 .0); + + // Make sure parent exists in mutated, and the version is bumped. + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_wrap() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M1::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M1::wrapper(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.deleted.is_empty()); + // We created an object since we wrapped this when we received the transaction + assert_eq!(effects.created.len(), 1); + // Wrapped should be non-empty + assert_eq!(effects.wrapped.len(), 1); + // Wrapped should contain the child object + assert_eq!(effects.wrapped[0].0, child.0 .0); + + // Make sure parent exists in mutated, and the version is bumped. + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_unwrap_transfer() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M2::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + // No receive the sent object + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M2::unwrap_receiver(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + + // Unwrapped should be size 1 + assert_eq!(effects.unwrapped.len(), 1); + // The now-unwrapped object should be sent to 0x0 + assert_eq!( + effects.unwrapped[0].1, + Owner::AddressOwner(SuiAddress::ZERO) + ); + + // Receiving object ID is deleted + assert_eq!(effects.deleted.len(), 1); + // Deleted should contain the child object id + assert_eq!(effects.deleted[0].0, child.0 .0); + + for (obj_ref, owner) in effects.mutated.iter() { + // child ref should not be mutated since it was deleted + assert_ne!(obj_ref.0, child.0 .0); + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_unwrap_delete() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M2::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + // No receive the sent object + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M2::unwrap_deleter(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.wrapped.is_empty()); + + // The deleted should be of size 1, and should contain the child address + assert_eq!(effects.deleted.len(), 1); + assert_eq!(effects.deleted[0].0, child.0 .0); + + // Unwrapped then deleted should be of size 1 since we deleted the inner object as well. + assert_eq!(effects.unwrapped_then_deleted.len(), 1); + + for (obj_ref, owner) in effects.mutated.iter() { + // child ref should not be mutated since it was deleted + assert_ne!(obj_ref.0, child.0 .0); + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +#[tokio::test] +async fn test_tto_unwrap_add_as_dynamic_field() { + let mut runner = TestRunner::new("tto").await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M2::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + + // No receive the sent object + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M2::unwrap_add_dyn(parent, child) + }; + builder.finish() + }) + .await; + + assert!(effects.status.is_ok()); + // Since it's placed as a dynamic field it will be rewrapped. So `unwrapped` should be empty + assert!(effects.unwrapped.is_empty()); + // Similarly it was already wrapped, so even though we're wrapping with the dynamic field `wrapped` should be empty + assert!(effects.wrapped.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + + assert_eq!(effects.created.len(), 1); + + // The deleted should be of size 1, and should contain the child address + assert_eq!(effects.deleted.len(), 1); + assert_eq!(effects.deleted[0].0, child.0 .0); + + for (obj_ref, owner) in effects.mutated.iter() { + assert_ne!(obj_ref.0, child.0 .0); + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + } + } +} + +// This tests that locks are not grabbed for receiving objects. +// This test does this by +// 1. Creating a parent object and child object +// 2. Creating a fake parent object +// 3. Create and sign a transaction `tx1` that tries to receive the child object using +// the fake parent. +// 4. Create and sign a transaction `tx2` that receives the child object using the valid parent +// object. +// 5. Execute `tx2` and verify that it can be executed successfully. +// 6. Execute `tx1` and verify that it can be executed, but will result in a Move abort. +// The order of steps 5 and 6 are swapped if `flipper` is `true`. +async fn verify_tto_not_locked(flipper: bool) -> (TransactionEffects, TransactionEffects) { + let mut runner = TestRunner::new_with_objects("tto", 2).await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M3::start() + }; + builder.finish() + }) + .await; + + let (parent, child) = get_parent_and_child(&effects.created); + let fake_parent = effects + .created + .iter() + .find(|(obj_ref, _)| obj_ref.0 != parent.0 .0 && obj_ref.0 != child.0 .0) + .unwrap(); + + // Now get a certificate for fake_parent/child1. This will lock input objects. + // NB: the receiving object is _not_ locked. + let cert_for_fake_parent = runner + .lock_and_verify_transaction( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder + .obj(ObjectArg::ImmOrOwnedObject(fake_parent.0)) + .unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M3::receiver(parent, child) + }; + builder.finish() + }, + 0, + ) + .await; + + // After the other (fake) transaction has been created and signed, sign and execute this + // transaction. This should have no issues because the receiving object is not locked by the + // signing of the transaction above. + let valid_cert = runner + .lock_and_verify_transaction( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M3::receiver(parent, child) + }; + builder.finish() + }, + 1, + ) + .await; + + // The order of the execution of these transactions is flipped depending on the value of + // flipper. However, the result should be the same in either case. + let (valid_effects, invalid_effects) = if flipper { + let invalid_effects = runner.execute_certificate(cert_for_fake_parent).await; + let valid_effects = runner.execute_certificate(valid_cert).await; + (valid_effects, invalid_effects) + } else { + let valid_effects = runner.execute_certificate(valid_cert).await; + let invalid_effects = runner.execute_certificate(cert_for_fake_parent).await; + (valid_effects, invalid_effects) + }; + + assert!(valid_effects.status().is_ok()); + assert!(invalid_effects.status().is_err()); + assert!(matches!( + invalid_effects.status(), + ExecutionStatus::Failure { + error: ExecutionFailureStatus::MoveAbort(_, _), + .. + } + )); + (valid_effects, invalid_effects) +} + +fn assert_effects_equivalent(ef1: &TransactionEffects, ef2: &TransactionEffects) { + assert_eq!(ef1.status(), ef2.status()); + assert_eq!(ef1.executed_epoch(), ef2.executed_epoch()); + assert_eq!(ef1.gas_cost_summary(), ef2.gas_cost_summary()); + assert_eq!( + ef1.modified_at_versions().len(), + ef2.modified_at_versions().len() + ); + assert_eq!(ef1.created().len(), ef2.created().len()); + assert_eq!(ef1.mutated().len(), ef2.mutated().len()); + assert_eq!(ef1.unwrapped().len(), ef2.unwrapped().len()); + assert_eq!(ef1.deleted().len(), ef2.deleted().len()); + assert_eq!( + ef1.unwrapped_then_deleted().len(), + ef2.unwrapped_then_deleted().len() + ); + assert_eq!(ef1.wrapped().len(), ef2.wrapped().len()); + assert_eq!(ef1.dependencies().len(), ef2.dependencies().len()); +} + +#[tokio::test] +async fn test_tto_not_locked() { + // The transaction effects for the valid and invalid transactions should be the same regardless + // of the order in which they are run. + let (valid1, invalid1) = verify_tto_not_locked(false).await; + let (valid2, invalid2) = verify_tto_not_locked(true).await; + assert_effects_equivalent(&valid1, &valid2); + assert_effects_equivalent(&invalid1, &invalid2); +} + +#[tokio::test] +async fn test_tto_valid_dependencies() { + let mut runner = TestRunner::new_with_objects("tto", 3).await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start1() + }; + builder.finish() + }) + .await; + let parent = effects.created[0]; + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start2() + }; + builder.finish() + }) + .await; + let child = effects.created[0]; + + // Use a different gas coin than for all the other transactions. This serves two purposes: + // 1. Makes sure that we are registering the dependency on the transaction that transferred the + // object solely because of the fact that we received it in this transaction. + // 2. Since the gas coin is fresh it will have a smaller version, so this will test that we + // properly compute and update the lamport version that we should use for the transaction. + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + builder + .transfer_object(SuiAddress::from(parent.0 .0), child.0) + .unwrap(); + builder.finish() + }, + 1, + ) + .await; + + let child = effects + .mutated + .iter() + .find(|(o, _)| o.0 == child.0 .0) + .unwrap(); + let transfer_digest = effects.transaction_digest; + + // No receive the sent object + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M4::receiver(parent, child) + }; + builder.finish() + }, + 2, + ) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + assert!(effects.dependencies.contains(&transfer_digest)); + + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == child.0 .0 { + // Child should be sent to 0x0 + assert_eq!(owner, &Owner::AddressOwner(SuiAddress::ZERO)); + // It's version should be bumped as well + assert!(obj_ref.1 > child.0 .1); + // The child should be the max version + assert_eq!(obj_ref.1.value(), child.0 .1.value() + 1); + } + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + // The child should be the max version + assert_eq!(obj_ref.1.value(), child.0 .1.value() + 1); + } + } +} + +#[tokio::test] +async fn test_tto_valid_dependencies_delete_on_receive() { + let mut runner = TestRunner::new_with_objects("tto", 3).await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start1() + }; + builder.finish() + }) + .await; + let parent = effects.created[0]; + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start2() + }; + builder.finish() + }) + .await; + let child = effects.created[0]; + + // Use a different gas coin than for all the other transactions. This serves two purposes: + // 1. Makes sure that we are registering the dependency on the transaction that transferred the + // object solely because of the fact that we received it in this transaction. + // 2. Since the gas coin is fresh it will have a smaller version, so this will test that we + // properly compute and update the lamport version that we should use for the transaction. + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + builder + .transfer_object(SuiAddress::from(parent.0 .0), child.0) + .unwrap(); + builder.finish() + }, + 1, + ) + .await; + + let child = effects + .mutated + .iter() + .find(|(o, _)| o.0 == child.0 .0) + .unwrap(); + let transfer_digest = effects.transaction_digest; + + // No receive and delete the sent object + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M4::deleter(parent, child) + }; + builder.finish() + }, + 2, + ) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + // Deleted should be non-empty + assert_eq!(effects.deleted.len(), 1); + // Deleted should contain the child object + assert_eq!(effects.deleted[0].0, child.0 .0); + assert!(effects.dependencies.contains(&transfer_digest)); + + // Make sure parent exists in mutated, and the version is bumped and is equal to the child's + // version + 1 since the child has the highest version number in the transaction. + for (obj_ref, owner) in effects.mutated.iter() { + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + assert_eq!(obj_ref.1.value(), child.0 .1.value() + 1); + } + } +} + +#[tokio::test] +async fn test_tto_dependencies_dont_receive() { + let mut runner = TestRunner::new_with_objects("tto", 3).await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start1() + }; + builder.finish() + }) + .await; + let parent = effects.created[0]; + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start2() + }; + builder.finish() + }) + .await; + let old_child = effects.created[0]; + + // Use a different gas coin than for all the other transactions. This: + // 1. Makes sure that we are registering the dependency on the transaction that transferred the + // object solely because of the fact that we received it in this transaction. + // 2. Since the gas coin is fresh it will have a smaller version, so this will test that we + // properly compute and update the lamport version that we should use for the transaction. + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + builder + .transfer_object(SuiAddress::from(parent.0 .0), old_child.0) + .unwrap(); + builder.finish() + }, + 1, + ) + .await; + + let child = effects + .mutated + .iter() + .find(|(o, _)| o.0 == old_child.0 .0) + .unwrap(); + let transfer_digest = effects.transaction_digest; + + // ensure child version is greater than parent version, otherwise the check afterwards won't be + // checking the correct thing. + assert!(parent.0 .1.value() < child.0 .1.value()); + + // Now dont receive the sent object but include it in the arguments for the PTB. + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M4::nop(parent, child) + }; + builder.finish() + }, + 2, + ) + .await; + + assert!(effects.status.is_ok()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + // Not received but dependency is still added. + assert!(effects.dependencies.contains(&transfer_digest)); + + for (obj_ref, owner) in effects.mutated.iter() { + assert_ne!(obj_ref.0, child.0 .0); + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + // Parent version is the largest in this transaction + assert_eq!(obj_ref.1.value(), child.0 .1.value() + 1); + } + } +} + +#[tokio::test] +async fn test_tto_dependencies_dont_receive_but_abort() { + let mut runner = TestRunner::new_with_objects("tto", 3).await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start1() + }; + builder.finish() + }) + .await; + let parent = effects.created[0]; + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start2() + }; + builder.finish() + }) + .await; + let old_child = effects.created[0]; + + // Use a different gas coin than for all the other transactions. This: + // 1. Makes sure that we are registering the dependency on the transaction that transferred the + // object solely because of the fact that we received it in this transaction. + // 2. Since the gas coin is fresh it will have a smaller version, so this will test that we + // properly compute and update the lamport version that we should use for the transaction. + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + builder + .transfer_object(SuiAddress::from(parent.0 .0), old_child.0) + .unwrap(); + builder.finish() + }, + 1, + ) + .await; + + let child = effects + .mutated + .iter() + .find(|(o, _)| o.0 == old_child.0 .0) + .unwrap(); + let transfer_digest = effects.transaction_digest; + + assert!(parent.0 .1.value() < child.0 .1.value()); + + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M4::aborter(parent, child) + }; + builder.finish() + }, + 2, + ) + .await; + + assert!(effects.status.is_err()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + // Not received but dependency still added + assert!(effects.dependencies.contains(&transfer_digest)); + + for (obj_ref, owner) in effects.mutated.iter() { + assert_ne!(obj_ref.0, child.0 .0); + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + // child version is the largest in this transaction, and even though it's not received + // it still contributes to the lamport version of the transaction. + assert_eq!(obj_ref.1.value(), child.0 .1.value() + 1); + } + } +} + +#[tokio::test] +async fn test_tto_dependencies_receive_and_abort() { + let mut runner = TestRunner::new_with_objects("tto", 3).await; + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start1() + }; + builder.finish() + }) + .await; + let parent = effects.created[0]; + + let TransactionEffects::V1(effects) = runner + .run({ + let mut builder = ProgrammableTransactionBuilder::new(); + move_call! { + builder, + (runner.package.0)::M4::start2() + }; + builder.finish() + }) + .await; + let old_child = effects.created[0]; + + // Use a different gas coin than for all the other transactions. This: + // 1. Makes sure that we are registering the dependency on the transaction that transferred the + // object solely because of the fact that we received it in this transaction. + // 2. Since the gas coin is fresh it will have a smaller version, so this will test that we + // properly compute and update the lamport version that we should use for the transaction. + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + builder + .transfer_object(SuiAddress::from(parent.0 .0), old_child.0) + .unwrap(); + builder.finish() + }, + 1, + ) + .await; + + let child = effects + .mutated + .iter() + .find(|(o, _)| o.0 == old_child.0 .0) + .unwrap(); + let transfer_digest = effects.transaction_digest; + + assert!(parent.0 .1.value() < child.0 .1.value()); + + let TransactionEffects::V1(effects) = runner + .run_with_gas_object( + { + let mut builder = ProgrammableTransactionBuilder::new(); + let parent = builder.obj(ObjectArg::ImmOrOwnedObject(parent.0)).unwrap(); + let child = builder.obj(ObjectArg::Receiving(child.0)).unwrap(); + move_call! { + builder, + (runner.package.0)::M4::receive_abort(parent, child) + }; + builder.finish() + }, + 2, + ) + .await; + + assert!(effects.status.is_err()); + assert!(effects.created.is_empty()); + assert!(effects.unwrapped.is_empty()); + assert!(effects.deleted.is_empty()); + assert!(effects.unwrapped_then_deleted.is_empty()); + assert!(effects.wrapped.is_empty()); + // Not received, but dependency is still added. + assert!(effects.dependencies.contains(&transfer_digest)); + + for (obj_ref, owner) in effects.mutated.iter() { + assert_ne!(obj_ref.0, child.0 .0); + if obj_ref.0 == parent.0 .0 { + // owner of the parent stays the same + assert_eq!(owner, &parent.1); + // parent version is also bumped + assert!(obj_ref.1 > parent.0 .1); + // Child version is the largest in this transaction even though it's not received + assert_eq!(obj_ref.1.value(), child.0 .1.value() + 1); + } + } +} diff --git a/crates/sui-core/tests/staged/sui.yaml b/crates/sui-core/tests/staged/sui.yaml index 156d44223b81b..3804c9efcd250 100644 --- a/crates/sui-core/tests/staged/sui.yaml +++ b/crates/sui-core/tests/staged/sui.yaml @@ -581,6 +581,13 @@ ObjectArg: - initial_shared_version: TYPENAME: SequenceNumber - mutable: BOOL + 2: + Receiving: + NEWTYPE: + TUPLE: + - TYPENAME: ObjectID + - TYPENAME: SequenceNumber + - TYPENAME: ObjectDigest ObjectDigest: NEWTYPESTRUCT: TYPENAME: Digest diff --git a/crates/sui-framework/docs/transfer.md b/crates/sui-framework/docs/transfer.md index b3619cc7d3c90..41c21cc109c04 100644 --- a/crates/sui-framework/docs/transfer.md +++ b/crates/sui-framework/docs/transfer.md @@ -5,6 +5,7 @@ +- [Struct `Receiving`](#0x2_transfer_Receiving) - [Constants](#@Constants_0) - [Function `transfer`](#0x2_transfer_transfer) - [Function `public_transfer`](#0x2_transfer_public_transfer) @@ -12,14 +13,57 @@ - [Function `public_freeze_object`](#0x2_transfer_public_freeze_object) - [Function `share_object`](#0x2_transfer_share_object) - [Function `public_share_object`](#0x2_transfer_public_share_object) +- [Function `receive`](#0x2_transfer_receive) - [Function `freeze_object_impl`](#0x2_transfer_freeze_object_impl) - [Function `share_object_impl`](#0x2_transfer_share_object_impl) - [Function `transfer_impl`](#0x2_transfer_transfer_impl) +- [Function `receive_impl`](#0x2_transfer_receive_impl) -
+
use 0x2::object;
+
+ + + + + +## Struct `Receiving` + +This represents the ability to receive an object of type T. +This type is ephemeral per-transaction and cannot be stored on-chain. +This does not represent the obligation to receive the object that it +references, but simply the ability to receive the object with object ID +id at version version if you can prove mutable access to the parent +object during the transaction. +Internals of this struct are opaque outside this module. + + +
struct Receiving<T: key> has drop
+
+ + + +
+Fields + +
+
+id: object::ID +
+
+
+
+version: u64 +
+
+ +
+
+ + +
@@ -211,6 +255,37 @@ The object must have store to be shared outside of its module. + + + + +## Function `receive` + +Given mutable (i.e., locked) access to the parent and a Receiving argument +referencing an object of type T owned by parent use the to_receive +argument to receive and return the referenced owned object of type T. + + +
public fun receive<T: key>(parent: &mut object::UID, to_receive: transfer::Receiving<T>): T
+
+ + + +
+Implementation + + +
public fun receive<T: key>(parent: &mut UID, to_receive: Receiving<T>): T {
+    let Receiving {
+        id,
+        version,
+    } = to_receive;
+    receive_impl(object::uid_to_address(parent), id, version)
+}
+
+ + +
@@ -326,4 +401,26 @@ The object must have store to be shared outside of its module. + + + + +## Function `receive_impl` + + + +
fun receive_impl<T: key>(parent: address, to_receive: object::ID, version: u64): T
+
+ + + +
+Implementation + + +
native fun receive_impl<T: key>(parent: address, to_receive: object::ID, version: u64): T;
+
+ + +
diff --git a/crates/sui-framework/packages/sui-framework/sources/transfer.move b/crates/sui-framework/packages/sui-framework/sources/transfer.move index 40c5bd438f9f9..dd725ea1392b4 100644 --- a/crates/sui-framework/packages/sui-framework/sources/transfer.move +++ b/crates/sui-framework/packages/sui-framework/sources/transfer.move @@ -3,12 +3,23 @@ module sui::transfer { - use sui::object; + use sui::object::{Self, ID, UID}; use sui::prover; #[test_only] friend sui::test_scenario; + /// This represents the ability to `receive` an object of type `T`. + /// This type is ephemeral per-transaction and cannot be stored on-chain. + /// This does not represent the obligation to receive the object that it + /// references, but simply the ability to receive the object with object ID + /// `id` at version `version` if you can prove mutable access to the parent + /// object during the transaction. + /// Internals of this struct are opaque outside this module. + struct Receiving has drop { + id: ID, + version: u64, + } /// Shared an object that was previously created. Shared objects must currently /// be constructed in the transaction they are created. @@ -70,6 +81,17 @@ module sui::transfer { share_object_impl(obj) } + /// Given mutable (i.e., locked) access to the `parent` and a `Receiving` argument + /// referencing an object of type `T` owned by `parent` use the `to_receive` + /// argument to receive and return the referenced owned object of type `T`. + public fun receive(parent: &mut UID, to_receive: Receiving): T { + let Receiving { + id, + version, + } = to_receive; + receive_impl(object::uid_to_address(parent), id, version) + } + public(friend) native fun freeze_object_impl(obj: T); spec freeze_object_impl { @@ -107,4 +129,6 @@ module sui::transfer { ensures [abstract] global(object::id(obj).bytes).owner == recipient; ensures [abstract] global(object::id(obj).bytes).status == prover::OWNED; } + + native fun receive_impl(parent: address, to_receive: object::ID, version: u64): T; } diff --git a/crates/sui-genesis-builder/src/lib.rs b/crates/sui-genesis-builder/src/lib.rs index 4dc47a55c9766..89778b623aaa3 100644 --- a/crates/sui-genesis-builder/src/lib.rs +++ b/crates/sui-genesis-builder/src/lib.rs @@ -811,6 +811,7 @@ fn create_genesis_transaction( let temporary_store = TemporaryStore::new( InMemoryStorage::new(Vec::new()), InputObjects::new(vec![]), + vec![], genesis_digest, protocol_config, ); @@ -963,6 +964,7 @@ fn process_package( let mut temporary_store = TemporaryStore::new( store.clone(), InputObjects::new(loaded_dependencies), + vec![], genesis_digest, protocol_config, ); @@ -1019,6 +1021,7 @@ pub fn generate_genesis_system_object( let mut temporary_store = TemporaryStore::new( store.clone(), InputObjects::new(vec![]), + vec![], genesis_digest, &protocol_config, ); diff --git a/crates/sui-json-rpc-types/src/sui_transaction.rs b/crates/sui-json-rpc-types/src/sui_transaction.rs index 7b18326ab036d..eda1cefda7841 100644 --- a/crates/sui-json-rpc-types/src/sui_transaction.rs +++ b/crates/sui-json-rpc-types/src/sui_transaction.rs @@ -1535,6 +1535,13 @@ impl SuiCallArg { initial_shared_version, mutable, }), + // TODO(tzakian)[tto] + CallArg::Object(ObjectArg::Receiving(_obj_ref)) => std::todo!( + "Implement SuiCallArg::try_from for CallArg::Object(ObjectArg::Receiving(obj_ref))" + ), + // { + // SuiCallArg::Object(SuiObjectArg::Receiving(SuiObjectRef::from(obj_ref))) + // } }) } @@ -1599,6 +1606,10 @@ pub enum SuiObjectArg { initial_shared_version: SequenceNumber, mutable: bool, }, + // TODO(tzakian)[tto]: uncomment this when we are ready to expose this to the RPC interface. + // A reference to a Move object that's going to be received in the transaction. + // #[serde(rename_all = "camelCase")] + // Receiving(SuiObjectRef), } #[serde_as] diff --git a/crates/sui-move/src/unit_test.rs b/crates/sui-move/src/unit_test.rs index f72300ae6ab5f..d60125b5dd069 100644 --- a/crates/sui-move/src/unit_test.rs +++ b/crates/sui-move/src/unit_test.rs @@ -69,6 +69,7 @@ static TEST_STORE: Lazy = Lazy::new(|| { TemporaryStore::new( InMemoryStorage::new(vec![]), InputObjects::new(vec![]), + vec![], TransactionDigest::random(), &ProtocolConfig::get_for_min_version(), ) @@ -119,6 +120,7 @@ fn new_testing_object_and_natives_cost_runtime(ext: &mut NativeContextExtensions false, &ProtocolConfig::get_for_min_version(), metrics, + 0, // epoch id )); ext.add(NativesCostTable::from_protocol_config( &ProtocolConfig::get_for_min_version(), diff --git a/crates/sui-open-rpc/spec/openrpc.json b/crates/sui-open-rpc/spec/openrpc.json index cdb9dd611cd3e..a5d943a22e0c9 100644 --- a/crates/sui-open-rpc/spec/openrpc.json +++ b/crates/sui-open-rpc/spec/openrpc.json @@ -1365,6 +1365,7 @@ "no_extraneous_module_bytes": false, "package_digest_hash_module": false, "package_upgrades": true, + "receive_objects": false, "scoring_decision_with_validity_cutoff": true, "simplified_unwrap_then_delete": false, "txn_base_cost_as_multiplier": false, @@ -1828,6 +1829,7 @@ "transfer_freeze_object_cost_base": { "u64": "52" }, + "transfer_receive_object_cost_base": null, "transfer_share_object_cost_base": { "u64": "52" }, diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index 0bdcc917698bd..ebb0a080569f7 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -257,6 +257,10 @@ struct FeatureFlags { // A list of supported OIDC providers that can be used for zklogin. #[serde(skip_serializing_if = "is_empty")] zklogin_supported_providers: BTreeSet, + + // Enable receiving sent objects + #[serde(skip_serializing_if = "is_false")] + receive_objects: bool, } fn is_false(b: &bool) -> bool { @@ -621,6 +625,9 @@ pub struct ProtocolConfig { transfer_freeze_object_cost_base: Option, // Cost params for the Move native function `share_object(obj: T)` transfer_share_object_cost_base: Option, + // Cost params for the Move native function + // `receive_object(p: &mut UID, recv: ReceivingT)` + transfer_receive_object_cost_base: Option, // TxContext // Cost params for the Move native function `transfer_impl(obj: T, recipient: address)` @@ -759,6 +766,17 @@ impl ProtocolConfig { } } + pub fn check_receiving_objects_supported(&self) -> Result<(), Error> { + if self.feature_flags.receive_objects { + Ok(()) + } else { + Err(Error(format!( + "receiving objects is not support at {:?}", + self.version + ))) + } + } + pub fn package_upgrades_supported(&self) -> bool { self.feature_flags.package_upgrades } @@ -1096,6 +1114,7 @@ impl ProtocolConfig { transfer_freeze_object_cost_base: Some(52), // Cost params for the Move native function `share_object(obj: T)` transfer_share_object_cost_base: Some(52), + transfer_receive_object_cost_base: None, // `tx_context` module // Cost params for the Move native function `transfer_impl(obj: T, recipient: address)` @@ -1385,6 +1404,12 @@ impl ProtocolConfig { "Twitch".to_string(), ]); } + + // TODO(tzakian)[tto] This should only be set in the protocol version that we + // release with. + cfg.transfer_receive_object_cost_base = Some(52); + cfg.feature_flags.receive_objects = true; + cfg } // Use this template when making changes: diff --git a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Mainnet_version_21.snap b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Mainnet_version_21.snap index c395c6a2eb9c0..9a986ca244fa8 100644 --- a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Mainnet_version_21.snap +++ b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Mainnet_version_21.snap @@ -23,6 +23,7 @@ feature_flags: simplified_unwrap_then_delete: true upgraded_multisig_supported: true txn_base_cost_as_multiplier: true + receive_objects: true max_tx_size_bytes: 131072 max_input_objects: 2048 max_size_written_objects: 5000000 @@ -123,6 +124,7 @@ object_record_new_uid_cost_base: 52 transfer_transfer_internal_cost_base: 52 transfer_freeze_object_cost_base: 52 transfer_share_object_cost_base: 52 +transfer_receive_object_cost_base: 52 tx_context_derive_id_cost_base: 52 types_is_one_time_witness_cost_base: 52 types_is_one_time_witness_type_tag_cost_per_byte: 2 diff --git a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Testnet_version_21.snap b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Testnet_version_21.snap index 19901cd46d813..90b527b93685a 100644 --- a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Testnet_version_21.snap +++ b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__Testnet_version_21.snap @@ -28,6 +28,7 @@ feature_flags: - Facebook - Google - Twitch + receive_objects: true max_tx_size_bytes: 131072 max_input_objects: 2048 max_size_written_objects: 5000000 @@ -128,6 +129,7 @@ object_record_new_uid_cost_base: 52 transfer_transfer_internal_cost_base: 52 transfer_freeze_object_cost_base: 52 transfer_share_object_cost_base: 52 +transfer_receive_object_cost_base: 52 tx_context_derive_id_cost_base: 52 types_is_one_time_witness_cost_base: 52 types_is_one_time_witness_type_tag_cost_per_byte: 2 diff --git a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_21.snap b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_21.snap index 2253b7dfd0807..9f6d52c5c87fe 100644 --- a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_21.snap +++ b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_21.snap @@ -29,6 +29,7 @@ feature_flags: - Facebook - Google - Twitch + receive_objects: true max_tx_size_bytes: 131072 max_input_objects: 2048 max_size_written_objects: 5000000 @@ -129,6 +130,7 @@ object_record_new_uid_cost_base: 52 transfer_transfer_internal_cost_base: 52 transfer_freeze_object_cost_base: 52 transfer_share_object_cost_base: 52 +transfer_receive_object_cost_base: 52 tx_context_derive_id_cost_base: 52 types_is_one_time_witness_cost_base: 52 types_is_one_time_witness_type_tag_cost_per_byte: 2 diff --git a/crates/sui-replay/src/replay.rs b/crates/sui-replay/src/replay.rs index 71fbebc51eb5e..0fe803fe66ab1 100644 --- a/crates/sui-replay/src/replay.rs +++ b/crates/sui-replay/src/replay.rs @@ -471,11 +471,18 @@ impl LocalExec { &mut self, tx_digest: &TransactionDigest, input_objects: InputObjects, + receiving_objects: Vec, protocol_config: &ProtocolConfig, ) -> TemporaryStore { // Wrap `&mut self` in an `Arc` because of `TemporaryStore`'s interface, not because it will // be shared across multiple threads - TemporaryStore::new(Arc::new(self), input_objects, *tx_digest, protocol_config) + TemporaryStore::new( + Arc::new(self), + input_objects, + receiving_objects, + *tx_digest, + protocol_config, + ) } pub async fn multi_download_and_store( @@ -711,6 +718,8 @@ impl LocalExec { let metrics = self.metrics.clone(); + let receiving_objects = tx_info.kind.receiving_objects()?; + // Extract the epoch start timestamp let (epoch_start_timestamp, rgp) = self .get_epoch_start_timestamp_and_rgp(tx_info.executed_epoch) @@ -719,8 +728,12 @@ impl LocalExec { let ov = self.executor_version_override; // Temp store for data - let temporary_store = - self.to_temporary_store(tx_digest, InputObjects::new(input_objects), protocol_config); + let temporary_store = self.to_temporary_store( + tx_digest, + InputObjects::new(input_objects), + receiving_objects, + protocol_config, + ); // We could probably cache the executor per protocol config let executor = get_executor(ov, protocol_config, expensive_safety_check_config); @@ -1708,6 +1721,48 @@ impl ChildObjectResolver for LocalExec { ); res } + + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + _epoch_id: EpochId, + ) -> SuiResult> { + fn inner( + self_: &LocalExec, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + ) -> SuiResult> { + let recv_object = match self_.get_object(receiving_object_id)? { + None => return Ok(None), + Some(o) => o, + }; + if recv_object.version() != receive_object_at_version { + return Err(SuiError::Unknown(format!( + "Invariant Violation. Replay loaded child_object {receiving_object_id} at version \ + {receive_object_at_version} but expected the version to be == {receive_object_at_version}" + ))); + } + if recv_object.owner != Owner::AddressOwner((*owner).into()) { + return Ok(None); + } + Ok(Some(recv_object)) + } + + let res = inner(self, owner, receiving_object_id, receive_object_at_version); + self.exec_store_events + .lock() + .expect("Unable to lock events list") + .push(ExecutionStoreEvent::ReceiveObject { + owner: *owner, + receive: *receiving_object_id, + receive_at_version: receive_object_at_version, + result: res.clone(), + }); + res + } } impl ParentSync for LocalExec { diff --git a/crates/sui-replay/src/types.rs b/crates/sui-replay/src/types.rs index 6b9d815905450..0d1624ec7ebff 100644 --- a/crates/sui-replay/src/types.rs +++ b/crates/sui-replay/src/types.rs @@ -276,4 +276,10 @@ pub enum ExecutionStoreEvent { id: ModuleId, result: SuiResult>, }, + ReceiveObject { + owner: ObjectID, + receive: ObjectID, + receive_at_version: SequenceNumber, + result: SuiResult>, + }, } diff --git a/crates/sui-storage/src/indexes.rs b/crates/sui-storage/src/indexes.rs index 89228e2d766e6..ee3d7d2881fe6 100644 --- a/crates/sui-storage/src/indexes.rs +++ b/crates/sui-storage/src/indexes.rs @@ -16,6 +16,7 @@ use move_core_types::language_storage::{ModuleId, StructTag, TypeTag}; use prometheus::{register_int_counter_with_registry, IntCounter, Registry}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::collections::BTreeMap; +use sui_types::execution::LoadedRuntimeObjectMetadata; use tokio::sync::OwnedMutexGuard; use crate::mutex_table::MutexTable; @@ -459,7 +460,7 @@ impl IndexStore { digest: &TransactionDigest, timestamp_ms: u64, tx_coins: Option, - loaded_child_objects: &BTreeMap, + loaded_child_objects: &BTreeMap, ) -> SuiResult { let sequence = self.next_sequence_number.fetch_add(1, Ordering::SeqCst); let mut batch = self.tables.transactions_from_addr.batch(); @@ -603,7 +604,10 @@ impl IndexStore { )?; // Loaded child objects table - let loaded_child_objects: Vec<_> = loaded_child_objects.clone().into_iter().collect(); + let loaded_child_objects: Vec<_> = loaded_child_objects + .iter() + .map(|(oid, meta)| (*oid, meta.version)) + .collect(); batch.insert_batch( &self.tables.loaded_child_object_versions, std::iter::once((*digest, loaded_child_objects)), diff --git a/crates/sui-swarm-config/src/network_config_builder.rs b/crates/sui-swarm-config/src/network_config_builder.rs index 4d1ad48b9b6a9..7d49601266291 100644 --- a/crates/sui-swarm-config/src/network_config_builder.rs +++ b/crates/sui-swarm-config/src/network_config_builder.rs @@ -394,6 +394,7 @@ mod test { let temporary_store = TemporaryStore::new( InMemoryStorage::new(Vec::new()), InputObjects::new(vec![]), + vec![], genesis_digest, &protocol_config, ); diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__network_config_snapshot_matches.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__network_config_snapshot_matches.snap index b8a385114f9f8..07ea9487b823f 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__network_config_snapshot_matches.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__network_config_snapshot_matches.snap @@ -78,6 +78,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: @@ -164,6 +165,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: @@ -250,6 +252,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: @@ -336,6 +339,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: @@ -422,6 +426,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: @@ -508,6 +513,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: @@ -594,6 +600,7 @@ validator_configs: package_upgrade_disabled: false shared_object_disabled: false user_transaction_disabled: false + receiving_objects_disabled: false certificate-deny-config: {} state-debug-dump-config: {} state-archive-write-config: diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap index 5aebc794bc8f1..2d209baeb8371 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap @@ -240,13 +240,13 @@ validators: next_epoch_worker_address: ~ extra_fields: id: - id: "0x34354cae1d54edd0cba046058b262c9b242e239e0dc7f42e1d6ea021e811f740" + id: "0xe9f7b8b9944a1b932d6a125c3b5651c3d42f4091f02b60ff23de702d108544b9" size: 0 voting_power: 10000 - operation_cap_id: "0x8cf1b049e1afe36ab530cd66e6bb41d2214fed8071f1dfe203fa2605467fa1c0" + operation_cap_id: "0xb921e7ea556941c2c63e17ac303ed827ab2f77d3ff32eab792d50e10a84affcd" gas_price: 1000 staking_pool: - id: "0xd97bea2763e3dbaa450f6eb9cda394677ea700df2389f309bad76fb81528aedd" + id: "0x30e9d6448379bbe3733c6a51aa46534ac535113b48b059d3c689cbc8c9366d23" activation_epoch: 0 deactivation_epoch: ~ sui_balance: 20000000000000000 @@ -254,14 +254,14 @@ validators: value: 0 pool_token_balance: 20000000000000000 exchange_rates: - id: "0xe1148394617093c5049c5d87b9138dbaaea72380cc143660263123e2a430084b" + id: "0x30ddea3aa4c581bbabb652ae66fe31060745241bb8636a8aa17441f3f2de8365" size: 1 pending_stake: 0 pending_total_sui_withdraw: 0 pending_pool_token_withdraw: 0 extra_fields: id: - id: "0xf869cdd89bc70d80193f91539c4d645c8b27a32e2440cfe8e1d663146b3f8068" + id: "0xbddf411362a415220acb56f92930f4f0f46255b2215c79216d6a66240c642eb9" size: 0 commission_rate: 200 next_epoch_stake: 20000000000000000 @@ -269,27 +269,27 @@ validators: next_epoch_commission_rate: 200 extra_fields: id: - id: "0xa6d91235dd4b913c643a3bb5eab2252dad840be1502b27dec03a1ef236cb515c" + id: "0x6b88dc21e2400a1846ac81f214b82380ed7131b642379e8238d1ba4a56cdedcc" size: 0 pending_active_validators: contents: - id: "0x076536eca5b44d5808907f1211863d36aad01b39d13b6a528a089e3a73d05b71" + id: "0x8dc6038eddbf8a37c6a655b4b62c871e43418bbd23c3a1e83055a668a3093672" size: 0 pending_removals: [] staking_pool_mappings: - id: "0x1b5528195e2a3a8915f69f0aae91bf785638b1377d6ed314967153ed96f4f623" + id: "0x5f1a31a2061e998f97f2925e76dcda6e146752b7698366de930f3084abea4337" size: 1 inactive_validators: - id: "0x49a1072cd1285c38cd23de94e6bbf36439d3f177ebbd310f237b97cbb7e1bb7b" + id: "0x5ba44658f532ada887c3f5577902524683b76c5137c9125362c72edf519b715f" size: 0 validator_candidates: - id: "0x811b308c075eda48aac81068038ac1622c3c96fa28f3bd11e2b563cf58905dd8" + id: "0x4217bd6cf2a0a448f14cfc40337735b02a72d9943ddbc15c0b63af7224a3b6b5" size: 0 at_risk_validators: contents: [] extra_fields: id: - id: "0x09a37b6f5ee6d7d183ff5c017f072f7f2bc59b8e60b7dba9ff90b7c6c5a14d28" + id: "0x6648b14062e23af1affbff9acc44820392f0f66dd14c73d9eb28550d851faa85" size: 0 storage_fund: total_object_storage_rebates: @@ -306,7 +306,7 @@ parameters: validator_low_stake_grace_period: 7 extra_fields: id: - id: "0x651add50db127d2fbc7baf22a2363fef2cea973238412a06870e67ccb116f087" + id: "0xbb30eb42238341d86fbcedc69a0124222a608fab3e4ee027768f928de979c911" size: 0 reference_gas_price: 1000 validator_report_records: @@ -320,7 +320,7 @@ stake_subsidy: stake_subsidy_decrease_rate: 1000 extra_fields: id: - id: "0x15a2829fbc34736822fdcd7df59acd0e4c4a5950e7866e470b9e845fcc825306" + id: "0x5e01b979248d7a6179e3eaf97e744e97e805e01e3f6c872c9d99aa685f921e79" size: 0 safe_mode: false safe_mode_storage_rewards: @@ -332,6 +332,6 @@ safe_mode_non_refundable_storage_fee: 0 epoch_start_timestamp_ms: 10 extra_fields: id: - id: "0x321d0a93ed6dab62858d99d4b45cc8c9ee57ed77e7c7bb58d1032b106c352d35" + id: "0x8ad66654d82f0e3c814f087ab4ae2ccc05ab21840eb0e32f66b6cc4d4b946372" size: 0 diff --git a/crates/sui-transactional-test-runner/src/args.rs b/crates/sui-transactional-test-runner/src/args.rs index e9694815be52d..6a8d274e4e440 100644 --- a/crates/sui-transactional-test-runner/src/args.rs +++ b/crates/sui-transactional-test-runner/src/args.rs @@ -13,7 +13,7 @@ use move_symbol_pool::Symbol; use move_transactional_test_runner::tasks::SyntaxChoice; use sui_types::base_types::{SequenceNumber, SuiAddress}; use sui_types::move_package::UpgradePolicy; -use sui_types::object::Owner; +use sui_types::object::{Object, Owner}; use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; use sui_types::storage::ObjectStore; use sui_types::transaction::{Argument, CallArg, ObjectArg}; @@ -163,6 +163,7 @@ pub enum SuiSubcommand { pub enum SuiExtraValueArgs { Object(FakeID, Option), Digest(String), + Receiving(FakeID, Option), } pub enum SuiValue { @@ -170,14 +171,41 @@ pub enum SuiValue { Object(FakeID, Option), ObjVec(Vec<(FakeID, Option)>), Digest(String), + Receiving(FakeID, Option), } impl SuiExtraValueArgs { fn parse_object_value<'a, I: Iterator>( parser: &mut MoveCLParser<'a, ValueToken, I>, ) -> anyhow::Result { + let (fake_id, version) = Self::parse_receiving_or_object_value(parser, "object")?; + Ok(SuiExtraValueArgs::Object(fake_id, version)) + } + + fn parse_receiving_value<'a, I: Iterator>( + parser: &mut MoveCLParser<'a, ValueToken, I>, + ) -> anyhow::Result { + let (fake_id, version) = Self::parse_receiving_or_object_value(parser, "receiving")?; + Ok(SuiExtraValueArgs::Receiving(fake_id, version)) + } + + fn parse_digest_value<'a, I: Iterator>( + parser: &mut MoveCLParser<'a, ValueToken, I>, + ) -> anyhow::Result { + let contents = parser.advance(ValueToken::Ident)?; + ensure!(contents == "digest"); + parser.advance(ValueToken::LParen)?; + let package = parser.advance(ValueToken::Ident)?; + parser.advance(ValueToken::RParen)?; + Ok(SuiExtraValueArgs::Digest(package.to_owned())) + } + + fn parse_receiving_or_object_value<'a, I: Iterator>( + parser: &mut MoveCLParser<'a, ValueToken, I>, + ident_name: &str, + ) -> anyhow::Result<(FakeID, Option)> { let contents = parser.advance(ValueToken::Ident)?; - ensure!(contents == "object"); + ensure!(contents == ident_name); parser.advance(ValueToken::LParen)?; let i_str = parser.advance(ValueToken::Number)?; let (i, _) = parse_u256(i_str)?; @@ -204,18 +232,7 @@ impl SuiExtraValueArgs { } else { None }; - Ok(SuiExtraValueArgs::Object(fake_id, version)) - } - - fn parse_digest_value<'a, I: Iterator>( - parser: &mut MoveCLParser<'a, ValueToken, I>, - ) -> anyhow::Result { - let contents = parser.advance(ValueToken::Ident)?; - ensure!(contents == "digest"); - parser.advance(ValueToken::LParen)?; - let package = parser.advance(ValueToken::Ident)?; - parser.advance(ValueToken::RParen)?; - Ok(SuiExtraValueArgs::Digest(package.to_owned())) + Ok((fake_id, version)) } } @@ -226,6 +243,7 @@ impl SuiValue { SuiValue::Object(_, _) => panic!("unexpected nested Sui object in args"), SuiValue::ObjVec(_) => panic!("unexpected nested Sui object vector in args"), SuiValue::Digest(_) => panic!("unexpected nested Sui package digest in args"), + SuiValue::Receiving(_, _) => panic!("unexpected nested Sui receiving object in args"), } } @@ -235,14 +253,15 @@ impl SuiValue { SuiValue::Object(id, version) => (id, version), SuiValue::ObjVec(_) => panic!("unexpected nested Sui object vector in args"), SuiValue::Digest(_) => panic!("unexpected nested Sui package digest in args"), + SuiValue::Receiving(_, _) => panic!("unexpected nested Sui receiving object in args"), } } - fn object_arg( + fn resolve_object( fake_id: FakeID, version: Option, test_adapter: &SuiTestAdapter, - ) -> anyhow::Result { + ) -> anyhow::Result { let id = match test_adapter.fake_to_real_object_id(fake_id) { Some(id) => id, None => bail!("INVALID TEST. Unknown object, object({})", fake_id), @@ -256,6 +275,25 @@ impl SuiValue { Ok(Some(obj)) => obj, Err(_) | Ok(None) => bail!("INVALID TEST. Could not load object argument {}", id), }; + Ok(obj) + } + + fn receiving_arg( + fake_id: FakeID, + version: Option, + test_adapter: &SuiTestAdapter, + ) -> anyhow::Result { + let obj = Self::resolve_object(fake_id, version, test_adapter)?; + Ok(ObjectArg::Receiving(obj.compute_object_reference())) + } + + fn object_arg( + fake_id: FakeID, + version: Option, + test_adapter: &SuiTestAdapter, + ) -> anyhow::Result { + let obj = Self::resolve_object(fake_id, version, test_adapter)?; + let id = obj.id(); match obj.owner { Owner::Shared { initial_shared_version, @@ -277,6 +315,9 @@ impl SuiValue { CallArg::Object(Self::object_arg(fake_id, version, test_adapter)?) } SuiValue::MoveValue(v) => CallArg::Pure(v.simple_serialize().unwrap()), + SuiValue::Receiving(fake_id, version) => { + CallArg::Object(Self::receiving_arg(fake_id, version, test_adapter)?) + } SuiValue::ObjVec(_) => bail!("obj vec is not supported as an input"), SuiValue::Digest(pkg) => { let pkg = Symbol::from(pkg); @@ -316,6 +357,7 @@ impl ParsableValue for SuiExtraValueArgs { match parser.peek()? { (ValueToken::Ident, "object") => Some(Self::parse_object_value(parser)), (ValueToken::Ident, "digest") => Some(Self::parse_digest_value(parser)), + (ValueToken::Ident, "receiving") => Some(Self::parse_receiving_value(parser)), _ => None, } } @@ -359,6 +401,7 @@ impl ParsableValue for SuiExtraValueArgs { match self { SuiExtraValueArgs::Object(id, version) => Ok(SuiValue::Object(id, version)), SuiExtraValueArgs::Digest(pkg) => Ok(SuiValue::Digest(pkg)), + SuiExtraValueArgs::Receiving(id, version) => Ok(SuiValue::Receiving(id, version)), } } } diff --git a/crates/sui-transactional-test-runner/src/test_adapter.rs b/crates/sui-transactional-test-runner/src/test_adapter.rs index a7acd16d5e753..83f40f81dbbf6 100644 --- a/crates/sui-transactional-test-runner/src/test_adapter.rs +++ b/crates/sui-transactional-test-runner/src/test_adapter.rs @@ -947,6 +947,7 @@ impl<'a> MoveTestAdapter<'a> for SuiTestAdapter<'a> { } SuiValue::Digest(_) => bail!("digest is not supported as an input"), SuiValue::ObjVec(_) => bail!("obj vec is not supported as an input"), + SuiValue::Receiving(_, _) => bail!("receiving is not supported as an input"), }; let value = NumericalAddress::new(value.into_bytes(), NumberFormat::Hex); self.compiled_state diff --git a/crates/sui-types/src/execution.rs b/crates/sui-types/src/execution.rs index 507ec53d9f46c..982eb275d6ea5 100644 --- a/crates/sui-types/src/execution.rs +++ b/crates/sui-types/src/execution.rs @@ -15,11 +15,12 @@ use serde::Deserialize; use crate::{ base_types::{ObjectID, SequenceNumber, SuiAddress}, coin::Coin, - digests::ObjectDigest, + digests::{ObjectDigest, TransactionDigest}, error::{ExecutionError, ExecutionErrorKind, SuiError}, execution_status::CommandArgumentError, object::Owner, storage::{BackingPackageStore, ChildObjectResolver, ObjectChange, StorageView}, + transfer::Receiving, }; pub trait SuiResolver: @@ -74,18 +75,25 @@ pub struct ExecutionResults { } #[derive(Clone, Debug)] -pub struct InputObjectMetadata { - pub id: ObjectID, - pub is_mutable_input: bool, - pub owner: Owner, - pub version: SequenceNumber, +pub enum InputObjectMetadata { + Receiving { + id: ObjectID, + version: SequenceNumber, + }, + InputObject { + id: ObjectID, + is_mutable_input: bool, + owner: Owner, + version: SequenceNumber, + }, } -#[derive(Debug, PartialEq, Eq)] -pub struct LoadedChildObjectMetadata { +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LoadedRuntimeObjectMetadata { pub version: SequenceNumber, pub digest: ObjectDigest, pub storage_rebate: u64, + pub previous_transaction: TransactionDigest, } #[derive(Clone, Debug)] @@ -115,6 +123,7 @@ pub enum UsageKind { pub enum Value { Object(ObjectValue), Raw(RawValueType, Vec), + Receiving(ObjectID, SequenceNumber), } #[derive(Debug, Clone)] @@ -159,6 +168,22 @@ pub enum CommandKind<'a> { Upgrade, } +impl InputObjectMetadata { + pub fn id(&self) -> ObjectID { + match self { + InputObjectMetadata::Receiving { id, .. } => *id, + InputObjectMetadata::InputObject { id, .. } => *id, + } + } + + pub fn version(&self) -> SequenceNumber { + match self { + InputObjectMetadata::Receiving { version, .. } => *version, + InputObjectMetadata::InputObject { version, .. } => *version, + } + } +} + impl InputValue { pub fn new_object(object_metadata: InputObjectMetadata, value: ObjectValue) -> Self { InputValue { @@ -173,6 +198,13 @@ impl InputValue { inner: ResultValue::new(Value::Raw(ty, value)), } } + + pub fn new_receiving_object(id: ObjectID, version: SequenceNumber) -> Self { + InputValue { + object_metadata: Some(InputObjectMetadata::Receiving { id, version }), + inner: ResultValue::new(Value::Receiving(id, version)), + } + } } impl ResultValue { @@ -190,6 +222,7 @@ impl Value { Value::Object(_) => false, Value::Raw(RawValueType::Any, _) => true, Value::Raw(RawValueType::Loaded { abilities, .. }, _) => abilities.has_copy(), + Value::Receiving(_, _) => false, } } @@ -197,6 +230,9 @@ impl Value { match self { Value::Object(obj_value) => obj_value.write_bcs_bytes(buf), Value::Raw(_, bytes) => buf.extend(bytes), + Value::Receiving(id, version) => { + buf.extend(&Receiving::new(*id, *version).to_bcs_bytes()) + } } } @@ -213,6 +249,9 @@ impl Value { }, _, ) => *used_in_non_entry_move_call, + // Only thing you can do with a `Receiving` is consume it, so once it's used it + // can't be used again. + Value::Receiving(_, _) => false, } } } @@ -260,6 +299,7 @@ impl TryFromValue for ObjectValue { Value::Object(o) => Ok(o), Value::Raw(RawValueType::Any, _) => Err(CommandArgumentError::TypeMismatch), Value::Raw(RawValueType::Loaded { .. }, _) => Err(CommandArgumentError::TypeMismatch), + Value::Receiving(_, _) => Err(CommandArgumentError::TypeMismatch), } } } @@ -282,6 +322,7 @@ fn try_from_value_prim<'a, T: Deserialize<'a>>( ) -> Result { match value { Value::Object(_) => Err(CommandArgumentError::TypeMismatch), + Value::Receiving(_, _) => Err(CommandArgumentError::TypeMismatch), Value::Raw(RawValueType::Any, bytes) => { bcs::from_bytes(bytes).map_err(|_| CommandArgumentError::InvalidBCSBytes) } diff --git a/crates/sui-types/src/execution_mode.rs b/crates/sui-types/src/execution_mode.rs index ffbd302cdfced..20e4610a9d114 100644 --- a/crates/sui-types/src/execution_mode.rs +++ b/crates/sui-types/src/execution_mode.rs @@ -7,6 +7,7 @@ use crate::{ error::ExecutionError, execution::{RawValueType, Value}, transaction::Argument, + transfer::Receiving, type_resolver::TypeTagResolver, }; @@ -256,6 +257,10 @@ fn value_to_bytes_and_tag( let tag = resolver.get_type_tag(ty)?; (tag, bytes.clone()) } + Value::Receiving(id, seqno) => ( + Receiving::type_tag(), + Receiving::new(*id, *seqno).to_bcs_bytes(), + ), }; Ok((bytes, type_tag)) } diff --git a/crates/sui-types/src/in_memory_storage.rs b/crates/sui-types/src/in_memory_storage.rs index 5d99e33f99038..c40b097f22d54 100644 --- a/crates/sui-types/src/in_memory_storage.rs +++ b/crates/sui-types/src/in_memory_storage.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::base_types::VersionNumber; +use crate::committee::EpochId; use crate::storage::get_module_by_id; use crate::{ base_types::{ObjectID, ObjectRef, SequenceNumber}, @@ -59,6 +60,27 @@ impl ChildObjectResolver for InMemoryStorage { } Ok(Some(child_object)) } + + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + _epoch_id: EpochId, + ) -> SuiResult> { + let recv_object = match self.persistent.get(receiving_object_id).cloned() { + None => return Ok(None), + Some(obj) => obj, + }; + if recv_object.owner != Owner::AddressOwner((*owner).into()) { + return Ok(None); + } + + if recv_object.version() != receive_object_at_version { + return Ok(None); + } + Ok(Some(recv_object)) + } } impl ParentSync for InMemoryStorage { diff --git a/crates/sui-types/src/lib.rs b/crates/sui-types/src/lib.rs index 3c9c4396d0642..48958eff84460 100644 --- a/crates/sui-types/src/lib.rs +++ b/crates/sui-types/src/lib.rs @@ -66,6 +66,7 @@ pub mod sui_serde; pub mod sui_system_state; pub mod temporary_store; pub mod transaction; +pub mod transfer; pub mod type_resolver; pub mod versioned; pub mod zk_login_authenticator; @@ -196,7 +197,7 @@ pub fn is_primitive( S::StructInstantiation(idx, targs) => { let resolved_struct = resolve_struct(view, *idx); - // is option of a primitive + // option is a primitive resolved_struct == RESOLVED_STD_OPTION && targs.len() == 1 && is_primitive(view, function_type_args, &targs[0]) diff --git a/crates/sui-types/src/storage.rs b/crates/sui-types/src/storage.rs index e570cf5576636..b2a8789ebffba 100644 --- a/crates/sui-types/src/storage.rs +++ b/crates/sui-types/src/storage.rs @@ -8,7 +8,7 @@ use crate::digests::{ }; use crate::effects::{TransactionEffects, TransactionEvents}; use crate::error::SuiError; -use crate::execution::LoadedChildObjectMetadata; +use crate::execution::LoadedRuntimeObjectMetadata; use crate::message_envelope::Message; use crate::messages_checkpoint::{ CheckpointContents, CheckpointSequenceNumber, FullCheckpointContents, VerifiedCheckpoint, @@ -112,12 +112,26 @@ impl StorageView for T {} /// An abstraction of the (possibly distributed) store for objects. This /// API only allows for the retrieval of objects, not any state changes pub trait ChildObjectResolver { + /// `child` must have an `ObjectOwner` ownership equal to `owner`. fn read_child_object( &self, parent: &ObjectID, child: &ObjectID, child_version_upper_bound: SequenceNumber, ) -> SuiResult>; + + /// `receiving_object_id` must have an `AddressOwner` ownership equal to `owner`. + /// `get_object_received_at_version` must be the exact version at which the object will be received, + /// and it cannot have been previously received at that version. NB: An object not existing at + /// that version, and not having valid access to the object will be treated exactly the same + /// and `Ok(None)` must be returned. + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult>; } /// An abstraction of the (possibly distributed) store for objects, and (soon) events and transactions @@ -131,9 +145,9 @@ pub trait Storage { fn apply_object_changes(&mut self, changes: BTreeMap); - fn save_loaded_child_objects( + fn save_loaded_runtime_objects( &mut self, - loaded_child_objects: BTreeMap, + loaded_runtime_objects: BTreeMap, ); } @@ -269,6 +283,21 @@ impl ChildObjectResolver for std::sync::Arc { child_version_upper_bound, ) } + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult> { + ChildObjectResolver::get_object_received_at_version( + self.as_ref(), + owner, + receiving_object_id, + receive_object_at_version, + epoch_id, + ) + } } impl ChildObjectResolver for &S { @@ -280,6 +309,21 @@ impl ChildObjectResolver for &S { ) -> SuiResult> { ChildObjectResolver::read_child_object(*self, parent, child, child_version_upper_bound) } + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult> { + ChildObjectResolver::get_object_received_at_version( + *self, + owner, + receiving_object_id, + receive_object_at_version, + epoch_id, + ) + } } impl ChildObjectResolver for &mut S { @@ -291,6 +335,21 @@ impl ChildObjectResolver for &mut S { ) -> SuiResult> { ChildObjectResolver::read_child_object(*self, parent, child, child_version_upper_bound) } + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult> { + ChildObjectResolver::get_object_received_at_version( + *self, + owner, + receiving_object_id, + receive_object_at_version, + epoch_id, + ) + } } pub trait ReadStore { diff --git a/crates/sui-types/src/temporary_store.rs b/crates/sui-types/src/temporary_store.rs index c02f3f47d29b2..f5a5fbb291260 100644 --- a/crates/sui-types/src/temporary_store.rs +++ b/crates/sui-types/src/temporary_store.rs @@ -3,7 +3,7 @@ use crate::committee::EpochId; use crate::effects::{TransactionEffects, TransactionEvents}; -use crate::execution::LoadedChildObjectMetadata; +use crate::execution::LoadedRuntimeObjectMetadata; use crate::execution_status::ExecutionStatus; use crate::storage::{DeleteKindWithOldVersion, ObjectStore}; use crate::sui_system_state::{get_sui_system_state_wrapper, AdvanceEpochParams}; @@ -31,9 +31,7 @@ use move_core_types::account_address::AccountAddress; use move_core_types::language_storage::{ModuleId, StructTag}; use move_core_types::resolver::{ModuleResolver, ResourceResolver}; use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::sync::Arc; use sui_protocol_config::ProtocolConfig; @@ -41,8 +39,7 @@ pub type WrittenObjects = BTreeMap; pub type ObjectMap = BTreeMap; pub type TxCoins = (ObjectMap, WrittenObjects); -#[serde_as] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InnerTemporaryStore { pub objects: ObjectMap, pub mutable_inputs: Vec, @@ -51,7 +48,7 @@ pub struct InnerTemporaryStore { // deleted or wrapped or unwrap-then-delete. The sequence number should have been updated to // the lamport version. pub deleted: BTreeMap, - pub loaded_child_objects: BTreeMap, + pub loaded_runtime_objects: BTreeMap, pub events: TransactionEvents, pub max_binary_format_version: u32, pub no_extraneous_module_bytes: bool, @@ -135,9 +132,8 @@ pub struct TemporaryStore<'backing> { written: BTreeMap, // Objects written /// Objects actively deleted. deleted: BTreeMap, - /// Child objects loaded during dynamic field opers - /// Currently onply populated for full nodes, not for validators - loaded_child_objects: BTreeMap, + /// Objects that were loaded during execution (dynamic fields + received objects). + loaded_runtime_objects: BTreeMap, /// Ordered sequence of events emitted by execution events: Vec, protocol_config: ProtocolConfig, @@ -153,11 +149,12 @@ impl<'backing> TemporaryStore<'backing> { pub fn new( store: Arc, input_objects: InputObjects, + receiving_objects: Vec, tx_digest: TransactionDigest, protocol_config: &ProtocolConfig, ) -> Self { let mutable_inputs = input_objects.mutable_inputs(); - let lamport_timestamp = input_objects.lamport_timestamp(); + let lamport_timestamp = input_objects.lamport_timestamp(&receiving_objects); let objects = input_objects.into_object_map(); Self { store, @@ -169,7 +166,7 @@ impl<'backing> TemporaryStore<'backing> { deleted: BTreeMap::new(), events: Vec::new(), protocol_config: protocol_config.clone(), - loaded_child_objects: BTreeMap::new(), + loaded_runtime_objects: BTreeMap::new(), runtime_packages_loaded_from_db: RwLock::new(BTreeMap::new()), } } @@ -199,7 +196,7 @@ impl<'backing> TemporaryStore<'backing> { deleted: BTreeMap::new(), events: Vec::new(), protocol_config: protocol_config.clone(), - loaded_child_objects: BTreeMap::new(), + loaded_runtime_objects: BTreeMap::new(), runtime_packages_loaded_from_db: RwLock::new(BTreeMap::new()), } } @@ -273,11 +270,7 @@ impl<'backing> TemporaryStore<'backing> { deleted, events: TransactionEvents { data: self.events }, max_binary_format_version: self.protocol_config.move_binary_format_version(), - loaded_child_objects: self - .loaded_child_objects - .into_iter() - .map(|(id, metadata)| (id, metadata.version)) - .collect(), + loaded_runtime_objects: self.loaded_runtime_objects, no_extraneous_module_bytes: self.protocol_config.no_extraneous_module_bytes(), runtime_packages_loaded_from_db: self.runtime_packages_loaded_from_db.read().clone(), } @@ -306,7 +299,7 @@ impl<'backing> TemporaryStore<'backing> { mut self, shared_object_refs: Vec, transaction_digest: &TransactionDigest, - transaction_dependencies: Vec, + transaction_dependencies: BTreeSet, gas_cost_summary: GasCostSummary, status: ExecutionStatus, gas_charger: &mut GasCharger, @@ -394,7 +387,7 @@ impl<'backing> TemporaryStore<'backing> { } else { Some(inner.events.digest()) }, - transaction_dependencies, + transaction_dependencies.into_iter().collect(), ); (inner, effects) } @@ -534,26 +527,26 @@ impl<'backing> TemporaryStore<'backing> { } } } - pub fn save_loaded_child_objects( + pub fn save_loaded_runtime_objects( &mut self, - loaded_child_objects: BTreeMap, + loaded_runtime_objects: BTreeMap, ) { #[cfg(debug_assertions)] { - for (id, v1) in &loaded_child_objects { - if let Some(v2) = self.loaded_child_objects.get(id) { + for (id, v1) in &loaded_runtime_objects { + if let Some(v2) = self.loaded_runtime_objects.get(id) { assert_eq!(v1, v2); } } - for (id, v1) in &self.loaded_child_objects { - if let Some(v2) = loaded_child_objects.get(id) { + for (id, v1) in &self.loaded_runtime_objects { + if let Some(v2) = loaded_runtime_objects.get(id) { assert_eq!(v1, v2); } } } // Merge the two maps because we may be calling the execution engine more than once // (e.g. in advance epoch transaction, where we may be publishing a new system package). - self.loaded_child_objects.extend(loaded_child_objects); + self.loaded_runtime_objects.extend(loaded_runtime_objects); } pub fn estimate_effects_size_upperbound(&self) -> usize { @@ -654,10 +647,12 @@ impl<'backing> TemporaryStore<'backing> { panic!("Mutated object must exist in the store: ID = {:?}", id) }); match &old_obj.owner { - Owner::ObjectOwner(_parent) => { + // ObjectOwner = dynamic field mutations + // AddressOwner = received object + Owner::ObjectOwner(_) | Owner::AddressOwner(_) => { objs_to_authenticate.push(*id); } - Owner::AddressOwner(_) | Owner::Shared { .. } => { + Owner::Shared { .. } => { unreachable!("Should already be in authenticated_objs") } Owner::Immutable => { @@ -687,10 +682,10 @@ impl<'backing> TemporaryStore<'backing> { // get owner at beginning of tx let old_obj = self.store.get_object(id)?.unwrap(); match &old_obj.owner { - Owner::ObjectOwner(_) => { + Owner::AddressOwner(_) | Owner::ObjectOwner(_) => { objs_to_authenticate.push(*id); } - Owner::AddressOwner(_) | Owner::Shared { .. } => { + Owner::Shared { .. } => { unreachable!("Should already be in authenticated_objs") } Owner::Immutable => unreachable!("Immutable objects cannot be deleted"), @@ -725,7 +720,7 @@ impl<'backing> TemporaryStore<'backing> { continue; }; let parent = match &old_obj.owner { - Owner::ObjectOwner(parent) => ObjectID::from(*parent), + Owner::ObjectOwner(parent) | Owner::AddressOwner(parent) => ObjectID::from(*parent), owner => panic!( "Unauthenticated root at {to_authenticate:?} with owner {owner:?}\n\ Potentially covering objects in: {covered:#?}", @@ -750,7 +745,7 @@ impl<'backing> TemporaryStore<'backing> { // A mutated object must either be from input object or child object. if let Some(old_obj) = self.input_objects.get(id) { old_obj.storage_rebate - } else if let Some(metadata) = self.loaded_child_objects.get(id) { + } else if let Some(metadata) = self.loaded_runtime_objects.get(id) { debug_assert_eq!(metadata.version, expected_version); metadata.storage_rebate } else { @@ -1103,6 +1098,25 @@ impl<'backing> ChildObjectResolver for TemporaryStore<'backing> { .read_child_object(parent, child, child_version_upper_bound) } } + + fn get_object_received_at_version( + &self, + owner: &ObjectID, + receiving_object_id: &ObjectID, + receive_object_at_version: SequenceNumber, + epoch_id: EpochId, + ) -> SuiResult> { + // You should never be able to try and receive an object after deleting it or writing it in the same + // transaction since `Receiving` doesn't have copy. + debug_assert!(self.deleted.get(receiving_object_id).is_none()); + debug_assert!(self.written.get(receiving_object_id).is_none()); + self.store.get_object_received_at_version( + owner, + receiving_object_id, + receive_object_at_version, + epoch_id, + ) + } } impl<'backing> Storage for TemporaryStore<'backing> { @@ -1124,11 +1138,11 @@ impl<'backing> Storage for TemporaryStore<'backing> { TemporaryStore::apply_object_changes(self, changes) } - fn save_loaded_child_objects( + fn save_loaded_runtime_objects( &mut self, - loaded_child_objects: BTreeMap, + loaded_runtime_objects: BTreeMap, ) { - TemporaryStore::save_loaded_child_objects(self, loaded_child_objects) + TemporaryStore::save_loaded_runtime_objects(self, loaded_runtime_objects) } } diff --git a/crates/sui-types/src/transaction.rs b/crates/sui-types/src/transaction.rs index 80caff6f7cd9f..f0dcb2751555d 100644 --- a/crates/sui-types/src/transaction.rs +++ b/crates/sui-types/src/transaction.rs @@ -96,6 +96,8 @@ pub enum ObjectArg { initial_shared_version: SequenceNumber, mutable: bool, }, + // A Move object that can be received in this transaction. + Receiving(ObjectRef), } fn type_tag_validity_check( @@ -203,7 +205,7 @@ pub enum TransactionKind { } impl VersionedProtocolMessage for TransactionKind { - fn check_version_supported(&self, _protocol_config: &ProtocolConfig) -> SuiResult { + fn check_version_supported(&self, protocol_config: &ProtocolConfig) -> SuiResult { // This code does nothing right now - it exists to cause a compiler error when new // enumerants are added to TransactionKind. // @@ -212,8 +214,19 @@ impl VersionedProtocolMessage for TransactionKind { match &self { TransactionKind::ChangeEpoch(_) | TransactionKind::Genesis(_) - | TransactionKind::ConsensusCommitPrologue(_) - | TransactionKind::ProgrammableTransaction(_) => Ok(()), + | TransactionKind::ConsensusCommitPrologue(_) => Ok(()), + TransactionKind::ProgrammableTransaction(pt) => { + // NB: we don't use the `receiving_objects` method here since we don't want to check + // for any validity requirements such as duplicate receiving inputs at this point. + let has_receiving_objects = pt + .inputs + .iter() + .any(|arg| !arg.receiving_objects().is_empty()); + if has_receiving_objects { + protocol_config.check_receiving_objects_supported()?; + } + Ok(()) + } } } } @@ -239,6 +252,19 @@ impl CallArg { mutable, }] } + // Receiving objects are not part of the input objects. + CallArg::Object(ObjectArg::Receiving(_)) => vec![], + } + } + + fn receiving_objects(&self) -> Vec { + match self { + CallArg::Pure(_) => vec![], + CallArg::Object(o) => match o { + ObjectArg::ImmOrOwnedObject(_) => vec![], + ObjectArg::SharedObject { .. } => vec![], + ObjectArg::Receiving(obj_ref) => vec![*obj_ref], + }, } } @@ -323,7 +349,9 @@ impl ObjectArg { pub fn id(&self) -> ObjectID { match self { - ObjectArg::ImmOrOwnedObject((id, _, _)) | ObjectArg::SharedObject { id, .. } => *id, + ObjectArg::Receiving((id, _, _)) + | ObjectArg::ImmOrOwnedObject((id, _, _)) + | ObjectArg::SharedObject { id, .. } => *id, } } } @@ -635,6 +663,21 @@ impl ProgrammableTransaction { .collect()) } + fn receiving_objects(&self) -> UserInputResult> { + let ProgrammableTransaction { inputs, .. } = self; + let receiving_objects = inputs + .iter() + .flat_map(|arg| arg.receiving_objects()) + .collect::>(); + + // all objects being received must be unique + let mut used = HashSet::new(); + if !receiving_objects.iter().all(|o| used.insert(o.0)) { + return Err(UserInputError::DuplicateObjectRefInput); + } + Ok(receiving_objects) + } + fn validity_check(&self, config: &ProtocolConfig) -> UserInputResult { let ProgrammableTransaction { inputs, commands } = self; fp_ensure!( @@ -657,7 +700,9 @@ impl ProgrammableTransaction { self.inputs .iter() .filter_map(|arg| match arg { - CallArg::Pure(_) | CallArg::Object(ObjectArg::ImmOrOwnedObject(_)) => None, + CallArg::Pure(_) + | CallArg::Object(ObjectArg::Receiving(_)) + | CallArg::Object(ObjectArg::ImmOrOwnedObject(_)) => None, CallArg::Object(ObjectArg::SharedObject { id, initial_shared_version, @@ -868,6 +913,15 @@ impl TransactionKind { } } + pub fn receiving_objects(&self) -> UserInputResult> { + Ok(match &self { + TransactionKind::ChangeEpoch(_) + | TransactionKind::Genesis(_) + | TransactionKind::ConsensusCommitPrologue(_) => vec![], + TransactionKind::ProgrammableTransaction(pt) => pt.receiving_objects()?, + }) + } + /// Return the metadata of each of the input objects for the transaction. /// For a Move object, we attach the object reference; /// for a Move package, we provide the object id only since they never change on chain. @@ -1446,6 +1500,8 @@ pub trait TransactionDataAPI { fn input_objects(&self) -> UserInputResult>; + fn receiving_objects(&self) -> UserInputResult>; + fn validity_check(&self, config: &ProtocolConfig) -> UserInputResult; fn validity_check_no_gas_check(&self, config: &ProtocolConfig) -> UserInputResult; @@ -1545,6 +1601,10 @@ impl TransactionDataAPI for TransactionDataV1 { Ok(inputs) } + fn receiving_objects(&self) -> UserInputResult> { + self.kind.receiving_objects() + } + fn validity_check(&self, config: &ProtocolConfig) -> UserInputResult { fp_ensure!(!self.gas().is_empty(), UserInputError::MissingGasPayment); fp_ensure!( @@ -2151,12 +2211,14 @@ impl InputObjects { } /// The version to set on objects created by the computation that `self` is input to. - /// Guaranteed to be strictly greater than the versions of all input objects. - pub fn lamport_timestamp(&self) -> SequenceNumber { + /// Guaranteed to be strictly greater than the versions of all input objects and objects + /// received in the transaction. + pub fn lamport_timestamp(&self, receiving_objects: &[ObjectRef]) -> SequenceNumber { let input_versions = self .objects .iter() - .filter_map(|(_, object)| object.data.try_as_move().map(MoveObject::version)); + .filter_map(|(_, object)| object.data.try_as_move().map(MoveObject::version)) + .chain(receiving_objects.iter().map(|object_ref| object_ref.1)); SequenceNumber::lamport_increment(input_versions) } diff --git a/crates/sui-types/src/transfer.rs b/crates/sui-types/src/transfer.rs new file mode 100644 index 0000000000000..91a7b210cb6d3 --- /dev/null +++ b/crates/sui-types/src/transfer.rs @@ -0,0 +1,74 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::{binary_views::BinaryIndexedView, file_format::SignatureToken}; +use move_bytecode_utils::resolve_struct; +use move_core_types::{ + account_address::AccountAddress, + ident_str, + identifier::IdentStr, + language_storage::{StructTag, TypeTag}, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + base_types::{ObjectID, SequenceNumber}, + id::ID, + SUI_FRAMEWORK_ADDRESS, +}; + +const TRANSFER_MODULE_NAME: &IdentStr = ident_str!("transfer"); +const RECEIVING_STRUCT_NAME: &IdentStr = ident_str!("Receiving"); + +pub const RESOLVED_RECEIVING_STRUCT: (&AccountAddress, &IdentStr, &IdentStr) = ( + &SUI_FRAMEWORK_ADDRESS, + TRANSFER_MODULE_NAME, + RECEIVING_STRUCT_NAME, +); + +/// Rust version of the Move sui::transfer::Receiving type +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Receiving { + pub id: ID, + pub version: SequenceNumber, +} + +impl Receiving { + pub fn new(id: ObjectID, version: SequenceNumber) -> Self { + Self { + id: ID::new(id), + version, + } + } + + pub fn to_bcs_bytes(&self) -> Vec { + bcs::to_bytes(self).expect("Value representation is owned and should always serialize") + } + + pub fn struct_tag() -> StructTag { + StructTag { + address: SUI_FRAMEWORK_ADDRESS, + module: TRANSFER_MODULE_NAME.to_owned(), + name: RECEIVING_STRUCT_NAME.to_owned(), + // TODO: this should really include the type parameters eventually when we add type + // parameters to the other polymorphic types like this. + type_params: vec![], + } + } + + pub fn type_tag() -> TypeTag { + TypeTag::Struct(Box::new(Self::struct_tag())) + } + + pub fn is_valid(view: &BinaryIndexedView<'_>, s: &SignatureToken) -> bool { + use SignatureToken as S; + match s { + S::MutableReference(inner) | S::Reference(inner) => Self::is_valid(view, inner), + S::StructInstantiation(idx, type_args) => { + let struct_tag = resolve_struct(view, *idx); + struct_tag == RESOLVED_RECEIVING_STRUCT && type_args.len() == 1 + } + _ => false, + } + } +} diff --git a/crates/sui-verifier-transactional-tests/tests/private_generics/receive_with_and_without_store.exp b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_with_and_without_store.exp new file mode 100644 index 0000000000000..70aa43bbd70e6 --- /dev/null +++ b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_with_and_without_store.exp @@ -0,0 +1,11 @@ +processed 2 tasks + +task 0 'publish'. lines 1-12: +created: object(1,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 4362400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 1 'publish'. lines 14-25: +created: object(2,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 4377600, storage_rebate: 978120, non_refundable_storage_fee: 9880 diff --git a/crates/sui-verifier-transactional-tests/tests/private_generics/receive_with_and_without_store.mvir b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_with_and_without_store.mvir new file mode 100644 index 0000000000000..c93eaa4e9a424 --- /dev/null +++ b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_with_and_without_store.mvir @@ -0,0 +1,25 @@ +//# publish +module 0x0.m { + import 0x2.transfer; + import 0x2.object; + + receive_good(m: &mut object.UID, r: transfer.Receiving): T { + let x: T; + label l0: + x = transfer.receive(move(m), move(r)); + return move(x); + } +} + +//# publish +module 0x0.m1 { + import 0x2.transfer; + import 0x2.object; + + receive_good(m: &mut object.UID, r: transfer.Receiving): T { + let x: T; + label l0: + x = transfer.receive(move(m), move(r)); + return move(x); + } +} diff --git a/crates/sui-verifier-transactional-tests/tests/private_generics/receive_without_key.exp b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_without_key.exp new file mode 100644 index 0000000000000..61c788d6086de --- /dev/null +++ b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_without_key.exp @@ -0,0 +1,5 @@ +processed 1 task + +task 0 'publish'. lines 1-12: +Error: Transaction Effects Status: Move Bytecode Verification Error. Please run the Bytecode Verifier for more information. +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: VMVerificationOrDeserializationError, source: Some(VMError { major_status: CONSTRAINT_NOT_SATISFIED, sub_status: None, message: Some("expected type with abilities [Key, ] got type actual TypeParameter(0) with incompatible abilities []"), exec_state: None, location: Module(ModuleId { address: _, name: Identifier("m2") }), indices: [(Signature, 0), (FunctionHandle, 0)], offsets: [] }), command: Some(0) } } diff --git a/crates/sui-verifier-transactional-tests/tests/private_generics/receive_without_key.mvir b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_without_key.mvir new file mode 100644 index 0000000000000..49deadeb1ab3a --- /dev/null +++ b/crates/sui-verifier-transactional-tests/tests/private_generics/receive_without_key.mvir @@ -0,0 +1,12 @@ +//# publish +module 0x0.m2 { + import 0x2.transfer; + import 0x2.object; + + receive_bad(m: &mut object.UID, r: transfer.Receiving): T { + let x: T; + label l0: + x = transfer.receive(move(m), move(r)); + return move(x); + } +} diff --git a/external-crates/move/move-compiler/src/sui_mode/mod.rs b/external-crates/move/move-compiler/src/sui_mode/mod.rs index acbad14bd64f6..2d4457786f7d2 100644 --- a/external-crates/move/move-compiler/src/sui_mode/mod.rs +++ b/external-crates/move/move-compiler/src/sui_mode/mod.rs @@ -30,6 +30,8 @@ pub const TX_CONTEXT_MODULE_NAME: Symbol = symbol!("tx_context"); pub const TX_CONTEXT_TYPE_NAME: Symbol = symbol!("TxContext"); pub const SUI_MODULE_NAME: Symbol = symbol!("sui"); pub const SUI_OTW_NAME: Symbol = symbol!("SUI"); +pub const TRANSFER_MODULE_NAME: Symbol = symbol!("transfer"); +pub const RECEIVING_TYPE_NAME: Symbol = symbol!("Receiving"); pub const SUI_SYSTEM_ADDR_NAME: Symbol = symbol!("sui_system"); pub const SUI_SYSTEM_MODULE_NAME: Symbol = symbol!("sui_system"); diff --git a/external-crates/move/move-compiler/src/sui_mode/typing.rs b/external-crates/move/move-compiler/src/sui_mode/typing.rs index dd96bb6eba5bd..5c8a138e8c17a 100644 --- a/external-crates/move/move-compiler/src/sui_mode/typing.rs +++ b/external-crates/move/move-compiler/src/sui_mode/typing.rs @@ -21,6 +21,7 @@ use crate::{ OPTION_MODULE_NAME, OPTION_TYPE_NAME, OTW_DECL_DIAG, OTW_USAGE_DIAG, SCRIPT_DIAG, STD_ADDR_NAME, SUI_ADDR_NAME, SUI_MODULE_NAME, TX_CONTEXT_MODULE_NAME, TX_CONTEXT_TYPE_NAME, UTF_MODULE_NAME, UTF_TYPE_NAME, + RECEIVING_TYPE_NAME, TRANSFER_MODULE_NAME, }, typing::{ ast as T, @@ -554,7 +555,9 @@ fn entry_param_ty( // which should give a contextual error about `MyObject` having `key`, but the instantiation // `MyObject` not having `key` due to `InnerTypeWithoutStore` not having // `store` - let is_valid = is_entry_primitive_ty(param_ty) || is_entry_object_ty(param_ty); + let is_valid = is_entry_primitive_ty(param_ty) + || is_entry_object_ty(param_ty) + || is_entry_receiving_ty(param_ty); if is_mut_clock || !is_valid { let pmsg = format!( "Invalid 'entry' parameter type for parameter '{}'", @@ -569,7 +572,7 @@ fn entry_param_ty( ) } else { "'entry' parameters must be primitives (by-value), vectors of primitives, objects \ - (by-reference or by-value), or vectors of objects" + (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value)" .to_owned() }; let emsg = format!("'{name}' was declared 'entry' here"); @@ -595,6 +598,22 @@ fn is_mut_clock(param_ty: &Type) -> bool { } } +fn is_entry_receiving_ty(param_ty: &Type) -> bool { + match ¶m_ty.value { + Type_::Ref(_, t) => is_entry_receiving_ty(t), + Type_::Apply(_, sp!(_, n), targs) + if n.is(SUI_ADDR_NAME, TRANSFER_MODULE_NAME, RECEIVING_TYPE_NAME) => + { + debug_assert!(targs.len() == 1); + // Don't care about the type parameter, just that it's a receiving type -- since it has + // a `key` requirement on the type parameter it must be an object or type checking will + // fail. + true + } + _ => false, + } +} + fn is_entry_primitive_ty(param_ty: &Type) -> bool { use BuiltinTypeName_ as B; use TypeName_ as N; diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_mut_ref_vector.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_mut_ref_vector.exp index 2905c590e419a..606e3b03050b6 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_mut_ref_vector.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_mut_ref_vector.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/generic_obj_mut_ref_vector.move:5:32 │ 5 │ public entry fun no(_: &mut vector) { - │ ----- ^ -------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ -------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'no' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_ref_vector.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_ref_vector.exp index 38fee9c59523b..323d92010ff1e 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_ref_vector.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_obj_ref_vector.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/generic_obj_ref_vector.move:4:32 │ 4 │ public entry fun no(_: &vector) { - │ ----- ^ ---------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ---------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'no' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_with_key_invalid.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_with_key_invalid.exp index 91443f5c8c0af..5e9ceb81a485a 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_with_key_invalid.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/generic_with_key_invalid.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/generic_with_key_invalid.move:6:31 │ 6 │ public entry fun t(_: option::Option) { - │ ----- ^ ----------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ----------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 't' was declared 'entry' here @@ -11,7 +11,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/generic_with_key_invalid.move:10:32 │ 10 │ public entry fun t2(_: vector>) { - │ ----- ^ ------------------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ------------------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 't2' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/immut_receiving_ref.move b/external-crates/move/move-compiler/tests/sui_mode/entry_points/immut_receiving_ref.move new file mode 100644 index 0000000000000..3af390d56a3de --- /dev/null +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/immut_receiving_ref.move @@ -0,0 +1,22 @@ +// valid, Receiving type by immut ref with object type param + +module a::m { + use sui::object; + use sui::transfer::Receiving; + + struct S has key { id: object::UID } + + public entry fun yes(_: &Receiving) { } +} + +module sui::object { + struct UID has store { + id: address, + } +} + +module sui::transfer { + struct Receiving has drop { + id: address + } +} diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/mut_receiving_ref.move b/external-crates/move/move-compiler/tests/sui_mode/entry_points/mut_receiving_ref.move new file mode 100644 index 0000000000000..fbaead9a07fa7 --- /dev/null +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/mut_receiving_ref.move @@ -0,0 +1,22 @@ +// valid, Receiving type by mut ref with object type param + +module a::m { + use sui::object; + use sui::transfer::Receiving; + + struct S has key { id: object::UID } + + public entry fun yes(_: &mut Receiving) { } +} + +module sui::object { + struct UID has store { + id: address, + } +} + +module sui::transfer { + struct Receiving has drop { + id: address + } +} diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/nested_key_generic_vector_param.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/nested_key_generic_vector_param.exp index 53cc2246edaee..5600e180cdca3 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/nested_key_generic_vector_param.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/nested_key_generic_vector_param.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/nested_key_generic_vector_param.move:3:34 │ 3 │ public entry fun foo(_: vector>) { - │ ----- ^ ----------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ----------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'foo' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct.exp index e5a5dc312487d..9c376375124aa 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/non_key_struct.move:6:25 │ 6 │ public entry fun no(_: S) { - │ ----- ^ - 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ - 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'no' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_generic.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_generic.exp index 7199dc2e4a63e..8b4c4a45d2a8d 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_generic.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_generic.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/non_key_struct_generic.move:9:25 │ 9 │ public entry fun t1(_: Obj) { - │ ----- ^ ------------ 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ------------ 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 't1' was declared 'entry' here @@ -11,7 +11,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/non_key_struct_generic.move:14:28 │ 14 │ public entry fun t2(_: Obj) { - │ ----- ^ ------ 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ------ 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 't2' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_vector.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_vector.exp index 824114c7ed797..f3d21d93e4660 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_vector.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/non_key_struct_vector.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/non_key_struct_vector.move:6:25 │ 6 │ public entry fun no(_: vector) { - │ ----- ^ --------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ --------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'no' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_mut_ref_vector.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_mut_ref_vector.exp index 135d377806dba..8a7d091e35d1f 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_mut_ref_vector.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_mut_ref_vector.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/obj_mut_ref_vector.move:8:25 │ 8 │ public entry fun no(_: &mut vector) { - │ ----- ^ -------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ -------------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'no' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_ref_vector.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_ref_vector.exp index 8f6e4cff22364..8f772bd6130fd 100644 --- a/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_ref_vector.exp +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/obj_ref_vector.exp @@ -2,7 +2,7 @@ error[Sui E02002]: invalid 'entry' function signature ┌─ tests/sui_mode/entry_points/obj_ref_vector.move:8:25 │ 8 │ public entry fun no(_: &vector) { - │ ----- ^ ---------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), or vectors of objects + │ ----- ^ ---------- 'entry' parameters must be primitives (by-value), vectors of primitives, objects (by-reference or by-value), vectors of objects, or 'Receiving' arguments (by-reference or by-value) │ │ │ │ │ Invalid 'entry' parameter type for parameter '_' │ 'no' was declared 'entry' here diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_non_object_type.exp b/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_non_object_type.exp new file mode 100644 index 0000000000000..3422c96ab49c1 --- /dev/null +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_non_object_type.exp @@ -0,0 +1,12 @@ +error[E05001]: ability constraint not satisfied + ┌─ tests/sui_mode/entry_points/receiving_non_object_type.move:6:28 + │ + 6 │ public entry fun no(_: Receiving) { abort 0 } + │ ^^^^^^^^^^^^^^ + │ │ │ + │ │ The type 'u64' does not have the ability 'key' + │ 'key' constraint not satisifed + · +10 │ struct Receiving has drop { + │ --- 'key' constraint declared here + diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_non_object_type.move b/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_non_object_type.move new file mode 100644 index 0000000000000..56aad03267cb6 --- /dev/null +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_non_object_type.move @@ -0,0 +1,13 @@ +// invalid, Receiving type with non-object type param + +module a::m { + use sui::transfer::Receiving; + + public entry fun no(_: Receiving) { abort 0 } +} + +module sui::transfer { + struct Receiving has drop { + id: address + } +} diff --git a/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_with_object_type.move b/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_with_object_type.move new file mode 100644 index 0000000000000..9979bdc60761f --- /dev/null +++ b/external-crates/move/move-compiler/tests/sui_mode/entry_points/receiving_with_object_type.move @@ -0,0 +1,22 @@ +// valid, Receiving type with object type param + +module a::m { + use sui::object; + use sui::transfer::Receiving; + + struct S has key { id: object::UID } + + public entry fun yes(_: Receiving) { } +} + +module sui::object { + struct UID has store { + id: address, + } +} + +module sui::transfer { + struct Receiving has drop { + id: address + } +} diff --git a/external-crates/move/move-symbol-pool/src/lib.rs b/external-crates/move/move-symbol-pool/src/lib.rs index 7d33a5f7ecda8..f40ef5f2766bc 100644 --- a/external-crates/move/move-symbol-pool/src/lib.rs +++ b/external-crates/move/move-symbol-pool/src/lib.rs @@ -70,6 +70,8 @@ static_symbols!( "TxContext", "ID", "SUI", + "transfer", + "Receiving", ); /// The global, unique cache of strings. diff --git a/sdk/typescript/test/e2e/data/coin_metadata/Move.lock b/sdk/typescript/test/e2e/data/coin_metadata/Move.lock index 6e667fae0ba36..42c3713296c3e 100644 --- a/sdk/typescript/test/e2e/data/coin_metadata/Move.lock +++ b/sdk/typescript/test/e2e/data/coin_metadata/Move.lock @@ -2,6 +2,8 @@ [move] version = 0 +manifest_digest = "7A3E58BABED5BE213B2D13F59CA97ADA902BC6991FFEC11380A85DCC90474F75" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" dependencies = [ { name = "Sui" }, diff --git a/sdk/typescript/test/e2e/data/dynamic_fields/Move.lock b/sdk/typescript/test/e2e/data/dynamic_fields/Move.lock index 6e667fae0ba36..4107f50d194a8 100644 --- a/sdk/typescript/test/e2e/data/dynamic_fields/Move.lock +++ b/sdk/typescript/test/e2e/data/dynamic_fields/Move.lock @@ -2,6 +2,8 @@ [move] version = 0 +manifest_digest = "B40EE846FCD061ECFAF408AB1E7131807063BBEDD7AC3621BD645A43777ED8B7" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" dependencies = [ { name = "Sui" }, diff --git a/sdk/typescript/test/e2e/data/id_entry_args/Move.lock b/sdk/typescript/test/e2e/data/id_entry_args/Move.lock index 6e667fae0ba36..609f1639f1264 100644 --- a/sdk/typescript/test/e2e/data/id_entry_args/Move.lock +++ b/sdk/typescript/test/e2e/data/id_entry_args/Move.lock @@ -2,6 +2,8 @@ [move] version = 0 +manifest_digest = "4F852708D2D9E3561D34082B6DA794612C4552656C22EDECD08574680DE38A7D" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" dependencies = [ { name = "Sui" }, diff --git a/sdk/typescript/test/e2e/data/serializer/Move.lock b/sdk/typescript/test/e2e/data/serializer/Move.lock index 6e667fae0ba36..a4edecb0cf5ec 100644 --- a/sdk/typescript/test/e2e/data/serializer/Move.lock +++ b/sdk/typescript/test/e2e/data/serializer/Move.lock @@ -2,6 +2,8 @@ [move] version = 0 +manifest_digest = "6528254C89402BECE74A8948CFEC532831D96494E8A7481523D6C24CEB90A3CF" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" dependencies = [ { name = "Sui" }, diff --git a/sui-execution/latest/sui-adapter/src/adapter.rs b/sui-execution/latest/sui-adapter/src/adapter.rs index 4c5f12c4470b0..520d3311765ec 100644 --- a/sui-execution/latest/sui-adapter/src/adapter.rs +++ b/sui-execution/latest/sui-adapter/src/adapter.rs @@ -117,6 +117,7 @@ mod checked { is_metered: bool, protocol_config: &ProtocolConfig, metrics: Arc, + current_epoch_id: EpochId, ) -> NativeContextExtensions<'r> { let mut extensions = NativeContextExtensions::default(); extensions.add(ObjectRuntime::new( @@ -125,6 +126,7 @@ mod checked { is_metered, protocol_config, metrics, + current_epoch_id, )); extensions.add(NativesCostTable::from_protocol_config(protocol_config)); extensions diff --git a/sui-execution/latest/sui-adapter/src/execution_engine.rs b/sui-execution/latest/sui-adapter/src/execution_engine.rs index 0e8f18237e623..e7e2e6e539dff 100644 --- a/sui-execution/latest/sui-adapter/src/execution_engine.rs +++ b/sui-execution/latest/sui-adapter/src/execution_engine.rs @@ -192,7 +192,7 @@ mod checked { let (inner, effects) = temporary_store.to_effects( shared_object_refs, &transaction_digest, - transaction_dependencies.into_iter().collect(), + transaction_dependencies, gas_cost_summary, status, gas_charger, diff --git a/sui-execution/latest/sui-adapter/src/programmable_transactions/context.rs b/sui-execution/latest/sui-adapter/src/programmable_transactions/context.rs index 98de751d80297..b3b8f415052f8 100644 --- a/sui-execution/latest/sui-adapter/src/programmable_transactions/context.rs +++ b/sui-execution/latest/sui-adapter/src/programmable_transactions/context.rs @@ -31,7 +31,6 @@ mod checked { self, get_all_uids, max_event_error, ObjectRuntime, RuntimeResults, }; use sui_protocol_config::ProtocolConfig; - use sui_types::gas::GasCharger; use sui_types::{ balance::Balance, base_types::{MoveObjectType, ObjectID, SequenceNumber, SuiAddress, TxContext}, @@ -51,6 +50,7 @@ mod checked { transaction::{Argument, CallArg, ObjectArg}, type_resolver::TypeTagResolver, }; + use sui_types::{committee::EpochId, gas::GasCharger}; use sui_types::{ error::command_argument_error, execution::{CommandKind, ObjectContents, TryFromValue, Value}, @@ -130,6 +130,7 @@ mod checked { !gas_charger.is_unmetered(), protocol_config, metrics.clone(), + tx_context.epoch(), ); let mut input_object_map = BTreeMap::new(); let inputs = inputs @@ -196,6 +197,7 @@ mod checked { !gas_charger.is_unmetered(), protocol_config, metrics.clone(), + tx_context.epoch(), ); // Set the profiler if in debug mode @@ -378,7 +380,7 @@ mod checked { // Immutable objects and shared objects cannot be taken by value if matches!( input_metadata_opt, - Some(InputObjectMetadata { + Some(InputObjectMetadata::InputObject { owner: Owner::Immutable | Owner::Shared { .. }, .. }) @@ -422,7 +424,11 @@ mod checked { // error if taken return Err(CommandArgumentError::InvalidValueUsage); }; - if input_metadata_opt.is_some() && !input_metadata_opt.unwrap().is_mutable_input { + if let Some(InputObjectMetadata::InputObject { + is_mutable_input: false, + .. + }) = input_metadata_opt + { return Err(CommandArgumentError::InvalidObjectByMutRef); } // if it is copyable, don't take it as we allow for the value to be copied even if @@ -479,15 +485,28 @@ mod checked { The take+restore is an implementation detail of mutable references" ); // restore is exclusively used for mut - let Ok((_, value_opt)) = self.borrow_mut_impl(arg, None) else { + let Ok((input_object_metadata, value_opt)) = self.borrow_mut_impl(arg, None) else { invariant_violation!("Should be able to borrow argument to restore it") }; - let old_value = value_opt.replace(value); - assert_invariant!( - old_value.is_none() || old_value.unwrap().is_copyable(), - "Should never restore a non-taken value, unless it is copyable. \ + match input_object_metadata { + Some(InputObjectMetadata::Receiving { id, version }) => { + let old_value = value_opt.replace(Value::Receiving(*id, *version)); + // If the value is receiving and it's being restored it must have been taken since + // the `Receiving` struct is not copy. + assert_invariant!( + old_value.is_none(), + "Should never restore a receiving value without having taken it" + ); + } + _ => { + let old_value = value_opt.replace(value); + assert_invariant!( + old_value.is_none() || old_value.unwrap().is_copyable(), + "Should never restore a non-taken value, unless it is copyable. \ The take+restore is an implementation detail of mutable references" - ); + ); + } + } Ok(()) } @@ -583,11 +602,22 @@ mod checked { object_metadata: object_metadata_opt, inner: ResultValue { value, .. }, } = input; + let Some(object_metadata) = object_metadata_opt else { return Ok(()) }; - let is_mutable_input = object_metadata.is_mutable_input; - let owner = object_metadata.owner; - let id = object_metadata.id; - input_object_metadata.insert(object_metadata.id, object_metadata); + let (id, owner_opt, is_mutable_input) = match &object_metadata { + InputObjectMetadata::InputObject { + id, + owner, + is_mutable_input, + .. + } => (*id, Some(*owner), *is_mutable_input), + InputObjectMetadata::Receiving { id, .. } => (*id, None, false), + }; + input_object_metadata.insert(id, object_metadata); + let Some(owner) = owner_opt else { + return Ok(()) + }; + let Some(Value::Object(object_value)) = value else { by_value_inputs.insert(id); return Ok(()) @@ -597,7 +627,7 @@ mod checked { } Ok(()) }; - let gas_id_opt = gas.object_metadata.as_ref().map(|info| info.id); + let gas_id_opt = gas.object_metadata.as_ref().map(|info| info.id()); add_input_object_write(gas)?; for input in inputs { add_input_object_write(input)? @@ -647,6 +677,8 @@ mod checked { )); } } + // Receiving arguments can be dropped without being received + Some(Value::Receiving(_, _)) => (), } } } @@ -701,6 +733,7 @@ mod checked { !gas_charger.is_unmetered(), protocol_config, metrics, + tx_context.epoch(), ); for (id, additional_write) in additional_writes { let AdditionalWrite { @@ -781,10 +814,10 @@ mod checked { let old_version = match input_object_metadata.get(&id) { Some(metadata) => { assert_invariant!( - !matches!(metadata.owner, Owner::Immutable), + !matches!(metadata, InputObjectMetadata::InputObject { owner: Owner::Immutable, .. }), "Attempting to delete immutable object {id} via delete kind {delete_kind}" ); - metadata.version + metadata.version() } None => { match loaded_child_objects.get(&id) { @@ -926,6 +959,7 @@ mod checked { is_metered: bool, protocol_config: &ProtocolConfig, metrics: Arc, + current_epoch_id: EpochId, ) -> Session<'state, 'vm, LinkageView<'state>> { vm.new_session_with_extensions( linkage, @@ -935,6 +969,7 @@ mod checked { is_metered, protocol_config, metrics, + current_epoch_id, ), ) } @@ -1146,7 +1181,7 @@ mod checked { }; let owner = obj.owner; let version = obj.version(); - let object_metadata = InputObjectMetadata { + let object_metadata = InputObjectMetadata::InputObject { id, is_mutable_input, owner, @@ -1219,6 +1254,9 @@ mod checked { /* imm override */ !mutable, id, ), + ObjectArg::Receiving((id, version, _)) => { + Ok(InputValue::new_receiving_object(id, version)) + } } } @@ -1307,12 +1345,12 @@ mod checked { ); let old_obj_ver = metadata_opt - .map(|metadata| metadata.version) + .map(|metadata| metadata.version()) .or_else(|| loaded_child_version_opt.copied()); debug_assert!( (write_kind == WriteKind::Mutate) == old_obj_ver.is_some(), - "Inconsistent state: write_kind: {write_kind:?}, old ver: {old_obj_ver:?}" + "Inconsistent state: write_kind: {write_kind:?}, old ver: {old_obj_ver:?} id: {id:?}" ); let type_tag = session diff --git a/sui-execution/latest/sui-adapter/src/programmable_transactions/execution.rs b/sui-execution/latest/sui-adapter/src/programmable_transactions/execution.rs index 3cc13dd7b1884..11adaf1a57fe1 100644 --- a/sui-execution/latest/sui-adapter/src/programmable_transactions/execution.rs +++ b/sui-execution/latest/sui-adapter/src/programmable_transactions/execution.rs @@ -52,6 +52,7 @@ mod checked { }, storage::get_packages, transaction::{Argument, Command, ProgrammableMoveCall, ProgrammableTransaction}, + transfer::RESOLVED_RECEIVING_STRUCT, SUI_FRAMEWORK_ADDRESS, }; use sui_types::{ @@ -91,22 +92,24 @@ mod checked { if let Err(err) = execute_command::(&mut context, &mut mode_results, command) { let object_runtime: &ObjectRuntime = context.session.get_native_extensions().get(); // We still need to record the loaded child objects for replay - let loaded_child_objects = object_runtime.loaded_child_objects(); + let loaded_runtime_objects = object_runtime.loaded_runtime_objects(); drop(context); - state_view.save_loaded_child_objects(loaded_child_objects); + state_view.save_loaded_runtime_objects(loaded_runtime_objects); return Err(err.with_command_index(idx)); }; } // Save loaded objects table in case we fail in post execution let object_runtime: &ObjectRuntime = context.session.get_native_extensions().get(); - // We still need to record the loaded child objects for replay - let loaded_child_objects = object_runtime.loaded_child_objects(); + // Record the objects loaded at runtime (dynamic fields + received) for + // storage rebate calculation. + let loaded_runtime_objects = object_runtime.loaded_runtime_objects(); // apply changes let finished = context.finish::(); - // Save loaded objects for debug. We dont want to lose the info - state_view.save_loaded_child_objects(loaded_child_objects); + // Save loaded objects -- needed to calculate any storage rebates at the end of the + // transaction. + state_view.save_loaded_runtime_objects(loaded_runtime_objects); let ExecutionResults { object_changes, @@ -1271,7 +1274,7 @@ mod checked { value: &Value, param_ty: &Type, ) -> Result<(), ExecutionError> { - let ty = match value { + match value { // For dev-spect, allow any BCS bytes. This does mean internal invariants for types can // be violated (like for string or Option) Value::Raw(RawValueType::Any, _) if Mode::allow_arbitrary_values() => return Ok(()), @@ -1301,18 +1304,45 @@ mod checked { Mode::allow_arbitrary_values() || !abilities.has_key(), "Raw value should never be an object" ); - ty + if ty != param_ty { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } + } + Value::Object(obj) => { + let ty = &obj.type_; + if ty != param_ty { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } + } + Value::Receiving(_, _) => { + let Type::StructInstantiation(sidx, targs) = param_ty else { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )) + }; + let Some(s) = context + .session + .get_struct_type(*sidx) else { + invariant_violation!("sui::transfer::Receiving struct not found in session") + }; + let resolved_struct = get_struct_ident(&s); + + if resolved_struct != RESOLVED_RECEIVING_STRUCT || targs.len() != 1 { + return Err(command_argument_error( + CommandArgumentError::TypeMismatch, + idx, + )); + } } - Value::Object(obj) => &obj.type_, - }; - if ty != param_ty { - Err(command_argument_error( - CommandArgumentError::TypeMismatch, - idx, - )) - } else { - Ok(()) } + Ok(()) } fn get_struct_ident(s: &StructType) -> (&AccountAddress, &IdentStr, &IdentStr) { diff --git a/sui-execution/latest/sui-move-natives/src/dynamic_field.rs b/sui-execution/latest/sui-move-natives/src/dynamic_field.rs index 726c957d5f98f..2304c14792f5f 100644 --- a/sui-execution/latest/sui-move-natives/src/dynamic_field.rs +++ b/sui-execution/latest/sui-move-natives/src/dynamic_field.rs @@ -11,7 +11,6 @@ use move_core_types::{ account_address::AccountAddress, gas_algebra::InternalGas, language_storage::{StructTag, TypeTag}, - value::MoveTypeLayout, vm_status::StatusCode, }; use move_vm_runtime::native_charge_gas_early_exit; @@ -39,7 +38,8 @@ macro_rules! get_or_fetch_object { ); assert!($ty_args.is_empty()); - let (tag, layout, annotated_layout) = match get_tag_and_layouts($context, &child_ty)? { + let (tag, layout, annotated_layout) = match crate::get_tag_and_layouts($context, &child_ty)? + { Some(res) => res, None => { return Ok(NativeResult::err( @@ -497,25 +497,3 @@ pub fn has_child_object_with_ty( smallvec![Value::bool(has_child)], )) } - -fn get_tag_and_layouts( - context: &NativeContext, - ty: &Type, -) -> PartialVMResult> { - let tag = match context.type_to_type_tag(ty)? { - TypeTag::Struct(s) => s, - _ => { - return Err( - PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) - .with_message("Sui verifier guarantees this is a struct".to_string()), - ) - } - }; - let Some(layout) = context.type_to_type_layout(ty)? else { - return Ok(None) - }; - let Some(annotated_layout) = context.type_to_fully_annotated_layout(ty)? else { - return Ok(None) - }; - Ok(Some((*tag, layout, annotated_layout))) -} diff --git a/sui-execution/latest/sui-move-natives/src/lib.rs b/sui-execution/latest/sui-move-natives/src/lib.rs index 8f1f05edcfd17..29d82e0dcc192 100644 --- a/sui-execution/latest/sui-move-natives/src/lib.rs +++ b/sui-execution/latest/sui-move-natives/src/lib.rs @@ -35,16 +35,24 @@ use self::{ }; use better_any::{Tid, TidAble}; use move_binary_format::errors::{PartialVMError, PartialVMResult}; -use move_core_types::{gas_algebra::InternalGas, identifier::Identifier}; +use move_core_types::{ + gas_algebra::InternalGas, + identifier::Identifier, + language_storage::{StructTag, TypeTag}, + value::MoveTypeLayout, + vm_status::StatusCode, +}; use move_stdlib::natives::{GasParameters, NurseryGasParameters}; -use move_vm_runtime::native_functions::{NativeFunction, NativeFunctionTable}; +use move_vm_runtime::native_functions::{NativeContext, NativeFunction, NativeFunctionTable}; use move_vm_types::{ + loaded_data::runtime_types::Type, natives::function::NativeResult, values::{Struct, Value}, }; use std::sync::Arc; use sui_protocol_config::ProtocolConfig; use sui_types::{MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS}; +use transfer::TransferReceiveObjectInternalCostParams; mod address; mod crypto; @@ -129,6 +137,9 @@ pub struct NativesCostTable { // hmac pub hmac_hmac_sha3_256_cost_params: HmacHmacSha3256CostParams, + + // Receive object + pub transfer_receive_object_internal_cost_params: TransferReceiveObjectInternalCostParams, } impl NativesCostTable { @@ -465,6 +476,12 @@ impl NativesCostTable { .hmac_hmac_sha3_256_input_cost_per_block() .into(), }, + transfer_receive_object_internal_cost_params: TransferReceiveObjectInternalCostParams { + transfer_receive_object_internal_cost_base: protocol_config + .transfer_receive_object_cost_base_as_option() + .unwrap_or(0) + .into(), + }, } } } @@ -641,6 +658,11 @@ pub fn all_natives(silent: bool) -> NativeFunctionTable { "share_object_impl", make_native!(transfer::share_object), ), + ( + "transfer", + "receive_impl", + make_native!(transfer::receive_object_internal), + ), ( "tx_context", "derive_id", @@ -701,6 +723,12 @@ pub fn all_natives(silent: bool) -> NativeFunctionTable { .collect() } +// ID { bytes: address } +// Extract the first field of the struct to get the address bytes. +pub fn get_receiver_object_id(object: Value) -> Result { + get_nested_struct_field(object, &[0]) +} + // Object { id: UID { id: ID { bytes: address } } .. } // Extract the first field of the struct 3 times to get the id bytes. pub fn get_object_id(object: Value) -> Result { @@ -721,6 +749,29 @@ pub fn get_nth_struct_field(v: Value, n: usize) -> Result Ok(itr.nth(n).unwrap()) } +/// Returns the struct tag, non-annotated type layout, and fully annotated type layout of `ty`. +pub(crate) fn get_tag_and_layouts( + context: &NativeContext, + ty: &Type, +) -> PartialVMResult> { + let tag = match context.type_to_type_tag(ty)? { + TypeTag::Struct(s) => s, + _ => { + return Err( + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .with_message("Sui verifier guarantees this is a struct".to_string()), + ) + } + }; + let Some(layout) = context.type_to_type_layout(ty)? else { + return Ok(None) + }; + let Some(annotated_layout) = context.type_to_fully_annotated_layout(ty)? else { + return Ok(None) + }; + Ok(Some((*tag, layout, annotated_layout))) +} + #[macro_export] macro_rules! make_native { ($native: expr) => { diff --git a/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs b/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs index 68560a6a57c59..0b4117d401de9 100644 --- a/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs +++ b/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs @@ -22,8 +22,9 @@ use std::{ use sui_protocol_config::{check_limit_by_meter, LimitThresholdCrossed, ProtocolConfig}; use sui_types::{ base_types::{MoveObjectType, ObjectID, SequenceNumber, SuiAddress}, + committee::EpochId, error::{ExecutionError, ExecutionErrorKind, VMMemoryLimitExceededSubStatusCode}, - execution::LoadedChildObjectMetadata, + execution::LoadedRuntimeObjectMetadata, id::UID, metrics::LimitsMetrics, object::{MoveObject, Owner}, @@ -83,6 +84,7 @@ pub(crate) struct ObjectRuntimeState { events: Vec<(Type, StructTag, Value)>, // total size of events emitted so far total_events_size: u64, + received: LinkedHashMap, } #[derive(Clone)] @@ -168,6 +170,7 @@ impl<'a> ObjectRuntime<'a> { is_metered: bool, protocol_config: &ProtocolConfig, metrics: Arc, + epoch_id: EpochId, ) -> Self { let mut input_object_owners = BTreeMap::new(); let mut root_version = BTreeMap::new(); @@ -190,6 +193,7 @@ impl<'a> ObjectRuntime<'a> { is_metered, LocalProtocolConfig::new(protocol_config), metrics.clone(), + epoch_id, ), test_inventories: TestInventories::new(), state: ObjectRuntimeState { @@ -199,6 +203,7 @@ impl<'a> ObjectRuntime<'a> { transfers: LinkedHashMap::new(), events: vec![], total_events_size: 0, + received: LinkedHashMap::new(), }, is_metered, constants: LocalProtocolConfig::new(protocol_config), @@ -342,6 +347,31 @@ impl<'a> ObjectRuntime<'a> { .object_exists_and_has_type(parent, child, child_type) } + pub(super) fn receive_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_version: SequenceNumber, + child_ty: &Type, + child_layout: &MoveTypeLayout, + child_fully_annotated_layout: &MoveTypeLayout, + child_move_type: MoveObjectType, + ) -> PartialVMResult>> { + let Some((value, obj_meta)) = self.child_object_store.receive_object( + parent, + child, + child_version, + child_ty, + child_layout, + child_fully_annotated_layout, + child_move_type, + )? else { + return Ok(None); + }; + self.state.received.insert(child, obj_meta); + Ok(Some(value)) + } + pub(crate) fn get_or_fetch_child_object( &mut self, parent: ObjectID, @@ -402,7 +432,7 @@ impl<'a> ObjectRuntime<'a> { self.child_object_store.all_active_objects() } - pub fn loaded_child_objects(&self) -> BTreeMap { + pub fn loaded_runtime_objects(&self) -> BTreeMap { self.child_object_store .cached_objects() .iter() @@ -410,14 +440,21 @@ impl<'a> ObjectRuntime<'a> { obj_opt.as_ref().map(|obj| { ( *id, - LoadedChildObjectMetadata { + LoadedRuntimeObjectMetadata { version: obj.version(), digest: obj.digest(), storage_rebate: obj.storage_rebate, + previous_transaction: obj.previous_transaction, }, ) }) }) + .chain( + self.state + .received + .iter() + .map(|(id, meta)| (*id, meta.clone())), + ) .collect() } } @@ -497,6 +534,7 @@ impl ObjectRuntimeState { transfers, events: user_events, total_events_size: _, + received, } = self; // Check new owners from transfers, reports an error on cycles. // TODO can we have cycles in the new system? @@ -505,17 +543,19 @@ impl ObjectRuntimeState { let writes: LinkedHashMap<_, _> = transfers .into_iter() .map(|(id, (owner, type_, value))| { - let write_kind = - if input_objects.contains_key(&id) || loaded_child_objects.contains_key(&id) { - debug_assert!(!new_ids.contains_key(&id)); - WriteKind::Mutate - } else if id == SUI_SYSTEM_STATE_OBJECT_ID || new_ids.contains_key(&id) { - // SUI_SYSTEM_STATE_OBJECT_ID is only transferred during genesis - // TODO find a way to insert this in the new_ids during genesis transactions - WriteKind::Create - } else { - WriteKind::Unwrap - }; + let write_kind = if input_objects.contains_key(&id) + || loaded_child_objects.contains_key(&id) + || received.contains_key(&id) + { + debug_assert!(!new_ids.contains_key(&id)); + WriteKind::Mutate + } else if id == SUI_SYSTEM_STATE_OBJECT_ID || new_ids.contains_key(&id) { + // SUI_SYSTEM_STATE_OBJECT_ID is only transferred during genesis + // TODO find a way to insert this in the new_ids during genesis transactions + WriteKind::Create + } else { + WriteKind::Unwrap + }; (id, (write_kind, owner, type_, value)) }) .collect(); @@ -524,18 +564,21 @@ impl ObjectRuntimeState { .into_iter() .map(|(id, ())| { debug_assert!(!new_ids.contains_key(&id)); - let delete_kind = - if input_objects.contains_key(&id) || loaded_child_objects.contains_key(&id) { - DeleteKind::Normal - } else { - DeleteKind::UnwrapThenDelete - }; + let delete_kind = if input_objects.contains_key(&id) + || loaded_child_objects.contains_key(&id) + || received.contains_key(&id) + { + DeleteKind::Normal + } else { + DeleteKind::UnwrapThenDelete + }; (id, delete_kind) }) .collect(); // remaining by value objects must be wrapped let remaining_by_value_objects = by_value_inputs .into_iter() + .chain(received.keys().copied()) .filter(|id| { !writes.contains_key(id) && !deletions.contains_key(id) diff --git a/sui-execution/latest/sui-move-natives/src/object_runtime/object_store.rs b/sui-execution/latest/sui-move-natives/src/object_runtime/object_store.rs index 1ac359bad63ed..bc9e75fcb0aea 100644 --- a/sui-execution/latest/sui-move-natives/src/object_runtime/object_store.rs +++ b/sui-execution/latest/sui-move-natives/src/object_runtime/object_store.rs @@ -15,7 +15,9 @@ use std::{ use sui_protocol_config::{check_limit_by_meter, LimitThresholdCrossed}; use sui_types::{ base_types::{MoveObjectType, ObjectID, SequenceNumber}, + committee::EpochId, error::VMMemoryLimitExceededSubStatusCode, + execution::LoadedRuntimeObjectMetadata, metrics::LimitsMetrics, object::{Data, MoveObject, Object, Owner}, storage::ChildObjectResolver, @@ -54,6 +56,8 @@ struct Inner<'a> { constants: LocalProtocolConfig, // Metrics for reporting exceeded limits metrics: Arc, + // Epoch ID for the current transaction. Used for receiving objects. + current_epoch_id: EpochId, } // maintains the runtime GlobalValues for child objects and manages the fetching of objects @@ -79,6 +83,68 @@ pub(crate) enum ObjectResult { } impl<'a> Inner<'a> { + fn receive_object_from_store( + &self, + owner: ObjectID, + child: ObjectID, + version: SequenceNumber, + ) -> PartialVMResult> { + let child_opt = self + .resolver + .get_object_received_at_version(&owner, &child, version, self.current_epoch_id) + .map_err(|msg| { + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!("{msg}")) + })?; + let obj_opt = if let Some(object) = child_opt { + // guard against bugs in `receive_object_at_version`: if it returns a child object such that + // C.parent != parent, we raise an invariant violation since that should be checked by + // `receive_object_at_version`. + if object.owner != Owner::AddressOwner(owner.into()) { + return Err( + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!( + "Bad owner for {child}. \ + Expected owner {owner} but found owner {}", + object.owner + )), + ); + } + let loaded_metadata = LoadedRuntimeObjectMetadata { + version, + digest: object.digest(), + storage_rebate: object.storage_rebate, + previous_transaction: object.previous_transaction, + }; + + // `ChildObjectResolver::receive_object_at_version` should return the object at the + // version or nothing at all. If it returns an object with a different version, we + // should raise an invariant violation since it should be checked by + // `receive_object_at_version`. + if object.version() != version { + return Err( + PartialVMError::new(StatusCode::STORAGE_ERROR).with_message(format!( + "Bad version for {child}. \ + Expected version {version} but found version {}", + object.version() + )), + ); + } + match object.data { + Data::Package(_) => { + return Err(PartialVMError::new(StatusCode::STORAGE_ERROR).with_message( + format!( + "Mismatched object type for {child}. \ + Expected a Move object but found a Move package" + ), + )) + } + Data::Move(mo @ MoveObject { .. }) => Some((mo, loaded_metadata)), + } + } else { + None + }; + Ok(obj_opt) + } + fn get_or_fetch_object_from_store( &mut self, parent: ObjectID, @@ -235,6 +301,34 @@ impl<'a> Inner<'a> { } } +fn deserialize_move_object( + obj: &MoveObject, + child_ty: &Type, + child_ty_layout: &MoveTypeLayout, + child_move_type: MoveObjectType, +) -> PartialVMResult> { + let child_id = obj.id(); + // object exists, but the type does not match + if obj.type_() != &child_move_type { + return Ok(ObjectResult::MismatchedType); + } + let value = match Value::simple_deserialize(obj.contents(), child_ty_layout) { + Some(v) => v, + None => { + return Err( + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE).with_message( + format!("Failed to deserialize object {child_id} with type {child_move_type}",), + ), + ) + } + }; + Ok(ObjectResult::Loaded(( + child_ty.clone(), + child_move_type, + value, + ))) +} + impl<'a> ChildObjectStore<'a> { pub(super) fn new( resolver: &'a dyn ChildObjectResolver, @@ -242,6 +336,7 @@ impl<'a> ChildObjectStore<'a> { is_metered: bool, constants: LocalProtocolConfig, metrics: Arc, + current_epoch_id: EpochId, ) -> Self { Self { inner: Inner { @@ -251,6 +346,7 @@ impl<'a> ChildObjectStore<'a> { is_metered, constants: constants.clone(), metrics, + current_epoch_id, }, store: BTreeMap::new(), is_metered, @@ -258,6 +354,41 @@ impl<'a> ChildObjectStore<'a> { } } + pub(super) fn receive_object( + &mut self, + parent: ObjectID, + child: ObjectID, + child_version: SequenceNumber, + child_ty: &Type, + child_layout: &MoveTypeLayout, + child_fully_annotated_layout: &MoveTypeLayout, + child_move_type: MoveObjectType, + ) -> PartialVMResult, LoadedRuntimeObjectMetadata)>> { + let Some((obj, obj_meta)) = self + .inner + .receive_object_from_store(parent, child, child_version)? else { + return Ok(None) + }; + + let deserialized_value = + match deserialize_move_object(&obj, child_ty, child_layout, child_move_type)? { + ObjectResult::MismatchedType => ObjectResult::MismatchedType, + ObjectResult::Loaded((_, _, gv)) => ObjectResult::Loaded(gv), + }; + // Find all UIDs inside of the value and update the object parent maps with the contained + // UIDs in the received value. They should all have an upper bound version as the receiving object. + let contained_uids = + get_all_uids(child_fully_annotated_layout, obj.contents()).map_err(|e| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message( + format!("Failed to find UIDs for receiving object. ERROR: {e}"), + ) + })?; + for id in contained_uids { + self.inner.root_version.insert(id, child_version); + } + Ok(Some((deserialized_value, obj_meta))) + } + pub(super) fn object_exists( &mut self, parent: ObjectID, diff --git a/sui-execution/latest/sui-move-natives/src/transfer.rs b/sui-execution/latest/sui-move-natives/src/transfer.rs index d6fa07f4a1d00..c1a53b757ea00 100644 --- a/sui-execution/latest/sui-move-natives/src/transfer.rs +++ b/sui-execution/latest/sui-move-natives/src/transfer.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use super::object_runtime::{ObjectRuntime, TransferResult}; -use crate::NativesCostTable; +use crate::{ + get_receiver_object_id, get_tag_and_layouts, object_runtime::object_store::ObjectResult, + NativesCostTable, +}; use move_binary_format::errors::{PartialVMError, PartialVMResult}; use move_core_types::{ account_address::AccountAddress, gas_algebra::InternalGas, language_storage::TypeTag, @@ -14,9 +17,92 @@ use move_vm_types::{ }; use smallvec::smallvec; use std::collections::VecDeque; -use sui_types::{base_types::SequenceNumber, object::Owner}; +use sui_types::{ + base_types::{MoveObjectType, ObjectID, SequenceNumber}, + object::Owner, +}; const E_SHARED_NON_NEW_OBJECT: u64 = 0; +const E_BCS_SERIALIZATION_FAILURE: u64 = 1; +const E_RECEIVING_OBJECT_TYPE_MISMATCH: u64 = 2; +// Represents both the case where the object does not exist and the case where the object is not +// able to be accessed through the parent that is passed-in. +const E_UNABLE_TO_RECEIVE_OBJECT: u64 = 3; + +#[derive(Clone, Debug)] +pub struct TransferReceiveObjectInternalCostParams { + pub transfer_receive_object_internal_cost_base: InternalGas, +} +/*************************************************************************************************** +* native fun receive_object_internal +* Implementation of the Move native function `receive_object_internal(parent: &mut UID, rec: Receiver): T` +* gas cost: transfer_receive_object_internal_cost_base | covers various fixed costs in the oper +**************************************************************************************************/ + +pub fn receive_object_internal( + context: &mut NativeContext, + mut ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.len() == 1); + debug_assert!(args.len() == 3); + let transfer_receive_object_internal_cost_params = context + .extensions_mut() + .get::() + .transfer_receive_object_internal_cost_params + .clone(); + native_charge_gas_early_exit!( + context, + transfer_receive_object_internal_cost_params.transfer_receive_object_internal_cost_base + ); + let child_ty = ty_args.pop().unwrap(); + let child_receiver_sequence_number: SequenceNumber = pop_arg!(args, u64).into(); + let child_receiver_object_id = args.pop_back().unwrap(); + let parent = pop_arg!(args, AccountAddress).into(); + assert!(args.is_empty()); + let child_id: ObjectID = get_receiver_object_id(child_receiver_object_id.copy_value().unwrap()) + .unwrap() + .value_as::() + .unwrap() + .into(); + assert!(ty_args.is_empty()); + + let Some((tag, layout, annotated_layout)) = get_tag_and_layouts(context, &child_ty)? else { + return Ok(NativeResult::err( + context.gas_used(), + E_BCS_SERIALIZATION_FAILURE, + )) + }; + + let object_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut(); + let child = match object_runtime.receive_object( + parent, + child_id, + child_receiver_sequence_number, + &child_ty, + &layout, + &annotated_layout, + MoveObjectType::from(tag), + ) { + // NB: Loaded and doesn't exist and inauthenticated read should lead to the exact same error + Ok(None) => { + return Ok(NativeResult::err( + context.gas_used(), + E_UNABLE_TO_RECEIVE_OBJECT, + )) + } + Ok(Some(ObjectResult::Loaded(gv))) => gv, + Ok(Some(ObjectResult::MismatchedType)) => { + return Ok(NativeResult::err( + context.gas_used(), + E_RECEIVING_OBJECT_TYPE_MISMATCH, + )) + } + Err(x) => return Err(x), + }; + + Ok(NativeResult::ok(context.gas_used(), smallvec![child])) +} #[derive(Clone, Debug)] pub struct TransferInternalCostParams { diff --git a/sui-execution/latest/sui-verifier/src/entry_points_verifier.rs b/sui-execution/latest/sui-verifier/src/entry_points_verifier.rs index 6a2dfb2a98b29..2101a696c3fe3 100644 --- a/sui-execution/latest/sui-verifier/src/entry_points_verifier.rs +++ b/sui-execution/latest/sui-verifier/src/entry_points_verifier.rs @@ -14,6 +14,7 @@ use sui_types::{ error::ExecutionError, is_object, is_object_vector, is_primitive, move_package::{is_test_fun, FnInfoMap}, + transfer::Receiving, SUI_FRAMEWORK_ADDRESS, }; @@ -236,6 +237,7 @@ fn verify_param_type( if is_primitive(view, function_type_args, param) || is_object(view, function_type_args, param)? || is_object_vector(view, function_type_args, param)? + || Receiving::is_valid(view, param) { Ok(()) } else { diff --git a/sui-execution/latest/sui-verifier/src/private_generics.rs b/sui-execution/latest/sui-verifier/src/private_generics.rs index 29213d150c0ac..f7d5ca092ce71 100644 --- a/sui-execution/latest/sui-verifier/src/private_generics.rs +++ b/sui-execution/latest/sui-verifier/src/private_generics.rs @@ -23,6 +23,7 @@ pub const PUBLIC_TRANSFER_FUNCTIONS: &[&IdentStr] = &[ ident_str!("public_transfer"), ident_str!("public_freeze_object"), ident_str!("public_share_object"), + ident_str!("receive"), ]; pub const PRIVATE_TRANSFER_FUNCTIONS: &[&IdentStr] = &[ ident_str!("transfer"), @@ -33,6 +34,7 @@ pub const TRANSFER_IMPL_FUNCTIONS: &[&IdentStr] = &[ ident_str!("transfer_impl"), ident_str!("freeze_object_impl"), ident_str!("share_object_impl"), + ident_str!("receive_impl"), ]; /// All transfer functions (the functions in `sui::transfer`) are "private" in that they are diff --git a/sui-execution/v0/sui-adapter/src/programmable_transactions/context.rs b/sui-execution/v0/sui-adapter/src/programmable_transactions/context.rs index 93575662f8cac..5e7e514ffc1d4 100644 --- a/sui-execution/v0/sui-adapter/src/programmable_transactions/context.rs +++ b/sui-execution/v0/sui-adapter/src/programmable_transactions/context.rs @@ -383,7 +383,7 @@ mod checked { // Immutable objects and shared objects cannot be taken by value if matches!( input_metadata_opt, - Some(InputObjectMetadata { + Some(InputObjectMetadata::InputObject { owner: Owner::Immutable | Owner::Shared { .. }, .. }) @@ -427,7 +427,11 @@ mod checked { // error if taken return Err(CommandArgumentError::InvalidValueUsage); }; - if input_metadata_opt.is_some() && !input_metadata_opt.unwrap().is_mutable_input { + if let Some(InputObjectMetadata::InputObject { + is_mutable_input: false, + .. + }) = input_metadata_opt + { return Err(CommandArgumentError::InvalidObjectByMutRef); } // if it is copyable, don't take it as we allow for the value to be copied even if @@ -589,20 +593,25 @@ mod checked { inner: ResultValue { value, .. }, } = input; let Some(object_metadata) = object_metadata_opt else { return Ok(()) }; - let is_mutable_input = object_metadata.is_mutable_input; - let owner = object_metadata.owner; - let id = object_metadata.id; - input_object_metadata.insert(object_metadata.id, object_metadata); + let InputObjectMetadata::InputObject { + is_mutable_input, + owner, + id, + .. + } = &object_metadata else { + unreachable!("Found non-input object metadata for input object when adding writes to input objects -- impossible in v0"); + }; + input_object_metadata.insert(object_metadata.id(), object_metadata.clone()); let Some(Value::Object(object_value)) = value else { - by_value_inputs.insert(id); + by_value_inputs.insert(*id); return Ok(()) }; - if is_mutable_input { - add_additional_write(&mut additional_writes, owner, object_value)?; + if *is_mutable_input { + add_additional_write(&mut additional_writes, *owner, object_value)?; } Ok(()) }; - let gas_id_opt = gas.object_metadata.as_ref().map(|info| info.id); + let gas_id_opt = gas.object_metadata.as_ref().map(|info| info.id()); add_input_object_write(gas)?; for input in inputs { add_input_object_write(input)? @@ -652,6 +661,9 @@ mod checked { )); } } + Some(Value::Receiving(_, _)) => { + unreachable!("Impossible to hit Receiving in v0") + } } } } @@ -788,10 +800,10 @@ mod checked { let old_version = match input_object_metadata.get(&id) { Some(metadata) => { assert_invariant!( - !matches!(metadata.owner, Owner::Immutable), + !matches!(metadata, InputObjectMetadata::InputObject { owner: Owner::Immutable, .. }), "Attempting to delete immutable object {id} via delete kind {delete_kind}" ); - metadata.version + metadata.version() } None => { match loaded_child_objects.get(&id) { @@ -1166,7 +1178,7 @@ mod checked { }; let owner = obj.owner; let version = obj.version(); - let object_metadata = InputObjectMetadata { + let object_metadata = InputObjectMetadata::InputObject { id, is_mutable_input, owner, @@ -1239,6 +1251,7 @@ mod checked { /* imm override */ !mutable, id, ), + ObjectArg::Receiving(_) => unreachable!("Impossible to hit Receiving in v0"), } } @@ -1327,7 +1340,7 @@ mod checked { ); let old_obj_ver = metadata_opt - .map(|metadata| metadata.version) + .map(|metadata| metadata.version()) .or_else(|| loaded_child_version_opt.copied()); debug_assert!( diff --git a/sui-execution/v0/sui-adapter/src/programmable_transactions/execution.rs b/sui-execution/v0/sui-adapter/src/programmable_transactions/execution.rs index 70dc1bba8b154..364eed1bd5ad0 100644 --- a/sui-execution/v0/sui-adapter/src/programmable_transactions/execution.rs +++ b/sui-execution/v0/sui-adapter/src/programmable_transactions/execution.rs @@ -95,7 +95,7 @@ mod checked { // We still need to record the loaded child objects for replay let loaded_child_objects = object_runtime.loaded_child_objects(); drop(context); - state_view.save_loaded_child_objects(loaded_child_objects); + state_view.save_loaded_runtime_objects(loaded_child_objects); return Err(err.with_command_index(idx)); }; } @@ -108,7 +108,7 @@ mod checked { // apply changes let finished = context.finish::(); // Save loaded objects for debug. We dont want to lose the info - state_view.save_loaded_child_objects(loaded_child_objects); + state_view.save_loaded_runtime_objects(loaded_child_objects); let ExecutionResults { object_changes, @@ -1374,6 +1374,9 @@ mod checked { ty } Value::Object(obj) => &obj.type_, + Value::Receiving(_, _) => { + unreachable!("Receiving value should never occur in v0 execution") + } }; if ty != param_ty { Err(command_argument_error( diff --git a/sui-execution/v0/sui-move-natives/src/object_runtime/mod.rs b/sui-execution/v0/sui-move-natives/src/object_runtime/mod.rs index 5eb7708885bc2..b50e3dd47bbc2 100644 --- a/sui-execution/v0/sui-move-natives/src/object_runtime/mod.rs +++ b/sui-execution/v0/sui-move-natives/src/object_runtime/mod.rs @@ -23,7 +23,7 @@ use sui_protocol_config::{check_limit_by_meter, LimitThresholdCrossed, ProtocolC use sui_types::{ base_types::{MoveObjectType, ObjectID, SequenceNumber, SuiAddress}, error::{ExecutionError, ExecutionErrorKind, VMMemoryLimitExceededSubStatusCode}, - execution::LoadedChildObjectMetadata, + execution::LoadedRuntimeObjectMetadata, id::UID, metrics::LimitsMetrics, object::{MoveObject, Owner}, @@ -401,7 +401,7 @@ impl<'a> ObjectRuntime<'a> { self.object_store.all_active_objects() } - pub fn loaded_child_objects(&self) -> BTreeMap { + pub fn loaded_child_objects(&self) -> BTreeMap { self.object_store .cached_objects() .iter() @@ -409,10 +409,11 @@ impl<'a> ObjectRuntime<'a> { obj_opt.as_ref().map(|obj| { ( *id, - LoadedChildObjectMetadata { + LoadedRuntimeObjectMetadata { version: obj.version(), digest: obj.digest(), storage_rebate: obj.storage_rebate, + previous_transaction: obj.previous_transaction, }, ) })