This is a proposal that introduces ordering to the Farcaster protocol. (See farcasterxyz/protocol#193)
The main advantage of this proposal is that it does not require any coordination (for example no need to appoint special roles or powers to a specific serializer).
There is a mempool where every hub submits deltas. Miners propose new blocks, each block is linked to the previous one in a blockchain. Anyone can spin up a miner.
Miners have to stake (lock) an amount of ETH but this amount is not fixed and may go up automatically if many miners compete in block creation. The stake is not slashed or lost in any way, but it will remain locked for 100 days, before the miner can withdraw it.
The architecture takes advantage of the OP sequencer (onchain events) which in practice becomes the Farcaster chain’s clock. Forks are very rare, but can be resolved if/when they happen. The design provides good censorship resistance.
For a given set of Forecaster deltas, any ordering that is sequentially valid (every message is processed in order, and is found to be valid at the time of processing) will result in the same state.
(Proof to be added, but I think it's what makes CRDTs work anyways.)
The architecture assumes that there are at least a few miners interested in keeping the Forecaster chain healthy and optimized. These miners will be operated by entities such as Warpcast, Neynar and other apps that have a considerable investment in the protocol. These miners are expected to have sufficient capacity and uptime to lead block proposal most of the time.
There is a mempool where any hub can submit messages.
There is a new type of OnChainEventType, EVENT_TYPE_OP_BLOCK
that corresponds to an OP block hash with height divisible by 5.
The reason we introduce this new event is:
- Onchain events are more rare than deltas, and we want to make sure every block has at least one onchain event (reduces the possibility of forks).
- By requiring each block to include one OP_BLOCK event (see Miners section), we practically set the rate of block generation. OP block generation rate is one every 2 seconds, so requiring its height to be a multiple of 5 corresponds to one OP_BLOCK event every 10 seconds.
There is a new type of OnChainEventType, EVENT_TYPE_CHECKPOINT
that corresponds to a block height and its hash.
Miners (or anyone, practically) can commit the height and the hash of an old block to a predefined smart contract. The event is validated by hubs, and it must correspond to a block they have in store. A valid checkpoint event for block N must be included in a block M where M-N < 100,000 (~ 12 days, ie. a miner can’t include a checkpoint event that goes back 1m blocks and may no longer be present in the chain stored by hubs.)
Miners are nodes that bundle messages in blocks. Anyone can spin up a miner, no permission is required. Miners have to stake a small amount of ETH that they can claim back after 100 days. (See “Staking” section.)
Miners read deltas from the mempool and events from the blockchain and bundle them in blocks. They are also free to include deltas they received from other sources, for example, directly from an app, as long as they are valid.
- Every block must include an OnChainEvent of type OP_BLOCK. (I.e. a new block can be generated only every 10 seconds.)
- When they have assembled a new block, miners broadcast it to the network.
- Deltas in a block must be ordered lexicographically by hash. Before ordering, hashes are prepended a byte, that makes onchain events come before other deltas (first op_block, then id registrations, then storage rents, then signers, then other deltas). This is a simple way to make the block sequentially valid.
- A block must be sequentially valid.
- Messages in a block must not be in a previous block
- Each block links to the hash of the previous block
- Each block is signed by the miner’s key.
- Well-behaved miners should check with their peers before broadcasting a block. If there is a better block (that will be preferred by hubs), they should not broadcast their block. This will reduce unnecessary traffic, and hub processing power. However, this is not enforced by the protocol.
The block summary consists of the contents of a block without the message bodies (only block header and the message hashes). Miners may not need to maintain the full state of the network, just the block summary, because once they validate messages, they only care about the message hashes.
Every block has a BlockScore:
BlockScore = number_of_messages / number_of_blocks
where number_of_messages
is the number of messages in the block and number_of_blocks
is the number of blocks this miner contributed in the last N (for example 100) previous blocks of the chain.
The score is higher if a block has more messages, and goes down as a miner adds more blocks.
Each chain has an OCEC which is the number of OnChainEvents it contains (the whole chain).
ocec = sum( count(e) for e in OnChainEvents in all blocks )
Hubs work similar to how they work today, with the following changes:
- They do not listen proactively for onchain events, but they validate them when they find them in a block.
- They submit new messages to the mempool.
- They validate blocks and reject invalid ones.
- They update their state when they receive a new valid block.
- If they are missing blocks, they ask other hubs to provide them.
- In addition to updating their state, hubs also store block summaries of the chain they are on. This means they can reconstruct any block.
When deciding which block to include in the chain, hubs prioritize blocks with the most onchain events, and blocks that minimize chain reordering. If multiple blocks meet these criteria, the block with the higher BlockScore is chosen.
The rules hubs use to decide between new valid blocks:
- Highest OCEC: Hubs prefer the block that results in the highest OCEC, effectively prioritizing the chain with the most onchain events.
- Shallowest Fork: Among blocks with the highest OCEC, hubs select the one causing the fewest replacements in their current chain, minimizing forking.
- Higher BlockScore: When multiple blocks satisfy the above conditions, the block with the higher BlockScore is preferred, i.e. blocks with more messages and from miners who haven't contributed as many blocks recently.
Note 1: Hubs check if messages in a block are new (they must be) by comparing them to the block summaries, not their state. This is important when it comes to deciding between forks (covered later).
Note 2: In a certain point of view, the “Highest OCEC” rule, uses the OP sequencer to provide order in the Farcaster blockchain. Each block has a very specific point in time, defined by the onchain events it contains.
For a reorg to happen, a miner must present a chain that has more onchain events than the other blocks proposed and don’t lead to reordering. Since onchain events are broadcasted by the OP chain, this will be a rare event.
Also, the protocol favors blocks that do not lead to reorgs. For example, it favors adding a missing onchain event in the next block of a chain, vs a chain that added it 2 blocks earlier.
Finally, the design favors miners who have access to apps that generate a lot of deltas. These miners (probably operated by entities such as Warpcast and Neynar) can put together blocks that contain a lot of deltas and will probably combine their private deltas with the mempool. This is intentional, because these miners have a stake in ensuring that the protocol work optimally.
Combining “big app” miners, with a handful of good-intended, independently ran miners (that build blocks based on the most complete chain), can ensure that no deep forks will happen, and that the race is only about the next block.
However, when a fork happens, hubs can switch chains relatively easy.
Assuming that a hub is on Chain A and wants to switch to chain B, it has to follow these steps.
First, identify the fork block, block N.
Then, starting from block N on chain A, replay all blocks with the following rules (practically, re-add signers that were removed after the fork):
- SignerAdd --> Skip
- SignerRemove --> Add Signer. Reasoning: This signer was approved before Chain B block N+1, so, let’s approve it again, before looking at Chain B. This is fast and no checks are needed, because the hub had already checked the validity of the tx that approved the signer in the past.
- Messages submitted by users --> Skip
Next, group all messages in Chain B blocks M > N, and apply the following procedure to the group created:
- Check that SignerAdd events are valid, if not, abort. Add all the new signers to the local state. (Some of them will already be present.)
- Validate all user deltas. If they are all valid, update the local state. If a delta is already in the local state, skip. If a delta is invalid, abort.
- Check and replay all SignerRemove events, and delete messages signed by these signers.
- Switch to chain B.
- Submit deltas that were present in Chain A but are missing from Chain B to the mempool.
If the process aborts, then replay all Signer Remove messages in chain A, in blocks M>N.
It is important to note that this procedure does not remove user messages that existed in chain A and are missing in chain B.
An attacker can spoof 100 new hub ids and new deltas and flood the network with its deltas, leaving the rest of the network’s deltas out.
Note: Even for this attacker it will be extremely difficult to trigger reordering, because they are unlikely to have more onchain events than other miners, and the protocol favors new blocks that do not cause reordering.
To mitigate this type of attacks, we introduce two parameters adjusted automatically by the protocol (hubs).
Staking amount. Every miner must stake a small amount of ETH in a smart contract that allows withdrawals after X days (for example, 0.1 ETH, that can be withdrawn after 90 days). The onchain event of the deposit must be present in the chain proposed by the miner.
Window. This is the number of blocks the block score is calculated over.
Hubs adjust the staking amount
and the window
, based on the number of miners that generated the last window
blocks. If the last window
blocks (ex. 100) were mined by more than window/5
unique miners, then window
is adjusted to window*2
and staking amount
is also doubled. (No onchain change is required, just what the hubs expect as valid value.)
This means that an attacker that intends to use multiple miners, will need an exponential amount of ETH staked to maintain the attack.
Example:
Let’s say window=100
, stake=0.1
and the network has 15 miners. An attacker who wants to force their blocks will have to:
- Create 5 miners and stake 0.5 for 60 days. The best they can do with this is mine the next 5 blocks.
- If they increase the number of miners they control (for example add 20 more), then
window
will be adjusted to 200, and the stake will be 0.2. So now they need to stake additional 0.2*20 = 4 ETH. - For the next 80 blocks, the additional stake will be 8 ETH and
- For the next 160 blocks, the additional stake will be 64 ETH.
So, in this example, an attacker who wants to mine all the next 265 blocks, will have to stake 76.5 ETH. Assuming the network generates one block every 5 seconds, will need 76 ETH to censor the network for 22 minutes, and then it will go back to normal.
A miner with privileged access to new deltas (for example Warpcast or Neynar) can try to censor specific deltas. Since they have access to deltas that may not be submitted to the mempool, they can always create blocks with more deltas than other miners, and will usually win the next block.
However, as they fill the chain with more blocks, their blocks get a lower block score (percentage of contributed blocks goes up) which makes it easier for a competing miner to propose a new block that includes the censored messages and has a higher score.
At some point, hubs will have to prune old blocks, or storage requirements will become unsustainable. However, pruning blocks from a chain makes the whole chain invalid.
To mitigate this, we take advantage of CHECKPOINT
onchain events. A hub can discard a block and all previous blocks if a valid BLOCK_HASH
event that corresponds to the highest block to be pruned is present in the current chain.
Pruning will have to follow predefined rules (for example, hubs must hold the last 1m blocks), and is not triggered by CHECKPOINT
events, but these events allow a hub to prune old data without worrying about chain consistency.
Anyone can participate in block production by setting up a miner.
Applications that generate most of Farcaster traffic, like Warpcast and Neynar have an advantage, because they do not have to publish their messages to the mempool, just generate the next block that includes them.
This is not a defect. It’s good to have the parties that are most invested in the protocol have a lead in block generation as long as there is a mechanism (BlockScore) to bypass them if they don’t do their job properly.
Hubs don’t have to listen for onchain events, which should lower their load, and probably their RPC requirements.
In order to protect the network from some types of attacks, we need to introduce staking. This is not bad necessarily, but it is an extra requirement.
Hubs can maintain as much history they want (provided they keep at least X blocks), because it does not affect syncing (like it does currently, where hubs have to sync state).
TBA
This is not a frequent event but it must be accounted for. There are workarounds, like allowing the last X miners that contributed a block to be able to contribute blocks) that can be evaluated if this is the only blocker.
when I say PoS i don't mean just putting up a stake - the reason ethereum's model works well is because they have a random selection model for block producer which reduces the number of valid block producers at any given time. i think this is very important in any "block like" model where you are trying to build consensus over a chain. even a small number of simultaneous forking blocks (10 or so) can cause a lot of thrash at the network level. also they have provable slashing which is an important "stick" to encourage good behavior.