Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

document parallel evm funnel delay/confirmationDepth #39

Merged
merged 4 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- The blockchain node (`CHAIN_URI`)
- The deployed Paima Contract address (`CONTRACT_ADDRESS`)
- The set of [Primitives](../2-primitive-catalogue/1-introduction.md) that the developer provided
- The set of [Primitives](../../2-primitive-catalogue/1-introduction.md) that the developer provided

Notably, funnels play a key role in allowing Paima to not just synchronize a single chain, but also combine multiple different data sources together such as DA layers, merging L1+L2 data together, or merging NFT data from different chains.

Expand Down Expand Up @@ -51,9 +51,9 @@ export interface ChainData {

When extensions are used, the runtime must start polling from a block height that was before any of the contracts referenced in the Primitives were deployed. Thus all events that take place (ie. all NFT mints/transfer events) are accounted for and are saved in the DB so the state machine has proper access to a valid copy of the current state of the contract. We call this the _pre-sync_ phase.

In other words, this function is meant to gather events for [Primitives](../2-primitive-catalogue/1-introduction.md#accessing-the-collected-data) that happened before `START_BLOCKHEIGHT`, or the equivalent point if other primitives from other chains are used.
In other words, this function is meant to gather events for [Primitives](../../2-primitive-catalogue/1-introduction.md#accessing-the-collected-data) that happened before `START_BLOCKHEIGHT`, or the equivalent point if other primitives from other chains are used.

Any [scheduled events](../1-scheduled-events.md) that are created during this
Any [scheduled events](../../1-scheduled-events.md) that are created during this
phase are expected to be scheduled at the beginning of the _sync_ phase, which
can be at either `START_BLOCKHEIGHT` or 0 (if using emulated block heights). This
way any state derived from events by the state transition function can still be
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Merge from "./parallel-funnel-merge.svg";
import MergeDelayed from "./parallel-funnel-merge-delayed.svg";
import ParallelToMain from "./parallel-to-main.svg";
import DataAddition from "./parallel-data-addition.svg";
import Finalized from "./parallel-finalized.svg";

# Parallel funnels

## Merging state from multiple chains

When running a game, there is always a (single) primary EVM network which is
synchronized by the [block funnel](../block-funnel). This is the network that has
the Paima L2 contract deployed, and it's the one that provides the inputs for
the game. In addition to this, there are funnel types that are used for
synchronizing additional networks in parallel:

- [parallel evm funnel](../parallel-evm-funnel)
- [carp funnel](../carp-funnel)
- [mina funnel](../mina-funnel)

Conceptually these funnels are independent from each other, so there can be as
many as needed, but they all depend on the block funnel.

<ParallelToMain className="img-full" style={{ height: "200px" }} />

The reason for this is
that the engine makes it _appear_ as if events happened in a single network by
_merging_ the events from the parallel chains into the blocks from the block
funnel. This provides a single interface where adding an extension in an extra
network is no different than adding an extension in a single network.

<DataAddition className="img-full" style={{ marginTop: "8px" }} />

### Determinism

Merging of data is done in a way to ensure that the state transition is deterministic.

For example,<br />
if on the main chain we have blocks with timestamps <span style={{ color: "red" }}>3</span> and <span style={{ color: "red" }}>5</span>,<br/>
and on the parallel chain we have blocks with timestamps <span style={{ color: "green" }}>4</span> and <span style={{ color: "green" }}>5</span>,<br />
the events in the parallel chain will <span style={{ color: "lightblue" }}>look as if</span> they happened in the main chain's block that has timestamp <span style={{ color: "red" }}>5</span>.
And this is always the same regardless of the time of the sync, since the chains are always processed in tandem.
Note that it is possible that there are no blocks to merge at a certain point (if no events we're monitoring occur in these blocks).

Visually:

<Merge className="img-full" style={{ height: "auto" }} />

## Finalizing blocks

We cannot go back in time to add information to old blocks that have already been parsed by the game's state machine.
Therefore, it means that a block can only be considered finalized and ready to be given to the state machine once we're certain we have all the information for all chains being monitored.
The only way for us to know that we have all the information for a block is if the _latest_ [**confirmed**](#confirmation-depth) timestamp of all networks we're monitoring is more recent *or* equal ( depending on whether the network can have multiple blocks with the same timestamp), than the timestamp of the main chain block timestamp we're looking at.

*Note*: this behavior helps protect apps built with Paima from getting in a bad state. If an RPC you're connecting to for a network gets stuck, it should stop block production for the entire application.
Otherwise, it can break determinism because your node (that is missing events from the parallel chain RPC that is stuck) will see a different block history from somebody else running a node connecting to a properly functioning RPC for that parallel chain.

<Finalized className="img-full" style={{ height: "auto" }} />

## Delayed state

When getting information from a chain with probabilistic finality, it's
necessary to have a setup that can avoid rollbacks, since there is no way in for
the engine to handle that otherwise. Because of this, the parallel funnels have
the ability to run in a delayed state. This involves two different variables:

```
confirmationDepth: number
delay: number
```

### Confirmation depth

`confirmationDepth` is a parameter that is based on blocks. A
`confirmationDepth` of 0 would be proper for a network with instant finality.
Increasing the argument depends on the underlying network, and the amount of
confidence desired.

### Delay

If the `delay` setting is used, the parallel chain is delayed by this amount of
seconds. This is equivalent to either subtracting the delay for the timestamps
of the main network before merging (without changing the result), or to adding
the delay to the parallel network timestamps. In that case the merge process
changes in the following way (with a delay of 60 seconds).

<MergeDelayed className="img-full" style={{ height: "auto" }} />

This has the effect that no events newer than `current time - delay` will be
processed by the funnel (relative to the main chain's latest synced block).

### Guidelines

In general both of these are needed for the funnel to properly function, and a
good guideline is to set `delay` to a value greater or equal than
`confirmationDepth * block_production_speed`. Having a greater value adds
latency for the engine to react to events, but decreases the chances of the
funnel needing to wait for block production in order to get a confirmed block
(see [finalizing blocks](#finalizing-blocks)). This is particularly problematic
because a single network may stall all of the other ones.

Considering the above, setting up a `confirmationDepth` without `delay` it's not
really advised, since the funnel will stall with every block. On the other hand,
it's possible to set a `delay` without a `confirmationDepth`, but if the
underlying network stops producing blocks for some reason, it may lead to
eventually finalizing a block that it's not stable, unless the underlying
network has some extra guarantees based exclusively on timings.

## Performance implications

Leveraging parallel funnels has multiple performance implications. In some cases the performance impact can be mitigated by [emulated blocks](./400-stable-tick-rate-funnel.mdx),
but we will cover all performance implications in this section

### Networking overhead

From Paima's perspective, a block can only be considered complete and ready to send to the game's state machine after it's fetched all necessary information.
That means that fetching blocks is only as fast as your slowest connection when fetching the latest block.

If you're connecting to a public node for a network and receiving data from that node is slow (ex: geographically far from you, running on cheap hardware, etc.),
the delay in fetching data from that node will delay the final block creation process and cause slowdowns in your game node.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label": "Common concepts"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading