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

Pure consensus upgrade version of the merge #2257

Merged
merged 24 commits into from
Mar 26, 2021

Conversation

mkalinin
Copy link
Collaborator

@mkalinin mkalinin commented Mar 20, 2021

What's done

Differences from quick merge proposal

  • ApplicationPayload retained and used instead of application_block bytes

Inspired by

UPD

Application client requirements

Application client (Ethereum Mainnet client) will have to implement a new RPC protocol and the underlying logic to become merge compliant. The protocol has following methods:

  • eth2_insertBlock(parent_hash: Bytes32, application_payload: ApplicationPayload) -> boolean
    Given the application_payload and parent_hash assembles an application block and inserts it into the application chain. If any of the pre or post verification conditions including state_root check are not satisfied then the method returns false, otherwise, returns true.
    If parent block has been considered as the head of the chain then the newly inserted block becomes the head of chain if the import succeeds.

  • eth2_produceBlock(parent_hash: Bytes32) -> ApplicationPayload
    Fetches transactions from the transaction pool and assembles a block on top of the parent block (specified by parent_hash) and returns it in a form of ApplicationPayload. Returns error code if parent block is not found.

    Note: Pending block may be used as a source of a new block if given parent_hash matches the one that is in the pending block.

  • eth2_setHead(hash: Bytes32) -> None
    Given the hash of a block sets the head of the application chain. Returns error code if block is not found.

  • eth2_finalizeBlock(hash: Bytes32) -> boolean
    Given the hash of a block finalises the application block. Returns error code if block is not found.

    Note: Chain and state clean ups may be triggered upon this call meaning that given block becomes an irreversible point on the chain. First finalisation event after the transition may have additional logic like disabling block gossip on the application layer.

  • eth2_submitTransitionBlockHash(hash: Bytes32) -> TransitionBlockStatus
    Given the hash of a block returns the status information of the block. If block is not found then triggers the same processing chain inside of a client as if this hash was received with NewBlockHashes message. That is, asking the sync protocol to fetch the block and its ancestors and insert them into the application chain.

    Note: Required by transition process only.

class TransitionBlockStatus:
    is_processed: boolean
    is_valid: boolean
    total_difficulty: uint256

Quick merge proposal

Quick merge proposal doesn't require extra eth2_submitTransitionBlockHash RPC method, this logic is implemented as a part of the submitBlock RPC method instead.

specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Show resolved Hide resolved
specs/merge/beacon-chain.md Show resolved Hide resolved
specs/merge/fork-choice.md Outdated Show resolved Hide resolved
Copy link
Contributor

@djrtwo djrtwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a review of beacon-chain.md. Mostly formatting suggestions. One substantive question about the transition block payload

specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
Comment on lines +154 to +166
##### `get_application_state`

*Note*: `ApplicationState` class is an abstract class representing ethereum application state.

Let `get_application_state(application_state_root: Bytes32) -> ApplicationState` be the function that given the root hash returns a copy of ethereum application state.
The body of the function is implementation dependent.

##### `application_state_transition`

Let `application_state_transition(application_state: ApplicationState, application_payload: ApplicationPayload) -> None` be the transition function of ethereum application state.
The body of the function is implementation dependent.

*Note*: `application_state_transition` must throw `AssertionError` if either the transition itself or one of the post-transition verifications has failed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need get_application_state() at all? (also see other comment below)

Suggested change
##### `get_application_state`
*Note*: `ApplicationState` class is an abstract class representing ethereum application state.
Let `get_application_state(application_state_root: Bytes32) -> ApplicationState` be the function that given the root hash returns a copy of ethereum application state.
The body of the function is implementation dependent.
##### `application_state_transition`
Let `application_state_transition(application_state: ApplicationState, application_payload: ApplicationPayload) -> None` be the transition function of ethereum application state.
The body of the function is implementation dependent.
*Note*: `application_state_transition` must throw `AssertionError` if either the transition itself or one of the post-transition verifications has failed.
##### `application_state_transition`
Let `application_state_transition(application_state_root: Bytes32, application_payload: ApplicationPayload) -> Bytes32` be the transition function of ethereum application state. This function takes the pre-state associated with `application_state_root`, applies the `application_payload`, and returns the post-state root.
The body of the function is implementation dependent.
*Note*: `application_state_transition` must throw `AssertionError` if either the transition itself or one of the post-transition verifications has failed.

Comment on lines +178 to +181
application_state = get_application_state(state.application_state_root)
application_state_transition(application_state, body.application_payload)

state.application_state_root = body.application_payload.state_root
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need for the Eth2 node to access application_state?
A cleaner approach would be to outsource all the work to application_state_transition(), so that it operates using the application_state_root. (see suggestion for the application_state_transition function)
Also, need to check that whatever is returned by application_state_transition() actually matches with body.application_payload.state_root.

Suggested change
application_state = get_application_state(state.application_state_root)
application_state_transition(application_state, body.application_payload)
state.application_state_root = body.application_payload.state_root
state.application_state_root = application_state_transition(state.application_state_root, body.application_payload)
assert state.application_state_root == body.application_payload.state_root

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely see the value in simplifying this part. But then it would not read like the beacon state and the application state are tightly coupled.

For the root check we can do the following:

application_state = get_application_state(state.application_state_root)
application_state_transition(application_state, body.application_payload)

assert body.application_payload.state_root == get_application_state_root(application_state)

state.application_state_root = body.application_payload.state_root
state.application_block_hash = body.application_payload.block_hash

But that would require yet another abstract function. So, it might really be better to have the following (according to your suggestion):

application_state_root = application_state_transition(state.application_state_root, body.application_payload)

assert application_state_root == body.application_payload.state_root

state.application_state_root = body.application_payload.state_root
state.application_block_hash = body.application_payload.block_hash

The latter form also fits stateless verification.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking: application_state_root = application_state_transition(...) requires application_state_transition to return Bytes32, but it doesn't align with the naming pattern as beacon state transition function state_transition(...) -> None. So having a get_application_state_root(application_state) makes sense to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this call conformant with state_transition(...) was the original intention put behind this extra function call and using application state object rather than the state root.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also define ApplicationState in a following way

class ApplicationState(Container):
    root: Bytes32

And then add an explicit check of the state root after application state transition

application_state = get_application_state(state.application_state_root)
application_state_transition(application_state, body.application_payload)

assert application_state.root == body.application_payload.state_root

state.application_state_root = body.application_payload.state_root
state.application_block_hash = body.application_payload.block_hash

The application payload included in a `BeaconBlock`.

```python
class ApplicationPayload(Container):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-London, we'll want to add the BASE FEE here.

def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
process_randao(state, block.body)
process_eth1_data(state, block.body)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
process_eth1_data(state, block.body)
process_application_data(state, block.body)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that Eth1Data name will look odd after the merge but we might want to keep it as it this time. The follow up cleanups are going to reduce the follow distance of Eth1Data and it would be a good time to make the renaming too.

Copy link
Contributor

@djrtwo djrtwo Mar 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more of DepositContractData if we are going to rename it.
It is more specific than application layer data

@djrtwo
Copy link
Contributor

djrtwo commented Mar 22, 2021

Is there a need for the Eth2 node to access application_state?

@adiasg I would argue that this is the cleaner approach because "outsourcing" all the work is actually an implementation detail (e.g. a client could bundle pos and application components tightly or could do something like we are doing with the separation of consensus vs application client)

Copy link
Contributor

@djrtwo djrtwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fantastic work! got through the next two docs. happy to discuss here or otherwise

class PowBlock(Container):
is_processed: boolean
is_valid: boolean
total_difficulty: uint256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this granularity required?

uint's larger than 64 have been entirely avoided in the consensus-layer so far

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total difficulty is on the order of 10**22 so it doesn't fit in uint64...

We could reduce precision to avoid uint256. Need to consider our options here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, total difficulty falls into > 2**74 interval today. The other potential way of handling this is to return an offset wrt some absolute total difficulty value. Current block's difficulty is around 2**64, so we indeed may divide total difficulty by 2**20 without loss of generality.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it worth noting that uint256 type is used by Transaction data structure to define several fields.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, right. I don't thnk we'll be able to get around value with 256 granularity

I suppose beacon clients don't actually have to support arithmetic on these TX values because all arithmetic and validations happen in application layer so as long as they can serialize and deserialize, that's enough support

specs/merge/fork-choice.md Outdated Show resolved Hide resolved
specs/merge/fork-choice.md Outdated Show resolved Hide resolved

```python
class PowBlock(Container):
is_processed: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is is_processed just a super set of is_valid?

Do eth1 clients remember if an invalid block has already been processed? or does it just drop it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current behaviour is to return error if either invalid or not yet processed block is requested meaning that these two statuses are indistinguishable. So, if we want this level of granularity then JSON-RPC implementation will have to be adjusted but we probably don't want it. It's worth discussing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do eth1 clients remember if an invalid block has already been processed? or does it just drop it

I think with some configuration it stores invalid blocks to be able to serve debug_getBadBlock


```python
def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
block = signed_block.message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably move some of his logic into sub-functions on phase0 so we can have better code reuse. Can do that in a separate pr

specs/merge/validator.md Outdated Show resolved Hide resolved
specs/merge/validator.md Outdated Show resolved Hide resolved
specs/merge/validator.md Outdated Show resolved Hide resolved
specs/merge/validator.md Outdated Show resolved Hide resolved
specs/merge/validator.md Outdated Show resolved Hide resolved
@adiasg
Copy link
Contributor

adiasg commented Mar 23, 2021

@djrtwo

@adiasg I would argue that this is the cleaner approach because "outsourcing" all the work is actually an implementation detail (e.g. a client could bundle pos and application components tightly or could do something like we are doing with the separation of consensus vs application client)

I partly agree - clients may do application processing in the Eth2 node itself, or outsource the work to a separate application client. That's why I think application_state_transition(application_state_root: Bytes32, application_payload: ApplicationPayload) is better, as it accounts for both cases. If outsourcing the work, ApplicationState is not required in the Eth2 node. If not outsourcing the work, the function can be implemented to handle processing without exposing the ApplicationState anywhere else.

The way this behavior is currently defined (with get_application_state(application_state_root: Bytes32) -> ApplicationState) makes it seem like ApplicationState is essential for Eth2 validation logic, which is not the case.

@hwwhww hwwhww added the Bellatrix CL+EL Merge label Mar 23, 2021
@djrtwo djrtwo mentioned this pull request Mar 23, 2021
4 tasks
gas_used: uint64
receipt_root: Bytes32
logs_bloom: Vector[Bytes1, BYTES_PER_LOGS_BLOOM]
difficulty: uint64 # Temporary field, will be removed later on
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't we remove this field now?

@paulmillr
Copy link

Thank you for the hard work!

A quick question from a guy who has not been following the plans very thoroughly.

What hazardous or bad tech debt does this merge proposal create when compared to the "old" merge plan?


if is_transition_completed(state):
application_state = get_application_state(state.application_state_root)
application_state_transition(application_state, body.application_payload)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to pass in the randao or some other seed for DIFFICULTY/BLOCKHASH here?

obviously, easiest to just stick them on the application_payload so the eth1 engine doesn't even have to know about this new logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think it would make sense to prepare a consensus bundle that will be passed onto this function with randao mix and further extended by other bits. I'd add it later once we made the decision about difficulty and whether to use randao or not.

specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
Comment on lines +178 to +181
application_state = get_application_state(state.application_state_root)
application_state_transition(application_state, body.application_payload)

state.application_state_root = body.application_payload.state_root
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking: application_state_root = application_state_transition(...) requires application_state_transition to return Bytes32, but it doesn't align with the naming pattern as beacon state transition function state_transition(...) -> None. So having a get_application_state_root(application_state) makes sense to me.

specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
@vbuterin
Copy link
Contributor

It does seem like this goes pretty far in changing formatting at the same time as the merge, particularly the transactions list. Remember that by this point there will be plenty of transaction types: old-style with no replay protection, old-style with replay protection, EIP 2930 access list-carrying txs, EIP 1559 basefee-setting txs. These would all have to be changed into an SSZ format.

To keep merge complexity down, might it not be simpler to just keep transactions as a list of blobs, and then have some post-merge fork replace them with a new SSZ-based transaction type? (first make it voluntary, and then make it mandatory)

class ApplicationPayload(Container):
    block_hash: Bytes32  # Hash of application block
    coinbase: Bytes20
    state_root: Bytes32
    gas_limit: uint64
    gas_used: uint64
    receipt_root: Bytes32
    logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
    transactions: List[Transaction, MAX_APPLICATION_TRANSACTIONS]

I noticed that this does not include a bunch of fields (uncles_hash, difficulty, number, timestamp, mixhash, nonce). This makes the conversion between old-style and new-style application blocks not a clean reversible function. Perhaps this does not matter, because those fields are not relevant anymore, but it does create some extra special cases. Particularly, it requires an extra special case for the transition application block itself: if the transition application block is the last PoW-bearing block, then you would need some special logic for including that block, because the last PoW-bearing block does have an uncles_hash, nonce, etc.

Or is the idea to change that bit in the spec, so that instead of the transition beacon block including the last PoW block, it merely includes a block whose parent is the last PoW block? If so, then I could see how this can work.

@protolambda
Copy link
Collaborator

protolambda commented Mar 25, 2021

@vbuterin fair point. I have #2270 open to track Union support to switch between (the many) transaction types, and already proposed the option of a list of opaque transactions.

Another option is to do transactions: List[Union[TxBlob], MAX_APPLICATION_TRANSACTIONS] and expand the Union[TxBlob] later with more SSZ transaction types. That way we avoid the immediate complexity of many new SSZ transaction types, have something to fall back on with all legacy transactions, and it's much more compatible to extend with SSZ transactions in future forks.

Edit: added missing issue link

@mkalinin
Copy link
Collaborator Author

@vbuterin @protolambda IMO, an opaque transactions option is the best for the beginning. IIUC, some of old-styled transaction types will become unacceptable starting from some point in time and it would make more sense to replace transaction blobs with SSZ objects once all the formats are settled down to avoid them being heavily dependent on the beacon chain spec.

@mkalinin
Copy link
Collaborator Author

Particularly, it requires an extra special case for the transition application block itself

@vbuterin It will be a special case. As ApplicationPayload does not explicitly contain parent_hash, the hash of the last PoW block will be brought up on chain before producing the first PoS block. It's gonna be done by adding ApplicationPayload(block_hash=transition_block_hash) to the beacon block body to denote a transition block. It also requires a special case in the state_transition function to process transition block which just sets state.application_block_hash = application_payload.block_hash without interacting with the application layer.

@mkalinin
Copy link
Collaborator Author

Thank you for the hard work!

A quick question from a guy who has not been following the plans very thoroughly.

What hazardous or bad tech debt does this merge proposal create when compared to the "old" merge plan?

The list of changes that this proposal sets for the hard fork(s) after the merge is as follows:

  • Eth1Data follow distance reduction
  • New EVM opcodes, RANDOM, BEACONBLOCKROOT, etc.
  • Validator withdrawals

Note: Withdrawals are dependent on the opcodes

@djrtwo
Copy link
Contributor

djrtwo commented Mar 26, 2021

It's gonna be done by adding ApplicationPayload(block_hash=transition_block_hash) to the beacon block body to denote a transition bloc

Note that, another option to signal transition is to just include a full duplicate of the last PoW block. I'm not sure if this would result in more or less exceptional logic but probably worth considering

specs/merge/beacon-chain.md Outdated Show resolved Hide resolved
specs/merge/beacon-chain.md Show resolved Hide resolved
Co-authored-by: terence tsao <terence@prysmaticlabs.com>
@mkalinin mkalinin marked this pull request as ready for review March 26, 2021 19:19
@djrtwo djrtwo force-pushed the consensus-upgrade branch 5 times, most recently from a96e318 to f263b95 Compare March 26, 2021 19:48
Copy link
Contributor

@djrtwo djrtwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic work @mkalinin and to the many reviewers!

To better manage the complexity, we're going to merge the current PR as is. I've created an issue (#2280) to track the open discussion points and todos that were brought up but not resolved in PR.

From here, we'll move toward smaller and more iterative PRs to address these and any other points that come up along the way

@djrtwo djrtwo merged commit 9f8e627 into ethereum:the-merge Mar 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bellatrix CL+EL Merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.