-
Notifications
You must be signed in to change notification settings - Fork 970
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
Conversation
Co-authored-by: Paul Hauner <paul@paulhauner.com>
Co-authored-by: Paul Hauner <paul@paulhauner.com>
Co-authored-by: Danny Ryan <dannyjryan@gmail.com>
Co-authored-by: Danny Ryan <dannyjryan@gmail.com>
Co-authored-by: terence tsao <terence@prysmaticlabs.com>
There was a problem hiding this 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
##### `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. |
There was a problem hiding this comment.
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)
##### `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. |
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 |
There was a problem hiding this comment.
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
.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
process_eth1_data(state, block.body) | |
process_application_data(state, block.body) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@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) |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
||
```python | ||
class PowBlock(Container): | ||
is_processed: boolean |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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 The way this behavior is currently defined (with |
specs/merge/beacon-chain.md
Outdated
gas_used: uint64 | ||
receipt_root: Bytes32 | ||
logs_bloom: Vector[Bytes1, BYTES_PER_LOGS_BLOOM] | ||
difficulty: uint64 # Temporary field, will be removed later on |
There was a problem hiding this comment.
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?
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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.
It does seem like this goes pretty far in changing formatting at the same time as the merge, particularly the To keep merge complexity down, might it not be simpler to just keep
I noticed that this does not include a bunch of fields ( 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. |
@vbuterin fair point. I have #2270 open to track Another option is to do Edit: added missing issue link |
@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. |
@vbuterin It will be a special case. As |
The list of changes that this proposal sets for the hard fork(s) after the merge is as follows:
Note: Withdrawals are dependent on the opcodes |
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 |
Co-authored-by: terence tsao <terence@prysmaticlabs.com>
a96e318
to
f263b95
Compare
f263b95
to
223aba3
Compare
There was a problem hiding this 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
What's done
Differences from quick merge proposal
ApplicationPayload
retained and used instead ofapplication_block
bytesInspired 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
andparent_hash
assembles an application block and inserts it into the application chain. If any of the pre or post verification conditions includingstate_root
check are not satisfied then the method returnsfalse
, otherwise, returnstrue
.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 ofApplicationPayload
. 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.
Quick merge proposal
Quick merge proposal doesn't require extra
eth2_submitTransitionBlockHash
RPC method, this logic is implemented as a part of thesubmitBlock
RPC method instead.