Last active
June 7, 2024 15:10
-
-
Save Frando/f31755cff68994a51659bf8ad7d03d1c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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