Skip to content

Instantly share code, notes, and snippets.

@vrypan
Last active October 4, 2024 06:31
Show Gist options
  • Save vrypan/1ae6a60ecb3741ab031b5b06c974acab to your computer and use it in GitHub Desktop.
Save vrypan/1ae6a60ecb3741ab031b5b06c974acab to your computer and use it in GitHub Desktop.
Farcaster Ordering (miners)

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).

High level architecture

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.

Basic Assumptions

The order of deltas is not important

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.)

There are at least a couple of “good” miners.

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.

Components

Mempool

There is a mempool where any hub can submit messages.

New OnChainEvent type: OP_BLOCK

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:

  1. 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).
  2. 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.

New OnChainEvent type: CHECKPOINT

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

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.
blockchain

Block summary

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.

Block Score

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.

OnChainEvents Count (OCEC)

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

Hubs work similar to how they work today, with the following changes:

  1. They do not listen proactively for onchain events, but they validate them when they find them in a block.
  2. They submit new messages to the mempool.
  3. They validate blocks and reject invalid ones.
  4. They update their state when they receive a new valid block.
  5. If they are missing blocks, they ask other hubs to provide them.
  6. 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.
next-block

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.

Fork resolution

Reorgs are expected to be rare

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.

Handling forks

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):

  1. SignerAdd --> Skip
  2. 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.
  3. Messages submitted by users --> Skip

Next, group all messages in Chain B blocks M > N, and apply the following procedure to the group created:

  1. 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.)
  2. 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.
  3. Check and replay all SignerRemove events, and delete messages signed by these signers.
  4. Switch to chain B.
  5. 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.

Staking and automatic parameters adjustment

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:

  1. Create 5 miners and stake 0.5 for 60 days. The best they can do with this is mine the next 5 blocks.
  2. 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.
  3. For the next 80 blocks, the additional stake will be 8 ETH and
  4. 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.

Censorship by “big” miners

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.

Pruning

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.

Pros and Cons

(+) Decentralized and censorship resistant

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.

(+) Lower hub specs

Hubs don’t have to listen for onchain events, which should lower their load, and probably their RPC requirements.

(-) Requires staking

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.

(+) No need to prune history

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).

(?) Hubs can decide what they keep in local storage

TBA

(-) The Forecaster chain will stop if OP is down

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.

@vrypan
Copy link
Author

vrypan commented Sep 29, 2024

The BloackScore function should probably take into account the number of deltas contained in the previous N blocks. Because if a miner has access to 1000 deltas that are not in the mempool, messages/blocks may always be high enough for it to win the next block.

@vrypan
Copy link
Author

vrypan commented Sep 29, 2024

It is easy to add fid limits per block or per N blocks.

@varunsrin
Copy link

hey, i think you should post this publicly, maybe as a separate fip to get feedback on this idea. would also be good to expose people to interesting new ideas.

my feedback:

  1. the core challenge is trying to enable multiple writers - you need some mechanism that limits production of blocks simultaneously otherwise you have the potential to have a very large number of forks at the same time which will cause thrash. this means that you either need PoW/PoS/PoA or something which roughly reduces the number of possible collisions at a block for 2-3 so that the network is not in a state of permanent thrash. or an alternative mechanism like CRDTs which do not require coordination for consensu.

  2. you also need a better system of reaching finality. youve proposed a combination of OCEC + Shallowest Fork + Higher Blockscore. But in a world of infinite writers its highly likely that there are many that rank the same for all three criteria. how do you then handle it?

i don't think there are many free lunches in the distributed systems world and any system that has "infinite" writers will end up with the same problems that our current CRDT systems have, but with the additional complexity of block production.

@varunsrin
Copy link

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.

@vrypan
Copy link
Author

vrypan commented Oct 2, 2024

The only attack vector I can see is censorship: Someone who is spawning new miners, and mines the next block in order to censor some deltas.

I think that the cost of locking 100 ETH for 100 days, just to censor the network for 30 minutes, knowing that the network will return to its normal state after this period, is high enough to prevent this type attack. So, the rest is miners who locked a little ETH, pay for hosting a hub, and just don't do a good job (because of inadequate hw for example). These miner will never cause a fork. At best they may win a block every now and then, that will not cause any problems, it will just have fewer deltas than the ideal block someone else would build.

@vrypan
Copy link
Author

vrypan commented Oct 3, 2024

Q: Can an attacker prepare onchain events (for example, signer approvals) and wait for the last minute to submit them, but use their hashes before anyone else has the chance to receive the events, and win the next block?

A: No. Onchain events include the block hash, which cannot be known in advance. Everyone gets the events at the same time, i.e. when the OP block is published.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment