Skip to content

Instantly share code, notes, and snippets.

@Frando
Last active June 7, 2024 15:10
Show Gist options
  • Save Frando/f31755cff68994a51659bf8ad7d03d1c to your computer and use it in GitHub Desktop.
Save Frando/f31755cff68994a51659bf8ad7d03d1c to your computer and use it in GitHub Desktop.
// Exploration how we will expose willow in iroh.
// No client API exists atm in the willow branch. This is a design sketch.
// Willow adds more complexity especially around capabilities.
// I will first write a "full power" version, and then try to simplify it for common use cases.
let node = Node::memory().spawn().await?;
// We create an author. This could stay roughly the same as currently.
// Note that in iroh-willow, what we call an author is called a user.
// Because willow brings read capabilities in addition to write capabilities,
// and the read caps are attached to the same class of keypairs as write caps,
// I do think that that the `user` term makes more sense than `author`.
let user = node.users.create().await?; // alternatively use node.users.default();
// To use a document, we have to create a namespace keypair.
// In iroh-docs this is called "creating a document", and we can keep that terminology if we want.
// We have to decide whether the namespace is owned or communal though. This property is embedded in the namespace keypair itself.
// We could decide to, at least initially, only expose owned namespaces. If we do that, the argument would not be needed here.
let doc = node.docs.create(NamespaceKind::Owned).await?;
// We also have to create a capability that gives our user a write permission for that namespace.
// This is only possible if we have the secret key for the namespace and would otherwise fail with an error.
let write_cap = node.caps.mint(doc.id(), user, AccessMode::Write, Area::full()).await?;
// Now we can use our doc!
let doc = node.docs.open(namespace).await?;
// What was `key` in iroh-docs is `path` in iroh-willow, and is always is a list of components now
let path = Path::new(&[b"foo", b"bar"]);
// Entries in willow are authenticated by a capability token attached to each entry.
// So instead of passing a user, we need to pass the capability.
// We'd throw an error if the capability's area does not include the entry.
doc.insert_bytes(write_cap, path, b"hello world").await?;
// ... and similar methods for insert_stream, insert_from_path etc like we have in current docs client.
// OK, we created an entry! Reading from the doc locally can roughly like in iroh-docs.
let mut entries = doc.get_many(Query::new().prefix(&[b"foo"]).subspace(&user)).await?;
while let Some(entries) = entries.try_next().await? {
// ...
}
// Bueno! But what do we do if we created the capability previously, and now want to use it again?
// mintCapability() would create a capability token, and store it in the redb or memory depending on node storage.
// We need an API to retrieve these capabilities.
// This would return the first capability that can authorize writes for a specific author and namespace at some path.
let user = node.users.default().await?;
let cap = node.caps.find_one(namespace, user, AccessMode::Write, Path::new(&[b"foo", b"bar"]).await?;
// Note that there could be multiple! For example because we were given two different delegated capabilities from different peers.
let caps = node.auth.find_many(namespace, user, AccessMode::Write, Path::new(&[b"foo", b"bar"]).await?;
// Now we can use the capability!
let doc = node.docs.open(id).await?;
doc.insert_bytes(cap, &[b"foo", b"bar", b"baz"], b"hi there!").await?;
// Puh, this is a lot of wrangling. Note that we needed a path in findWriteCapability even!
// So we really need to simplify this for the common case.
// Maybe we can add an enum like this:
enum CapabilityOpt {
Any(UserId),
Explicit(WriteCapability)
}
// And let's add From<&AuthorId> for CapabilityOpt -> CapbilityOpt::Any(AuthorId)
// And on the Doc, we could have
impl Doc {
fn insert_bytes(&self, cap: impl Into<CapabilityOpt>, path: impl Into<Path>, bytes: impl AsRef<[u8]>) { .. }
// etc.
}
// With this, we could be back at were we were in iroh-docs:
doc.insert_bytes(&user, &[b"foo", b"bar"], b"hello world").await?;
// In the RPC handler or in willow, we'd use `node.caps.find_one()`
// to find the first capability that gives `author` write access to `path`, and use that.
// If no matching cap is found, we'd return an Error.
// OK! Next up: Sharing!
// In willow, you need a read capability issued for a user keypair to retrieve entries through sync.
// This means we could have an API like this:
let ticket = doc.share_with(other_user, AccessMode::Read, Area::full()).await?;
// Which would be a short-hand for:
let my_read_cap = node.caps.find_one(namespace, my_user, AccessMode::Read, Area::full()).await?;
let delegated_cap = node.caps.delegate(my_read_cap, other_user, AccessMode::Read, Area::full()).await?;
let ticket = DocTicket::new(doc.id()).with_cap(delegated_cap).with_nodes(my_node_addr);
// This flow is the primary one in willow. However these caps and tickets are issued for a specific user.
// We likely want to have public docs as well where you can post a ticket somewhere and any user can use it.
// There's two ways how we can enable that:
// 1) Issue a capability to a user whose secret key is [0u8; 32] - which therefore anyone can use.
// This would look very similar and be quite transparent.
let ticket = doc.share_with(PUBLIC_GUEST_USER, AccessMode::Read, Area::full()).await?;
// Using this ticket would just work: anyone has the secret key for this user, because it is [0u8; 32]
// 2) Have a notion of "public" and "private" documents, and do not require a read capability for public docs.
// Even though a "read capability" is always required in willow, it is a generic protocol parameter, so we can
// use an enum for it
enum ReadCapability {
Anon(NamespaceId),
Permissioned(McCapability)
}
// However we'd need a place to embed the notion whether a document is public or not,
// and this notion should travel to other peers during sync. So either we have to always add some info to a namespace pubkey
// or we embed it *within*, like we do for the communal vs owned distinction. I would prefer the latter. This would mean
// that you'd have to decide a doc creation time whether a doc is public or private and cannot change it later.
let doc = node.docs.create(NamespaceKind::Owned, PermissionKind::Public).await?;
let ticket = doc.share_read_public(Area::full()).await?;
// We likely have to take a decision between the two options presented above.
// I am not sure yet which one I prefer.
// OK, I have a ticket, how do I use it?
// This can remain simple:
let doc = node.docs.import_ticket(&ticket).await?;
// .. but we likely would also need the manual way, because complex apps will manage capabilities themselves:
let cap = node.caps.import(&exported_cap).await?;
let doc = node.docs.open(&namespace_id).await?;
// OK! How would syncing work?
// We should definitely expose a simple way to do 1-on-1 syncs.
doc.sync_with_peer(&node_id).await?;
// ^^ would start to sync with a peer
let opts = SyncOpts::new()
.mode(SyncMode::ReconcileOnce)
.area(Area::with_prefix(&[b"foo"]));
doc.sync_with_peer_with_opts(&node_id, opts).await?;
// ^^ would run a single set reconciliation and only on a specific area
// For swarm mode, we have a big unsolved constraint:
// With selective read capabilities over sections of namespaces, we cannot assume that everyone can forward everything.
// So if your neighbors have only limited capabilities, it might happen that you insert a new entry but can't gossip it
// because your direct neighbors have no capability to read it. Other peers in the swarm might have such a capability,
// but you wouldn't know that. Bad! I don't have a solution for that.
// So maybe we only can do swarming for full capabilities (that cover the full area of a namespace).
// If we do this, it would be the same as with iroh-docs:
doc.join_swarm(vec![node_a, node_b]).await?;
// would fail with error if we have only a partial read capability for the doc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment