Farcaster L2s
Before you read
I often consider casts to be a special "Farcaster application": I could see an alternative universe where Farcaster launched without casts, and casts were an application leveraging Farcaster identity and social graph 1.
A Farcaster without protocol-level casts may sound crazy, but think of what Direct Casts are today: An application that leverages the Farcaster identity and social graph, without being part of the protocol.
This is not an FIP: I find the idea interesting, but it comes with a number of technical, architectural, business and social implications. Adopting this architecture could change what Farcaster is —I'm not even sure if I'd like a Farcaster that moved in this direction.
That said, I also think it’s a space we should explore, discuss and debate, so here’s my contribution.
Problem
Every technical design has its limitations, and so does Farcaster. One of them is the speed that messages propagate throughout the network. An other one is storage limits. And of course, there are a number of widely accepted norms, for example, we expect a cast to be readable text.
An other problem is disk space required for hubs: We know that if Farcaster grew 100x or 1000x, storage requirements for hubs would probably grow to a point that hosting a hub would require very expensive hardware, inevitably leading to centralization.
Specification
Bundles
are Farcaster messages that contain references to messages stored externally (not in hubs).
A Bundle message can be added with a BundleAdd
message and removed3 with a tombstone BundleRemove
message.
A new UnbundleMessages
RPC endpoint allows a client to submit the bundle data to a hub and get the messages back. The hub will validate each message included in the bundle and return only the messages that are valid.
Only Cast add/remove and Reaction add/remove messages can be part of a bundle.
message MessageBundle {
// Only CastAdd, CastRemove, ReactionAdd and RecationRemove
// messages are allowed in messages.
repeated Message messages = 1;
bytes data_hash = 2;
}
message BundleAdd {
string url = 1;
bytes data_hash = 2; // Hash of the data stored at url
bytes parent_bundle = 3; // Message.hash value of the previous bundle
}
message BundleRemove {
bytes target_hash = 1; // Message.hash value of the bundle being removed
}
rpc UnbundleMessages(MessageBundle) returns (MessagesResponse);
Rationale
The high-level idea of this proposal is (is some ways) similar to an Ethereum Validium L2: Messages are bundled and they are stored externally, and hubs store references to these bundles, instead of the messages themselves. In this context, hubs are "Farcaster L1", and the bundles submitted by a specific FID form a "Farcaster L2".
Hubs remain the authority for identity (FIDs), social graph (Links), user profiles and signers.
A bundler bundles multiple messages in a bundle, stores them and posts BundleAdd
messages to hubs. A bundler can be centralized (a typical web2 application) or decentralized (for example, a group of hubs running a modified version of Hubble). If bundles are used to create an L2, then the bundler’s FID defines the L2.
This is the typical flow:
- The application allows its users to send Farcaster messages such as
CastAdd
andReactionAdd
, but these messages are not submitted to a hub. - The application/bundler packages these messages as a
MessageBundle
blob, and stores them somewhere online. The storage location can be public (for example, a public URL or IPFS) or even private (for example, the app database or a protected S3 bucket). - The application/bundler submits a
BundleAdd
message to hubs. The message states the location of the blob and its hash. It may also reference a parent bundle from the same fid. - If an other application wants to retrieve these messages, it has to fetch the blob, and call
UnbundleMessages()
to unbundle them.
Notes:
- Messages are expected to be valid messages, that comply to message rules imposed by hubs (size, signatures, signer validity, etc)
- Users of the application have to trust that the application will actually include their messages in the bundle. They also trust that the application will not delete a bundle that contain their messages.
Cross-layer architecture
In interesting property of this architecture is that messages can be moved between layers:
A message posted on a L2 is a valid message that can be posted at any time to a hub (L1), or copied to any other L2. And a message that was posted to a hub (L1) can be copied to an L2 -even after it’s been purged.
Why would an app use this and not build something from scratch?
The design has a high level compatibility with existing tools and patterns used by developers. For example, shuttle 2 could be extended to support Bundles (developers could also configure their shuttle instance to accept only specific bundlers, using their FID) and make bundles messages available to apps that use it. Similarly, services like Neynar could support bundles from specific FIDs and make their messages available to devs using the existing APIs.
The design also takes advantage of the Farcaster identity infrastructure, social graph, and everything that comes with Farcaster.
Depending on the implementation (this is the case with any “layer 2”), users can have strong assurances that once a message is bundled and submitted, it will be available to others. In any case, users could “move” their messages to L1 if they like.
Farcaster scalability
Farcaster could decrease storage unit limits to push users to use bundles (L2s).
An extreme scenario that would force most users to use a L2 (but would also make it even cheaper for them to use Facraster):
- The first storage unit for each FID could have 0 (zero) casts, 0 bundles, 2500 Links, 50 Verifications and 50 User Data and cost even less than today.
- Storage units after the first one, cost 10x more than today and allow 5000 casts or bundles.
Do we need an FIP for this?
The truth is someone could implement most of what’s described here by using CastAdd
and setting embed.url
to the location where messages are stored, in some custom format.
However, this would be a hack. The protocol gets no benefit, its messy, and the lack of standardisation limits interoperability between tools, layers and services.
Possible use-case examples
The default use case is an app like Warpcast has it's own L2, and posts a bundle every few seconds. All users of the app see the messages instantly, and everyone else who tracks the specific L2 sees them with a small delay of a few seconds.
Other, possible use cases:
- A sports app that allows users to comment in real-time during a game, offering a chat-like experience. The app also uses a special pattern for each cast, where the first characters are reserved for the minute/second of the game, and the user’s favorite team. All messages are stored in a RDBMS during the game. At the end of the game, they are bundled, stored on IPFS and a
BundleAdd
is submitted to Farcaster. - A Farcaster archiver keeps track of pruned casts and reactions. Every day, it stores them on IPFS and submits them as a bundle to Farcaster.
- An app designed to preserve privacy encrypts casts, without worrying that they will appear as noise to the rest of the users.
- An L2 that hosts content not allowed in Warpcast, such as adult content.
- A "private" L2: Messages are stored in a non-public location that requires authentication and are not available to the public.
- A bundler can be open (for example, Warpcast could maintain their own bundler that exposes an open API to be used by other apps), or closed (for example one that can only be used by a proprietary app, and requires users to pay to use it.)
Concerns/TBD
An assorted list of concerns and points that need further discussion:
- A malicious user can copy all messages of an FID from a bundle (L2) and post them to L1, consuming all of FID’s storage space. Is this really a problem?
- Could this design lead to Balkanization of Farcaster, with multiple, isolated L2s that don’t interact or are not even aware of each other?
- Do we want to allow bundlers that do not store bundles in a publicly accessible storage? If not, how can this be prevented other than by social norms?
- How will an app handle bundled messages when a signer used to sign some of them is removed? Where does this logic reside?
- To which extent
UnbundleMessages
performs tests? Should it reject duplicate messages in a bundle? Should it reject a cast that is already on L1? - Are apps expected to mix bundled messages and “normal” messages? Is it either L1 or L2 and messages are not mixed? Or is it L1 + a single L2?
- How can someone reference a message included in a bundle in such a way that a third party knows it should fetch the bundle first?
- What happens if a
CastRemove
message is submitted to a hub, that refers to a message included in a bundle? - How can this model be abused?
- Set a limit on the size of
MessageBundle
. - App guidelines to avoid user confusion?
UnbundleMessage
will be a resource-intensive call. Hubs must be able to restrict it to specific users or IPs.