Skip to content
This repository has been archived by the owner on Sep 21, 2024. It is now read-only.

Petname resolution and synchronization in spheres and gateways #253

Merged
merged 7 commits into from
Mar 10, 2023

Conversation

cdata
Copy link
Collaborator

@cdata cdata commented Mar 7, 2023

This change pertains to #12 and started as an effort to close the loop on end-to-end name publishing and resolution. Unfortunately, one thing lead to another and the change came to embody a significant refactor of how we read from and write to spheres in general.

Publishing and resolving names

As of this change, we have implemented everything needed for a user to publish a version of their sphere to the Noosphere Name System, and for another user in the distance to resolve that published version by petname. We've also stubbed out the corresponding C FFI in preparation for #243.

Petname entries are added by a user to their own sphere. When the user sync's with their gateway, the gateway adopts the user's petname changes and kicks off a background job to resolve all the updated names from the Noosphere Name System. As those names are resolved, the gateway incorporates the resolved values into its address book. The next time a user sync's with the gateway after the resolutions are complete, the resolved values propagate back to their local device, and the sync strategy incorporates the new resolutions into the user's own address book.

Publishing happens on every push of the user's history to the sphere. It is technically optional to publish; it requires a specially crafted UCAN invocation, which is marked as optional in the body of the API request. But, we currently do it on every push anyway. When the user pushes, the gateway forwards the updated record to the Noosphere Name System in a background thread.

The SphereContext refactor

The rationale for the refactor is that previously we had only one domain where we were performing a lot of user-driven reads and writes: SphereFs. We had factored things such that SphereFs did internal accounting of pending mutation state to a sphere. This immediately became a problem when we moved to add reading and writing APIs for a different domain (petnames), because SphereFs only dealt with the content part of a sphere.

The new factoring discards SphereFs and replaces it with a series of traits that ship the same API, but that are implemented directly for containers such as Arc<SphereContext<_, _>>. So, where previously we used one construct as a handle to a sphere - SphereContext - and a different one as a read/write API - SphereFs - we now do everything with SphereContext. New traits have been introduced to deal with petnames in a similar style to the traits that deal with content. And, all pending mutations are now "centrally" tracked by the SphereContext itself, so there is no risk of forking history by writing to two different parts of the sphere at the same time.

Fixes #246
Fixes #214
Fixes #215

@cdata cdata force-pushed the feat/resolve-names-in-gateway branch 2 times, most recently from 50ff040 to ae6997c Compare March 7, 2023 20:39
Copy link
Contributor

@jsantell jsantell left a comment

Choose a reason for hiding this comment

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

Looking great! some early comments so far

rust/noosphere-core/src/view/sphere.rs Outdated Show resolved Hide resolved

