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

fix(graphql): add root object version for dynamic field queries #17934

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8adafa1
fix(graphql): add root object version for dynamic field queries
unmaykr-aftermath May 25, 2024
c045af6
chore(graphql): remove TODO
unmaykr-aftermath May 25, 2024
c61cbe8
refactor(graphql): remove redundant `DynamicField::parent_version` attr
unmaykr-aftermath Jun 4, 2024
5afe7a4
chore(graphql): address review comments
unmaykr-aftermath Jun 4, 2024
8cbe505
feat(graphql): add MissingRootObject error for DF queries
unmaykr-aftermath Jun 9, 2024
52527c9
Revert "feat(graphql): add MissingRootObject error for DF queries"
unmaykr-aftermath Jun 9, 2024
592ffbb
feat(graphql): add MissingRootVersion error
unmaykr-aftermath Jun 9, 2024
dedad22
Revert "feat(graphql): add MissingRootVersion error"
unmaykr-aftermath Jun 13, 2024
bc4d3af
Reapply "feat(graphql): add MissingRootObject error for DF queries"
unmaykr-aftermath Jun 13, 2024
886cbd5
Revert "feat(graphql): add MissingRootObject error for DF queries"
unmaykr-aftermath Jun 13, 2024
3b74764
feat(graphql): relax mechanics for setting the root obj version
unmaykr-aftermath Jun 13, 2024
1f41206
docs(graphql): apply suggestions
unmaykr-aftermath Jun 14, 2024
1ea31c2
docs(graphql): update DynamicField.value
unmaykr-aftermath Jun 15, 2024
cda0593
docs(graphql): apply suggestion for DynamicField.value
unmaykr-aftermath Jun 17, 2024
fecea0c
chore(graphql): update schema for DynamicField.value
unmaykr-aftermath Jun 17, 2024
03124cf
chore(graphql): always return a version in version_for_dynamic_fields
unmaykr-aftermath Jun 17, 2024
538ccff
chore(graphql): update comments and docs
unmaykr-aftermath Jun 17, 2024
4f74c5f
e2e test for rooted dynamic field queries
wlmyng Jun 4, 2024
a0a9f12
test immutable behavior
wlmyng Jun 18, 2024
f6fe4a7
Merge pull request #1 from wlmyng/wlmyng/test-unmaykr-fix-graphql-nes…
unmaykr-aftermath Jun 19, 2024
dfda753
Merge branch 'main' into unmaykr/fix-graphql-nested-dof-queries
unmaykr-aftermath Jun 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -889,9 +889,10 @@ type DynamicField {
"""
name: MoveValue
"""
The actual data stored in the dynamic field.
The returned dynamic field is an object if its return type is MoveObject,
in which case it is also accessible off-chain via its address.
The returned dynamic field is an object if its return type is `MoveObject`,
and its contents will be from the latest version that is at most equal to
its parent object's version. Otherwise, it is also accessible off-chain
via its address.
unmaykr-aftermath marked this conversation as resolved.
Show resolved Hide resolved
"""
value: DynamicFieldValue
}
Expand Down
9 changes: 5 additions & 4 deletions crates/sui-graphql-rpc/src/types/coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ impl Coin {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_.super_)
.dynamic_field(ctx, name, Some(self.super_.super_.version_impl()))
.dynamic_field(ctx, name, Some(self.super_.root_version()))
.await
}

Expand All @@ -261,7 +261,7 @@ impl Coin {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_.super_)
.dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl()))
.dynamic_object_field(ctx, name, Some(self.super_.root_version()))
.await
}

Expand All @@ -284,7 +284,7 @@ impl Coin {
after,
last,
before,
Some(self.super_.super_.version_impl()),
Some(self.super_.root_version()),
)
.await
}
Expand Down Expand Up @@ -336,7 +336,8 @@ impl Coin {
// To maintain consistency, the returned cursor should have the same upper-bound as the
// checkpoint found on the cursor.
let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
let object = Object::try_from_stored_history_object(stored, checkpoint_viewed_at)?;
let object =
Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?;

let move_ = MoveObject::try_from(&object).map_err(|_| {
Error::Internal(format!(
Expand Down
6 changes: 3 additions & 3 deletions crates/sui-graphql-rpc/src/types/coin_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl CoinMetadata {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_.super_)
.dynamic_field(ctx, name, Some(self.super_.super_.version_impl()))
.dynamic_field(ctx, name, Some(self.super_.root_version()))
.await
}

Expand All @@ -250,7 +250,7 @@ impl CoinMetadata {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_.super_)
.dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl()))
.dynamic_object_field(ctx, name, Some(self.super_.root_version()))
.await
}

Expand All @@ -273,7 +273,7 @@ impl CoinMetadata {
after,
last,
before,
Some(self.super_.super_.version_impl()),
Some(self.super_.root_version()),
)
.await
}
Expand Down
24 changes: 14 additions & 10 deletions crates/sui-graphql-rpc/src/types/dynamic_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ impl DynamicField {
Ok(Some(MoveValue::new(type_tag, Base64::from(bcs))))
}

/// The actual data stored in the dynamic field.
/// The returned dynamic field is an object if its return type is MoveObject,
/// in which case it is also accessible off-chain via its address.
/// The returned dynamic field is an object if its return type is `MoveObject`,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this comment still needs updating.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I think I accidentally undid it because I committed your suggestion directly on the schema file instead of on the source code and then running tests

/// and its contents will be from the latest version that is at most equal to
/// its parent object's version. Otherwise, it is also accessible off-chain
/// via its address.
async fn value(&self, ctx: &Context<'_>) -> Result<Option<DynamicFieldValue>> {
if self.df_kind == DynamicFieldType::DynamicObject {
// If `df_kind` is a DynamicObject, the object we are currently on is the field object,
Expand All @@ -107,12 +108,7 @@ impl DynamicField {
let obj = MoveObject::query(
ctx,
self.df_object_id,
Object::under_parent(
// TODO (RPC-131): The dynamic object field value's version should be bounded by
// the field's parent version, not the version of the field object itself.
self.super_.super_.version_impl(),
self.super_.super_.checkpoint_viewed_at,
),
Object::under_parent(self.root_version(), self.super_.super_.checkpoint_viewed_at),
)
.await
.extend()?;
Expand Down Expand Up @@ -228,7 +224,11 @@ impl DynamicField {
// checkpoint found on the cursor.
let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();

let object = Object::try_from_stored_history_object(stored, checkpoint_viewed_at)?;
let object = Object::try_from_stored_history_object(
stored,
checkpoint_viewed_at,
parent_version,
)?;

let move_ = MoveObject::try_from(&object).map_err(|_| {
Error::Internal(format!(
Expand All @@ -243,6 +243,10 @@ impl DynamicField {

Ok(conn)
}

pub(crate) fn root_version(&self) -> u64 {
self.super_.root_version()
}
}

impl TryFrom<MoveObject> for DynamicField {
Expand Down
20 changes: 10 additions & 10 deletions crates/sui-graphql-rpc/src/types/move_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ impl MoveObject {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_)
.dynamic_field(ctx, name, Some(self.super_.version_impl()))
.dynamic_field(ctx, name, Some(self.root_version()))
.await
}

Expand All @@ -329,7 +329,7 @@ impl MoveObject {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_)
.dynamic_object_field(ctx, name, Some(self.super_.version_impl()))
.dynamic_object_field(ctx, name, Some(self.root_version()))
.await
}

Expand All @@ -346,14 +346,7 @@ impl MoveObject {
before: Option<object::Cursor>,
) -> Result<Connection<String, DynamicField>> {
OwnerImpl::from(&self.super_)
.dynamic_fields(
ctx,
first,
after,
last,
before,
Some(self.super_.version_impl()),
)
.dynamic_fields(ctx, first, after, last, before, Some(self.root_version()))
.await
}

Expand Down Expand Up @@ -461,6 +454,13 @@ impl MoveObject {
})
.await
}

/// Root parent object version for dynamic fields.
///
/// Check [`Object::root_version`] for details.
pub(crate) fn root_version(&self) -> u64 {
self.super_.root_version()
}
}

impl TryFrom<&Object> for MoveObject {
Expand Down
78 changes: 70 additions & 8 deletions crates/sui-graphql-rpc/src/types/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ pub(crate) struct Object {
pub kind: ObjectKind,
/// The checkpoint sequence number at which this was viewed at.
pub checkpoint_viewed_at: u64,
/// Root parent object version for dynamic fields.
///
/// This enables consistent dynamic field reads in the case of chained dynamic object fields,
/// e.g., `Parent -> DOF1 -> DOF2`. In such cases, the object versions may end up like
/// `Parent >= DOF1, DOF2` but `DOF1 < DOF2`. Thus, database queries for dynamic fields must
/// bound the object versions by the version of the root object of the tree.
///
/// Essentially, lamport timestamps of objects are updated for all top-level mutable objects
/// provided as inputs to a transaction as well as any mutated dynamic child objects. However,
/// any dynamic child objects that were loaded but not actually mutated don't end up having
/// their versions updated.
root_version: u64,
}

/// Type to implement GraphQL fields that are shared by all Objects.
Expand Down Expand Up @@ -462,7 +474,7 @@ impl Object {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(self)
.dynamic_field(ctx, name, Some(self.version_impl()))
.dynamic_field(ctx, name, Some(self.root_version()))
.await
}

Expand All @@ -479,7 +491,7 @@ impl Object {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(self)
.dynamic_object_field(ctx, name, Some(self.version_impl()))
.dynamic_object_field(ctx, name, Some(self.root_version()))
.await
}

Expand All @@ -496,7 +508,7 @@ impl Object {
before: Option<Cursor>,
) -> Result<Connection<String, DynamicField>> {
OwnerImpl::from(self)
.dynamic_fields(ctx, first, after, last, before, Some(self.version_impl()))
.dynamic_fields(ctx, first, after, last, before, Some(self.root_version()))
.await
}

Expand Down Expand Up @@ -672,15 +684,24 @@ impl Object {
/// `checkpoint_viewed_at` represents the checkpoint sequence number at which this `Object` was
/// constructed in. This is stored on `Object` so that when viewing that entity's state, it will
/// be as if it was read at the same checkpoint.
///
/// `root_version` represents the version of the root object in some nested chain of dynamic
/// fields. This should typically be left `None`, unless the object(s) being resolved is a
/// dynamic field, or if `root_version` has been explicitly set for this object. If None, then
/// we use [`version_for_dynamic_fields`] to infer a root version to then propagate from this
/// object down to its dynamic fields.
pub(crate) fn from_native(
address: SuiAddress,
native: NativeObject,
checkpoint_viewed_at: u64,
root_version: Option<u64>,
wlmyng marked this conversation as resolved.
Show resolved Hide resolved
) -> Object {
let root_version = root_version.unwrap_or_else(|| version_for_dynamic_fields(&native));
Object {
address,
kind: ObjectKind::NotIndexed(native),
checkpoint_viewed_at,
root_version,
}
}

Expand All @@ -702,6 +723,13 @@ impl Object {
}
}

/// Root parent object version for dynamic fields.
///
/// Check [`Object::root_version`] for details.
pub(crate) fn root_version(&self) -> u64 {
self.root_version
}

/// Query the database for a `page` of objects, optionally `filter`-ed.
///
/// `checkpoint_viewed_at` represents the checkpoint sequence number at which this page was
Expand Down Expand Up @@ -766,7 +794,8 @@ impl Object {
// To maintain consistency, the returned cursor should have the same upper-bound as the
// checkpoint found on the cursor.
let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
let object = Object::try_from_stored_history_object(stored, checkpoint_viewed_at)?;
let object =
Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?;
conn.edges.push(Edge::new(cursor, downcast(object)?));
}

Expand Down Expand Up @@ -854,9 +883,16 @@ impl Object {
/// `checkpoint_viewed_at` represents the checkpoint sequence number at which this `Object` was
/// constructed in. This is stored on `Object` so that when viewing that entity's state, it will
/// be as if it was read at the same checkpoint.
///
/// `root_version` represents the version of the root object in some nested chain of dynamic
/// fields. This should typically be left `None`, unless the object(s) being resolved is a
/// dynamic field, or if `root_version` has been explicitly set for this object. If None, then
/// we use [`version_for_dynamic_fields`] to infer a root version to then propagate from this
/// object down to its dynamic fields.
pub(crate) fn try_from_stored_history_object(
history_object: StoredHistoryObject,
checkpoint_viewed_at: u64,
root_version: Option<u64>,
wlmyng marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<Self, Error> {
let address = addr(&history_object.object_id)?;

Expand All @@ -881,10 +917,13 @@ impl Object {
Error::Internal(format!("Failed to deserialize object {address}"))
})?;

let root_version =
root_version.unwrap_or_else(|| version_for_dynamic_fields(&native_object));
Ok(Self {
address,
kind: ObjectKind::Indexed(native_object, history_object),
checkpoint_viewed_at,
root_version,
})
}
NativeObjectStatus::WrappedOrDeleted => Ok(Self {
Expand All @@ -896,11 +935,23 @@ impl Object {
checkpoint_sequence_number: history_object.checkpoint_sequence_number,
}),
checkpoint_viewed_at,
root_version: history_object.object_version as u64,
}),
}
}
}

/// We're deliberately choosing to use a child object's version as the root here, and letting the
/// caller override it with the actual root object's version if it has access to it.
///
/// Using the child object's version as the root means that we're seeing the dynamic field tree
/// under this object at the state resulting from the transaction that produced this version.
Comment on lines +944 to +948
Copy link
Contributor

Choose a reason for hiding this comment

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

This might make more sense as a doc comment for fn value of DynamicField, something like "If the value is a child object, its contents come from the latest version that is at most equal to its parent object's version."

Copy link
Contributor

Choose a reason for hiding this comment

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

If that makes sense, you'll need to run snapshot_tests.rs to regenerate the schema and update the snapshot test

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 what I came up for DynamicField.value:

     /// The actual data stored in the dynamic field.
     /// The returned dynamic field is an object if its return type is MoveObject,
-    /// in which case it is also accessible off-chain via its address.
+    /// in which case it is also accessible off-chain via its address and the latest
+    /// version that is at most equal to its parent object's version.

Good to go?

Copy link
Contributor

Choose a reason for hiding this comment

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

Apologies in advance for bikeshedding 😅 but it's possible for someone to access the child object directly off-chain via its address and at any version, so this is not entirely accurate. Proposing...

The returned dynamic field is an object if its return type is MoveObject, and its contents will be from the latest version that is at most equal to its parent object's version. Otherwise, it is also accessible off-chain via its address.

///
/// See [`Object::root_version`] for more details on parent/child object version mechanics.
fn version_for_dynamic_fields(native: &NativeObject) -> u64 {
native.as_inner().version().into()
}

impl ObjectFilter {
/// Try to create a filter whose results are the intersection of objects in `self`'s results and
/// objects in `other`'s results. This may not be possible if the resulting filter is
Expand Down Expand Up @@ -1167,8 +1218,14 @@ impl Loader<HistoricalKey> for Db {
continue;
}

let object =
Object::try_from_stored_history_object(stored.clone(), key.checkpoint_viewed_at)?;
let object = Object::try_from_stored_history_object(
stored.clone(),
key.checkpoint_viewed_at,
// This conversion will use the object's own version as the `Object::root_version`
// if it's not a child object, but if it is, it will be set to `None`, since we do
// not have any information on the `root_version` of the fetched dynamic object.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this comment needs to updated as well now, right? Because we'll always set to the object's own version if this is None.

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

None,
)?;
result.insert(*key, object);
}

Expand Down Expand Up @@ -1255,8 +1312,13 @@ impl Loader<LatestAtKey> for Db {
for (group_key, stored) in
group.map_err(|e| Error::Internal(format!("Failed to fetch objects: {e}")))?
{
let object =
Object::try_from_stored_history_object(stored, group_key.checkpoint_viewed_at)?;
let object = Object::try_from_stored_history_object(
stored,
group_key.checkpoint_viewed_at,
// If `LatestAtKey::parent_version` is set, it must have been correctly
// propagated from the `Object::root_version` of some object.
group_key.parent_version,
)?;

let key = LatestAtKey {
id: object.address,
Expand Down
6 changes: 3 additions & 3 deletions crates/sui-graphql-rpc/src/types/stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ impl StakedSui {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_.super_)
.dynamic_field(ctx, name, Some(self.super_.super_.version_impl()))
.dynamic_field(ctx, name, Some(self.super_.root_version()))
.await
}

Expand All @@ -269,7 +269,7 @@ impl StakedSui {
name: DynamicFieldName,
) -> Result<Option<DynamicField>> {
OwnerImpl::from(&self.super_.super_)
.dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl()))
.dynamic_object_field(ctx, name, Some(self.super_.root_version()))
.await
}

Expand All @@ -292,7 +292,7 @@ impl StakedSui {
after,
last,
before,
Some(self.super_.super_.version_impl()),
Some(self.super_.root_version()),
)
.await
}
Expand Down
Loading
Loading