-
Notifications
You must be signed in to change notification settings - Fork 11.3k
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
[Object Ownership #4] Prevent circular ownership #295
Conversation
@@ -364,6 +382,20 @@ fn process_successful_execution< | |||
// Charge extra gas based on object size if we are creating a new object. | |||
gas_used += gas::calculate_object_creation_cost(&obj); | |||
} | |||
let obj_id = obj.id(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this prevent circular ownership in all cases? E.g., wondering about the following scenario:
initial state: { obj1 => A1, obj2 => A2 }
tx1 [signed by A1]: transfer obj1 to obj2
tx2 [signed by A2]: transfer obj2 to obj1
In tx2, I think:
object_owner_map is
{}` at line 90- at line 390,
obj_id
is obj2parent
is obj1 - this means the loop at 391 won't be entered + the else branch will be taken at 394
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In tx2, object_owner_map
won't be {}
at line 90, because obj1
is already owned by obj2
, hence object_owner_map
will start with {obj1: obj2}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks. I think my scenario is an issue if we expose transfer_to_id
(sorry, was still in that mental model), but not if we go with your (much wiser, much safer) transfer_to_object<T: key, R: key>(obj: T, owner: &mut R)
approach of requiring a mutable reference to the owner object.
// If we are transfering the ownership to an object, | ||
// we need to check for circular ownership. | ||
let mut parent = owner_object_id; | ||
while parent != obj_id && object_owner_map.contains_key(&parent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a comment here pointing to transfer_to_object
+ explaining how the invariants it enforces relate to algorithm here would help future readers understand why this is safe (and instill appropriate fear for anyone who tries to modify this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could allow transfer_to_id
directly and it should be safe too. The object (that corresponds to the id) to be transferred to must also be in the mutable input object list as well (i.e. any object that could be mutated in an order must be in the input and is mutable). So in that regard, whether we expose transfer_to_object
or transfer_to_id
shouldn't affect the algorithm here. The only reason I chose to expose transfer_to_object
is it seems easier to use.
for object in object_args.iter().filter(|obj| !obj.is_read_only()) { | ||
if let Authenticator::Object(owner_object_id) = object.owner { | ||
object_owner_map.insert(object.id(), owner_object_id); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the ownership controlled by the client? As a consequence, could this be exploited to create a memory blowup?
We could, as a defense:
- use an O(1) space algorithm for cycle detection, à la Floyd, though it does not protect against the time complexity issue,
- better yet, tell users that ownership chains are bounded by a constant (e.g. 100), reject transfers creating a longer chain.
- better yet, since all input objects are mutable, tell users that past a certain boundary we will forcibly implement path compression (i.e. the 101th owning objects and all its ancestors are forcibly transferred to the eventual transitive owner).
Maybe not for this PR, but do we have an issue to track the resource abuse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! Yes I do plan to bound the ownership chain by a small constant. Will add TODO there.
@@ -364,6 +382,20 @@ fn process_successful_execution< | |||
// Charge extra gas based on object size if we are creating a new object. | |||
gas_used += gas::calculate_object_creation_cost(&obj); | |||
} | |||
let obj_id = obj.id(); | |||
object_owner_map.remove(&obj_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we return with CircularObjectOwnership
at 392, this side effect is still in the owner_object_map
.
Are we sure that in all future versions of the code base, nobody will reuse that map? Should we take ownership of that argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function already took ownership of owner_object_map
, so nothing outside can reuse it.
6821ce5
to
3a28d0a
Compare
3447bf8
to
a8b1593
Compare
3a28d0a
to
56246e5
Compare
a8b1593
to
2a3922b
Compare
2a3922b
to
5ca9191
Compare
This PR is also part of #99.
One potential issue that could happen is creating circular ownership: after transferring the owner of an object to another object, the ownership chain forms a cycle.
There are two ways to go about this:
This PR goes with 2 for now, mostly because 1 would put limitations on the expressiveness of the programmability. For instance, we cannot express an intent where we want to either give out a bundle of objects or give out a specific item in that bundle.
It comes with the trade-off that we are only able to catch circular ownership after finishing the execution, instead of at order handling phase, leading to more gas charges for a failed transaction.
TODO: There are a few more things left: