Skip to content

Commit

Permalink
feat: constraining slots (#7758)
Browse files Browse the repository at this point in the history
Fixes 7656. 

Following an earlier PR (#7594), we added slots into the mix. This PR
tackles actually using those slots for something. When adding these
constraints, several fixes were needed on the sequencing clients and its
components.

So why are we introducing slots? There is really two things that we are
going to use the slots for:
- i) We are going to use it to deal with time within the L2
- Previously a sequencer had flexibility on how the defined the
timestamp, now it is instead defined as `GENESIS + slot * slot_duration`
depending solely on the slot.
- ii) We will be using it as the unit of time in which a specific
proposer have rights to proposer a block and where the block will be
accepted on chain.
- This means that it is a timeliness requirement for block inclusion on
L1
- Since the proposer can only use the "current" slot as the value for
his block, he no flexibility on the timestamp of the L2 block.

The first term is mainly of interest to applications, but the second
term is really important for how our sequencer selection works. If you
do not propose within your block, you are tough out of luck, the block
will not be accepted on L1.

This means that it is also very important for us to figure out a proper
slot duration, since a short slot duration can lead to plenty of empty
slots, while too long lead to loss in throughput.

Introducing such a strict timeliness requirement does impact our current
tests quite a lot, since they are all created without that in mind
(similar was the sequencer client).

Since we in our E2E are the only producer of L1 blocks as well, we have
full control over those. However, using automine on Anvil as the
underlying chain makes the behaviour of sequencing with slots quite odd,
as things that would normally be within the same block (multiple tx)
will now progress time at every tx. This meant that the tests broke
horribly, as we with one tx was publishing the data for our block, and
then when we wanted to publish the block afterwards the time was
progressed, so we might no longer be the proposer.

This was handled by adding a combination call that is doing
`publishAndProcess` of a block, making sure that we don't progress time
unnecessarily.

Secondly, when we are using on-demand mining (automine), we won't have
that we get to the "next" slot after a block have been included, so to
not mess to much with timing, we have specified the `SLOT_DURATION` to
the same as the underlying chain slot duration. Since we are using
anvil, this is by default 1s. However, we also change the block interval
to 12s to behave closer to ethereum.

Having just a single l1 slot for every l2 slot means that the proposer
is rapidly changing as l1 blocks are coming in. Something that WILL NOT
work out the box for a real chain, but is accepted as a first step here
in our setup to get started.

Since we do not want to break everything just ahead of dev net, we have
introduced a flag for turning off the most strict checks and rotation of
the sequencers. By setting `IS_DEV_NET = true` in the `constants.nr` the
rollup will accept blocks from anyone in the validator set (if not
empty), without checking if it is specifically their turn. You can more
specifically look at this in the `Rollup` contract.

Since it is now required to provide attestations from the L2 network if
not `IS_DEV_NET = true` the sequencer client will `collectAttestations`
before publishing the block. Currently it is a war crime of a function
that is just collecting its own signature and providing that as the
attestations. This mean that the current setup ONLY works for validator
set size 1. This is to be addressed with the extensions that @Maddiaa0
is making with #7681.

When body and headers arrive around the same time (in same l1 block) the
archive seemed to run into issues as it would sometime only have loaded
the header and not the body, and then fail as it tries to combine the
two into an l2 block.
If @alexghr, @spypsy, @spalladino or @PhilWindle is able to take a look
to the changes made to it (mainly in
e321449)
that would be great.

Notable changes in this PR:
- `publishAndProcess` 
- Publishing the block body AND processing the pending chain is now done
in the same TX to not cause issues with timeliness
- Block time is set to 12 seconds 
- When deploying the L1 contracts, we are setting the block time of the
underlying Anvil chain to 12 seconds. (Still in automine).
- Introduces `IS_DEV_NET` flag in `constants.nr` 
- When `IS_DEV_NET = true`
- Skips some of the stricter timeliness requirements since they are not
needed for centralised sequencers.
- `IS_DEV_NET = false`
  - Run with all checks on
- Will need signatures from the committee (ordered correctly) to propose
a block
- Made changes to the archiver as it sometimes broke when block body and
header was in the same l1 block.

Future work:
- Collect attestations properly
- E2E should survive running with interval mining and `SLOT_DURATION = n
* ETHEREUM_SLOT_DURATION` for `n>1`.

---

We will have the test pass with `IS_DEV_NET=false` and then we will do
another run where we set it to `true` such that we do not break things
for people unnecessarily.

---

Issues spawned by this pr:
- #7849
- #7821
- #7850 
- #7837
  • Loading branch information
LHerskind authored Aug 9, 2024
1 parent 55c33bd commit f8b0de6
Show file tree
Hide file tree
Showing 46 changed files with 1,557 additions and 664 deletions.
9 changes: 4 additions & 5 deletions docs/docs/guides/developer_guides/js_apps/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,12 @@ The [`CheatCodes`](../../../reference/developer_references/sandbox_reference/che

### Set next block timestamp

The `warp` method sets the time for next execution, both on L1 and L2. We can test this using an `isTimeEqual` function in a `Test` contract defined like the following:
Since the rollup time is dependent on what "slot" the block is included in, time can be progressed by progressing slots.
The duration of a slot is available by calling `SLOT_DURATION()` on the Rollup (code in Leonidas.sol).

#include_code is-time-equal noir-projects/noir-contracts/contracts/test_contract/src/main.nr rust
You can then use the `warp` function on the EthCheatCodes to progress the underlying chain.

We can then call `warp` and rely on the `isTimeEqual` function to check that the timestamp was properly modified.

#include_code warp /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript
<!--#include_code warp /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript-->

## Further reading

Expand Down
31 changes: 23 additions & 8 deletions docs/docs/protocol-specs/l1-smart-contracts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,33 @@ The state transitioner is the heart of the validating light node for the L2.
The contract keeps track of the current state of the L2 and progresses this state when a valid L2 block is received.
It also facilitates cross-chain communication (communication between the L1 inbox and outbox contracts).

:::info
The following example shows a simplified case where proof and block are provided in the same transaction.
:::

```python
class StateTransitioner:

struct BlockLog:
archive: bytes32
slot_number: uint128

VERIFIER: immutable(IVerifier)
AVAILABILITY_ORACLE: immutable(IAvailabilityOracle)
INBOX: immutable(IInbox)
OUTBOX: immutable(IOutbox)
VERSION: immutable(uint256)
GENESIS_TIME: immutable(uint256)
SLOT_DURATION: immutable(uint256)

archive: TreeSnapshot
block_number: uint256
last_block_ts: uint256
blocks: BlockLog[]

def __init__(self, ...):
'''
Initialize the state transitioner
'''
self.blocks.append(BlockLog({archive: bytes32(0), slot_number: 0}))
self.GENESIS_TIME = block.timestamp

def process(
self,
Expand All @@ -131,19 +141,24 @@ class StateTransitioner:
header.content_commitment.out_hash,
header.content_commitment.tx_tree_height + math.ceil(log2(MAX_L2_TO_L1_MSGS_PER_TX))
)
self.blocks.append(BlockLog({
archive: archive,
slot_number: header.global_variables.slot_number
}))
self.archive = archive
emit BlockProcessed(block_number)

def validate_header(
self,
header: Header
) -> bool:
assert header.global_variables.block_number = self.block_number + 1
assert header.global_variables.block_number = len(self.blocks)
assert header.global_variables.chain_id == block.chain_id
assert header.global_variables.version == self.version
assert header.global_variables.timestamp < block.timestamp
assert header.global_variables.timestamp > self.last_block_ts
assert header.archive == self.archive
assert header.global_variables.version == self.VERSION
assert header.global_variables.timestamp == self.GENESIS_TIME + self.SLOT_DURATION * header.global_variables.slot_number
last_block = self.blocks[-1]
assert header.global_variables.slot_number > last_block.slot_number
assert header.archive == last_block.archive
return True
```

Expand Down
Loading

0 comments on commit f8b0de6

Please sign in to comment.