/// Consume the [Sphere] and get a [Stream] that yields the [ChangelogIpld]
/// for content slugs at each version of the sphere.
pub fn into_link_changelog_stream(
Copy link
Contributor

Choose a reason for hiding this comment

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

Depending on future plans for these stream functions, they could maybe have an enum or query interface, e.g.

enum SphereMetaType {
  Name,
  Link
};

into_changelog_stream(self, SphereMetaType, Option<&Cid>)

or

struct ChangeLogQuery {
  since: Option<&Cid>,
  type: SphereMetaType,
}
into_changelog_stream(self, ChangeLogQuery);

Copy link
Collaborator Author

@cdata cdata Mar 9, 2023

Choose a reason for hiding this comment

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

We might have to manifest this as a macro helper. Changes here refer to the change in a VersionedMap and are typed Changelog<K, V>, so the method invocation needs to imply key and value types in addition to the field.

rust/noosphere-core/src/view/versioned_map.rs Outdated Show resolved Hide resolved
rust/noosphere-gateway/src/nns.rs Outdated Show resolved Hide resolved
rust/noosphere-gateway/src/nns.rs Show resolved Hide resolved
rust/noosphere-sphere/src/sync/gateway.rs Outdated Show resolved Hide resolved
#[ffi_export]
/// Assign a DID to a petname. This will overwrite a petname entry if one already exists
/// with the given name (and reset the resolved CID, if any).
pub fn ns_sphere_petname_set(
Copy link
Contributor

Choose a reason for hiding this comment

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

The tuple-nature of the address book ((id, petname) => cid) doesn't lead to obvious FFI patterns. We should document outcome if petname/did are NULL (implementation of course TBD). I don't think I'd expect the petname to be the "key" here, but changing DID or petname are both valid use cases. Wonder what it'd look like if e.g.

links = ns_sphere_links_get();
for (uint8_t i = 0; i < links.len; i++) {
  ns_link* link = links.array[i];
  printf("name: %s", ns_link_get_name(link));
  printf("did: %s", ns_link_get_did(link));
  ns_link_set_name(link, "alice");
}

Though may cause similar synchronization issues as separation of Fs from Context.

Copy link
Collaborator Author

@cdata cdata Mar 9, 2023

Choose a reason for hiding this comment

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

Note that under the hood, names (like slugs) are keys in a HAMT. This data structure is convenient (and necessary) because it allows us operate on the data with map semantics, while also allowing us to avoid loading in the entire data structure just to read one key.

The implication here is that loading in the entire set of names could be quite expensive as a general purpose operation, especially if we are working on one key at a time. It's expensive in terms of memory allocation and it's expensive in terms of storage access because the key space of the "map" is effectively sharded across many blocks.

That having been said, even in cases where synchronization is not an issue, we would still want to mediate access to the list of petnames via FFI. And, on the Rust side we implement iteration over all petnames/slugs/etc. as Streams - which are effectively async generators - so that we can side-step the pathological case of loading the entire key space into memory at once.

So, I guess the takeaway here is that we need a FFI to access the full space of petnames.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well explained, thanks!

rust/noosphere/src/ffi/petname.rs Show resolved Hide resolved
rust/noosphere/src/ffi/petname.rs Show resolved Hide resolved
rust/noosphere-api/Cargo.toml Show resolved Hide resolved
@cdata cdata force-pushed the feat/resolve-names-in-gateway branch 4 times, most recently from 5b15264 to 04e28fc Compare March 9, 2023 19:48
@cdata cdata force-pushed the feat/resolve-names-in-gateway branch from 04e28fc to d74e0c0 Compare March 9, 2023 19:52
jsantell
jsantell previously approved these changes Mar 9, 2023
Copy link
Contributor

@jsantell jsantell left a comment

Choose a reason for hiding this comment

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

Handful of comments with a closer eye on the NS changes, and some small things like comments. Excellent, gigantic work!!

@@ -101,6 +98,7 @@ Type or paste the code here and press enter:"#,
let cid = Cid::from_str(cid_string.trim())
.map_err(|_| anyhow!("Could not parse the authorization identity as a CID"))?;

debug!("A");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove or clarify

@@ -109,6 +107,7 @@ Type or paste the code here and press enter:"#,
.authorized_by(Some(&Authorization::Cid(cid)))
.build()
.await?;
debug!("B");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove or clarify

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Explanation: b

if updated_names.len() == 0 {
return Ok(None);
}
println!(
Copy link
Contributor

Choose a reason for hiding this comment

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

No println in noosphere imo, this should be rendered in orb if needed.

There's plenty of existing println here though, let's kick off our rules to writing to stdout in a separate issue

if let Some(jwt) = last_known_record {
context.adopt_petname(&name, &identity, &jwt).await?;
} else {
warn!("UH OH");
Copy link
Contributor

Choose a reason for hiding this comment

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

😨

},
can: SphereAction::Publish,
})
.with_lifetime(120)
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this record expire in 2 minutes? We should have some constant, default lifetime duration somewhere

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

SG, although worth noting that at this time we have no expiration of name records (at least, not one that we enforce).

Already filed #259 to discuss

config: DhtConfig,
processor: DhtMessageProcessor,
) -> Result<tokio::task::JoinHandle<Result<(), DhtError>>, DhtError> {
// debug!("SPAWNING WITH VALIDATOR: {:?}", validator);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: comment

store: SphereDb<S>,
dht_config: DHTConfig,
dht_config: DhtConfig,
validator: Option<V>,
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 like a configurable/optional top-level validator (wanting a network of nodes to have relatively similar/consistent validators), but I don't have any other ideas in order to support this (and more pushing and pulling between static DhtConfig values and living instances. I'm glad you've spent some time in this module and the Dht* counterparts for future discussions on some of the cleanup challenges ahead!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Pasting my related comment from Discord:

I'm sympathetic to your position on configurable Validator. Ultimately, I don't think orb-ns will offer a configuration here. Nor would orb. And, it may prove to be an anti-pattern altogether.

And filed #268

@@ -12,7 +12,7 @@ use std::{convert::TryFrom, str, str::FromStr};
use ucan::{builder::UcanBuilder, crypto::KeyMaterial};
use ucan::{chain::ProofChain, crypto::did::DidParser, Ucan};

/// An [NSRecord] is the internal representation of a mapping from a
/// An [NsRecord] is the internal representation of a mapping from a
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not ready to talk about how NsRecord/NSRecord is the only struct prefixed by NS 🙃

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

😅

.build()?
.sign()
.await?;
let _ = store.write_token(&delegation.encode()?).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Kind of a bummer that store is being passed around these utils only for this side-stepping-Ipfs hack here, now that store is internal to the validator

@@ -22,17 +22,31 @@ where
}
}

impl<S> Clone for Validator<S>
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also implement Display like the AllowAllValidator?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, Display is vestigial, I can remove that implementation

@cdata
Copy link
Collaborator Author

cdata commented Mar 10, 2023

Alright folks, we're gonna land this one.

@cdata cdata merged commit 95ea405 into main Mar 10, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
2 participants