Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[0/n][transfer-to-object] Transfer to Object Implementation #12611

Merged
merged 5 commits into from
Sep 22, 2023
Merged

Conversation

tzakian
Copy link
Contributor

@tzakian tzakian commented Jun 21, 2023

Description

This PR implements the core transfer-to-object functionality. In particular it implements the ability to "receive" an object that was sent to the address (object ID) of another object using one of the transfer or transfer_public functions in the transfer module.

More detail is given on the programming model in the attached issue so I will not go into that.

SDK support for receiving objects has been added in the two PRs stacked on this one:

Test Plan

I've written a number of tests for this that I believe cover things:

  • Execution-correctness tests for this in the transactional tests
  • Tests for effect computation in the new sui-core transfer_to_object.rs tests (e.g., receive-then-unwrap, receive-unwrap-wrap, etc).
  • Tests for lock-freeness of receiving arguments (i.e., that the object identified by the Receiving argument is not locked at signing) in the sui-core transfer_to_object.rs tests
  • Tests that dependencies are correctly registered, and notified in the transaction manager for Receiving arguments to transactions (see new tests in the transaction_manager_tests.rs file).

A more detailed listing of the tests:

  • PTBs
    • Receive object and return to PTB
      • Do not do anything with the returned (non-drop) value [receive_return_object_dont_touch.move]
      • Call transfer object on it [receive_return_object_then_transfer.move]
    • Basic "can receive and then do something in the function" [basic_receive.move]
    • Duplicate "Receive" arguments to the PTB [duplicate_receive_argument.move]
    • Pass but don't use Receiving argument, then later use it in PTB.
      • By immut ref [pass_receiver_immut_then_reuse.move]
      • By mut ref [pass_receiver_mut_then_reuse.move]
      • By value and returning it [pass_through_then_receive.move]
    • Various combinations of receivership being passed [receive_by_ref.move]
      (checking borrow/borrow_mut, and restore rules for PTB execution)
    • Receive object of different type [receive_invalid_type.move]
    • Receive object with non-address owner ownership [receive_object_owner.move]
    • Reuse of input receiving argument [take_receiver_then_try_to_reuse.move]
  • Type malleability [receive_invalid_param_ty.move]
    • Pass receiver into a non-receiver type
      • primitive type
      • struct type with same layout
      • struct type with different layout
    • Pass non-receiver into a receiver type
      • primitive type
      • struct type with same layout
      • struct type with different layout
  • Resource conservation/Effects calculation (both transactional tests and sui-core tests for explicit effects checks)
    • Do various things with object after receiving it:
      • Immediately place it as a dynamic field [receive_dof_and_mutate.move]
      • Immediately add a dynamic field to it [receive_add_dof_and_mutate.move]
      • Immediately add a dynamic field to it, add as a dynamic field to parent object, then mutate both [receive_add_dof_and_mutate.move]
      • Immediately transfer it [receive_and_send_back.move]
      • Immediately delete it [receive_and_deleted.move]
      • Immediately wrap it [receive_and_wrap.move]
      • Immediately abort [receive_and_abort.move]
      • Don't use it [receive_by_value_flow_through.move]
      • Receive multiple times in a row making sure effects stay in-sync as expected [receive_multiple_times_in_row.move]
    • Shared objects
      • Make sure we can receive if object is transferred to an object which is already shared [shared_parent/basic_receive.move]
      • Make sure we can receive if object is transferred to an object which is then shared [shared_parent/transfer_then_share.move]
      • Non-usage of receiving object argument off a shared parent object [shared_parent/drop_receiving.move]
      • Receive object off of shared parent, add as dynamic field of shared parent and then mutate through the parent [shared_parent/receive_dof_and_mutate.move]
      • Send and receive the same object to the same shared parent multiple times [shared_parent/receive_multiple_times_in_row.move]
    • MVCC -- Test that we calculate contained UIDs correctly when we receive an
      object. This is tested in [mvcc/receive_object_dof.move] and
      [mvcc/receive_object_split_changes_dof.move]
  • Sui core tests checking explicit parts of the calculated effects to make sure they match what we expect:
    • Immediately unwrap then transfer inner object [transfer_to_object_tests.rs/test_tto_unwrap_transfer]
    • Immediately unwrap then delete inner object as well [transfer_to_object_tests.rs/test_tto_unwrap_delete]
    • Immediately unwrap then add inner object as dynamic field [transfer_to_object_tests.rs/test_tto_unwrap_add_as_dynamic_field]
      • Immediately unwrap, then wrap again -- this is part of the above since adding a dynamic field wraps the object
    • Basic object receive [transfer_to_object_tests/test_tto_transfer]
    • Pass but don't ise Receiving argument [transfer_to_object_tests/test_tto_unused_receiver]
    • Pass by different references [transfer_to_object_tests/test_tto_pass_receiving_by_refs]
    • Receive and immediately delete [transfer_to_object_tests/test_tto_delete]
    • Receive, wrap, and then transfer wrapped object [transfer_to_object_tests/test_tto_wrap]
  • Sui Core for object locking and transaction dependendency calculation in effects
    • Test that receiving object arguments are not locked, and that different
      orders of execution for two certs that want to receive the same argument
      (but only one is valid) can both be run in either order, and both return
      the same execution effects in either order
      [transfer_to_object_tests/test_tt_not_locked]
    • Test that transaction dependencies are added correctly:
      • Basic test that we add transaction dependendency if we execute
        successfully and receive the object
        [transfer_to_object_tests/test_tto_valid_dependencies]
      • Similar case for if we delete the object immediately
        [transfer_to_object_tests/test_tto_valid_dependencies_delete_on_receive]
      • That we don't register the transaction dependendency if we don't receive
        the object
        [transfer_to_object_tests/test_tto_dependencies_dont_receive]
      • That we don't register the transaction dependendency if we don't receive
        the object and we abort
        [transfer_to_object_tests/test_tto_dependencies_dont_receive_but_abort]
      • That we register the dependendency if we received the object, even if we
        then went on to abort in the transaction
        [transfer_to_object_tests/test_tto_dependencies_receive_and_abort]
      • Dynamic object field spoofing: make sure we don't accidentally register a
        dynamic object field load of an object that we want to receive at a
        different version as a receivership of that object (i.e., don't register
        the transaction dependendency)
        [transfer_to_object_tests/receive_and_dof_interleave]

Additional tests

  • PTBs

    • MakeMoveVec:
      • create but don't use [receive_many_move_vec.move]
      • pass vec by value but don't receive [receive_many_move_vec.move]
      • pass vec by ref then use value to receive in later command [receive_many_move_vec.move]
      • Pass vec by mut ref and pop/receive some, then receive rest in other call [receive_many_move_vec.move]
      • Pass vec by mut ref, only receive some [receive_many_move_vec.move]
      • Pass vec by value, only receive some [receive_many_move_vec.move]
      • Pass vec by value, receive all [receive_many_move_vec.move]
    • Pack receiving tickets into a struct (some/all) then receive transitively [receive_duo_struct.move]
  • Type mismatches:

    • Receiving and phony struct with same struct layout and right type args ([receive_invalid_param_ty.move])
    • Receiving with mismatched type args [move_vec_receiving_types.move]
    • Receiving with multiple different type args [move_vec_receiving_types.move]
  • TransferObjects

    • Try to transfer receiving ticket [receive_ticket_coin_operations.move]
  • SplitCoins

    • Try to split a receiving ticket [receive_ticket_coin_operations.move]
  • MergeCoins

    • Try to merge a receiving ticket [receive_ticket_coin_operations.move]
  • MVCC [receive_object_access_through_parent[dof/df].move]

  • Transaction input checks (in sui-core tests)

    • Delete between cert and execution [tests in test_tto_not_lockedin the sui-core tests
    • Cert denial if sending a transaction where input_objects \intersect receiving_object != {} [test_tto_intersection_input_and_receiving_objects]
  • Type-fixing for receiving arguments [pt_receive_type_fixing.move]


If your changes are not user-facing and not a breaking change, you can skip the following section. Otherwise, please indicate what changed, and then add to the Release Notes section as highlighted during the release process.

Type of Change (Check all that apply)

  • protocol change
  • user-visible impact
  • breaking change for a client SDKs
  • breaking change for FNs (FN binary must upgrade)
  • breaking change for validators or node operators (must upgrade binaries)
  • breaking change for on-chain data layout
  • necessitate either a data wipe or data migration

Release notes

Added the ability to receive objects off of another object. This is currently only turned on in devnet. More information on transfer-to-object, and receiving objects off of other objects can be found in the GitHub issue on it which can be found here: #12658

@vercel
Copy link

vercel bot commented Jun 21, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
sui-typescript-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 22, 2023 8:17pm
7 Ignored Deployments
Name Status Preview Comments Updated (UTC)
explorer ⬜️ Ignored (Inspect) Visit Preview Sep 22, 2023 8:17pm
explorer-storybook ⬜️ Ignored (Inspect) Sep 22, 2023 8:17pm
multisig-toolkit ⬜️ Ignored (Inspect) Visit Preview Sep 22, 2023 8:17pm
mysten-ui ⬜️ Ignored (Inspect) Visit Preview Sep 22, 2023 8:17pm
sui-kiosk ⬜️ Ignored (Inspect) Visit Preview Sep 22, 2023 8:17pm
sui-wallet-kit ⬜️ Ignored (Inspect) Sep 22, 2023 8:17pm
wallet-adapter ⬜️ Ignored (Inspect) Sep 22, 2023 8:17pm

@@ -205,6 +218,8 @@ impl Value {
},
_,
) => *used_in_non_entry_move_call,
// TODO(tzakian): Need to figure this out.
Copy link
Contributor

Choose a reason for hiding this comment

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

The only thing you can do with a Receiver<T> is consume it to receive the object, right? So it doesn't seem possible to use the one in a non-entry move call and then have the value live to be queried about it, which implies false is the correct return value here.

@tzakian tzakian linked an issue Jun 23, 2023 that may be closed by this pull request
}
}

fn receiving_objects(&self) -> Vec<ObjectRef> {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reason to not include the Receiving in the input objects with a new InputObjectKind, and handle this new kind differently when needed? Asking because I believe there are much more similarities between input objects and receiving objects, than differences.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My first crack at implementing this is exactly as you describe (received objects are returned as input objects with a new InputObjectKind). That direction is definitely workable I believe, however what I encountered when doing that was there were a lot of places where InputObjectKinds flow where Receiving objects make no sense (e.g., in checking or loading object arguments to a transaction). So this led to a bunch of unreachable!/invariant_violation code. @tnowacki had some opinions on this I believe as well (or maybe I'm misremembering).

Moving to separating them out seemed like a cleaner option since it led to significantly less unreachable/invariant_violation code paths and (IMO) seemed to fit more. I actually view Receiving objects as more akin to dynamic fields than input objects other than the addition of dependencies, but I'm more than happy to also prototype treating them as input objects some more and we can take a look at both options and decide if you'd like :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the main thing with not treating Receiving objects as input objects is there are also a number of places where they need to be treated differently from other input objects, especially around the fact that we (1) don't lock them like we do with other objects, and (2) we don't check their existence prior to execution -- in this way they're more akin to dynamic fields with a specified version than an input object. Additionally at the execution layer, Receiving objects and other input objects differ significantly in terms of how they should be treated and checked.

Because of this the worry is that if we treat Receiving objects as input objects, they could accidentally leak to someplace they should and we might, e.g., accidentally grab a lock on them (or try to load them etc.) when we shouldn't. Treating them separately makes this much less likely IMO.

@@ -68,9 +68,11 @@ pub async fn check_transaction_input(
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()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

There are several similar functions in this file that checks input for different kinds of transactions. It's an unfortunate duplication of code but it does mean we have to handle receiving objects in all those places. This further backs my question regarding whether we should include receiving objects in input objects, otherwise you would have to chase down all calls to input_objects().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is that receiving objects can only be used for PTBs (outside of those they don't make any sense I believe). So I don't believe we need to handle these separate apart from possibly checking that the set of receiving_objects is empty in that case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if I understand, but check_transaction_input_with_given_gas and check_dev_inspect_input could be called on PTBs as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh correct, these will need to be added there as well.

@tzakian tzakian force-pushed the tzakian/tto-1 branch 2 times, most recently from c9eb4ee to bbb4c95 Compare June 28, 2023 19:10
@tzakian tzakian changed the title [RFC][transfer-to-object] Initial plumbing of InboxRef transaction argument type [RFC][transfer-to-object] Transfer to Object Implementation Jul 5, 2023
tzakian added a commit that referenced this pull request Jul 6, 2023
…n and transfer-to-object (#12852)

## Description 

This adds a new table to the ``PerpetualTables` that holds the set of
received and shared deleted objects during the epoch.

This table is not currently used anywhere and is unused. However, both
shared-object deletion #12623 and transfer-to-object #12611 PRs rely on
this table.

This table can be pruned at epoch boundaries so the table key contains
the epoch ID.

@laura-makdah and I have discussed and we're happy to either land this
soon, or to wait until one or the other above PRs is ready to land and
land this change at the same time. Either way this change on its own
should be fine to land any time as the new table is not used anywhere
yet.

## Test Plan 

Not tested yet -- functionality will be tested in the two PRs being
built on top of this.

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
@tzakian tzakian force-pushed the tzakian/tto-1 branch 2 times, most recently from 84c9e1f to b889c94 Compare July 11, 2023 21:10
@tzakian tzakian changed the title [RFC][transfer-to-object] Transfer to Object Implementation [transfer-to-object] Transfer to Object Implementation Jul 11, 2023
ebmifa pushed a commit that referenced this pull request Jul 12, 2023
…n and transfer-to-object (#12852)

## Description 

This adds a new table to the ``PerpetualTables` that holds the set of
received and shared deleted objects during the epoch.

This table is not currently used anywhere and is unused. However, both
shared-object deletion #12623 and transfer-to-object #12611 PRs rely on
this table.

This table can be pruned at epoch boundaries so the table key contains
the epoch ID.

@laura-makdah and I have discussed and we're happy to either land this
soon, or to wait until one or the other above PRs is ready to land and
land this change at the same time. Either way this change on its own
should be fine to land any time as the new table is not used anywhere
yet.

## Test Plan 

Not tested yet -- functionality will be tested in the two PRs being
built on top of this.

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
Copy link
Contributor

@tnowacki tnowacki left a comment

Choose a reason for hiding this comment

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

Continuing to review, but giving feedback as immediately as I can. So expect some more comments over the afternoon here. But the receiving value should not need to use global value, because we are protecting the loading by the uniqueness of the Receiving value

@@ -1216,6 +1222,7 @@ fn load_object_arg<'vm, 'state>(
/* imm override */ !mutable,
id,
),
ObjectArg::Receiving(_) => invariant_violation!("Impossible to hit Receiving in v0"),
Copy link
Contributor

Choose a reason for hiding this comment

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

How do these things work? Should we just panic as we will otherwise fork? Not sure I have personally made such a decision yet, so mostly wondering what is standard for myself :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same! I think panicking might be the best call here since that would be better. @amnn any thoughts on this?

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this not an invariant violation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was going off of and replicating similar error handling as in the get_or_fetch_object macro. But happy to make it an invariant violation (storage error).

Comment on lines 583 to 586
// We don't count receiving object arguments as something that needs to be written
// unless it has actually been received -- this is tracked in the `received` field of
// the object store.
if let InputObjectMetadata::Receiving { .. } = &object_metadata { return Ok(()) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we early return above if we are in this case? Then we shouldn't need to clone.

Copy link
Contributor

Choose a reason for hiding this comment

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

(you might need to copy out the owner/id fields again but should be easy enough

Comment on lines 468 to 471
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!(
Copy link
Contributor

Choose a reason for hiding this comment

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

restore is used to implement borrow_mut, so I think we will be able to hit this case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we're able to hit this case since it doesn't have copy -- if we mutably borrow the value will be taken and then restored so old_value should always be none

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see. Makes sense (just be sure to test it ;))

CallArg::Object(o) => match o {
ObjectArg::ImmOrOwnedObject(_) => vec![],
ObjectArg::SharedObject { .. } => vec![],
ObjectArg::Receiving(obj_ref) => vec![*obj_ref],
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this a vec instead of just an option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't have a super string opinion here, but I used a vec instead of an option here to keep consistency with the other interfaces around receiving objects, and input objects on PTB/PTB components. In particular both input_objects and with other implementations of receiving_objects on other parts of the PTB.

Comment on lines +29 to +35
public entry fun pass_through(x: Receiving<B>): Receiving<B> { x }

public entry fun pass_through_a(x: Receiving<A>): Receiving<A> { x }

public entry fun pass_through_ref_a(_x: &Receiving<A>) { }

public entry fun pass_through_mut_ref_a(_x: &mut Receiving<A>) { }
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we end up fixing the type on first usage? Or first mutable usage?
If the latter, I have some more tests for you :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We did windup fixing the type on first usage!

Comment on lines +43 to +44
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" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we get this error for this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an object-owned object that I am trying to pass-in as a normal object to the transaction, so the transaction input checks are going to complain about this (with this error).

Copy link
Contributor

@lxfind lxfind left a comment

Choose a reason for hiding this comment

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

Question: when a transaction receives an object, and deletes (or wraps) it in the same transaction, which code ensures that it's marked as modified?

@@ -207,6 +209,14 @@ pub mod checked {
self.gas_status.charge_publish_package(size)
}

pub fn charge_upgrade_package(&mut self, size: usize) -> Result<(), ExecutionError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems unrelated code?

Copy link
Contributor

Choose a reason for hiding this comment

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

Bad rebase from @dariorussi's fix maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The vm_rework branch was cut before Dario's fix, so when I rebased the changes from latest into vm_rework it pulled in the changes to latest with it. So I think this is fine/to be expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Happy to not replicate it over, but it feels kinda weird to not just do a full execution_layer.py rebase to replicate the changes over.

@tzakian
Copy link
Contributor Author

tzakian commented Sep 22, 2023

@lxfind

Question: when a transaction receives an object, and deletes (or wraps) it in the same transaction, which code ensures that it's marked as modified?

Good question, this is done by marking every received object as mutated when we finish off the ObjectRuntimeState:

https://github.com/MystenLabs/sui/pull/12611/files#diff-35345cac32d6832985925d47d02cf4d4ef34ed75330e1a94eb5cb50a03952beaR578

Edit: apparently github doesn't support sending lines to a PR, even though it seems like it should... here's a link to the line in question that should work:

for (received_object, _) in received.into_iter() {

@@ -531,8 +575,10 @@ impl ObjectRuntimeState {
(id, (owner, type_, value))
})
.collect();
for deleted_id in deleted_ids.keys() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Woooo I don't think we can delete this yet. This is required at least prior to @tnowacki's fix on the recent child object issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Must have happened in a bad rebase...I'll add this back in!

Copy link
Contributor

Choose a reason for hiding this comment

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

There were no tests that caught this? would it be possible to add one?

Copy link
Contributor

@lxfind lxfind left a comment

Choose a reason for hiding this comment

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

Overall LGTM on the core changes

@@ -537,6 +587,13 @@ impl ObjectRuntimeState {
}
}

// Any received objects are viewed as modified.
for (received_object, _) in received.into_iter() {
if let Some(loaded_child) = loaded_child_objects.get_mut(&received_object) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it be an invariant violation if it's not loaded?

if has_key {
versioned_results.push((*idx, true))
} else if receiving_objects.contains(input_key) {
// If there's a more recent version of this object, then it was mutated by someone
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this - the comment here implies that this branch only enters if there was a more recent version of the object. But I don't see why has_key can't also be true when a more recent version exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call out -- I'll update the comments to clarify.

You're correct that has_key could also be true if a more recent version exists (and that's fine).

What I really should say here (and I'll correct to this) is that has_key can also return false if a more recent version exists in the case where the object at the specified version has been pruned. In that case we need to "look up" to see if a more recent version of that object has been exists or has been deleted.

@@ -1884,6 +2046,35 @@ impl ChildObjectResolver for AuthorityStore {
}
Ok(Some(child_object))
}

fn get_object_received_at_version(
Copy link
Contributor

Choose a reason for hiding this comment

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

where is this actually called from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of the calls from the object runtime to receive an object call this method. E.g.,

.get_object_received_at_version(&owner, &child, version, self.current_epoch_id)
uses this to do the actual loading of the object while executing the transaction.

Copy link
Contributor

Choose a reason for hiding this comment

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

omg I was grepping in /crates so I didn't see those.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've been there...more times than I'd care to admit

Comment on lines +211 to +203
// If the receiving object is in the loaded runtime objects, then that means that it
// was actually successfully loaded (so existed, and there was authenticated mutable
// access to it). So we insert the previous transaction as a dependency.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why? This just seems correct?

if let Some(obj_meta) = self.loaded_runtime_objects.get(id) {
// Check that the expected version, digest, and owner match the loaded version,
// digest, and owner. If they don't then don't register a dependency.
// This is because this could be "spoofed" by loading a dynamic object field.
Copy link
Contributor

Choose a reason for hiding this comment

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

can you explain this spoofing? shouldn't an attempt to receive a child object fail because the object will be object owned?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically, due to the way dynamic object fields work, you can have the following sequence of certificates and executions:

  • Object O at version n with object id oid is sent to a shared object S
  • Tx1 creates a cert to receive O at version n but never actually tries to receive O. BUT Tx1 loads a dynamic object field F that will reference this object in the future. (Tx1 could not be executed successfully right now).
  • Tx2 creates a cert, and executes a transaction moving O to the dynamic object field F. O now has an ObjectOwner() ownership, but importantly the same object id oid.
  • Tx1 executes. This will load O via the dynamic object field at a different version n + k (k > 0). Because of this oid will be in the loaded_runtime_objects but at a different version.
  • If we don't have this check that the object in the loaded runtime object metadata was actually received at the specified version (and I added the other checks just to be doubly sure) then we could register a transaction dependency that actually didn't exist. Not a massive deal, but also not quite correct either.

A test that demonstrates this is here:

async fn receive_and_dof_interleave() {

…13896)

## Description 

This adds support for receiving transaction argument types to json-rpc

## Test Plan 

Tested by PRs above this.

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [X] protocol change
- [X] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes

Release notes included in base PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Transfer to Object
6 participants