`DynamicField` vs `DynamicObjectField`--why do we have both?

This is an answer to a question from Paul in the Suinami Riders group:

Also I still haven’t been able to figure out why DynamicObjectField exists when we can just use DynamicField. As far as I can tell, in Move, in Sui explorer, and for RPC requests, they’re both identical. I can’t think of any compelling reason to keep DynamicObjectField; it seems to just add 3 duplicate modules in sui-core without adding anything. I vote to deprecate it.

The key difference here is that creating a dynamic field allocates a new object for the field value, whereas creating a dynamic field reuses an existing object. This sounds like a minor difference, but this has some important implications:

  • Reusing an existing object means that it keeps the same object ID and can still be read from external APIs via getObjectById. This is highly desirable if (e.g.) you’re building a marketplace that indexes listings by dynamic fields, and you still want to be able to find each NFT by its ID
  • Reusing an existing object has lower storage overhead because there’s no need to allocated new object metadata for the value

In general, we would recommend using dynamic object fields when the type in question has key, and dynamic fields otherwise. The only exception would be if you want to “hide” the ID of a stored object by wrapping it for one reason or another.

For more on dynamic fields, check out:

22 Likes

To my knowledge, there is no sui_getObjectByID; it’s just sui_getObject (and it does in fact use the object-ID as the query param). And sui_getObject works with both Dynamic Fields and Dynamic Object Fields, because every dynamic field is given assigned an object-id.

I see what you mean about object-id durability however. Would it be possible that, when ObjectA is stored inside of ObjectB, that ObjectA retains its original UID? Because right now ObjectA’s UID is wrapped around a newly generated UID.

We could call this ‘stable id for dynamic fields’; we could probably eliminate dynamic_object_fields entirely if this change were made. I think it makes a lot of sense.

8 Likes

Would it be possible that, when ObjectA is stored inside of ObjectB, that ObjectA retains its original UID?

I’m not sure I fully understand this. To make sure we’re on the same page, here’s how things work today, let me know where you think it should be different

struct A has key, store { id: UID }
struct B has key { id: UID, a: A }

fun wrap(a: A, ctx: &mut TxContext): B {
  B { id: ..., a } // a retains its UID, but is not accessible via getObject
}

fun dyn_field(a: A, t: &mut Table<u64, A>) {
  // uses dynamic fields under the hood: create a new object and wrap a in it
  // a retains its UID, but is not accessible via getObject
  table::add(t, 0, a)
}

fun dyn_obj_field(a: A, t: &mut ObjectTable<u64, A>) {
  // uses dynamic object fields under the hood.
  // a retains its UID, and is accessible via getObject
  object_table::insert(t, 0, a)
}

fun dyn_field_prim(x: u64, t: &mut Table<u64, u64>) {
  // uses dynamic fields under the hood: create a new object and wrap x in it
  table::add(t, 0, x)
}

Dynamic object fields are needed because in code like dyn_obj_field, you (1) want a to continue being accessible via getObject and (2) want to save the storage cost of allocating an extra object to wrap a in.

5 Likes

Okay I think this is the point I didn’t understand; so dynamic_field creates a new object, and wraps A in it. A still has its own UID, but it’s now inside of another object. This wrapper object makes the original A inaccessible via getObject.

For dynamic_object_field, because the object is an object, we skip the wrapper for A, and store it naked, directly.

Would it be possible to combine these two functions together? In the sense that if A has key + store, it is stored ‘naked’, as in dynamic_object_field, whereas if A has just store, then a wrapper will be created and A placed inside?

What would be even better yet is if we could get rid of wrapper objects altogether; like if I want to store a single u64, which is just 8 bytes, do we really want to allocate an entire object to store it inside of? Wouldn’t it be better if the original object / Move struct that it’s stored inside of could be ‘extended’ to include new objects or primitive values, without needing to add wrapper objects in the first place? I’d think it would be more efficient (in terms of computation and storage space) if this were possible.

My understanding of it is that all these wrapper objects are basically a ‘hack’ in order to add dynamic fields to the original Move VM / Sui without having to rewrite them. (I don’t understand the inner workings of the Move VM however.)

1 Like

fun dyn_field(a: A, t: &mut Table<u64, A>) {
// uses dynamic fields under the hood: create a new object and wrap a in it
// a retains its UID, but is not accessible via getObject
table::add(t, 0, a)
}

Okay I think this is the part I didn’t understand properly; so dynamic_field creates a new object which is used to wrap A, whereas dynamic_field simply stores A naked, without a wrapper object.

Wouldn’t it be possible to combine these two together? So that if A has key + store, no wrapper object is created and A is stored naked, whereas if A only has store, then a wrapper object is used and that wrapper object is stored naked. I’d think that sort of conditional logic would simplify the dynamic field API.

Better yet; why use wrapper objects at all? If the original struct that we’re storing the field inside of could be extended that would be way more efficient (in terms of computation and storage). Why allocate an entire new object (20+ bytes?) to store an 8 byte u64?

To my best understanding, dynamic-fields are essentially a ‘hack’ on top of the Move VM / Sui to extend its behavior without having to rewrite it. It might be more efficient to modify the Move VM to allow for dynamically adding fields, rather than the current behavior where structs cannot be extended. (Although I don’t have a deep understanding of how the Move VM works under the hood.)

1 Like

fun dyn_field(a: A, t: &mut Table<u64, A>) {
// uses dynamic fields under the hood: create a new object and wrap a in it
// a retains its UID, but is not accessible via getObject
table::add(t, 0, a)
}

okay this is the part I wasn’t understanding; so dynamic field adds a wrapper object to store A, whereas dynamic object field stores A naked. Why not combine this behavior together into a single API, such that if A has key + store we store A naked, whereas if A has just store, then we create a wrapper object, place A inside of it and then store that instead. This conditional logic would simplify the dynamic field API greatly.

Better yet; why use wrapper objects altogether? If I’m storing an 8-byte u64, why allocate an additional 20+ bytes for another object-id as well? It would be better if the new field could simply be stored with the original Move struct that we’re expanding. This would be more efficient, in terms of storage and gas.

My understanding of dynamic fields is that they are essentially a ‘hack’ used to extend the Move VM / Sui without rewriting those core components, because the original Move VM does not supporting adding fields to structs itself, hence all the wrapper objects.

2 Likes

@shb I have a question about the accessibility of data on the dynamic fields.

I thought I could hide data by using a dynamic field, but it is not. But I think SUI should support it. For example, I want to store all paid content on SUI, but would require user to pay to view the gated content. How could I achieve this on SUI?

1 Like