From e6517f4870e1080ae0794ffa691a3ed176ec5be5 Mon Sep 17 00:00:00 2001 From: Cal Bera Date: Mon, 21 Jul 2025 20:28:27 -0700 Subject: [PATCH] feat: BRIP-0004 implementation (#21) * wip * more * prague1 engineapi * nits * undo * prague1 tx signer for pol tx * more * tx pol hash * switch order * use 0x7D * PoL Tx processing * nit * wip * wip * setup * process pol msg * fixes * todos * nit * tx args * proper prague1 block validation * cleanup * linter * move validation to BlockValidator * To is not nillable * tx type sorted out * reject from top level mempool * unit tests * working * set tx gas price to be block's base fee * tx pol * new Pectra11 engine API endpoints * handle fork prague1 versions * fix catalyst/api tests * fix TestGeneratePOSChain * remove some TODOs * Address review comments * use type check for critical path validation --- .gitignore | 5 +- beacon/blsync/engineclient.go | 13 ++ beacon/engine/gen_blockparams.go | 6 + beacon/engine/types.go | 55 ++++---- cmd/evm/internal/t8ntool/block.go | 40 +++--- cmd/evm/internal/t8ntool/execution.go | 2 + cmd/evm/internal/t8ntool/gen_header.go | 6 + cmd/evm/internal/t8ntool/gen_stenv.go | 6 + cmd/evm/internal/t8ntool/transition.go | 16 +++ cmd/geth/main.go | 2 +- common/types.go | 62 +++++++++ consensus/beacon/consensus.go | 11 ++ consensus/misc/eip4844/eip4844.go | 2 +- core/block_validator.go | 32 ++++- core/block_validator_test.go | 119 ++++++++++++++++ core/chain_makers.go | 18 ++- core/genesis.go | 6 + core/state_processor_test.go | 4 + core/state_transition.go | 25 ++++ core/txpool/txpool.go | 5 + core/types/block.go | 12 +- core/types/gen_header_json.go | 92 +++++++------ core/types/gen_header_rlp.go | 20 ++- core/types/receipt.go | 4 +- core/types/transaction.go | 5 + core/types/transaction_marshalling.go | 27 ++++ core/types/transaction_signing.go | 27 +++- core/types/tx_pol.go | 179 +++++++++++++++++++++++++ core/types/tx_pol_test.go | 128 ++++++++++++++++++ eth/catalyst/api.go | 96 +++++++++++-- eth/catalyst/api_test.go | 60 +++++---- eth/catalyst/simulated_beacon.go | 10 +- eth/catalyst/witness.go | 109 +++++++++++++-- eth/tracers/api.go | 8 +- graphql/graphql.go | 4 +- internal/ethapi/api.go | 67 +++++++-- internal/ethapi/api_test.go | 9 ++ internal/ethapi/override/override.go | 24 ++-- internal/ethapi/simulate.go | 64 +++++++-- internal/ethapi/transaction_args.go | 21 ++- miner/miner.go | 22 +-- miner/payload_building.go | 61 +++++---- miner/worker.go | 62 +++++++-- params/config.go | 25 ++-- params/protocol_params.go | 5 +- tests/block_test_util.go | 4 + tests/gen_btheader.go | 6 + 47 files changed, 1336 insertions(+), 250 deletions(-) create mode 100644 core/types/tx_pol.go create mode 100644 core/types/tx_pol_test.go diff --git a/.gitignore b/.gitignore index 269455db7a..5712e250e7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,7 @@ cmd/ethkey/ethkey cmd/evm/evm cmd/geth/geth cmd/rlpdump/rlpdump -cmd/workload/workload \ No newline at end of file +cmd/workload/workload + +# claude +.claude diff --git a/beacon/blsync/engineclient.go b/beacon/blsync/engineclient.go index f9821fc6f3..7c9d285433 100644 --- a/beacon/blsync/engineclient.go +++ b/beacon/blsync/engineclient.go @@ -101,6 +101,17 @@ func (ec *engineClient) callNewPayload(fork string, event types.ChainHeadEvent) params = []any{execData} ) switch fork { + case "electra1": + // TODO(BRIP-4): Add ParentProposerPubkey to the chainHeadEvent to add to the newPayload call. + method = "engine_newPayloadV4P11" + parentBeaconRoot := event.BeaconHead.ParentRoot + blobHashes := collectBlobHashes(event.Block) + hexRequests := make([]hexutil.Bytes, len(event.ExecRequests)) + for i := range event.ExecRequests { + hexRequests[i] = hexutil.Bytes(event.ExecRequests[i]) + } + parentProposerPubkey := &common.Pubkey{} + params = append(params, blobHashes, parentBeaconRoot, hexRequests, parentProposerPubkey) case "electra": method = "engine_newPayloadV4" parentBeaconRoot := event.BeaconHead.ParentRoot @@ -145,6 +156,8 @@ func (ec *engineClient) callForkchoiceUpdated(fork string, event types.ChainHead var method string switch fork { + case "electra1": + method = "engine_forkchoiceUpdatedV3P11" case "deneb", "electra": method = "engine_forkchoiceUpdatedV3" case "capella": diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/gen_blockparams.go index b1f01b50ff..a15186e3d5 100644 --- a/beacon/engine/gen_blockparams.go +++ b/beacon/engine/gen_blockparams.go @@ -21,6 +21,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + ProposerPubkey *common.Pubkey `json:"parentProposerPubkey"` } var enc PayloadAttributes enc.Timestamp = hexutil.Uint64(p.Timestamp) @@ -28,6 +29,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient enc.Withdrawals = p.Withdrawals enc.BeaconRoot = p.BeaconRoot + enc.ProposerPubkey = p.ProposerPubkey return json.Marshal(&enc) } @@ -39,6 +41,7 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { SuggestedFeeRecipient *common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + ProposerPubkey *common.Pubkey `json:"parentProposerPubkey"` } var dec PayloadAttributes if err := json.Unmarshal(input, &dec); err != nil { @@ -62,5 +65,8 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { if dec.BeaconRoot != nil { p.BeaconRoot = dec.BeaconRoot } + if dec.ProposerPubkey != nil { + p.ProposerPubkey = dec.ProposerPubkey + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 76bfd22a23..1bfbcce506 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -33,9 +33,10 @@ import ( type PayloadVersion byte var ( - PayloadV1 PayloadVersion = 0x1 - PayloadV2 PayloadVersion = 0x2 - PayloadV3 PayloadVersion = 0x3 + PayloadV1 PayloadVersion = 0x1 + PayloadV2 PayloadVersion = 0x2 + PayloadV3 PayloadVersion = 0x3 + PayloadV3P11 PayloadVersion = 0x31 ) //go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go @@ -48,6 +49,7 @@ type PayloadAttributes struct { SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + ProposerPubkey *common.Pubkey `json:"parentProposerPubkey"` // Berachain: only used prague1 and onwards } // JSON type overrides for PayloadAttributes. @@ -218,8 +220,8 @@ func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) { // and that the blockhash of the constructed block matches the parameters. Nil // Withdrawals value will propagate through the returned block. Empty // Withdrawals value must be passed via non-nil, length 0 value in data. -func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte) (*types.Block, error) { - block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot, requests) +func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, proposerPubkey *common.Pubkey) (*types.Block, error) { + block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot, requests, proposerPubkey) if err != nil { return nil, err } @@ -232,7 +234,7 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b // ExecutableDataToBlockNoHash is analogous to ExecutableDataToBlock, but is used // for stateless execution, so it skips checking if the executable data hashes to // the requested hash (stateless has to *compute* the root hash, it's not given). -func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte) (*types.Block, error) { +func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, proposerPubkey *common.Pubkey) (*types.Block, error) { txs, err := decodeTransactions(data.Transactions) if err != nil { return nil, err @@ -275,26 +277,27 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H } header := &types.Header{ - ParentHash: data.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: data.FeeRecipient, - Root: data.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: data.ReceiptsRoot, - Bloom: types.BytesToBloom(data.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(data.Number), - GasLimit: data.GasLimit, - GasUsed: data.GasUsed, - Time: data.Timestamp, - BaseFee: data.BaseFeePerGas, - Extra: data.ExtraData, - MixDigest: data.Random, - WithdrawalsHash: withdrawalsRoot, - ExcessBlobGas: data.ExcessBlobGas, - BlobGasUsed: data.BlobGasUsed, - ParentBeaconRoot: beaconRoot, - RequestsHash: requestsHash, + ParentHash: data.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: data.FeeRecipient, + Root: data.StateRoot, + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: data.ReceiptsRoot, + Bloom: types.BytesToBloom(data.LogsBloom), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(data.Number), + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Time: data.Timestamp, + BaseFee: data.BaseFeePerGas, + Extra: data.ExtraData, + MixDigest: data.Random, + WithdrawalsHash: withdrawalsRoot, + ExcessBlobGas: data.ExcessBlobGas, + BlobGasUsed: data.BlobGasUsed, + ParentBeaconRoot: beaconRoot, + RequestsHash: requestsHash, + ParentProposerPubkey: proposerPubkey, } return types.NewBlockWithHeader(header). WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}). diff --git a/cmd/evm/internal/t8ntool/block.go b/cmd/evm/internal/t8ntool/block.go index 37a6db9ffc..c81669ce05 100644 --- a/cmd/evm/internal/t8ntool/block.go +++ b/cmd/evm/internal/t8ntool/block.go @@ -56,6 +56,7 @@ type header struct { BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey" rlp:"optional"` } type headerMarshaling struct { @@ -117,25 +118,26 @@ func (c *cliqueInput) UnmarshalJSON(input []byte) error { // ToBlock converts i into a *types.Block func (i *bbInput) ToBlock() *types.Block { header := &types.Header{ - ParentHash: i.Header.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: common.Address{}, - Root: i.Header.Root, - TxHash: types.EmptyTxsHash, - ReceiptHash: types.EmptyReceiptsHash, - Bloom: i.Header.Bloom, - Difficulty: common.Big0, - Number: i.Header.Number, - GasLimit: i.Header.GasLimit, - GasUsed: i.Header.GasUsed, - Time: i.Header.Time, - Extra: i.Header.Extra, - MixDigest: i.Header.MixDigest, - BaseFee: i.Header.BaseFee, - WithdrawalsHash: i.Header.WithdrawalsHash, - BlobGasUsed: i.Header.BlobGasUsed, - ExcessBlobGas: i.Header.ExcessBlobGas, - ParentBeaconRoot: i.Header.ParentBeaconBlockRoot, + ParentHash: i.Header.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: common.Address{}, + Root: i.Header.Root, + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + Bloom: i.Header.Bloom, + Difficulty: common.Big0, + Number: i.Header.Number, + GasLimit: i.Header.GasLimit, + GasUsed: i.Header.GasUsed, + Time: i.Header.Time, + Extra: i.Header.Extra, + MixDigest: i.Header.MixDigest, + BaseFee: i.Header.BaseFee, + WithdrawalsHash: i.Header.WithdrawalsHash, + BlobGasUsed: i.Header.BlobGasUsed, + ExcessBlobGas: i.Header.ExcessBlobGas, + ParentBeaconRoot: i.Header.ParentBeaconBlockRoot, + ParentProposerPubkey: i.Header.ParentProposerPubkey, } // Fill optional values. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index da1fb3701f..6891147177 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -101,6 +101,7 @@ type stEnv struct { ParentExcessBlobGas *uint64 `json:"parentExcessBlobGas,omitempty"` ParentBlobGasUsed *uint64 `json:"parentBlobGasUsed,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey"` } type stEnvMarshaling struct { @@ -220,6 +221,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, ) core.ProcessParentBlockHash(prevHash, evm) } + // TODO(BRIP-4): Validate Prague1 block rules. for i := 0; txIt.Next(); i++ { tx, err := txIt.Tx() if err != nil { diff --git a/cmd/evm/internal/t8ntool/gen_header.go b/cmd/evm/internal/t8ntool/gen_header.go index a8c8668978..84a620f97b 100644 --- a/cmd/evm/internal/t8ntool/gen_header.go +++ b/cmd/evm/internal/t8ntool/gen_header.go @@ -38,6 +38,7 @@ func (h header) MarshalJSON() ([]byte, error) { BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey" rlp:"optional"` } var enc header enc.ParentHash = h.ParentHash @@ -60,6 +61,7 @@ func (h header) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*math.HexOrDecimal64)(h.BlobGasUsed) enc.ExcessBlobGas = (*math.HexOrDecimal64)(h.ExcessBlobGas) enc.ParentBeaconBlockRoot = h.ParentBeaconBlockRoot + enc.ParentProposerPubkey = h.ParentProposerPubkey return json.Marshal(&enc) } @@ -86,6 +88,7 @@ func (h *header) UnmarshalJSON(input []byte) error { BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey" rlp:"optional"` } var dec header if err := json.Unmarshal(input, &dec); err != nil { @@ -155,5 +158,8 @@ func (h *header) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { h.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.ParentProposerPubkey != nil { + h.ParentProposerPubkey = dec.ParentProposerPubkey + } return nil } diff --git a/cmd/evm/internal/t8ntool/gen_stenv.go b/cmd/evm/internal/t8ntool/gen_stenv.go index d47db4a876..ec16267f31 100644 --- a/cmd/evm/internal/t8ntool/gen_stenv.go +++ b/cmd/evm/internal/t8ntool/gen_stenv.go @@ -37,6 +37,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { ParentExcessBlobGas *math.HexOrDecimal64 `json:"parentExcessBlobGas,omitempty"` ParentBlobGasUsed *math.HexOrDecimal64 `json:"parentBlobGasUsed,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey"` } var enc stEnv enc.Coinbase = common.UnprefixedAddress(s.Coinbase) @@ -59,6 +60,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { enc.ParentExcessBlobGas = (*math.HexOrDecimal64)(s.ParentExcessBlobGas) enc.ParentBlobGasUsed = (*math.HexOrDecimal64)(s.ParentBlobGasUsed) enc.ParentBeaconBlockRoot = s.ParentBeaconBlockRoot + enc.ParentProposerPubkey = s.ParentProposerPubkey return json.Marshal(&enc) } @@ -85,6 +87,7 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { ParentExcessBlobGas *math.HexOrDecimal64 `json:"parentExcessBlobGas,omitempty"` ParentBlobGasUsed *math.HexOrDecimal64 `json:"parentBlobGasUsed,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey"` } var dec stEnv if err := json.Unmarshal(input, &dec); err != nil { @@ -154,5 +157,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { s.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.ParentProposerPubkey != nil { + s.ParentProposerPubkey = dec.ParentProposerPubkey + } return nil } diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index e946ccddd5..342ee595c7 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -150,6 +150,9 @@ func Transition(ctx *cli.Context) error { if err := applyCancunChecks(&prestate.Env, chainConfig); err != nil { return err } + if err := applyPrague1Checks(&prestate.Env, chainConfig); err != nil { + return err + } // Configure tracer if ctx.IsSet(TraceTracerFlag.Name) { // Custom tracing @@ -265,6 +268,19 @@ func applyCancunChecks(env *stEnv, chainConfig *params.ChainConfig) error { return nil } +func applyPrague1Checks(env *stEnv, chainConfig *params.ChainConfig) error { + if !chainConfig.IsPrague1(big.NewInt(int64(env.Number)), env.Timestamp) { + env.ParentProposerPubkey = nil // un-set it if it has been set too early + return nil + } + // Post-prague1 + // We require BRIP-0004 proposer pubkey to be set in the env + if env.ParentProposerPubkey == nil { + return NewError(ErrorConfig, errors.New("post-prague1 env requires parentProposerPubkey to be set")) + } + return nil +} + type Alloc map[common.Address]types.Account func (g Alloc) OnRoot(common.Hash) {} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index c08dc3ba37..7d32e3877a 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -286,7 +286,7 @@ func prepare(ctx *cli.Context) { // If we're running a known preset, log it for convenience. switch { case ctx.IsSet(utils.MainnetFlag.Name): - log.Info("Starting Geth on Berachain mainnet...") + log.Info("Starting Geth on Ethereum mainnet...") case ctx.IsSet(utils.SepoliaFlag.Name): log.Info("Starting Geth on Sepolia testnet...") diff --git a/common/types.go b/common/types.go index fdb25f1b34..d33451cdf9 100644 --- a/common/types.go +++ b/common/types.go @@ -39,11 +39,16 @@ const ( HashLength = 32 // AddressLength is the expected length of the address AddressLength = 20 + + // Berachain: PubkeyLength represents the expected byte length of a BLS12-381 public key + // as used by the beacon chain. + PubkeyLength = 48 ) var ( hashT = reflect.TypeOf(Hash{}) addressT = reflect.TypeOf(Address{}) + pubkeyT = reflect.TypeOf(Pubkey{}) // MaxAddress represents the maximum possible address value. MaxAddress = HexToAddress("0xffffffffffffffffffffffffffffffffffffffff") @@ -486,3 +491,60 @@ func (b PrettyBytes) TerminalString() string { } return fmt.Sprintf("%#x...%x (%dB)", b[:3], b[len(b)-3:], len(b)) } + +/////////// Berachain: Pubkey + +// Pubkey represents a fixed-length 48-byte BLS public key. +// JSON and text serialization use 0x-prefixed hex strings. +type Pubkey [PubkeyLength]byte + +// Bytes returns a copy of the underlying byte slice. +func (p Pubkey) Bytes() []byte { return p[:] } + +// String returns the hex-encoded string representation of the pubkey. +func (p Pubkey) String() string { return hexutil.Encode(p[:]) } + +// Format implements fmt.Formatter. +// Pubkey supports the %v, %s, %q, %x, %X and %d format verbs. +func (p Pubkey) Format(s fmt.State, c rune) { + hexb := make([]byte, 2+len(p)*2) + copy(hexb, "0x") + hex.Encode(hexb[2:], p[:]) + + switch c { + case 'x', 'X': + if !s.Flag('#') { + hexb = hexb[2:] + } + if c == 'X' { + hexb = bytes.ToUpper(hexb) + } + fallthrough + case 'v', 's': + s.Write(hexb) + case 'q': + q := []byte{'"'} + s.Write(q) + s.Write(hexb) + s.Write(q) + case 'd': + fmt.Fprint(s, ([len(p)]byte)(p)) + default: + fmt.Fprintf(s, "%%!%c(pubkey=%x)", c, p) + } +} + +// MarshalText encodes the pubkey as a 0x-prefixed hex string. +func (p Pubkey) MarshalText() ([]byte, error) { + return hexutil.Bytes(p[:]).MarshalText() +} + +// UnmarshalText decodes a 0x-prefixed hex string into the pubkey. +func (p *Pubkey) UnmarshalText(input []byte) error { + return hexutil.UnmarshalFixedText("Pubkey", input, p[:]) +} + +// UnmarshalJSON decodes a JSON string containing the 0x-prefixed hex pubkey. +func (p *Pubkey) UnmarshalJSON(input []byte) error { + return hexutil.UnmarshalFixedJSON(pubkeyT, input, p[:]) +} diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 196cbc857c..5cdc2a92e8 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -282,6 +282,17 @@ func (beacon *Beacon) verifyHeader(chain consensus.ChainHeaderReader, header, pa return err } } + // Berachain specific: verify the existence of the proposer pubkey. + prague1 := chain.Config().IsPrague1(header.Number, header.Time) + if !prague1 { + if header.ParentProposerPubkey != nil { + return fmt.Errorf("invalid proposer pubkey: have %#x, expected nil", header.ParentProposerPubkey) + } + } else { + if header.ParentProposerPubkey == nil { + return errors.New("header is missing proposer pubkey") + } + } return nil } diff --git a/consensus/misc/eip4844/eip4844.go b/consensus/misc/eip4844/eip4844.go index 32b34f4e53..faa1869b9a 100644 --- a/consensus/misc/eip4844/eip4844.go +++ b/consensus/misc/eip4844/eip4844.go @@ -85,7 +85,7 @@ func CalcBlobFee(config *params.ChainConfig, header *types.Header) *big.Int { switch config.LatestFork(header.Time) { case forks.Osaka: frac = config.BlobScheduleConfig.Osaka.UpdateFraction - case forks.Prague: + case forks.Prague, forks.Prague1: frac = config.BlobScheduleConfig.Prague.UpdateFraction case forks.Cancun: frac = config.BlobScheduleConfig.Cancun.UpdateFraction diff --git a/core/block_validator.go b/core/block_validator.go index 591e472bc1..85ecefc85d 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -19,7 +19,9 @@ package core import ( "errors" "fmt" + "math/big" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -47,7 +49,7 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain) *Bloc // ValidateBody validates the given block's uncles and verifies the block // header's transaction and uncle roots. The headers are assumed to be already -// validated at this point. +// validated at this point. Also it the Prague1 block according to BRIP-0004. func (v *BlockValidator) ValidateBody(block *types.Block) error { // Check whether the block is already imported. if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { @@ -81,9 +83,37 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return errors.New("withdrawals present in block body") } + // Berachain: Pre-compute expected PoL tx hash when in Prague1. + isPrague1 := v.config.IsPrague1(block.Number(), block.Time()) + var expectedPoLHash common.Hash + if isPrague1 { + polTx, err := types.NewPoLTx( + v.config.ChainID, + v.config.Berachain.Prague1.PoLDistributorAddress, + new(big.Int).Sub(block.Number(), big.NewInt(1)), + params.PoLTxGasLimit, + block.BaseFee(), + block.ProposerPubkey(), + ) + if err != nil { + return fmt.Errorf("failed to create expected PoL tx: %w", err) + } + expectedPoLHash = polTx.Hash() + } + // Blob transactions may be present after the Cancun fork. var blobs int for i, tx := range block.Transactions() { + // Berachain: validate the PoL tx is only the first tx in the block. + switch { + case isPrague1 && i == 0: + if tx.Hash() != expectedPoLHash { + return fmt.Errorf("PoL tx hash mismatch: have %v, want %v", tx.Hash(), expectedPoLHash) + } + case tx.Type() == types.PoLTxType: + return fmt.Errorf("invalid block: tx at index %d is a PoL tx", i) + } + // Count the number of blobs to validate against the header's blobGasUsed blobs += len(tx.BlobHashes()) diff --git a/core/block_validator_test.go b/core/block_validator_test.go index fcc99effd0..86b0ac76dd 100644 --- a/core/block_validator_test.go +++ b/core/block_validator_test.go @@ -30,8 +30,127 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" ) +// ----------------------------------------------------------------------------- +// Berachain:Prague-1 PoL validation tests +// ----------------------------------------------------------------------------- + +// newPrague1Config returns a ChainConfig where Prague (and Prague-1) are active +// from timestamp 0. The caller can specify the PoL distributor address. +func newPrague1Config(distributor common.Address) *params.ChainConfig { + zero := uint64(0) + cfg := *params.AllDevChainProtocolChanges // copy + cfg.Berachain.Prague1 = params.Prague1Config{ + Time: &zero, + PoLDistributorAddress: distributor, + } + return &cfg +} + +// buildTestChain initialises an in-memory blockchain with the provided config +// and returns the chain and its validator. +func buildTestChain(t *testing.T, cfg *params.ChainConfig) (*BlockChain, Validator) { + t.Helper() + db := rawdb.NewMemoryDatabase() + genesis := &Genesis{Config: cfg} + chain, err := NewBlockChain(db, genesis, ethash.NewFaker(), nil) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + return chain, chain.Validator() +} + +// samplePubkey returns a deterministic 48-byte pubkey for tests. +func samplePubkey() *common.Pubkey { + var pk common.Pubkey + for i := 0; i < common.PubkeyLength; i++ { + pk[i] = byte(i) + } + return &pk +} + +// makeBlock builds a child block on top of parent with the supplied txs and timestamp. +func makeBlock(parent *types.Header, txs types.Transactions, timestamp uint64) *types.Block { + header := &types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).Add(parent.Number, big.NewInt(1)), + Time: timestamp, + GasLimit: params.GenesisGasLimit * 10, + Difficulty: big.NewInt(1), + ParentProposerPubkey: samplePubkey(), + BaseFee: big.NewInt(1000000000), + } + return types.NewBlock(header, &types.Body{Transactions: txs}, nil, trie.NewStackTrie(nil)) +} + +func TestValidateBody_Prague1_Valid(t *testing.T) { + distributor := common.HexToAddress("0x1111111111111111111111111111111111111111") + cfg := newPrague1Config(distributor) + chain, validator := buildTestChain(t, cfg) + + // Build PoL tx + dummy tx. + polTx, err := types.NewPoLTx(cfg.ChainID, distributor, big.NewInt(0), params.PoLTxGasLimit, big.NewInt(1000000000), samplePubkey()) + if err != nil { + t.Fatalf("failed to create PoL tx: %v", err) + } + dummyTx := types.NewTx(&types.LegacyTx{Nonce: 1}) + + block := makeBlock(chain.CurrentHeader(), types.Transactions{polTx, dummyTx}, 1) + + if err := validator.ValidateBody(block); err != nil { + t.Fatalf("ValidateBody returned error for valid Prague1 block: %v", err) + } +} + +func TestValidateBody_Prague1_InvalidHash(t *testing.T) { + distributor := common.HexToAddress("0x2222222222222222222222222222222222222222") + cfg := newPrague1Config(distributor) + chain, validator := buildTestChain(t, cfg) + + // PoL tx with WRONG pubkey (different from header.ParentProposerPubkey). + wrongPk := &common.Pubkey{} + polTx, _ := types.NewPoLTx(cfg.ChainID, distributor, big.NewInt(0), params.PoLTxGasLimit, big.NewInt(1000000000), wrongPk) + block := makeBlock(chain.CurrentHeader(), types.Transactions{polTx}, 1) + + if err := validator.ValidateBody(block); err == nil { + t.Fatalf("expected error due to invalid PoL hash, got nil") + } +} + +func TestValidateBody_Prague1_MisplacedPoL(t *testing.T) { + distributor := common.HexToAddress("0x3333333333333333333333333333333333333333") + cfg := newPrague1Config(distributor) + chain, validator := buildTestChain(t, cfg) + + polTx, _ := types.NewPoLTx(cfg.ChainID, distributor, big.NewInt(0), params.PoLTxGasLimit, big.NewInt(1000000000), samplePubkey()) + dummyTx := types.NewTx(&types.LegacyTx{Nonce: 1}) + // PoL tx placed second. + block := makeBlock(chain.CurrentHeader(), types.Transactions{dummyTx, polTx}, 1) + + if err := validator.ValidateBody(block); err == nil { + t.Fatalf("expected error for PoL tx at index >0, got nil") + } +} + +func TestValidateBody_PrePrague1_PoLProhibited(t *testing.T) { + distributor := common.HexToAddress("0x4444444444444444444444444444444444444444") + // Prague time active, but Prague1 *future* at timestamp 1000. + future := uint64(1000) + cfg := *params.AllDevChainProtocolChanges + cfg.Berachain.Prague1.Time = &future + cfg.Berachain.Prague1.PoLDistributorAddress = distributor + chain, validator := buildTestChain(t, &cfg) + + polTx, _ := types.NewPoLTx(cfg.ChainID, distributor, big.NewInt(0), params.PoLTxGasLimit, big.NewInt(1000000000), samplePubkey()) + block := makeBlock(chain.CurrentHeader(), types.Transactions{polTx}, 1) // timestamp 1 < future + + if err := validator.ValidateBody(block); err == nil { + t.Fatalf("expected error: PoL tx before Prague1 fork should be invalid") + } +} + // Tests that simple header verification works, for both good and bad blocks. func TestHeaderVerification(t *testing.T) { testHeaderVerification(t, rawdb.HashScheme) diff --git a/core/chain_makers.go b/core/chain_makers.go index b2559495a1..ef680f8d27 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -102,6 +102,12 @@ func (b *BlockGen) SetParentBeaconRoot(root common.Hash) { ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{})) } +// SetParentProposerPubkey sets the parent proposer pubkey field of the generated +// block. +func (b *BlockGen) SetParentProposerPubkey(pubkey common.Pubkey) { + b.header.ParentProposerPubkey = &pubkey +} + // addTx adds a transaction to the generated block. If no coinbase has // been set, the block's coinbase is set to the zero address. // @@ -116,9 +122,16 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti var ( blockContext = NewEVMBlockContext(b.header, bc, &b.header.Coinbase) evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) + gasPool = b.gasPool + gasUsed = &b.header.GasUsed ) + // Berachain: PoL txs do not count towards block gas. + if tx.Type() == types.PoLTxType { + gasPool = new(GasPool).AddGas(params.PoLTxGasLimit) + gasUsed = new(uint64) + } b.statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed) + receipt, err := ApplyTransaction(evm, gasPool, b.statedb, b.header, tx, gasUsed) if err != nil { panic(err) } @@ -617,6 +630,9 @@ func (cm *chainMaker) makeHeader(parent *types.Block, state *state.StateDB, engi header.BlobGasUsed = new(uint64) header.ParentBeaconRoot = new(common.Hash) } + if cm.config.IsPrague1(header.Number, header.Time) { + header.ParentProposerPubkey = new(common.Pubkey) + } return header } diff --git a/core/genesis.go b/core/genesis.go index 42dc493522..1ba8bc9b23 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -526,6 +526,12 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if conf.IsPrague(num, g.Timestamp) { head.RequestsHash = &types.EmptyRequestsHash } + if conf.IsPrague1(num, g.Timestamp) { + // BRIP-0004: The parentProposerPubkey of the genesis block is always + // the zero pubkey. This is because the genesis block does not have a parent + // by definition. + head.ParentProposerPubkey = new(common.Pubkey) + } } return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index d175b5eac5..b4e7690639 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -406,6 +406,10 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr nBlobs += len(tx.BlobHashes()) } header.Root = common.BytesToHash(hasher.Sum(nil)) + if config.IsPrague1(header.Number, header.Time) { + proposerPubkey := common.Pubkey{0x01, 0x02, 0x03} + header.ParentProposerPubkey = &proposerPubkey + } if config.IsCancun(header.Number, header.Time) { excess := eip4844.CalcExcessBlobGas(config, parent.Header(), header.Time) used := uint64(nBlobs * params.BlobTxBlobGasPerBlob) diff --git a/core/state_transition.go b/core/state_transition.go index 3ec718c52c..aa1e20d5f4 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -166,6 +166,9 @@ type Message struct { // When SkipFromEOACheck is true, the message sender is not checked to be an EOA. SkipFromEOACheck bool + + // Berachain:IsPoLTx is true if the message is a PoL tx. + IsPoLTx bool } // TransactionToMessage converts a transaction into a Message. @@ -185,6 +188,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In SkipFromEOACheck: false, BlobHashes: tx.BlobHashes(), BlobGasFeeCap: tx.BlobGasFeeCap(), + IsPoLTx: tx.Type() == types.PoLTxType, } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -207,9 +211,30 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In // state and would never be accepted within a block. func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) { evm.SetTxContext(NewEVMTxContext(msg)) + + // Berachain: ApplyPoLMessage is used to apply the PoL tx to the EVM, skipping the + // normal state transition checks. + if msg.IsPoLTx { + return ApplyPoLMessage(msg, evm), nil + } + return newStateTransition(evm, msg, gp).execute() } +// Berachain: ApplyPoLMessage applies the BRIP-0004 PoL tx to the EVM. No gas is consumed. +func ApplyPoLMessage(msg *Message, evm *vm.EVM) *ExecutionResult { + evm.StateDB.AddAddressToAccessList(*msg.To) + + result := &ExecutionResult{} + ret, leftOverGas, err := evm.Call(msg.From, *msg.To, msg.Data, msg.GasLimit, common.U2560) + if err != nil { + result.Err = fmt.Errorf("PoL tx failed to execute: %v", err) + } + result.ReturnData = ret + result.MaxUsedGas = msg.GasLimit - leftOverGas // To inform how much gas was needed to run the msg. + return result +} + // stateTransition represents a state transition. // // == The State Transitioning Model diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index b5470cd7fc..9810e2249a 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -327,6 +327,11 @@ func (p *TxPool) Add(txs []*types.Transaction, sync bool) []error { // Mark this transaction belonging to no-subpool splits[i] = -1 + // Berachain: PoL txs are rejected. + if tx.Type() == types.PoLTxType { + continue + } + // Try to find a subpool that accepts the transaction for j, subpool := range p.subpools { if subpool.Filter(tx) { diff --git a/core/types/block.go b/core/types/block.go index b284fb3b16..f3cff4dd03 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -106,6 +106,9 @@ type Header struct { // RequestsHash was added by EIP-7685 and is ignored in legacy headers. RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + + // ParentProposerPubkey was added by BRIP-0004 and is ignored in legacy headers. + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey" rlp:"optional"` } // field type overrides for gencodec @@ -329,6 +332,10 @@ func CopyHeader(h *Header) *Header { cpy.RequestsHash = new(common.Hash) *cpy.RequestsHash = *h.RequestsHash } + if h.ParentProposerPubkey != nil { + cpy.ParentProposerPubkey = new(common.Pubkey) + *cpy.ParentProposerPubkey = *h.ParentProposerPubkey + } return &cpy } @@ -408,8 +415,9 @@ func (b *Block) BaseFee() *big.Int { return new(big.Int).Set(b.header.BaseFee) } -func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } -func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) ProposerPubkey() *common.Pubkey { return b.header.ParentProposerPubkey } func (b *Block) ExcessBlobGas() *uint64 { var excessBlobGas *uint64 diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 0af12500bd..1dd179b704 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -16,28 +16,29 @@ var _ = (*headerMarshaling)(nil) // MarshalJSON marshals as JSON. func (h Header) MarshalJSON() ([]byte, error) { type Header struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase common.Address `json:"miner"` - Root common.Hash `json:"stateRoot" gencodec:"required"` - TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest common.Hash `json:"mixHash"` - Nonce BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` - Hash common.Hash `json:"hash"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey" rlp:"optional"` + Hash common.Hash `json:"hash"` } var enc Header enc.ParentHash = h.ParentHash @@ -61,6 +62,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) enc.ParentBeaconRoot = h.ParentBeaconRoot enc.RequestsHash = h.RequestsHash + enc.ParentProposerPubkey = h.ParentProposerPubkey enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -68,27 +70,28 @@ func (h Header) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (h *Header) UnmarshalJSON(input []byte) error { type Header struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase *common.Address `json:"miner"` - Root *common.Hash `json:"stateRoot" gencodec:"required"` - TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom *Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest *common.Hash `json:"mixHash"` - Nonce *BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase *common.Address `json:"miner"` + Root *common.Hash `json:"stateRoot" gencodec:"required"` + TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom *Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest *common.Hash `json:"mixHash"` + Nonce *BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + ParentProposerPubkey *common.Pubkey `json:"parentProposerPubkey" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -169,5 +172,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.RequestsHash != nil { h.RequestsHash = dec.RequestsHash } + if dec.ParentProposerPubkey != nil { + h.ParentProposerPubkey = dec.ParentProposerPubkey + } return nil } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index c79aa8a250..842958ae93 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -43,7 +43,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { _tmp4 := obj.ExcessBlobGas != nil _tmp5 := obj.ParentBeaconRoot != nil _tmp6 := obj.RequestsHash != nil - if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { + _tmp7 := obj.ParentProposerPubkey != nil + if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -53,41 +54,48 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBigInt(obj.BaseFee) } } - if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { + if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.WithdrawalsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.WithdrawalsHash[:]) } } - if _tmp3 || _tmp4 || _tmp5 || _tmp6 { + if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.BlobGasUsed == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.BlobGasUsed)) } } - if _tmp4 || _tmp5 || _tmp6 { + if _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.ExcessBlobGas == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.ExcessBlobGas)) } } - if _tmp5 || _tmp6 { + if _tmp5 || _tmp6 || _tmp7 { if obj.ParentBeaconRoot == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.ParentBeaconRoot[:]) } } - if _tmp6 { + if _tmp6 || _tmp7 { if obj.RequestsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.RequestsHash[:]) } } + if _tmp7 { + if obj.ParentProposerPubkey == nil { + w.Write([]byte{0x80}) + } else { + w.WriteBytes(obj.ParentProposerPubkey[:]) + } + } w.ListEnd(_tmp0) return w.Flush() } diff --git a/core/types/receipt.go b/core/types/receipt.go index 5b6669f274..6bbf2c217d 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -205,7 +205,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errShortTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType, BlobTxType, SetCodeTxType: + case DynamicFeeTxType, AccessListTxType, BlobTxType, SetCodeTxType, PoLTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -368,7 +368,7 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { } w.WriteByte(r.Type) switch r.Type { - case AccessListTxType, DynamicFeeTxType, BlobTxType, SetCodeTxType: + case AccessListTxType, DynamicFeeTxType, BlobTxType, SetCodeTxType, PoLTxType: rlp.Encode(w, data) default: // For unsupported types, write nothing. Since this is for diff --git a/core/types/transaction.go b/core/types/transaction.go index 934feb7353..175434633e 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -49,6 +49,9 @@ const ( DynamicFeeTxType = 0x02 BlobTxType = 0x03 SetCodeTxType = 0x04 + + // Berachain specific. + PoLTxType = 0x7E ) // Transaction is an Ethereum transaction. @@ -211,6 +214,8 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { inner = new(BlobTx) case SetCodeTxType: inner = new(SetCodeTx) + case PoLTxType: + inner = new(PoLTx) default: return nil, ErrTxTypeNotSupported } diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index f76d4b92e9..e4363e0d5e 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -507,6 +507,33 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { } } + case PoLTxType: + var itx PoLTx + inner = &itx + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' in transaction") + } + itx.ChainID = (*big.Int)(dec.ChainID) + if dec.To == nil { + return errors.New("missing required field 'to' in transaction") + } + itx.To = *dec.To + if dec.Nonce == nil { + return errors.New("missing required field 'nonce' in transaction") + } + itx.Nonce = uint64(*dec.Nonce) + if dec.Gas == nil { + return errors.New("missing required field 'gas' for txdata") + } + itx.GasLimit = uint64(*dec.Gas) + if dec.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + if dec.Input == nil { + return errors.New("missing required field 'input' in transaction") + } + itx.Data = *dec.Input + default: return ErrTxTypeNotSupported } diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 01aa67c6ba..6bacc0ffcf 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -42,6 +42,8 @@ type sigCache struct { func MakeSigner(config *params.ChainConfig, blockNumber *big.Int, blockTime uint64) Signer { var signer Signer switch { + case config.IsPrague1(blockNumber, blockTime): + signer = NewPrague1Signer(config.ChainID) case config.IsPrague(blockNumber, blockTime): signer = NewPragueSigner(config.ChainID) case config.IsCancun(blockNumber, blockTime): @@ -71,6 +73,8 @@ func LatestSigner(config *params.ChainConfig) Signer { var signer Signer if config.ChainID != nil { switch { + case config.Berachain.Prague1.Time != nil: + signer = NewPrague1Signer(config.ChainID) case config.PragueTime != nil: signer = NewPragueSigner(config.ChainID) case config.CancunTime != nil: @@ -100,7 +104,7 @@ func LatestSigner(config *params.ChainConfig) Signer { func LatestSignerForChainID(chainID *big.Int) Signer { var signer Signer if chainID != nil { - signer = NewPragueSigner(chainID) + signer = NewPrague1Signer(chainID) } else { signer = HomesteadSigner{} } @@ -219,6 +223,9 @@ func newModernSigner(chainID *big.Int, fork forks.Fork) Signer { if fork >= forks.Prague { s.txtypes[SetCodeTxType] = struct{}{} } + if fork >= forks.Prague1 { + s.txtypes[PoLTxType] = struct{}{} + } return s } @@ -251,6 +258,12 @@ func (s *modernSigner) Sender(tx *Transaction) (common.Address, error) { if tx.ChainId().Cmp(s.chainID) != 0 { return common.Address{}, fmt.Errorf("%w: have %d want %d", ErrInvalidChainId, tx.ChainId(), s.chainID) } + + // Berachain specific: PoLTx is sent by system address and carries no signature. + if tt == PoLTxType { + return params.SystemAddress, nil + } + // 'modern' txs are defined to use 0 and 1 as their recovery // id, add 27 to become equivalent to unprotected Homestead signatures. V, R, S := tx.RawSignatureValues() @@ -276,6 +289,18 @@ func (s *modernSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *bi return R, S, V, nil } +// NewPrague1Signer returns a signer that accepts +// - BRIP-0004 PoL transactions +// - EIP-7702 set code transactions +// - EIP-4844 blob transactions +// - EIP-1559 dynamic fee transactions +// - EIP-2930 access list transactions, +// - EIP-155 replay protected transactions, and +// - legacy Homestead transactions. +func NewPrague1Signer(chainId *big.Int) Signer { + return newModernSigner(chainId, forks.Prague1) +} + // NewPragueSigner returns a signer that accepts // - EIP-7702 set code transactions // - EIP-4844 blob transactions diff --git a/core/types/tx_pol.go b/core/types/tx_pol.go new file mode 100644 index 0000000000..54cda4a05e --- /dev/null +++ b/core/types/tx_pol.go @@ -0,0 +1,179 @@ +// Copyright 2025 Berachain Foundation +// This file is part of the bera-geth library. +// +// The bera-geth library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The bera-geth library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the bera-geth library. If not, see . + +package types + +import ( + "bytes" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" +) + +// PoLTx implements the TxData interface. +var _ TxData = (*PoLTx)(nil) + +// PoLTx represents an BRIP-0004 transaction. No gas is consumed for execution. +type PoLTx struct { + ChainID *big.Int + From common.Address // system address + To common.Address // address of the PoL Distributor contract + Nonce uint64 // block number distributing for + GasLimit uint64 // artificial gas limit for the PoL tx, not consumed against the block gas limit + GasPrice *big.Int // gas price is set to the baseFee to make the tx valid for EIP-1559 rules + Data []byte // encodes the pubkey distributing for +} + +// NewPoLTx creates a new PoL transaction. +func NewPoLTx( + chainID *big.Int, + distributorAddress common.Address, + distributionBlockNumber *big.Int, + gasLimit uint64, + baseFee *big.Int, + pubkey *common.Pubkey, +) (*Transaction, error) { + data, err := getDistributeForData(pubkey) + if err != nil { + return nil, err + } + return NewTx(&PoLTx{ + ChainID: chainID, + From: params.SystemAddress, + To: distributorAddress, + Nonce: distributionBlockNumber.Uint64(), + GasLimit: gasLimit, + GasPrice: baseFee, + Data: data, + }), nil +} + +func (*PoLTx) txType() byte { return PoLTxType } + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *PoLTx) copy() TxData { + cpy := &PoLTx{ + ChainID: new(big.Int), + From: tx.From, + To: tx.To, + Nonce: tx.Nonce, + GasLimit: tx.GasLimit, + GasPrice: new(big.Int), + Data: common.CopyBytes(tx.Data), + } + if tx.ChainID != nil { + cpy.ChainID.Set(tx.ChainID) + } + if tx.GasPrice != nil { + cpy.GasPrice.Set(tx.GasPrice) + } + return cpy +} + +func (tx *PoLTx) chainID() *big.Int { return tx.ChainID } +func (*PoLTx) accessList() AccessList { return nil } +func (tx *PoLTx) data() []byte { return tx.Data } +func (tx *PoLTx) gas() uint64 { return tx.GasLimit } +func (tx *PoLTx) gasPrice() *big.Int { return tx.GasPrice } +func (tx *PoLTx) gasTipCap() *big.Int { return tx.GasPrice } +func (tx *PoLTx) gasFeeCap() *big.Int { return tx.GasPrice } +func (*PoLTx) value() *big.Int { return new(big.Int) } +func (tx *PoLTx) nonce() uint64 { return tx.Nonce } +func (tx *PoLTx) to() *common.Address { return &tx.To } + +// No-op: PoLTx is originated from the system address and carries no signature. +func (*PoLTx) rawSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} + +func (*PoLTx) setSignatureValues(chainID, v, r, s *big.Int) { + // No-op: PoLTx is originated from the system address and carries no signature. +} + +// effectiveGasPrice returns the gas price. PoLTx does not pay for gas, but we +// return the baseFee here to make the receipt valid for a 1559 tx. +func (tx *PoLTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { + return dst.Set(tx.GasPrice) +} + +func (tx *PoLTx) encode(b *bytes.Buffer) error { + return rlp.Encode(b, tx) +} + +func (tx *PoLTx) decode(input []byte) error { + return rlp.DecodeBytes(input, tx) +} + +func (tx *PoLTx) sigHash(chainID *big.Int) common.Hash { + return prefixedRlpHash( + PoLTxType, // tx type: 0x7E + []any{ + chainID, // chainID: EIP-155 chain ID + tx.From, // from = system address + tx.To, // to = address of the PoL Distributor contract + tx.Nonce, // nonce = block number distributing for + tx.GasLimit, // gasLimit = artificial gas limit for execution + tx.GasPrice, // gasPrice = baseFee to make the tx valid for EIP-1559 rules + tx.Data, // data ~= pubkey distributing for + }) +} + +var ( + bytesType, _ = abi.NewType("bytes", "", nil) + distributeForMethod = abi.NewMethod( + "distributeFor", "distributeFor", abi.Function, "nonpayable", false, false, []abi.Argument{ + {Name: "pubkey", Type: bytesType, Indexed: false}, + }, nil, + ) +) + +// getDistributeForData returns the tx data for the `distributeFor(bytes pubkey)` method. +func getDistributeForData(pubkey *common.Pubkey) ([]byte, error) { + var pubkeyBytes []byte + if pubkey == nil { + pubkeyBytes = common.Pubkey{}.Bytes() + } else { + pubkeyBytes = pubkey.Bytes() + } + + arguments, err := distributeForMethod.Inputs.Pack(pubkeyBytes) + if err != nil { + return nil, err + } + return append(distributeForMethod.ID, arguments...), nil +} + +// IsPoLDistribution returns true if the transaction is a PoL distribution. +func IsPoLDistribution(to *common.Address, data []byte, distributorAddress common.Address) bool { + // Txs that call the `distributeFor(bytes pubkey)` method on the PoL Distributor + // contract are also consideredPoL txs. + return to != nil && *to == distributorAddress && isDistributeForCall(data) +} + +// isDistributeForCall returns true if the provided calldata corresponds to a +// call to the `distributeFor(bytes pubkey)` method defined in BRIP-0004. +// +// The function checks that the first four bytes (the function selector) match +// the ID of the `distributeFor` ABI method declared in tx_pol.go. +func isDistributeForCall(data []byte) bool { + if len(data) < 4 { + return false + } + return bytes.Equal(data[:4], distributeForMethod.ID) +} diff --git a/core/types/tx_pol_test.go b/core/types/tx_pol_test.go new file mode 100644 index 0000000000..68233ee288 --- /dev/null +++ b/core/types/tx_pol_test.go @@ -0,0 +1,128 @@ +// Copyright 2025 Berachain Foundation +// This file is part of the bera-geth library. +// +// The bera-geth library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The bera-geth library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the bera-geth library. If not, see . + +package types + +import ( + "bytes" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +// samplePubkey returns a deterministic 48-byte pubkey for tests. +func samplePubkey() *common.Pubkey { + var pk common.Pubkey + for i := 0; i < common.PubkeyLength; i++ { + pk[i] = byte(i) + } + return &pk +} + +// TestNewPoLTx_DataPacking verifies that NewPoLTx produces the expected calldata +// (function selector + ABI-encoded pubkey) and that the transaction fields are +// wired up correctly. +func TestNewPoLTx_DataPacking(t *testing.T) { + chainID := big.NewInt(1) + distributor := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + blockNum := big.NewInt(123) + pubkey := samplePubkey() + baseFee := big.NewInt(1000000000) + + tx, err := NewPoLTx(chainID, distributor, blockNum, params.PoLTxGasLimit, baseFee, pubkey) + if err != nil { + t.Fatalf("NewPoLTx returned error: %v", err) + } + if tx.Type() != PoLTxType { + t.Fatalf("unexpected tx type: have %d, want %d", tx.Type(), PoLTxType) + } + + expectedData, err := getDistributeForData(pubkey) + if err != nil { + t.Fatalf("getDistributeForData failed: %v", err) + } + if !bytes.Equal(tx.Data(), expectedData) { + t.Fatalf("calldata mismatch\n have: %x\n want: %x", tx.Data(), expectedData) + } + // Extra sanity: first 4 bytes must equal the method selector. + if !bytes.Equal(tx.Data()[:4], distributeForMethod.ID) { + t.Fatalf("calldata selector mismatch") + } +} + +// TestNewPoLTx_NegativeBlockNumber ensures negative block numbers are handled +// without panicking (uint64 wrap-around is expected). +func TestNewPoLTx_NegativeBlockNumber(t *testing.T) { + chainID := big.NewInt(1) + distributor := common.Address{} + negBlock := big.NewInt(-1) + baseFee := big.NewInt(1000000000) + + tx, err := NewPoLTx(chainID, distributor, negBlock, params.PoLTxGasLimit, baseFee, samplePubkey()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got, want := tx.Nonce(), negBlock.Uint64(); got != want { + t.Fatalf("nonce mismatch: have %d, want %d", got, want) + } +} + +// TestIsPoLDistribution exercises positive and negative cases for the helper. +func TestIsPoLDistribution(t *testing.T) { + distributor := common.HexToAddress("0x1000000000000000000000000000000000000001") + baseFee := big.NewInt(1000000000) + tx, err := NewPoLTx(big.NewInt(1), distributor, big.NewInt(0), params.PoLTxGasLimit, baseFee, samplePubkey()) + if err != nil { + t.Fatalf("failed to build PoL tx: %v", err) + } + + // Positive case. + if !IsPoLDistribution(&distributor, tx.Data(), distributor) { + t.Fatalf("expected IsPoLDistribution to return true for valid PoL call") + } + + // Wrong address. + otherAddr := common.HexToAddress("0x0200000000000000000000000000000000000002") + if IsPoLDistribution(&otherAddr, tx.Data(), distributor) { + t.Fatalf("expected false when distributor address mismatches") + } + + // Too-short data. + shortData := []byte{0x01, 0x02, 0x03} + if IsPoLDistribution(&distributor, shortData, distributor) { + t.Fatalf("expected false for data shorter than selector") + } + + // Nil address. + if IsPoLDistribution(nil, tx.Data(), distributor) { + t.Fatalf("expected false when to==nil") + } +} + +// TestPoLTx_RawSignatureValues confirms that PoLTx reports no signature. +func TestPoLTx_RawSignatureValues(t *testing.T) { + baseFee := big.NewInt(1000000000) + tx, err := NewPoLTx(big.NewInt(1), common.Address{}, big.NewInt(0), params.PoLTxGasLimit, baseFee, samplePubkey()) + if err != nil { + t.Fatalf("failed to create PoL tx: %v", err) + } + v, r, s := tx.RawSignatureValues() + if v != nil || r != nil || s != nil { + t.Fatalf("expected nil signature values, have v=%v r=%v s=%v", v, r, s) + } +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 409899d582..b7dfd0f055 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -87,14 +87,17 @@ var caps = []string{ "engine_forkchoiceUpdatedV1", "engine_forkchoiceUpdatedV2", "engine_forkchoiceUpdatedV3", + "engine_forkchoiceUpdatedV3P11", "engine_forkchoiceUpdatedWithWitnessV1", "engine_forkchoiceUpdatedWithWitnessV2", "engine_forkchoiceUpdatedWithWitnessV3", + "engine_forkchoiceUpdatedWithWitnessV3P11", "engine_exchangeTransitionConfigurationV1", "engine_getPayloadV1", "engine_getPayloadV2", "engine_getPayloadV3", "engine_getPayloadV4", + "engine_getPayloadV4P11", "engine_getPayloadV5", "engine_getBlobsV1", "engine_getBlobsV2", @@ -102,14 +105,17 @@ var caps = []string{ "engine_newPayloadV2", "engine_newPayloadV3", "engine_newPayloadV4", + "engine_newPayloadV4P11", "engine_newPayloadWithWitnessV1", "engine_newPayloadWithWitnessV2", "engine_newPayloadWithWitnessV3", "engine_newPayloadWithWitnessV4", + "engine_newPayloadWithWitnessV4P11", "engine_executeStatelessPayloadV1", "engine_executeStatelessPayloadV2", "engine_executeStatelessPayloadV3", "engine_executeStatelessPayloadV4", + "engine_executeStatelessPayloadV4P11", "engine_getPayloadBodiesByHashV1", "engine_getPayloadBodiesByHashV2", "engine_getPayloadBodiesByRangeV1", @@ -209,6 +215,8 @@ func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI { func (api *ConsensusAPI) ForkchoiceUpdatedV1(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { if payloadAttributes != nil { switch { + case payloadAttributes.ProposerPubkey != nil: + return engine.STATUS_INVALID, attributesErr("proposer pubkey not supported in V1") case payloadAttributes.Withdrawals != nil || payloadAttributes.BeaconRoot != nil: return engine.STATUS_INVALID, paramsErr("withdrawals and beacon root not supported in V1") case !api.checkFork(payloadAttributes.Timestamp, forks.Paris, forks.Shanghai): @@ -223,6 +231,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV1(update engine.ForkchoiceStateV1, pa func (api *ConsensusAPI) ForkchoiceUpdatedV2(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { if params != nil { switch { + case params.ProposerPubkey != nil: + return engine.STATUS_INVALID, attributesErr("unexpected proposer pubkey") case params.BeaconRoot != nil: return engine.STATUS_INVALID, attributesErr("unexpected beacon root") case api.checkFork(params.Timestamp, forks.Paris) && params.Withdrawals != nil: @@ -241,6 +251,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV2(update engine.ForkchoiceStateV1, pa func (api *ConsensusAPI) ForkchoiceUpdatedV3(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { if params != nil { switch { + case params.ProposerPubkey != nil: + return engine.STATUS_INVALID, attributesErr("unexpected proposer pubkey") case params.Withdrawals == nil: return engine.STATUS_INVALID, attributesErr("missing withdrawals") case params.BeaconRoot == nil: @@ -256,6 +268,28 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV3(update engine.ForkchoiceStateV1, pa return api.forkchoiceUpdated(update, params, engine.PayloadV3, false) } +// ForkchoiceUpdatedV3P11 is equivalent to V3 with the addition of parents proposer pubkey +// in the payload attributes. It supports only PayloadAttributesV3P11. +func (api *ConsensusAPI) ForkchoiceUpdatedV3P11(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { + if params != nil { + switch { + case params.Withdrawals == nil: + return engine.STATUS_INVALID, attributesErr("missing withdrawals") + case params.BeaconRoot == nil: + return engine.STATUS_INVALID, attributesErr("missing beacon root") + case params.ProposerPubkey == nil: + return engine.STATUS_INVALID, attributesErr("missing proposer pubkey") + case !api.checkFork(params.Timestamp, forks.Prague1): + return engine.STATUS_INVALID, unsupportedForkErr("fcuV3P11 must only be called for prague1 payloads") + } + } + // TODO(matt): the spec requires that fcu is applied when called on a valid + // hash, even if params are wrong. To do this we need to split up + // forkchoiceUpdate into a function that only updates the head and then a + // function that kicks off block construction. + return api.forkchoiceUpdated(update, params, engine.PayloadV3P11, false) +} + func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes, payloadVersion engine.PayloadVersion, payloadWitness bool) (engine.ForkChoiceResponse, error) { api.forkchoiceLock.Lock() defer api.forkchoiceLock.Unlock() @@ -383,13 +417,14 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl // will replace it arbitrarily many times in between. if payloadAttributes != nil { args := &miner.BuildPayloadArgs{ - Parent: update.HeadBlockHash, - Timestamp: payloadAttributes.Timestamp, - FeeRecipient: payloadAttributes.SuggestedFeeRecipient, - Random: payloadAttributes.Random, - Withdrawals: payloadAttributes.Withdrawals, - BeaconRoot: payloadAttributes.BeaconRoot, - Version: payloadVersion, + Parent: update.HeadBlockHash, + Timestamp: payloadAttributes.Timestamp, + FeeRecipient: payloadAttributes.SuggestedFeeRecipient, + Random: payloadAttributes.Random, + Withdrawals: payloadAttributes.Withdrawals, + BeaconRoot: payloadAttributes.BeaconRoot, + ProposerPubkey: payloadAttributes.ProposerPubkey, + Version: payloadVersion, } id := args.Id() // If we already are busy generating this work, then we do not need @@ -472,6 +507,14 @@ func (api *ConsensusAPI) GetPayloadV4(payloadID engine.PayloadID) (*engine.Execu return api.getPayload(payloadID, false) } +// GetPayloadV4P11 returns a cached payload by id. +func (api *ConsensusAPI) GetPayloadV4P11(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + if !payloadID.Is(engine.PayloadV3P11) { + return nil, engine.UnsupportedFork + } + return api.getPayload(payloadID, false) +} + // GetPayloadV5 returns a cached payload by id. func (api *ConsensusAPI) GetPayloadV5(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { if !payloadID.Is(engine.PayloadV3) { @@ -589,7 +632,7 @@ func (api *ConsensusAPI) NewPayloadV1(params engine.ExecutableData) (engine.Payl if params.Withdrawals != nil { return invalidStatus, paramsErr("withdrawals not supported in V1") } - return api.newPayload(params, nil, nil, nil, false) + return api.newPayload(params, nil, nil, nil, false, nil) } // NewPayloadV2 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. @@ -610,7 +653,7 @@ func (api *ConsensusAPI) NewPayloadV2(params engine.ExecutableData) (engine.Payl case params.BlobGasUsed != nil: return invalidStatus, paramsErr("non-nil blobGasUsed pre-cancun") } - return api.newPayload(params, nil, nil, nil, false) + return api.newPayload(params, nil, nil, nil, false, nil) } // NewPayloadV3 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. @@ -629,7 +672,7 @@ func (api *ConsensusAPI) NewPayloadV3(params engine.ExecutableData, versionedHas case !api.checkFork(params.Timestamp, forks.Cancun): return invalidStatus, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads") } - return api.newPayload(params, versionedHashes, beaconRoot, nil, false) + return api.newPayload(params, versionedHashes, beaconRoot, nil, false, nil) } // NewPayloadV4 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. @@ -654,10 +697,37 @@ func (api *ConsensusAPI) NewPayloadV4(params engine.ExecutableData, versionedHas if err := validateRequests(requests); err != nil { return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) } - return api.newPayload(params, versionedHashes, beaconRoot, requests, false) + return api.newPayload(params, versionedHashes, beaconRoot, requests, false, nil) } -func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, witness bool) (engine.PayloadStatusV1, error) { +// NewPayloadV4P11 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. +func (api *ConsensusAPI) NewPayloadV4P11(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes, proposerPubkey *common.Pubkey) (engine.PayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return invalidStatus, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return invalidStatus, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return invalidStatus, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return invalidStatus, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return invalidStatus, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return invalidStatus, paramsErr("nil executionRequests post-prague") + case proposerPubkey == nil: + return invalidStatus, paramsErr("nil proposerPubkey post-prague1") + case !api.checkFork(params.Timestamp, forks.Prague1): + return invalidStatus, unsupportedForkErr("newPayloadV4P11 must only be called for prague1 payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.newPayload(params, versionedHashes, beaconRoot, requests, false, proposerPubkey) +} + +func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, witness bool, proposerPubkey *common.Pubkey) (engine.PayloadStatusV1, error) { // The locking here is, strictly, not required. Without these locks, this can happen: // // 1. NewPayload( execdata-N ) is invoked from the CL. It goes all the way down to @@ -675,7 +745,7 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe defer api.newPayloadLock.Unlock() log.Trace("Engine API request received", "method", "NewPayload", "number", params.Number, "hash", params.BlockHash) - block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot, requests) + block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot, requests, proposerPubkey) if err != nil { bgu := "nil" if params.BlobGasUsed != nil { diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index cb6ae053b6..9bc413d632 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -305,7 +305,7 @@ func TestEth2NewBlock(t *testing.T) { if err != nil { t.Fatalf("Failed to create the executable data, block %d: %v", i, err) } - block, err := engine.ExecutableDataToBlock(*execData, nil, nil, nil) + block, err := engine.ExecutableDataToBlock(*execData, nil, nil, nil, nil) if err != nil { t.Fatalf("Failed to convert executable data to block %v", err) } @@ -347,7 +347,7 @@ func TestEth2NewBlock(t *testing.T) { if err != nil { t.Fatalf("Failed to create the executable data %v", err) } - block, err := engine.ExecutableDataToBlock(*execData, nil, nil, nil) + block, err := engine.ExecutableDataToBlock(*execData, nil, nil, nil, nil) if err != nil { t.Fatalf("Failed to convert executable data to block %v", err) } @@ -472,10 +472,10 @@ func TestFullAPI(t *testing.T) { ethservice.TxPool().Add([]*types.Transaction{tx}, false) } - setupBlocks(t, ethservice, 10, parent, callback, nil, nil) + setupBlocks(t, ethservice, 10, parent, callback, nil, nil, nil) } -func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Header, callback func(parent *types.Header), withdrawals [][]*types.Withdrawal, beaconRoots []common.Hash) []*types.Header { +func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Header, callback func(parent *types.Header), withdrawals [][]*types.Withdrawal, beaconRoots []common.Hash, proposerPubkeys []common.Pubkey) []*types.Header { api := NewConsensusAPI(ethservice) var blocks []*types.Header for i := 0; i < n; i++ { @@ -488,9 +488,13 @@ func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.He if beaconRoots != nil { h = &beaconRoots[i] } + var p *common.Pubkey + if proposerPubkeys != nil { + p = &proposerPubkeys[i] + } - envelope := getNewEnvelope(t, api, parent, w, h) - execResp, err := api.newPayload(*envelope.ExecutionPayload, []common.Hash{}, h, envelope.Requests, false) + envelope := getNewEnvelope(t, api, parent, w, h, p) + execResp, err := api.newPayload(*envelope.ExecutionPayload, []common.Hash{}, h, envelope.Requests, false, p) if err != nil { t.Fatalf("can't execute payload: %v", err) } @@ -660,12 +664,13 @@ func TestNewPayloadOnInvalidChain(t *testing.T) { func assembleEnvelope(api *ConsensusAPI, parentHash common.Hash, params *engine.PayloadAttributes) (*engine.ExecutionPayloadEnvelope, error) { args := &miner.BuildPayloadArgs{ - Parent: parentHash, - Timestamp: params.Timestamp, - FeeRecipient: params.SuggestedFeeRecipient, - Random: params.Random, - Withdrawals: params.Withdrawals, - BeaconRoot: params.BeaconRoot, + Parent: parentHash, + Timestamp: params.Timestamp, + FeeRecipient: params.SuggestedFeeRecipient, + Random: params.Random, + Withdrawals: params.Withdrawals, + BeaconRoot: params.BeaconRoot, + ProposerPubkey: params.ProposerPubkey, } payload, err := api.eth.Miner().BuildPayload(args, false) if err != nil { @@ -691,10 +696,10 @@ func TestEmptyBlocks(t *testing.T) { api := NewConsensusAPI(ethservice) // Setup 10 blocks on the canonical chain - setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil) + setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil, nil) // (1) check LatestValidHash by sending a normal payload (P1'') - payload := getNewPayload(t, api, commonAncestor, nil, nil) + payload := getNewPayload(t, api, commonAncestor, nil, nil, nil) status, err := api.NewPayloadV1(*payload) if err != nil { @@ -708,7 +713,7 @@ func TestEmptyBlocks(t *testing.T) { } // (2) Now send P1' which is invalid - payload = getNewPayload(t, api, commonAncestor, nil, nil) + payload = getNewPayload(t, api, commonAncestor, nil, nil, nil) payload.GasUsed += 1 payload = setBlockhash(payload) // Now latestValidHash should be the common ancestor @@ -726,7 +731,7 @@ func TestEmptyBlocks(t *testing.T) { } // (3) Now send a payload with unknown parent - payload = getNewPayload(t, api, commonAncestor, nil, nil) + payload = getNewPayload(t, api, commonAncestor, nil, nil, nil) payload.ParentHash = common.Hash{1} payload = setBlockhash(payload) // Now latestValidHash should be the common ancestor @@ -742,13 +747,14 @@ func TestEmptyBlocks(t *testing.T) { } } -func getNewEnvelope(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal, beaconRoot *common.Hash) *engine.ExecutionPayloadEnvelope { +func getNewEnvelope(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal, beaconRoot *common.Hash, proposerPubkey *common.Pubkey) *engine.ExecutionPayloadEnvelope { params := engine.PayloadAttributes{ Timestamp: parent.Time + 1, Random: crypto.Keccak256Hash([]byte{byte(1)}), SuggestedFeeRecipient: parent.Coinbase, Withdrawals: withdrawals, BeaconRoot: beaconRoot, + ProposerPubkey: proposerPubkey, } envelope, err := assembleEnvelope(api, parent.Hash(), ¶ms) @@ -758,8 +764,8 @@ func getNewEnvelope(t *testing.T, api *ConsensusAPI, parent *types.Header, withd return envelope } -func getNewPayload(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal, beaconRoot *common.Hash) *engine.ExecutableData { - return getNewEnvelope(t, api, parent, withdrawals, beaconRoot).ExecutionPayload +func getNewPayload(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal, beaconRoot *common.Hash, proposerPubkey *common.Pubkey) *engine.ExecutableData { + return getNewEnvelope(t, api, parent, withdrawals, beaconRoot, proposerPubkey).ExecutionPayload } // setBlockhash sets the blockhash of a modified ExecutableData. @@ -820,7 +826,7 @@ func TestTrickRemoteBlockCache(t *testing.T) { commonAncestor := ethserviceA.BlockChain().CurrentBlock() // Setup 10 blocks on the canonical chain - setupBlocks(t, ethserviceA, 10, commonAncestor, func(parent *types.Header) {}, nil, nil) + setupBlocks(t, ethserviceA, 10, commonAncestor, func(parent *types.Header) {}, nil, nil, nil) commonAncestor = ethserviceA.BlockChain().CurrentBlock() var invalidChain []*engine.ExecutableData @@ -829,7 +835,7 @@ func TestTrickRemoteBlockCache(t *testing.T) { //invalidChain = append(invalidChain, payload1) // create an invalid payload2 (P2) - payload2 := getNewPayload(t, apiA, commonAncestor, nil, nil) + payload2 := getNewPayload(t, apiA, commonAncestor, nil, nil, nil) //payload2.ParentHash = payload1.BlockHash payload2.GasUsed += 1 payload2 = setBlockhash(payload2) @@ -838,7 +844,7 @@ func TestTrickRemoteBlockCache(t *testing.T) { head := payload2 // create some valid payloads on top for i := 0; i < 10; i++ { - payload := getNewPayload(t, apiA, commonAncestor, nil, nil) + payload := getNewPayload(t, apiA, commonAncestor, nil, nil, nil) payload.ParentHash = head.BlockHash payload = setBlockhash(payload) invalidChain = append(invalidChain, payload) @@ -875,10 +881,10 @@ func TestInvalidBloom(t *testing.T) { api := NewConsensusAPI(ethservice) // Setup 10 blocks on the canonical chain - setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil) + setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil, nil) // (1) check LatestValidHash by sending a normal payload (P1'') - payload := getNewPayload(t, api, commonAncestor, nil, nil) + payload := getNewPayload(t, api, commonAncestor, nil, nil, nil) payload.LogsBloom = append(payload.LogsBloom, byte(1)) status, err := api.NewPayloadV1(*payload) if err != nil { @@ -935,7 +941,7 @@ func TestSimultaneousNewBlock(t *testing.T) { t.Fatal(testErr) } } - block, err := engine.ExecutableDataToBlock(*execData, nil, nil, nil) + block, err := engine.ExecutableDataToBlock(*execData, nil, nil, nil, nil) if err != nil { t.Fatalf("Failed to convert executable data to block %v", err) } @@ -1275,7 +1281,7 @@ func setupBodies(t *testing.T) (*node.Node, *eth.Ethereum, []*types.Block) { } // Create the blocks. - newHeaders := setupBlocks(t, ethservice, 10, parent, callback, withdrawals, beaconRoots) + newHeaders := setupBlocks(t, ethservice, 10, parent, callback, withdrawals, beaconRoots, nil) newBlocks := make([]*types.Block, len(newHeaders)) for i, header := range newHeaders { newBlocks[i] = ethservice.BlockChain().GetBlock(header.Hash(), header.Number.Uint64()) @@ -1534,7 +1540,7 @@ func TestBlockToPayloadWithBlobs(t *testing.T) { if got := len(envelope.BlobsBundle.Blobs); got != want { t.Fatalf("invalid number of blobs: got %v, want %v", got, want) } - _, err := engine.ExecutableDataToBlock(*envelope.ExecutionPayload, make([]common.Hash, 1), nil, nil) + _, err := engine.ExecutableDataToBlock(*envelope.ExecutionPayload, make([]common.Hash, 1), nil, nil, nil) if err != nil { t.Error(err) } diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index 0642d6a1ad..dd46fcd43d 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -100,6 +100,8 @@ type SimulatedBeacon struct { func payloadVersion(config *params.ChainConfig, time uint64) engine.PayloadVersion { switch config.LatestFork(time) { + case forks.Prague1: + return engine.PayloadV3P11 case forks.Prague, forks.Cancun: return engine.PayloadV3 case forks.Paris, forks.Shanghai: @@ -191,6 +193,10 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u } version := payloadVersion(c.eth.BlockChain().Config(), timestamp) + var proposerPubkey *common.Pubkey + if version == engine.PayloadV3P11 { + proposerPubkey = &common.Pubkey{} + } var random [32]byte rand.Read(random[:]) @@ -200,6 +206,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u Withdrawals: withdrawals, Random: random, BeaconRoot: &common.Hash{}, + ProposerPubkey: proposerPubkey, }, version, false) if err != nil { return err @@ -256,7 +263,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u } // Mark the payload as canon - _, err = c.engineAPI.newPayload(*payload, blobHashes, beaconRoot, requests, false) + _, err = c.engineAPI.newPayload(*payload, blobHashes, beaconRoot, requests, false, proposerPubkey) if err != nil { return err } @@ -317,6 +324,7 @@ func (c *SimulatedBeacon) Commit() common.Hash { withdrawals := c.withdrawals.pop(10) if err := c.sealBlock(withdrawals, uint64(time.Now().Unix())); err != nil { log.Warn("Error performing sealing work", "err", err) + panic(err) } return c.eth.BlockChain().CurrentBlock().Hash() } diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index 712539c5e3..4c5aedddbd 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -37,6 +37,8 @@ import ( func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV1(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { if payloadAttributes != nil { switch { + case payloadAttributes.ProposerPubkey != nil: + return engine.STATUS_INVALID, attributesErr("proposer pubkey not supported in V1") case payloadAttributes.Withdrawals != nil || payloadAttributes.BeaconRoot != nil: return engine.STATUS_INVALID, paramsErr("withdrawals and beacon root not supported in V1") case !api.checkFork(payloadAttributes.Timestamp, forks.Paris, forks.Shanghai): @@ -51,6 +53,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV1(update engine.Forkchoice func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV2(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { if params != nil { switch { + case params.ProposerPubkey != nil: + return engine.STATUS_INVALID, attributesErr("unexpected proposer pubkey") case params.BeaconRoot != nil: return engine.STATUS_INVALID, attributesErr("unexpected beacon root") case api.checkFork(params.Timestamp, forks.Paris) && params.Withdrawals != nil: @@ -69,6 +73,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV2(update engine.Forkchoice func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { if params != nil { switch { + case params.ProposerPubkey != nil: + return engine.STATUS_INVALID, attributesErr("unexpected proposer pubkey") case params.Withdrawals == nil: return engine.STATUS_INVALID, attributesErr("missing withdrawals") case params.BeaconRoot == nil: @@ -84,13 +90,35 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(update engine.Forkchoice return api.forkchoiceUpdated(update, params, engine.PayloadV3, true) } +// ForkchoiceUpdatedWithWitnessV3P11 is analogous to ForkchoiceUpdatedV3P11, only it +// generates an execution witness too if block building was requested. +func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3P11(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { + if params != nil { + switch { + case params.Withdrawals == nil: + return engine.STATUS_INVALID, attributesErr("missing withdrawals") + case params.BeaconRoot == nil: + return engine.STATUS_INVALID, attributesErr("missing beacon root") + case params.ProposerPubkey == nil: + return engine.STATUS_INVALID, attributesErr("missing proposer pubkey") + case !api.checkFork(params.Timestamp, forks.Prague1): + return engine.STATUS_INVALID, unsupportedForkErr("fcuV3P11 must only be called for prague1 payloads") + } + } + // TODO(matt): the spec requires that fcu is applied when called on a valid + // hash, even if params are wrong. To do this we need to split up + // forkchoiceUpdate into a function that only updates the head and then a + // function that kicks off block construction. + return api.forkchoiceUpdated(update, params, engine.PayloadV3P11, true) +} + // NewPayloadWithWitnessV1 is analogous to NewPayloadV1, only it also generates // and returns a stateless witness after running the payload. func (api *ConsensusAPI) NewPayloadWithWitnessV1(params engine.ExecutableData) (engine.PayloadStatusV1, error) { if params.Withdrawals != nil { return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1")) } - return api.newPayload(params, nil, nil, nil, true) + return api.newPayload(params, nil, nil, nil, true, nil) } // NewPayloadWithWitnessV2 is analogous to NewPayloadV2, only it also generates @@ -112,7 +140,7 @@ func (api *ConsensusAPI) NewPayloadWithWitnessV2(params engine.ExecutableData) ( case params.BlobGasUsed != nil: return invalidStatus, paramsErr("non-nil blobGasUsed pre-cancun") } - return api.newPayload(params, nil, nil, nil, true) + return api.newPayload(params, nil, nil, nil, true, nil) } // NewPayloadWithWitnessV3 is analogous to NewPayloadV3, only it also generates @@ -132,7 +160,7 @@ func (api *ConsensusAPI) NewPayloadWithWitnessV3(params engine.ExecutableData, v case !api.checkFork(params.Timestamp, forks.Cancun): return invalidStatus, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads") } - return api.newPayload(params, versionedHashes, beaconRoot, nil, true) + return api.newPayload(params, versionedHashes, beaconRoot, nil, true, nil) } // NewPayloadWithWitnessV4 is analogous to NewPayloadV4, only it also generates @@ -158,7 +186,35 @@ func (api *ConsensusAPI) NewPayloadWithWitnessV4(params engine.ExecutableData, v if err := validateRequests(requests); err != nil { return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) } - return api.newPayload(params, versionedHashes, beaconRoot, requests, true) + return api.newPayload(params, versionedHashes, beaconRoot, requests, true, nil) +} + +// NewPayloadWithWitnessV4P11 is analogous to NewPayloadV4P11, only it also generates +// and returns a stateless witness after running the payload. +func (api *ConsensusAPI) NewPayloadWithWitnessV4P11(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes, proposerPubkey *common.Pubkey) (engine.PayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return invalidStatus, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return invalidStatus, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return invalidStatus, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return invalidStatus, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return invalidStatus, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return invalidStatus, paramsErr("nil executionRequests post-prague") + case proposerPubkey == nil: + return invalidStatus, paramsErr("nil proposerPubkey post-prague1") + case !api.checkFork(params.Timestamp, forks.Prague1): + return invalidStatus, unsupportedForkErr("newPayloadV4P11 must only be called for prague1 payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.newPayload(params, versionedHashes, beaconRoot, requests, true, proposerPubkey) } // ExecuteStatelessPayloadV1 is analogous to NewPayloadV1, only it operates in @@ -167,7 +223,7 @@ func (api *ConsensusAPI) ExecuteStatelessPayloadV1(params engine.ExecutableData, if params.Withdrawals != nil { return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1")) } - return api.executeStatelessPayload(params, nil, nil, nil, opaqueWitness) + return api.executeStatelessPayload(params, nil, nil, nil, opaqueWitness, nil) } // ExecuteStatelessPayloadV2 is analogous to NewPayloadV2, only it operates in @@ -189,7 +245,7 @@ func (api *ConsensusAPI) ExecuteStatelessPayloadV2(params engine.ExecutableData, case params.BlobGasUsed != nil: return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("non-nil blobGasUsed pre-cancun") } - return api.executeStatelessPayload(params, nil, nil, nil, opaqueWitness) + return api.executeStatelessPayload(params, nil, nil, nil, opaqueWitness, nil) } // ExecuteStatelessPayloadV3 is analogous to NewPayloadV3, only it operates in @@ -207,9 +263,9 @@ func (api *ConsensusAPI) ExecuteStatelessPayloadV3(params engine.ExecutableData, case beaconRoot == nil: return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun") case !api.checkFork(params.Timestamp, forks.Cancun): - return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads") + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("executeStatelessPayloadV3 must only be called for cancun payloads") } - return api.executeStatelessPayload(params, versionedHashes, beaconRoot, nil, opaqueWitness) + return api.executeStatelessPayload(params, versionedHashes, beaconRoot, nil, opaqueWitness, nil) } // ExecuteStatelessPayloadV4 is analogous to NewPayloadV4, only it operates in @@ -229,18 +285,46 @@ func (api *ConsensusAPI) ExecuteStatelessPayloadV4(params engine.ExecutableData, case executionRequests == nil: return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil executionRequests post-prague") case !api.checkFork(params.Timestamp, forks.Prague): - return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads") + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("executeStatelessPayloadV4 must only be called for prague payloads") } requests := convertRequests(executionRequests) if err := validateRequests(requests); err != nil { return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) } - return api.executeStatelessPayload(params, versionedHashes, beaconRoot, requests, opaqueWitness) + return api.executeStatelessPayload(params, versionedHashes, beaconRoot, requests, opaqueWitness, nil) } -func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) { +// ExecuteStatelessPayloadV4P11 is analogous to NewPayloadV4P11, only it operates in +// a stateless mode on top of a provided witness instead of the local database. +func (api *ConsensusAPI) ExecuteStatelessPayloadV4P11(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes, opaqueWitness hexutil.Bytes, proposerPubkey *common.Pubkey) (engine.StatelessPayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil executionRequests post-prague") + case proposerPubkey == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil proposerPubkey post-prague1") + case !api.checkFork(params.Timestamp, forks.Prague1): + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("executeStatelessPayloadV4P11 must only be called for prague1 payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.executeStatelessPayload(params, versionedHashes, beaconRoot, requests, opaqueWitness, proposerPubkey) +} + +func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, opaqueWitness hexutil.Bytes, proposerPubkey *common.Pubkey) (engine.StatelessPayloadStatusV1, error) { log.Trace("Engine API request received", "method", "ExecuteStatelessPayload", "number", params.Number, "hash", params.BlockHash) - block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, requests) + block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, requests, proposerPubkey) if err != nil { bgu := "nil" if params.BlobGasUsed != nil { @@ -268,6 +352,7 @@ func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, v "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), "beaconRoot", beaconRoot, + "parentProposerPubkey", proposerPubkey, "len(requests)", len(requests), "error", err) errorMsg := err.Error() diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 6962023343..c13a8ad3ad 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -971,9 +971,11 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc return nil, err } var ( - msg = args.ToMessage(vmctx.BaseFee, true, true) - tx = args.ToTransaction(types.LegacyTxType) - traceConfig *TraceConfig + isPrague1 = api.backend.ChainConfig().IsPrague1(vmctx.BlockNumber, vmctx.Time) + distributorAddress = api.backend.ChainConfig().Berachain.Prague1.PoLDistributorAddress + msg = args.ToMessage(vmctx.BaseFee, true, true, isPrague1, distributorAddress) + tx = args.ToTransaction(types.LegacyTxType, isPrague1, distributorAddress) + traceConfig *TraceConfig ) // Lower the basefee to 0 to avoid breaking EVM // invariants (basefee < feecap). diff --git a/graphql/graphql.go b/graphql/graphql.go index 0b2a77a3c4..6105f7a543 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -318,7 +318,7 @@ func (t *Transaction) MaxFeePerGas(ctx context.Context) *hexutil.Big { return nil } switch tx.Type() { - case types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType: + case types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType, types.PoLTxType: return (*hexutil.Big)(tx.GasFeeCap()) default: return nil @@ -331,7 +331,7 @@ func (t *Transaction) MaxPriorityFeePerGas(ctx context.Context) *hexutil.Big { return nil } switch tx.Type() { - case types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType: + case types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType, types.PoLTxType: return (*hexutil.Big)(tx.GasTipCap()) default: return nil diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 21c35b7665..4fdc7e2b9b 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -696,7 +696,7 @@ func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *s if err := args.CallDefaults(gp.Gas(), blockContext.BaseFee, b.ChainConfig().ChainID); err != nil { return nil, err } - msg := args.ToMessage(header.BaseFee, skipChecks, skipChecks) + msg := args.ToMessage(header.BaseFee, skipChecks, skipChecks, b.ChainConfig().IsPrague1(header.Number, header.Time), b.ChainConfig().Berachain.Prague1.PoLDistributorAddress) // Lower the basefee to 0 to avoid breaking EVM // invariants (basefee < feecap). if msg.GasPrice.Sign() == 0 { @@ -839,7 +839,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil { return 0, err } - call := args.ToMessage(header.BaseFee, true, true) + call := args.ToMessage(header.BaseFee, true, true, b.ChainConfig().IsPrague1(header.Number, header.Time), b.ChainConfig().Berachain.Prague1.PoLDistributorAddress) // Run the gas estimation and wrap any revertals into a custom return estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap) @@ -904,6 +904,9 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { if head.RequestsHash != nil { result["requestsHash"] = head.RequestsHash } + if head.ParentProposerPubkey != nil { + result["parentProposerPubkey"] = head.ParentProposerPubkey + } return result } @@ -1057,6 +1060,14 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) } result.AuthorizationList = tx.SetCodeAuthorizations() + + case types.PoLTxType: + al := tx.AccessList() + result.Accesses = &al + result.ChainID = (*hexutil.Big)(tx.ChainId()) + result.GasFeeCap = (*hexutil.Big)(tx.GasFeeCap()) + result.GasTipCap = (*hexutil.Big)(tx.GasTipCap()) + result.MaxFeePerBlobGas = (*hexutil.Big)(tx.BlobGasFeeCap()) } return result } @@ -1217,7 +1228,9 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH statedb := db.Copy() // Set the accesslist to the last al args.AccessList = &accessList - msg := args.ToMessage(header.BaseFee, true, true) + isPrague1 := b.ChainConfig().IsPrague1(header.Number, header.Time) + distributorAddress := b.ChainConfig().Berachain.Prague1.PoLDistributorAddress + msg := args.ToMessage(header.BaseFee, true, true, isPrague1, distributorAddress) // Apply the transaction with the access list tracer tracer := logger.NewAccessListTracer(accessList, addressesToExclude) @@ -1234,7 +1247,15 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH } res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) if err != nil { - return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.ToTransaction(types.LegacyTxType).Hash(), err) + return nil, 0, nil, fmt.Errorf( + "failed to apply transaction: %v err: %v", + args.ToTransaction( + types.LegacyTxType, + b.ChainConfig().IsPrague1(header.Number, header.Time), + b.ChainConfig().Berachain.Prague1.PoLDistributorAddress, + ).Hash(), + err, + ) } if tracer.Equal(prevTracer) { return accessList, res.UsedGas, res.Err, nil @@ -1503,7 +1524,13 @@ func (api *TransactionAPI) SendTransaction(ctx context.Context, args Transaction return common.Hash{}, err } // Assemble the transaction and sign with the wallet - tx := args.ToTransaction(types.LegacyTxType) + header := api.b.CurrentHeader() + + tx := args.ToTransaction( + types.LegacyTxType, + api.b.ChainConfig().IsPrague1(header.Number, header.Time), + api.b.ChainConfig().Berachain.Prague1.PoLDistributorAddress, + ) signed, err := wallet.SignTx(account, tx, api.b.ChainConfig().ChainID) if err != nil { @@ -1523,7 +1550,12 @@ func (api *TransactionAPI) FillTransaction(ctx context.Context, args Transaction return nil, err } // Assemble the transaction and obtain rlp - tx := args.ToTransaction(types.LegacyTxType) + header := api.b.CurrentHeader() + tx := args.ToTransaction( + types.LegacyTxType, + api.b.ChainConfig().IsPrague1(header.Number, header.Time), + api.b.ChainConfig().Berachain.Prague1.PoLDistributorAddress, + ) data, err := tx.MarshalBinary() if err != nil { return nil, err @@ -1591,7 +1623,12 @@ func (api *TransactionAPI) SignTransaction(ctx context.Context, args Transaction return nil, err } // Before actually sign the transaction, ensure the transaction fee is reasonable. - tx := args.ToTransaction(types.LegacyTxType) + header := api.b.CurrentHeader() + tx := args.ToTransaction( + types.LegacyTxType, + api.b.ChainConfig().IsPrague1(header.Number, header.Time), + api.b.ChainConfig().Berachain.Prague1.PoLDistributorAddress, + ) if err := checkTxFee(tx.GasPrice(), tx.Gas(), api.b.RPCTxFeeCap()); err != nil { return nil, err } @@ -1649,7 +1686,12 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs, if err := sendArgs.setDefaults(ctx, api.b, false); err != nil { return common.Hash{}, err } - matchTx := sendArgs.ToTransaction(types.LegacyTxType) + header := api.b.CurrentHeader() + matchTx := sendArgs.ToTransaction( + types.LegacyTxType, + api.b.ChainConfig().IsPrague1(header.Number, header.Time), + api.b.ChainConfig().Berachain.Prague1.PoLDistributorAddress, + ) // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. price := matchTx.GasPrice() @@ -1679,7 +1721,14 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs, if gasLimit != nil && *gasLimit != 0 { sendArgs.Gas = gasLimit } - signedTx, err := api.sign(sendArgs.from(), sendArgs.ToTransaction(types.LegacyTxType)) + header := api.b.CurrentHeader() + signedTx, err := api.sign( + sendArgs.from(), sendArgs.ToTransaction( + types.LegacyTxType, + api.b.ChainConfig().IsPrague1(header.Number, header.Time), + api.b.ChainConfig().Berachain.Prague1.PoLDistributorAddress, + ), + ) if err != nil { return common.Hash{}, err } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 1c9be836fc..f9340eae14 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -1217,6 +1217,15 @@ func TestCall(t *testing.T) { }, expectErr: errors.New(`block override "withdrawals" is not supported for this RPC method`), }, + { + name: "unsupported block override proposerPubkey", + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{}, + blockOverrides: override.BlockOverrides{ + ProposerPubkey: &common.Pubkey{}, + }, + expectErr: errors.New(`block override "proposerPubkey" is not supported for this RPC method`), + }, } for _, tc := range testSuite { result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides) diff --git a/internal/ethapi/override/override.go b/internal/ethapi/override/override.go index 0bcf3c444d..70449c1644 100644 --- a/internal/ethapi/override/override.go +++ b/internal/ethapi/override/override.go @@ -121,16 +121,17 @@ func (diff *StateOverride) Apply(statedb *state.StateDB, precompiles vm.Precompi // BlockOverrides is a set of header fields to override. type BlockOverrides struct { - Number *hexutil.Big - Difficulty *hexutil.Big // No-op if we're simulating post-merge calls. - Time *hexutil.Uint64 - GasLimit *hexutil.Uint64 - FeeRecipient *common.Address - PrevRandao *common.Hash - BaseFeePerGas *hexutil.Big - BlobBaseFee *hexutil.Big - BeaconRoot *common.Hash - Withdrawals *types.Withdrawals + Number *hexutil.Big + Difficulty *hexutil.Big // No-op if we're simulating post-merge calls. + Time *hexutil.Uint64 + GasLimit *hexutil.Uint64 + FeeRecipient *common.Address + PrevRandao *common.Hash + BaseFeePerGas *hexutil.Big + BlobBaseFee *hexutil.Big + BeaconRoot *common.Hash + Withdrawals *types.Withdrawals + ProposerPubkey *common.Pubkey } // Apply overrides the given header fields into the given block context. @@ -144,6 +145,9 @@ func (o *BlockOverrides) Apply(blockCtx *vm.BlockContext) error { if o.Withdrawals != nil { return errors.New(`block override "withdrawals" is not supported for this RPC method`) } + if o.ProposerPubkey != nil { + return errors.New(`block override "proposerPubkey" is not supported for this RPC method`) + } if o.Number != nil { blockCtx.BlockNumber = o.Number.ToInt() } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 362536bcb5..7f0f6537a7 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -199,6 +199,37 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]*simBlo if err != nil { return nil, err } + + // Berachain: Pre-compute expected PoL tx hash when in Prague1. + isPrague1 := sim.chainConfig.IsPrague1(result.Number(), result.Time()) + var expectedPoLHash common.Hash + if isPrague1 { + polTx, err := types.NewPoLTx( + sim.chainConfig.ChainID, + sim.chainConfig.Berachain.Prague1.PoLDistributorAddress, + new(big.Int).Sub(result.Number(), big.NewInt(1)), + params.PoLTxGasLimit, + result.BaseFee(), + result.ProposerPubkey(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create expected PoL tx: %w", err) + } + expectedPoLHash = polTx.Hash() + } + + // Berachain: validate the PoL tx is only the first tx in the block. + for i, tx := range result.Transactions() { + switch { + case isPrague1 && i == 0: + if tx.Hash() != expectedPoLHash { + return nil, fmt.Errorf("PoL tx hash mismatch: have %v, want %v", tx.Hash(), expectedPoLHash) + } + case tx.Type() == types.PoLTxType: + return nil, fmt.Errorf("invalid block: tx at index %d is a PoL tx", i) + } + } + headers[bi] = result.Header() results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults, senders: senders} parent = result.Header() @@ -279,15 +310,17 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, return nil, nil, nil, err } var ( - tx = call.ToTransaction(types.DynamicFeeTxType) - txHash = tx.Hash() + isPrague1 = sim.chainConfig.IsPrague1(header.Number, header.Time) + distributorAddress = sim.chainConfig.Berachain.Prague1.PoLDistributorAddress + tx = call.ToTransaction(types.DynamicFeeTxType, isPrague1, distributorAddress) + txHash = tx.Hash() ) txes[i] = tx senders[txHash] = call.from() tracer.reset(txHash, uint(i)) sim.state.SetTxContext(txHash, i) // EoA check is always skipped, even in validation mode. - msg := call.ToMessage(header.BaseFee, !sim.validate, true) + msg := call.ToMessage(header.BaseFee, !sim.validate, true, isPrague1, distributorAddress) result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp) if err != nil { txErr := txValidationError(err) @@ -484,15 +517,24 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { parentBeaconRoot = overrides.BeaconRoot } } + var parentProposerPubkey *common.Pubkey + if sim.chainConfig.IsPrague1(overrides.Number.ToInt(), (uint64)(*overrides.Time)) { + parentProposerPubkey = new(common.Pubkey) + if overrides.ProposerPubkey == nil { + return nil, errors.New("proposer pubkey override is required for post Prague1 blocks") + } + *parentProposerPubkey = *overrides.ProposerPubkey + } header = overrides.MakeHeader(&types.Header{ - UncleHash: types.EmptyUncleHash, - ReceiptHash: types.EmptyReceiptsHash, - TxHash: types.EmptyTxsHash, - Coinbase: header.Coinbase, - Difficulty: header.Difficulty, - GasLimit: header.GasLimit, - WithdrawalsHash: withdrawalsHash, - ParentBeaconRoot: parentBeaconRoot, + UncleHash: types.EmptyUncleHash, + ReceiptHash: types.EmptyReceiptsHash, + TxHash: types.EmptyTxsHash, + Coinbase: header.Coinbase, + Difficulty: header.Difficulty, + GasLimit: header.GasLimit, + WithdrawalsHash: withdrawalsHash, + ParentBeaconRoot: parentBeaconRoot, + ParentProposerPubkey: parentProposerPubkey, }) res[bi] = header } diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 7a7d63c535..cb6ec6fdb7 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -414,7 +414,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, // core evm. This method is used in calls and traces that do not require a real // live transaction. // Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called. -func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck, skipEoACheck bool) *core.Message { +func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck, skipEoACheck, isPrague1 bool, distributorAddress common.Address) *core.Message { var ( gasPrice *big.Int gasFeeCap *big.Int @@ -463,14 +463,17 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck, skipEoA SetCodeAuthorizations: args.AuthorizationList, SkipNonceChecks: skipNonceCheck, SkipFromEOACheck: skipEoACheck, + IsPoLTx: isPrague1 && types.IsPoLDistribution(args.To, args.data(), distributorAddress), } } // ToTransaction converts the arguments to a transaction. // This assumes that setDefaults has been called. -func (args *TransactionArgs) ToTransaction(defaultType int) *types.Transaction { +func (args *TransactionArgs) ToTransaction(defaultType int, isPrague1 bool, distributorAddress common.Address) *types.Transaction { usedType := types.LegacyTxType switch { + case isPrague1 && types.IsPoLDistribution(args.To, args.data(), distributorAddress): + usedType = types.PoLTxType case args.AuthorizationList != nil || defaultType == types.SetCodeTxType: usedType = types.SetCodeTxType case args.BlobHashes != nil || defaultType == types.BlobTxType: @@ -486,6 +489,20 @@ func (args *TransactionArgs) ToTransaction(defaultType int) *types.Transaction { } var data types.TxData switch usedType { + case types.PoLTxType: + gasPrice := (*big.Int)(args.GasPrice) + if args.MaxFeePerGas != nil || defaultType == types.DynamicFeeTxType { + gasPrice = (*big.Int)(args.MaxFeePerGas) + } + data = &types.PoLTx{ + ChainID: (*big.Int)(args.ChainID), + From: args.from(), + To: *args.To, + Nonce: uint64(*args.Nonce), + GasLimit: uint64(*args.Gas), + GasPrice: gasPrice, + Data: args.data(), + } case types.SetCodeTxType: al := types.AccessList{} if args.AccessList != nil { diff --git a/miner/miner.go b/miner/miner.go index ddfe0dcf26..cffb5b8f21 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -140,6 +140,11 @@ func (miner *Miner) BuildPayload(args *BuildPayloadArgs, witness bool) (*Payload // getPending retrieves the pending block based on the current head block. // The result might be nil if pending generation is failed. +// +// Berachain: Note that the correct PoL tx is not included in the pending block as the +// parent proposer pubkey of the pending block is not made available here. +// +// TODO(BRIP-4): Include parent proposer pubkey as soon as it is made available by CL. func (miner *Miner) getPending() *newPayloadResult { header := miner.chain.CurrentHeader() miner.pendingMu.Lock() @@ -156,14 +161,15 @@ func (miner *Miner) getPending() *newPayloadResult { withdrawal = []*types.Withdrawal{} } ret := miner.generateWork(&generateParams{ - timestamp: timestamp, - forceTime: false, - parentHash: header.Hash(), - coinbase: miner.config.PendingFeeRecipient, - random: common.Hash{}, - withdrawals: withdrawal, - beaconRoot: nil, - noTxs: false, + timestamp: timestamp, + forceTime: false, + parentHash: header.Hash(), + coinbase: miner.config.PendingFeeRecipient, + random: common.Hash{}, + withdrawals: withdrawal, + beaconRoot: nil, + proposerPubkey: &common.Pubkey{}, + noTxs: false, }, false) // we will never make a witness for a pending block if ret.err != nil { return nil diff --git a/miner/payload_building.go b/miner/payload_building.go index 6b010186bf..20a47d8959 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -37,13 +37,14 @@ import ( // Check engine-api specification for more details. // https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#payloadattributesv3 type BuildPayloadArgs struct { - Parent common.Hash // The parent block to build payload on top - Timestamp uint64 // The provided timestamp of generated payload - FeeRecipient common.Address // The provided recipient address for collecting transaction fee - Random common.Hash // The provided randomness value - Withdrawals types.Withdrawals // The provided withdrawals - BeaconRoot *common.Hash // The provided beaconRoot (Cancun) - Version engine.PayloadVersion // Versioning byte for payload id calculation. + Parent common.Hash // The parent block to build payload on top + Timestamp uint64 // The provided timestamp of generated payload + FeeRecipient common.Address // The provided recipient address for collecting transaction fee + Random common.Hash // The provided randomness value + Withdrawals types.Withdrawals // The provided withdrawals + BeaconRoot *common.Hash // The provided beaconRoot (Cancun) + ProposerPubkey *common.Pubkey // Berachain BRIP-0004: The provided proposer pubkey (Prague1) + Version engine.PayloadVersion // Versioning byte for payload id calculation. } // Id computes an 8-byte identifier by hashing the components of the payload arguments. @@ -57,6 +58,9 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID { if args.BeaconRoot != nil { hasher.Write(args.BeaconRoot[:]) } + if args.ProposerPubkey != nil { + hasher.Write(args.ProposerPubkey[:]) + } var out engine.PayloadID copy(out[:], hasher.Sum(nil)[:8]) out[0] = byte(args.Version) @@ -211,14 +215,15 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload // enough to run. The empty payload can at least make sure there is something // to deliver for not missing slot. emptyParams := &generateParams{ - timestamp: args.Timestamp, - forceTime: true, - parentHash: args.Parent, - coinbase: args.FeeRecipient, - random: args.Random, - withdrawals: args.Withdrawals, - beaconRoot: args.BeaconRoot, - noTxs: true, + timestamp: args.Timestamp, + forceTime: true, + parentHash: args.Parent, + coinbase: args.FeeRecipient, + random: args.Random, + withdrawals: args.Withdrawals, + beaconRoot: args.BeaconRoot, + proposerPubkey: args.ProposerPubkey, + noTxs: true, } empty := miner.generateWork(emptyParams, witness) if empty.err != nil { @@ -235,20 +240,24 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload timer := time.NewTimer(0) defer timer.Stop() - // Setup the timer for terminating the process if SECONDS_PER_SLOT (12s in - // the Mainnet configuration) have passed since the point in time identified + // Setup the timer for terminating the process if SECONDS_PER_SLOT (2s in + // the Berachain configuration) have passed since the point in time identified // by the timestamp parameter. - endTimer := time.NewTimer(time.Second * 12) + // + // TODO(Berachain): Using 4s for now to handle cases where block time exceeds 2s. To be + // verified further after stable block time is in effect. + endTimer := time.NewTimer(time.Second * 4) fullParams := &generateParams{ - timestamp: args.Timestamp, - forceTime: true, - parentHash: args.Parent, - coinbase: args.FeeRecipient, - random: args.Random, - withdrawals: args.Withdrawals, - beaconRoot: args.BeaconRoot, - noTxs: false, + timestamp: args.Timestamp, + forceTime: true, + parentHash: args.Parent, + coinbase: args.FeeRecipient, + random: args.Random, + withdrawals: args.Withdrawals, + beaconRoot: args.BeaconRoot, + proposerPubkey: args.ProposerPubkey, + noTxs: false, } for { diff --git a/miner/worker.go b/miner/worker.go index 198745ad27..cab6eed3f9 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -84,14 +84,15 @@ type newPayloadResult struct { // generateParams wraps various settings for generating sealing task. type generateParams struct { - timestamp uint64 // The timestamp for sealing task - forceTime bool // Flag whether the given timestamp is immutable or not - parentHash common.Hash // Parent block hash, empty means the latest chain head - coinbase common.Address // The fee recipient address for including transaction - random common.Hash // The randomness generated by beacon chain, empty before the merge - withdrawals types.Withdrawals // List of withdrawals to include in block (shanghai field) - beaconRoot *common.Hash // The beacon root (cancun field). - noTxs bool // Flag whether an empty block without any transaction is expected + timestamp uint64 // The timestamp for sealing task + forceTime bool // Flag whether the given timestamp is immutable or not + parentHash common.Hash // Parent block hash, empty means the latest chain head + coinbase common.Address // The fee recipient address for including transaction + random common.Hash // The randomness generated by beacon chain, empty before the merge + withdrawals types.Withdrawals // List of withdrawals to include in block (shanghai field) + beaconRoot *common.Hash // The beacon root (cancun field). + proposerPubkey *common.Pubkey // Berachain: The proposer pubkey (prague1 field). + noTxs bool // Flag whether an empty block without any transaction is expected } // generateWork generates a sealing block based on the given parameters. @@ -221,6 +222,10 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir header.ExcessBlobGas = &excessBlobGas header.ParentBeaconRoot = genParams.beaconRoot } + // Berachain: Apply BRIP-0004. + if miner.chainConfig.IsPrague1(header.Number, header.Time) { + header.ParentProposerPubkey = genParams.proposerPubkey + } // Could potentially happen if starting to mine in an odd state. // Note genParams.coinbase can be different with header.Coinbase // since clique algorithm can modify the coinbase field in header. @@ -306,13 +311,23 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio // applyTransaction runs the transaction. If execution fails, state and gas pool are reverted. func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { var ( - snap = env.state.Snapshot() - gp = env.gasPool.Gas() + gasPool = env.gasPool + blockGasUsed = &env.header.GasUsed + snap = env.state.Snapshot() + gasBefore uint64 ) - receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx, &env.header.GasUsed) + // Berachain: PoL tx does not consume gas. + if tx.Type() == types.PoLTxType { + gasPool = nil + blockGasUsed = new(uint64) + } + if gasPool != nil { + gasBefore = gasPool.Gas() + } + receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx, blockGasUsed) if err != nil { env.state.RevertToSnapshot(snap) - env.gasPool.SetGas(gp) + env.gasPool.SetGas(gasBefore) } return receipt, err } @@ -452,6 +467,29 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment) prio := miner.prio miner.confMu.RUnlock() + // Berachain: Post-Prague1, add PoL tx to the block according to BRIP-0004. + if miner.chainConfig.IsPrague1(env.header.Number, env.header.Time) { + tx, err := types.NewPoLTx( + miner.chainConfig.ChainID, + miner.chainConfig.Berachain.Prague1.PoLDistributorAddress, + new(big.Int).Sub(env.header.Number, big.NewInt(1)), + params.PoLTxGasLimit, + env.header.BaseFee, + env.header.ParentProposerPubkey, + ) + if err != nil { + return fmt.Errorf("failed to create PoL tx: %v", err) + } + + // Start executing the PoL transaction, outside of miner commitTransactions loop. + // env.tcount must be at 0 at this point. + env.state.SetTxContext(tx.Hash(), env.tcount) + if err = miner.commitTransaction(env, tx); err != nil { + log.Trace("Failed to commit PoL transaction", "hash", tx.Hash(), "err", err) + return err + } + } + // Retrieve the pending transactions pre-filtered by the 1559/4844 dynamic fees filter := txpool.PendingFilter{ MinTip: uint256.MustFromBig(tip), diff --git a/params/config.go b/params/config.go index 9f80fabae3..b5959f2833 100644 --- a/params/config.go +++ b/params/config.go @@ -548,6 +548,8 @@ type Prague1Config struct { BaseFeeChangeDenominator uint64 `json:"baseFeeChangeDenominator,omitempty"` // MinimumBaseFeeWei is the minimum base fee in wei. MinimumBaseFeeWei uint64 `json:"minimumBaseFeeWei,omitempty"` + // PoLDistributorAddress is the address of the PoL distributor. + PoLDistributorAddress common.Address `json:"polDistributorAddress,omitempty"` } // String implements the stringer interface. @@ -555,8 +557,8 @@ func (c Prague1Config) String() string { banner := "prague1" if c.Time != nil { banner += fmt.Sprintf( - "(time: %v, baseFeeChangeDenominator: %v, minimumBaseFeeWei: %v)", - *c.Time, c.BaseFeeChangeDenominator, c.MinimumBaseFeeWei, + "(time: %v, baseFeeChangeDenominator: %v, minimumBaseFeeWei: %v, polDistributorAddress: %v)", + *c.Time, c.BaseFeeChangeDenominator, c.MinimumBaseFeeWei, c.PoLDistributorAddress, ) } return banner @@ -1042,6 +1044,8 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork { switch { case c.IsOsaka(london, time): return forks.Osaka + case c.IsPrague1(london, time): + return forks.Prague1 case c.IsPrague(london, time): return forks.Prague case c.IsCancun(london, time): @@ -1059,6 +1063,8 @@ func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { switch { case fork == forks.Osaka: return c.OsakaTime + case fork == forks.Prague1: + return c.Berachain.Prague1.Time case fork == forks.Prague: return c.PragueTime case fork == forks.Cancun: @@ -1205,13 +1211,13 @@ func (err *ConfigCompatError) Error() string { // Rules is a one time interface meaning that it shouldn't be used in between transition // phases. type Rules struct { - ChainID *big.Int - IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool - IsEIP2929, IsEIP4762 bool - IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool - IsBerlin, IsLondon bool - IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool - IsVerkle bool + ChainID *big.Int + IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool + IsEIP2929, IsEIP4762 bool + IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool + IsBerlin, IsLondon bool + IsMerge, IsShanghai, IsCancun, IsPrague, IsPrague1, IsOsaka bool + IsVerkle bool } // Rules ensures c's ChainID is not nil. @@ -1240,6 +1246,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules IsShanghai: isMerge && c.IsShanghai(num, timestamp), IsCancun: isMerge && c.IsCancun(num, timestamp), IsPrague: isMerge && c.IsPrague(num, timestamp), + IsPrague1: isMerge && c.IsPrague1(num, timestamp), IsOsaka: isMerge && c.IsOsaka(num, timestamp), IsVerkle: isVerkle, IsEIP4762: isVerkle, diff --git a/params/protocol_params.go b/params/protocol_params.go index 6b06dadaef..b8d2bfc357 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -193,7 +193,7 @@ var ( // System contracts. var ( - // SystemAddress is where the system-transaction is sent from as per EIP-4788 + // SystemAddress is where the system-transaction is sent from as per EIP-4788 and BRIP-0004. SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe") // EIP-4788 - Beacon block root in the EVM @@ -211,4 +211,7 @@ var ( // EIP-7251 - Increase the MAX_EFFECTIVE_BALANCE ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251") ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd") + + // BRIP-0004 - PoL Distributor + PoLTxGasLimit uint64 = 30_000_000 ) diff --git a/tests/block_test_util.go b/tests/block_test_util.go index a76037405b..1ac361a6fb 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -97,6 +97,7 @@ type btHeader struct { BlobGasUsed *uint64 ExcessBlobGas *uint64 ParentBeaconBlockRoot *common.Hash + ParentProposerPubkey *common.Pubkey } type btHeaderMarshaling struct { @@ -336,6 +337,9 @@ func validateHeader(h *btHeader, h2 *types.Header) error { if !reflect.DeepEqual(h.ParentBeaconBlockRoot, h2.ParentBeaconRoot) { return fmt.Errorf("parentBeaconBlockRoot: want: %v have: %v", h.ParentBeaconBlockRoot, h2.ParentBeaconRoot) } + if !reflect.DeepEqual(h.ParentProposerPubkey, h2.ParentProposerPubkey) { + return fmt.Errorf("parentProposerPubkey: want: %v have: %v", h.ParentProposerPubkey, h2.ParentProposerPubkey) + } return nil } diff --git a/tests/gen_btheader.go b/tests/gen_btheader.go index 80ad89e03b..339d2d1e08 100644 --- a/tests/gen_btheader.go +++ b/tests/gen_btheader.go @@ -38,6 +38,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash + ParentProposerPubkey *common.Pubkey } var enc btHeader enc.Bloom = b.Bloom @@ -61,6 +62,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*math.HexOrDecimal64)(b.BlobGasUsed) enc.ExcessBlobGas = (*math.HexOrDecimal64)(b.ExcessBlobGas) enc.ParentBeaconBlockRoot = b.ParentBeaconBlockRoot + enc.ParentProposerPubkey = b.ParentProposerPubkey return json.Marshal(&enc) } @@ -88,6 +90,7 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash + ParentProposerPubkey *common.Pubkey } var dec btHeader if err := json.Unmarshal(input, &dec); err != nil { @@ -156,5 +159,8 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { b.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.ParentProposerPubkey != nil { + b.ParentProposerPubkey = dec.ParentProposerPubkey + } return nil }