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

core: implement EIP-4788 BeaconRoot precompile #27289

Closed

Conversation

MariusVanDerWijden
Copy link
Member

@MariusVanDerWijden MariusVanDerWijden commented May 17, 2023

EIP 4788 : Beacon block root in the EVM


WIP implementation of EIP-4788 for discussion on the eip
cc @ralexstokes

@MariusVanDerWijden MariusVanDerWijden changed the title core: implement modified version of EIP-4788 core: implement EIP-4788 BeaconRoot precompile Jun 29, 2023
@holiman holiman added the cancun label Jul 6, 2023
core/vm/instructions.go Outdated Show resolved Hide resolved
consensus/misc/eip4788.go Outdated Show resolved Hide resolved
core/vm/contracts.go Outdated Show resolved Hide resolved
core/vm/contracts.go Outdated Show resolved Hide resolved
consensus/misc/eip4788.go Outdated Show resolved Hide resolved
core/vm/eips.go Outdated Show resolved Hide resolved
params/protocol_params.go Outdated Show resolved Hide resolved
Comment on lines +93 to +95

// BeaconRoot was added by EIP-4788 and is ignored in legacy headers.
BeaconRoot *common.Hash `json:"beaconRoot" rlp:"optional"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding it like this delegates the responsibility of "when to start storing data for EIP-4788" entirely to the CL layer. When/of they start including a beaconroot, we start storing it into the state.

IMO we should

  • only accept BeaconRoot after the fork time,
  • require the BeaconRoot after the fork time,

As for RLP, similarly, we need to be careful, and strict.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is the order between BeaconRoot and DataGasUsed specified? Both are activated in the same fork...

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think its specified yet

Copy link
Member

Choose a reason for hiding this comment

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

it should be at the end of all the 4844 stuff; I was trying to specify this abstractly to avoid thrash from other EIPs but I can see how this is unclear so I made it explicit:

ethereum/EIPs#7297

core/state_processor.go Outdated Show resolved Hide resolved
Copy link
Member

@lightclient lightclient left a comment

Choose a reason for hiding this comment

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

Couple nits; really nice overall though.

params/protocol_params.go Outdated Show resolved Hide resolved
state.SetState(historicalStorageAddress, rootKey, root)
}

func calcBeaconRootIndices(header *types.Header) (timeKey, time, rootKey, root common.Hash) {
Copy link
Member

Choose a reason for hiding this comment

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

This function is kinda superfluous. It's just doing a bunch of one-line conversions. I think it is actually more comprehensible all in the same function, otherwise takes a bit of time to realize almost not computation is happening here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think Martin proposed spinning this out, so it can be tested easier, I'll add some test cases, so it actually makes sense to move this

params/protocol_params.go Outdated Show resolved Hide resolved
core/vm/contracts.go Outdated Show resolved Hide resolved
core/vm/contracts.go Outdated Show resolved Hide resolved
"github.com/ethereum/go-ethereum/params"
)

func TestCalcBeaconRootIndices(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good start. Mind if I move the actual test vectors of it out to a json file, so we can share it cross-client?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure would be nice to have some more test cases

Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

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

LGTM

@puma314
Copy link

puma314 commented Jul 28, 2023

@ralexstokes have you considered passing the entirety of the evm EVM object instead of just the stateDB StateReader object to the precompile? In particular, information in the EVM object might be useful for enabling more expressive future precompiles (and also more expressive custom precompile implementations on custom op-stack chains as another example), including things like more complex gas cost estimation (based on current gas remaining), ability to access values in the EVM config, surrounding call context, etc.

For some additional context, I've been working on implementing a proof of concept for remote static call for Optimism (ethereum-optimism/ecosystem-contributions#76). This involves implementing a precompile in op-geth that would issue a network call to an L1 archive node to get information about L1 state and return that within the contract execution on L2.

It would be useful to be able to access evm.config information in the remotestaticcall precompile to know what the L1 RPC URL is and also to know about the current amount of gas in the call to understand how much gas to pass to eth_call.

There might be very good reasons to not do this, but just curious if it would make sense.

@@ -121,6 +125,19 @@ var PrecompiledContractsBLS = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{18}): &bls12381MapG2{},
}

var PrecompiledContracts4788 = map[common.Address]PrecompiledContract{
Copy link
Member

Choose a reason for hiding this comment

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

Why this instead of in Cancun? Now we have cancun with KZG but no beaconnroot, and we have this with beaconroot but no KZG

common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{},
common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{9}): &blake2F{},
params.BeaconRootsStorageAddress: &beaconRoot{},
Copy link
Member

Choose a reason for hiding this comment

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

Given that all other contracts are hard coded with value addresses, I'd rather stick to that vs adding a param here

Copy link
Member

Choose a reason for hiding this comment

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

This one is slightly different though as it needs to be accessed in both consensus/misc/eip4788.go and in core/vm/contracts.go. It's weird it is the only non-hardcoded one here though, but is it better to hardcode in both places?

Copy link
Member

Choose a reason for hiding this comment

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

It should be consistent. If there's a need to move the beaconRoot into a param, then move all of them imo.

@@ -160,6 +164,9 @@ const (
RefundQuotient uint64 = 2
RefundQuotientEIP3529 uint64 = 5

HistoricalRootsModulus uint64 = 98304 // Limits how many historical roots are stored by EIP-4788
Copy link
Member

Choose a reason for hiding this comment

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

I'd recommend starting to prefix constants belonging to the same EIP/fork/whatnot since we already have tons of params and it's very hard to see what goes where. I.e. BeaconRootHistoricalRootsModulus

@@ -34,12 +34,16 @@ import (
"golang.org/x/crypto/ripemd160"
)

type StateReader interface {
Copy link
Member

Choose a reason for hiding this comment

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

Need docs

RequiredGas(input []byte) uint64 // RequiredPrice calculates the contract gas use
Run(input []byte) ([]byte, error) // Run runs the precompiled contract
RequiredGas(input []byte) uint64 // RequiredPrice calculates the contract gas use
Run(statedb StateReader, input []byte) ([]byte, error) // Run runs the precompiled contract
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick, we have statedb and stateDB naming in this single PR. Can we just have state instead?

@holiman
Copy link
Contributor

holiman commented Jul 31, 2023

The 4788 EIP needs to be amended, currently geth is doing something which is correct, but definitely not intended behaviour.

Since the beaconroot trie account is empty, by the consensus-definition of empty,

An account is empty when it has no code, zero nonce and zero balance

It is touch-deleted at the end of every block. So it works "fine" during a block, but then it becomes wiped at the end of the block. Discussion is happening regarding how to get around this behaviour. One example candidate solution is to amend the eip so that:

When updating the storage for the precompile, set nonce=1 iff nonce==0.

I see no point in merging this EIP until this has been resolved.

@holiman
Copy link
Contributor

holiman commented Jul 31, 2023

I have now updated this PR to set the nonce to 1, if it is zero. That change makes it not according to spec, but I made it so we can experiment with generating tests using this PR.

@@ -71,6 +71,15 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(statedb)
}
if p.config.IsCancun(blockNumber, block.Time()) {
Copy link
Member

Choose a reason for hiding this comment

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

should this be in Prepare in the consensus interface?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please clarify, @lightclient, I don't understand what you are suggesting

@holiman
Copy link
Contributor

holiman commented Aug 1, 2023

@puma314

have you considered passing the entirety of the evm EVM object instead of just the stateDB StateReader object to the precompile?

The original PR did that, but I suggested passing in a smaller interface. For downstream projects, it should be a pretty small change to just take the method(s) from EVM that you need, and add them to that interface. The big bulky change of passing the thing into all precompiles is already done via this PR. Hope that's good enough for you

Comment on lines +1151 to +1154
func (c *beaconRoot) Run(stateDB StateReader, input []byte) ([]byte, error) {
if len(input) != common.HashLength {
return nil, errors.New("invalid input length")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is (subtly wrong). From the EIP:

If the input is more than 32 bytes, the precompile only takes the first 32 bytes of the input buffer and ignores the rest. If the input is less than 32 bytes, the precompile should revert.

cc @shemnon will this make the next testnet go boom? Or does besu contain the same error?

Copy link
Member

Choose a reason for hiding this comment

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

we can also update the EIP here, if that's simpler

I don't know if there is precedent for precompile input handling one way or the other...

Copy link
Contributor

Choose a reason for hiding this comment

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

Besu hasn't implemented it yet. @pinges is supposed to be implementing that.

holiman added a commit that referenced this pull request Aug 22, 2023
This change implements "EIP 4788 : Beacon block root in the EVM". It implements version-2 of EPI-4788, main difference being that the contract is an actual contract rather than a precompile, as in #27289.
@holiman
Copy link
Contributor

holiman commented Aug 22, 2023

Superseded by #27849

@holiman holiman closed this Aug 22, 2023
devopsbo3 pushed a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
…27849)

This change implements "EIP 4788 : Beacon block root in the EVM". It implements version-2 of EPI-4788, main difference being that the contract is an actual contract rather than a precompile, as in ethereum#27289.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants