Commit ce8d940f authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

Merge pull request #8131 from ethereum-optimism/l1-dencun-support

op-chain-ops. op-e2e: L1 dencun support
parents 78bd8690 f5b55a13
...@@ -70,6 +70,8 @@ func NewBackendWithGenesisTimestamp(ts uint64, shanghai bool) *backends.Simulate ...@@ -70,6 +70,8 @@ func NewBackendWithGenesisTimestamp(ts uint64, shanghai bool) *backends.Simulate
LondonBlock: big.NewInt(0), LondonBlock: big.NewInt(0),
ArrowGlacierBlock: big.NewInt(0), ArrowGlacierBlock: big.NewInt(0),
GrayGlacierBlock: big.NewInt(0), GrayGlacierBlock: big.NewInt(0),
ShanghaiTime: nil,
CancunTime: nil,
// Activated proof of stake. We manually build/commit blocks in the simulator anyway, // Activated proof of stake. We manually build/commit blocks in the simulator anyway,
// and the timestamp verification of PoS is not against the wallclock, // and the timestamp verification of PoS is not against the wallclock,
// preventing blocks from getting stuck temporarily in the future-blocks queue, decreasing setup time a lot. // preventing blocks from getting stuck temporarily in the future-blocks queue, decreasing setup time a lot.
......
...@@ -217,6 +217,9 @@ type DeployConfig struct { ...@@ -217,6 +217,9 @@ type DeployConfig struct {
// RequiredProtocolVersion indicates the protocol version that // RequiredProtocolVersion indicates the protocol version that
// nodes are recommended to adopt, to stay in sync with the network. // nodes are recommended to adopt, to stay in sync with the network.
RecommendedProtocolVersion params.ProtocolVersion `json:"recommendedProtocolVersion"` RecommendedProtocolVersion params.ProtocolVersion `json:"recommendedProtocolVersion"`
// When Cancun activates. Relative to L1 genesis.
L1CancunTimeOffset *uint64 `json:"l1CancunTimeOffset,omitempty"`
} }
// Copy will deeply copy the DeployConfig. This does a JSON roundtrip to copy // Copy will deeply copy the DeployConfig. This does a JSON roundtrip to copy
......
...@@ -64,6 +64,7 @@ func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, erro ...@@ -64,6 +64,7 @@ func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, erro
RegolithTime: config.RegolithTime(block.Time()), RegolithTime: config.RegolithTime(block.Time()),
CanyonTime: config.CanyonTime(block.Time()), CanyonTime: config.CanyonTime(block.Time()),
ShanghaiTime: config.CanyonTime(block.Time()), ShanghaiTime: config.CanyonTime(block.Time()),
CancunTime: nil, // no Dencun on L2 yet.
Optimism: &params.OptimismConfig{ Optimism: &params.OptimismConfig{
EIP1559Denominator: eip1559Denom, EIP1559Denominator: eip1559Denom,
EIP1559Elasticity: eip1559Elasticity, EIP1559Elasticity: eip1559Elasticity,
...@@ -134,6 +135,8 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -134,6 +135,8 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) {
LondonBlock: big.NewInt(0), LondonBlock: big.NewInt(0),
ArrowGlacierBlock: big.NewInt(0), ArrowGlacierBlock: big.NewInt(0),
GrayGlacierBlock: big.NewInt(0), GrayGlacierBlock: big.NewInt(0),
ShanghaiTime: nil,
CancunTime: nil,
} }
extraData := make([]byte, 0) extraData := make([]byte, 0)
...@@ -168,6 +171,10 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -168,6 +171,10 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) {
if timestamp == 0 { if timestamp == 0 {
timestamp = hexutil.Uint64(time.Now().Unix()) timestamp = hexutil.Uint64(time.Now().Unix())
} }
if !config.L1UseClique && config.L1CancunTimeOffset != nil {
cancunTime := uint64(timestamp) + *config.L1CancunTimeOffset
chainConfig.CancunTime = &cancunTime
}
return &core.Genesis{ return &core.Genesis{
Config: &chainConfig, Config: &chainConfig,
......
...@@ -14,14 +14,15 @@ import ( ...@@ -14,14 +14,15 @@ import (
func TestDencunL1Fork(gt *testing.T) { func TestDencunL1Fork(gt *testing.T) {
t := NewDefaultTesting(gt) t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams) dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
offset := uint64(24)
dp.DeployConfig.L1CancunTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc) sd := e2eutils.Setup(t, dp, defaultAlloc)
activation := sd.L1Cfg.Timestamp + 24
sd.L1Cfg.Config.CancunTime = &activation
log := testlog.Logger(t, log.LvlDebug) log := testlog.Logger(t, log.LvlDebug)
_, _, miner, sequencer, _, verifier, _, batcher := setupReorgTestActors(t, dp, sd, log) _, _, miner, sequencer, _, verifier, _, batcher := setupReorgTestActors(t, dp, sd, log)
l1Head := miner.l1Chain.CurrentBlock() l1Head := miner.l1Chain.CurrentBlock()
require.False(t, sd.L1Cfg.Config.IsCancun(l1Head.Number, l1Head.Time), "Cancun not active yet") require.False(t, sd.L1Cfg.Config.IsCancun(l1Head.Number, l1Head.Time), "Cancun not active yet")
require.Nil(t, l1Head.ExcessBlobGas, "Cancun blob gas not in header")
// start op-nodes // start op-nodes
sequencer.ActL2PipelineFull(t) sequencer.ActL2PipelineFull(t)
...@@ -35,6 +36,7 @@ func TestDencunL1Fork(gt *testing.T) { ...@@ -35,6 +36,7 @@ func TestDencunL1Fork(gt *testing.T) {
// verify Cancun is active // verify Cancun is active
l1Head = miner.l1Chain.CurrentBlock() l1Head = miner.l1Chain.CurrentBlock()
require.True(t, sd.L1Cfg.Config.IsCancun(l1Head.Number, l1Head.Time), "Cancun active") require.True(t, sd.L1Cfg.Config.IsCancun(l1Head.Number, l1Head.Time), "Cancun active")
require.NotNil(t, l1Head.ExcessBlobGas, "Cancun blob gas in header")
// build L2 chain up to and including L2 blocks referencing Cancun L1 blocks // build L2 chain up to and including L2 blocks referencing Cancun L1 blocks
sequencer.ActL1HeadSignal(t) sequencer.ActL1HeadSignal(t)
...@@ -51,3 +53,47 @@ func TestDencunL1Fork(gt *testing.T) { ...@@ -51,3 +53,47 @@ func TestDencunL1Fork(gt *testing.T) {
require.Equal(t, l1Head.Hash(), verifier.SyncStatus().SafeL2.L1Origin.Hash, "verifier synced L1 chain that includes Cancun headers") require.Equal(t, l1Head.Hash(), verifier.SyncStatus().SafeL2.L1Origin.Hash, "verifier synced L1 chain that includes Cancun headers")
require.Equal(t, sequencer.SyncStatus().UnsafeL2, verifier.SyncStatus().UnsafeL2, "verifier and sequencer agree") require.Equal(t, sequencer.SyncStatus().UnsafeL2, verifier.SyncStatus().UnsafeL2, "verifier and sequencer agree")
} }
func TestDencunL1ForkAtGenesis(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
offset := uint64(0)
dp.DeployConfig.L1CancunTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
_, _, miner, sequencer, _, verifier, _, batcher := setupReorgTestActors(t, dp, sd, log)
l1Head := miner.l1Chain.CurrentBlock()
require.True(t, sd.L1Cfg.Config.IsCancun(l1Head.Number, l1Head.Time), "Cancun active at genesis")
require.NotNil(t, l1Head.ExcessBlobGas, "Cancun blob gas in header")
// start op-nodes
sequencer.ActL2PipelineFull(t)
verifier.ActL2PipelineFull(t)
// build empty L1 blocks
miner.ActL1SetFeeRecipient(common.Address{'A', 0})
miner.ActEmptyBlock(t)
miner.ActEmptyBlock(t)
// verify Cancun is still active
l1Head = miner.l1Chain.CurrentBlock()
require.True(t, sd.L1Cfg.Config.IsCancun(l1Head.Number, l1Head.Time), "Cancun active")
require.NotNil(t, l1Head.ExcessBlobGas, "Cancun blob gas in header")
// build L2 chain
sequencer.ActL1HeadSignal(t)
sequencer.ActBuildToL1Head(t)
miner.ActL1StartBlock(12)(t)
batcher.ActSubmitAll(t)
miner.ActL1IncludeTx(batcher.batcherAddr)(t)
miner.ActL1EndBlock(t)
// sync verifier
verifier.ActL1HeadSignal(t)
verifier.ActL2PipelineFull(t)
// verify verifier accepted Cancun L1 inputs
require.Equal(t, l1Head.Hash(), verifier.SyncStatus().SafeL2.L1Origin.Hash, "verifier synced L1 chain that includes Cancun headers")
require.Equal(t, sequencer.SyncStatus().UnsafeL2, verifier.SyncStatus().UnsafeL2, "verifier and sequencer agree")
}
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"errors" "errors"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
...@@ -51,6 +52,11 @@ func NewL1Replica(t Testing, log log.Logger, genesis *core.Genesis) *L1Replica { ...@@ -51,6 +52,11 @@ func NewL1Replica(t Testing, log log.Logger, genesis *core.Genesis) *L1Replica {
NetworkId: genesis.Config.ChainID.Uint64(), NetworkId: genesis.Config.ChainID.Uint64(),
Genesis: genesis, Genesis: genesis,
RollupDisableTxPoolGossip: true, RollupDisableTxPoolGossip: true,
BlobPool: blobpool.Config{
Datadir: t.TempDir(),
Datacap: blobpool.DefaultConfig.Datacap,
PriceBump: blobpool.DefaultConfig.PriceBump,
},
} }
nodeCfg := &node.Config{ nodeCfg := &node.Config{
Name: "l1-geth", Name: "l1-geth",
......
package fakebeacon
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/log"
)
// FakeBeacon presents a beacon-node in testing, without leading any chain-building.
// This merely serves a fake beacon API, and holds on to blocks,
// to complement the actual block-building to happen in testing (e.g. through the fake consensus geth module).
type FakeBeacon struct {
log log.Logger
// directory to store blob contents in after the blobs are persisted in a block
blobsDir string
blobsLock sync.Mutex
beaconSrv *http.Server
beaconAPIListener net.Listener
genesisTime uint64
blockTime uint64
}
func NewBeacon(log log.Logger, blobsDir string, genesisTime uint64, blockTime uint64) *FakeBeacon {
return &FakeBeacon{
log: log,
blobsDir: blobsDir,
genesisTime: genesisTime,
blockTime: blockTime,
}
}
func (f *FakeBeacon) Start(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to open tcp listener for http beacon api server: %w", err)
}
f.beaconAPIListener = listener
mux := new(http.ServeMux)
mux.HandleFunc("/eth/v1/beacon/genesis", func(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(&eth.APIGenesisResponse{Data: eth.ReducedGenesisData{GenesisTime: eth.Uint64String(f.genesisTime)}})
if err != nil {
f.log.Error("genesis handler err", "err", err)
}
})
mux.HandleFunc("/eth/v1/config/spec", func(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(&eth.APIConfigResponse{Data: eth.ReducedConfigData{SecondsPerSlot: eth.Uint64String(f.blockTime)}})
if err != nil {
f.log.Error("config handler err", "err", err)
}
})
mux.HandleFunc("/eth/v1/beacon/blob_sidecars/", func(w http.ResponseWriter, r *http.Request) {
blockID := strings.TrimPrefix(r.URL.Path, "/eth/v1/beacon/blob_sidecars/")
slot, err := strconv.ParseUint(blockID, 10, 64)
if err != nil {
f.log.Error("could not parse block id from request", "url", r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}
bundle, err := f.LoadBlobsBundle(slot)
if err != nil {
f.log.Error("failed to load blobs bundle", "slot", slot)
w.WriteHeader(http.StatusInternalServerError)
return
}
query := r.URL.Query()
rawIndices := query["indices"]
indices := make([]int, 0, len(bundle.Blobs))
if len(rawIndices) == 0 {
// request is for all blobs
for i := range bundle.Blobs {
indices = append(indices, i)
}
} else {
for _, raw := range rawIndices {
ix, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
f.log.Error("could not parse index from request", "url", r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
indices = append(indices, int(ix))
}
}
var mockBeaconBlockRoot [32]byte
mockBeaconBlockRoot[0] = 42
binary.LittleEndian.PutUint64(mockBeaconBlockRoot[32-8:], slot)
sidecars := make([]*eth.BlobSidecar, len(indices))
for i, ix := range indices {
if ix < 0 || ix >= len(bundle.Blobs) {
f.log.Error("blob index from request is out of range", "url", r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
sidecars[i] = &eth.BlobSidecar{
BlockRoot: mockBeaconBlockRoot,
Slot: eth.Uint64String(slot),
Index: eth.Uint64String(i),
KZGCommitment: eth.Bytes48(bundle.Commitments[ix]),
KZGProof: eth.Bytes48(bundle.Proofs[ix]),
}
copy(sidecars[i].Blob[:], bundle.Blobs[ix])
}
if err := json.NewEncoder(w).Encode(&eth.APIGetBlobSidecarsResponse{Data: sidecars}); err != nil {
f.log.Error("blobs handler err", "err", err)
}
})
f.beaconSrv = &http.Server{
Handler: mux,
ReadTimeout: time.Second * 20,
ReadHeaderTimeout: time.Second * 20,
WriteTimeout: time.Second * 20,
IdleTimeout: time.Second * 20,
}
go func() {
if err := f.beaconSrv.Serve(f.beaconAPIListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
f.log.Error("failed to start fake-pos beacon server for blobs testing", "err", err)
}
}()
return nil
}
func (f *FakeBeacon) StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1) error {
data, err := json.Marshal(bundle)
if err != nil {
return fmt.Errorf("failed to encode blobs bundle of slot %d: %w", slot, err)
}
f.blobsLock.Lock()
defer f.blobsLock.Unlock()
bundlePath := fmt.Sprintf("blobs_bundle_%d.json", slot)
if err := os.MkdirAll(f.blobsDir, 0755); err != nil {
return fmt.Errorf("failed to create dir for blob storage: %w", err)
}
err = os.WriteFile(filepath.Join(f.blobsDir, bundlePath), data, 0755)
if err != nil {
return fmt.Errorf("failed to write blobs bundle of slot %d: %w", slot, err)
}
return nil
}
func (f *FakeBeacon) LoadBlobsBundle(slot uint64) (*engine.BlobsBundleV1, error) {
f.blobsLock.Lock()
defer f.blobsLock.Unlock()
bundlePath := fmt.Sprintf("blobs_bundle_%d.json", slot)
data, err := os.ReadFile(filepath.Join(f.blobsDir, bundlePath))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("no blobs bundle found for slot %d (%q): %w", slot, bundlePath, ethereum.NotFound)
} else {
return nil, fmt.Errorf("failed to read blobs bundle of slot %d (%q): %w", slot, bundlePath, err)
}
}
var out engine.BlobsBundleV1
if err := json.Unmarshal(data, &out); err != nil {
return nil, fmt.Errorf("failed to decode blobs bundle of slot %d (%q): %w", slot, bundlePath, err)
}
return &out, nil
}
func (f *FakeBeacon) Close() error {
var out error
if f.beaconSrv != nil {
out = errors.Join(out, f.beaconSrv.Close())
}
if f.beaconAPIListener != nil {
out = errors.Join(out, f.beaconAPIListener.Close())
}
return out
}
func (f *FakeBeacon) BeaconAddr() string {
return "http://" + f.beaconAPIListener.Addr().String()
}
package geth package geth
import ( import (
"encoding/binary"
"math/big"
"math/rand" "math/rand"
"time" "time"
...@@ -8,15 +10,21 @@ import ( ...@@ -8,15 +10,21 @@ import (
"github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
opeth "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-service/testutils"
) )
type Beacon interface {
StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1) error
}
// fakePoS is a testing-only utility to attach to Geth, // fakePoS is a testing-only utility to attach to Geth,
// to build a fake proof-of-stake L1 chain with fixed block time and basic lagging safe/finalized blocks. // to build a fake proof-of-stake L1 chain with fixed block time and basic lagging safe/finalized blocks.
type fakePoS struct { type fakePoS struct {
...@@ -32,6 +40,14 @@ type fakePoS struct { ...@@ -32,6 +40,14 @@ type fakePoS struct {
engineAPI *catalyst.ConsensusAPI engineAPI *catalyst.ConsensusAPI
sub ethereum.Subscription sub ethereum.Subscription
beacon Beacon
}
func (f *fakePoS) FakeBeaconBlockRoot(time uint64) common.Hash {
var dat [8]byte
binary.LittleEndian.PutUint64(dat[:], time)
return crypto.Keccak256Hash(dat[:])
} }
func (f *fakePoS) Start() error { func (f *fakePoS) Start() error {
...@@ -81,16 +97,29 @@ func (f *fakePoS) Start() error { ...@@ -81,16 +97,29 @@ func (f *fakePoS) Start() error {
Amount: uint64(withdrawalsRNG.Intn(50_000_000_000) + 1), Amount: uint64(withdrawalsRNG.Intn(50_000_000_000) + 1),
} }
} }
res, err := f.engineAPI.ForkchoiceUpdatedV2(engine.ForkchoiceStateV1{ attrs := &engine.PayloadAttributes{
HeadBlockHash: head.Hash(),
SafeBlockHash: safe.Hash(),
FinalizedBlockHash: finalized.Hash(),
}, &engine.PayloadAttributes{
Timestamp: newBlockTime, Timestamp: newBlockTime,
Random: common.Hash{}, Random: common.Hash{},
SuggestedFeeRecipient: head.Coinbase, SuggestedFeeRecipient: head.Coinbase,
Withdrawals: withdrawals, Withdrawals: withdrawals,
}) }
parentBeaconBlockRoot := f.FakeBeaconBlockRoot(head.Time) // parent beacon block root
isCancun := f.eth.BlockChain().Config().IsCancun(new(big.Int).SetUint64(head.Number.Uint64()+1), newBlockTime)
if isCancun {
attrs.BeaconRoot = &parentBeaconBlockRoot
}
fcState := engine.ForkchoiceStateV1{
HeadBlockHash: head.Hash(),
SafeBlockHash: safe.Hash(),
FinalizedBlockHash: finalized.Hash(),
}
var err error
var res engine.ForkChoiceResponse
if isCancun {
res, err = f.engineAPI.ForkchoiceUpdatedV3(fcState, attrs)
} else {
res, err = f.engineAPI.ForkchoiceUpdatedV2(fcState, attrs)
}
if err != nil { if err != nil {
f.log.Error("failed to start building L1 block", "err", err) f.log.Error("failed to start building L1 block", "err", err)
continue continue
...@@ -114,10 +143,41 @@ func (f *fakePoS) Start() error { ...@@ -114,10 +143,41 @@ func (f *fakePoS) Start() error {
f.log.Error("failed to finish building L1 block", "err", err) f.log.Error("failed to finish building L1 block", "err", err)
continue continue
} }
if _, err := f.engineAPI.NewPayloadV2(*envelope.ExecutionPayload); err != nil {
f.log.Error("failed to insert built L1 block", "err", err) blobHashes := make([]common.Hash, 0) // must be non-nil even when empty, due to geth engine API checks
for _, commitment := range envelope.BlobsBundle.Commitments {
if len(commitment) != 48 {
f.log.Error("got malformed kzg commitment from engine", "commitment", commitment)
break
}
blobHashes = append(blobHashes, opeth.KZGToVersionedHash(*(*[48]byte)(commitment)))
}
if len(blobHashes) != len(envelope.BlobsBundle.Commitments) {
f.log.Error("invalid or incomplete blob data", "collected", len(blobHashes), "engine", len(envelope.BlobsBundle.Commitments))
continue continue
} }
if isCancun {
if _, err := f.engineAPI.NewPayloadV3(*envelope.ExecutionPayload, blobHashes, &parentBeaconBlockRoot); err != nil {
f.log.Error("failed to insert built L1 block", "err", err)
continue
}
} else {
if _, err := f.engineAPI.NewPayloadV2(*envelope.ExecutionPayload); err != nil {
f.log.Error("failed to insert built L1 block", "err", err)
continue
}
}
if envelope.BlobsBundle != nil {
slot := (envelope.ExecutionPayload.Timestamp - f.eth.BlockChain().Genesis().Time()) / f.blockTime
if f.beacon == nil {
f.log.Error("no blobs storage available")
continue
}
if err := f.beacon.StoreBlobsBundle(slot, envelope.BlobsBundle); err != nil {
f.log.Error("failed to persist blobs-bundle of block, not making block canonical now", "err", err)
continue
}
}
if _, err := f.engineAPI.ForkchoiceUpdatedV2(engine.ForkchoiceStateV1{ if _, err := f.engineAPI.ForkchoiceUpdatedV2(engine.ForkchoiceStateV1{
HeadBlockHash: envelope.ExecutionPayload.BlockHash, HeadBlockHash: envelope.ExecutionPayload.BlockHash,
SafeBlockHash: safe.Hash(), SafeBlockHash: safe.Hash(),
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
...@@ -21,10 +22,15 @@ import ( ...@@ -21,10 +22,15 @@ import (
_ "github.com/ethereum/go-ethereum/eth/tracers/native" _ "github.com/ethereum/go-ethereum/eth/tracers/native"
) )
func InitL1(chainID uint64, blockTime uint64, genesis *core.Genesis, c clock.Clock, opts ...GethOption) (*node.Node, *eth.Ethereum, error) { func InitL1(chainID uint64, blockTime uint64, genesis *core.Genesis, c clock.Clock, blobPoolDir string, beaconSrv Beacon, opts ...GethOption) (*node.Node, *eth.Ethereum, error) {
ethConfig := &ethconfig.Config{ ethConfig := &ethconfig.Config{
NetworkId: chainID, NetworkId: chainID,
Genesis: genesis, Genesis: genesis,
BlobPool: blobpool.Config{
Datadir: blobPoolDir,
Datacap: blobpool.DefaultConfig.Datacap,
PriceBump: blobpool.DefaultConfig.PriceBump,
},
} }
nodeConfig := &node.Config{ nodeConfig := &node.Config{
Name: "l1-geth", Name: "l1-geth",
...@@ -53,6 +59,7 @@ func InitL1(chainID uint64, blockTime uint64, genesis *core.Genesis, c clock.Clo ...@@ -53,6 +59,7 @@ func InitL1(chainID uint64, blockTime uint64, genesis *core.Genesis, c clock.Clo
finalizedDistance: 8, finalizedDistance: 8,
safeDistance: 4, safeDistance: 4,
engineAPI: catalyst.NewConsensusAPI(l1Eth), engineAPI: catalyst.NewConsensusAPI(l1Eth),
beacon: beaconSrv,
}) })
return l1Node, l1Eth, nil return l1Node, l1Eth, nil
......
...@@ -24,6 +24,8 @@ type ExternalRunner struct { ...@@ -24,6 +24,8 @@ type ExternalRunner struct {
BinPath string BinPath string
Genesis *core.Genesis Genesis *core.Genesis
JWTPath string JWTPath string
// 4844: a datadir specifically for tx-pool blobs
BlobPoolPath string
} }
type ExternalEthClient struct { type ExternalEthClient struct {
......
...@@ -42,6 +42,7 @@ import ( ...@@ -42,6 +42,7 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-e2e/config" "github.com/ethereum-optimism/optimism/op-e2e/config"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/fakebeacon"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-node/chaincfg" "github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/metrics"
...@@ -110,6 +111,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -110,6 +111,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
L1InfoPredeployAddress: predeploys.L1BlockAddr, L1InfoPredeployAddress: predeploys.L1BlockAddr,
JWTFilePath: writeDefaultJWT(t), JWTFilePath: writeDefaultJWT(t),
JWTSecret: testingJWTSecret, JWTSecret: testingJWTSecret,
BlobsPath: t.TempDir(),
Nodes: map[string]*rollupNode.Config{ Nodes: map[string]*rollupNode.Config{
"sequencer": { "sequencer": {
Driver: driver.Config{ Driver: driver.Config{
...@@ -176,6 +178,8 @@ type SystemConfig struct { ...@@ -176,6 +178,8 @@ type SystemConfig struct {
JWTFilePath string JWTFilePath string
JWTSecret [32]byte JWTSecret [32]byte
BlobsPath string
Premine map[common.Address]*big.Int Premine map[common.Address]*big.Int
Nodes map[string]*rollupNode.Config // Per node config. Don't use populate rollup.Config Nodes map[string]*rollupNode.Config // Per node config. Don't use populate rollup.Config
Loggers map[string]log.Logger Loggers map[string]log.Logger
...@@ -260,6 +264,8 @@ type System struct { ...@@ -260,6 +264,8 @@ type System struct {
BatchSubmitter *bss.BatcherService BatchSubmitter *bss.BatcherService
Mocknet mocknet.Mocknet Mocknet mocknet.Mocknet
L1BeaconAPIAddr string
// TimeTravelClock is nil unless SystemConfig.SupportL1TimeTravel was set to true // TimeTravelClock is nil unless SystemConfig.SupportL1TimeTravel was set to true
// It provides access to the clock instance used by the L1 node. Calling TimeTravelClock.AdvanceBy // It provides access to the clock instance used by the L1 node. Calling TimeTravelClock.AdvanceBy
// allows tests to quickly time travel L1 into the future. // allows tests to quickly time travel L1 into the future.
...@@ -438,8 +444,19 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste ...@@ -438,8 +444,19 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
} }
sys.RollupConfig = &defaultConfig sys.RollupConfig = &defaultConfig
// Create a fake Beacon node to hold on to blobs created by the L1 miner, and to serve them to L2
bcn := fakebeacon.NewBeacon(testlog.Logger(t, log.LvlInfo).New("role", "l1_cl"),
path.Join(cfg.BlobsPath, "l1_cl"), l1Genesis.Timestamp, cfg.DeployConfig.L1BlockTime)
t.Cleanup(func() {
_ = bcn.Close()
})
require.NoError(t, bcn.Start("127.0.0.1:0"))
beaconApiAddr := bcn.BeaconAddr()
require.NotEmpty(t, beaconApiAddr, "beacon API listener must be up")
// Initialize nodes // Initialize nodes
l1Node, l1Backend, err := geth.InitL1(cfg.DeployConfig.L1ChainID, cfg.DeployConfig.L1BlockTime, l1Genesis, c, cfg.GethOptions["l1"]...) l1Node, l1Backend, err := geth.InitL1(cfg.DeployConfig.L1ChainID, cfg.DeployConfig.L1BlockTime, l1Genesis, c,
path.Join(cfg.BlobsPath, "l1_el"), bcn, cfg.GethOptions["l1"]...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -155,6 +155,22 @@ func TestL2OutputSubmitter(t *testing.T) { ...@@ -155,6 +155,22 @@ func TestL2OutputSubmitter(t *testing.T) {
} }
} }
func TestSystemE2EDencunAtGenesis(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
genesisActivation := uint64(0)
cfg.DeployConfig.L1CancunTimeOffset = &genesisActivation
sys, err := cfg.Start(t)
require.Nil(t, err, "Error starting up system")
defer sys.Close()
runE2ESystemTest(t, sys)
head, err := sys.Clients["l1"].BlockByNumber(context.Background(), big.NewInt(0))
require.NoError(t, err)
require.NotNil(t, head.ExcessBlobGas(), "L1 is building dencun blocks since genesis")
}
// TestSystemE2E sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that L1 deposits are reflected on L2. // TestSystemE2E sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that L1 deposits are reflected on L2.
// All nodes are run in process (but are the full nodes, not mocked or stubbed). // All nodes are run in process (but are the full nodes, not mocked or stubbed).
func TestSystemE2E(t *testing.T) { func TestSystemE2E(t *testing.T) {
...@@ -165,7 +181,9 @@ func TestSystemE2E(t *testing.T) { ...@@ -165,7 +181,9 @@ func TestSystemE2E(t *testing.T) {
sys, err := cfg.Start(t) sys, err := cfg.Start(t)
require.Nil(t, err, "Error starting up system") require.Nil(t, err, "Error starting up system")
defer sys.Close() defer sys.Close()
}
func runE2ESystemTest(t *testing.T, sys *System) {
log := testlog.Logger(t, log.LvlInfo) log := testlog.Logger(t, log.LvlInfo)
log.Info("genesis", "l2", sys.RollupConfig.Genesis.L2, "l1", sys.RollupConfig.Genesis.L1, "l2_time", sys.RollupConfig.Genesis.L2Time) log.Info("genesis", "l2", sys.RollupConfig.Genesis.L2, "l1", sys.RollupConfig.Genesis.L1, "l2_time", sys.RollupConfig.Genesis.L2Time)
...@@ -185,11 +203,11 @@ func TestSystemE2E(t *testing.T) { ...@@ -185,11 +203,11 @@ func TestSystemE2E(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
// Send deposit transaction // Send deposit transaction
opts, err := bind.NewKeyedTransactorWithChainID(ethPrivKey, cfg.L1ChainIDBig()) opts, err := bind.NewKeyedTransactorWithChainID(ethPrivKey, sys.cfg.L1ChainIDBig())
require.Nil(t, err) require.Nil(t, err)
mintAmount := big.NewInt(1_000_000_000_000) mintAmount := big.NewInt(1_000_000_000_000)
opts.Value = mintAmount opts.Value = mintAmount
SendDepositTx(t, cfg, l1Client, l2Verif, opts, func(l2Opts *DepositTxOpts) {}) SendDepositTx(t, sys.cfg, l1Client, l2Verif, opts, func(l2Opts *DepositTxOpts) {})
// Confirm balance // Confirm balance
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
...@@ -202,7 +220,7 @@ func TestSystemE2E(t *testing.T) { ...@@ -202,7 +220,7 @@ func TestSystemE2E(t *testing.T) {
require.Equal(t, mintAmount, diff, "Did not get expected balance change") require.Equal(t, mintAmount, diff, "Did not get expected balance change")
// Submit TX to L2 sequencer node // Submit TX to L2 sequencer node
receipt := SendL2Tx(t, cfg, l2Seq, ethPrivKey, func(opts *TxOpts) { receipt := SendL2Tx(t, sys.cfg, l2Seq, ethPrivKey, func(opts *TxOpts) {
opts.Value = big.NewInt(1_000_000_000) opts.Value = big.NewInt(1_000_000_000)
opts.Nonce = 1 // Already have deposit opts.Nonce = 1 // Already have deposit
opts.ToAddr = &common.Address{0xff, 0xff} opts.ToAddr = &common.Address{0xff, 0xff}
......
package eth
import (
"crypto/sha256"
"fmt"
"reflect"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params"
)
const (
BlobSize = 4096 * 32
MaxBlobDataSize = 4096*31 - 4
)
type Blob [BlobSize]byte
func (b *Blob) KZGBlob() *kzg4844.Blob {
return (*kzg4844.Blob)(b)
}
func (b *Blob) UnmarshalJSON(text []byte) error {
return hexutil.UnmarshalFixedJSON(reflect.TypeOf(b), text, b[:])
}
func (b *Blob) UnmarshalText(text []byte) error {
return hexutil.UnmarshalFixedText("Bytes32", text, b[:])
}
func (b *Blob) MarshalText() ([]byte, error) {
return hexutil.Bytes(b[:]).MarshalText()
}
func (b *Blob) String() string {
return hexutil.Encode(b[:])
}
// TerminalString implements log.TerminalStringer, formatting a string for console
// output during logging.
func (b *Blob) TerminalString() string {
return fmt.Sprintf("%x..%x", b[:3], b[BlobSize-3:])
}
func (b *Blob) ComputeKZGCommitment() (kzg4844.Commitment, error) {
return kzg4844.BlobToCommitment(*b.KZGBlob())
}
// KZGToVersionedHash computes the "blob hash" (a.k.a. versioned-hash) of a blob-commitment, as used in a blob-tx.
// We implement it here because it is unfortunately not (currently) exposed by geth.
func KZGToVersionedHash(commitment kzg4844.Commitment) (out common.Hash) {
// EIP-4844 spec:
// def kzg_to_versioned_hash(commitment: KZGCommitment) -> VersionedHash:
// return VERSIONED_HASH_VERSION_KZG + sha256(commitment)[1:]
h := sha256.New()
h.Write(commitment[:])
_ = h.Sum(out[:0])
out[0] = params.BlobTxHashVersion
return out
}
// VerifyBlobProof verifies that the given blob and proof corresponds to the given commitment,
// returning error if the verification fails.
func VerifyBlobProof(blob *Blob, commitment kzg4844.Commitment, proof kzg4844.Proof) error {
return kzg4844.VerifyBlobProof(*blob.KZGBlob(), commitment, proof)
}
package eth
type BlobSidecar struct {
BlockRoot Bytes32 `json:"block_root"`
Slot Uint64String `json:"slot"`
Blob Blob `json:"blob"`
Index Uint64String `json:"index"`
KZGCommitment Bytes48 `json:"kzg_commitment"`
KZGProof Bytes48 `json:"kzg_proof"`
}
type APIGetBlobSidecarsResponse struct {
Data []*BlobSidecar `json:"data"`
}
type ReducedGenesisData struct {
GenesisTime Uint64String `json:"genesis_time"`
}
type APIGenesisResponse struct {
Data ReducedGenesisData `json:"data"`
}
type ReducedConfigData struct {
SecondsPerSlot Uint64String `json:"SECONDS_PER_SLOT"`
}
type APIConfigResponse struct {
Data ReducedConfigData `json:"data"`
}
...@@ -94,3 +94,11 @@ func (id L2BlockRef) ParentID() BlockID { ...@@ -94,3 +94,11 @@ func (id L2BlockRef) ParentID() BlockID {
Number: n, Number: n,
} }
} }
// IndexedDataHash represents a data-hash that commits to a single blob confirmed in a block.
// The index helps us avoid unnecessary blob to data-hash conversions to find the right content in a sidecar.
type IndexedDataHash struct {
Index uint64 // absolute index in the block, a.k.a. position in sidecar blobs array
DataHash common.Hash // hash of the blob, used for consistency checks
// Might add tx index and/or tx hash here later, depending on blobs API design
}
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"reflect" "reflect"
"strconv"
"github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -317,3 +318,44 @@ type SystemConfig struct { ...@@ -317,3 +318,44 @@ type SystemConfig struct {
GasLimit uint64 `json:"gasLimit"` GasLimit uint64 `json:"gasLimit"`
// More fields can be added for future SystemConfig versions. // More fields can be added for future SystemConfig versions.
} }
type Bytes48 [48]byte
func (b *Bytes48) UnmarshalJSON(text []byte) error {
return hexutil.UnmarshalFixedJSON(reflect.TypeOf(b), text, b[:])
}
func (b *Bytes48) UnmarshalText(text []byte) error {
return hexutil.UnmarshalFixedText("Bytes32", text, b[:])
}
func (b Bytes48) MarshalText() ([]byte, error) {
return hexutil.Bytes(b[:]).MarshalText()
}
func (b Bytes48) String() string {
return hexutil.Encode(b[:])
}
// TerminalString implements log.TerminalStringer, formatting a string for console
// output during logging.
func (b Bytes48) TerminalString() string {
return fmt.Sprintf("%x..%x", b[:3], b[45:])
}
// Uint64String is a decimal string representation of an uint64, for usage in the Beacon API JSON encoding
type Uint64String uint64
func (v Uint64String) MarshalText() (out []byte, err error) {
out = strconv.AppendUint(out, uint64(v), 10)
return
}
func (v *Uint64String) UnmarshalText(b []byte) error {
n, err := strconv.ParseUint(string(b), 0, 64)
if err != nil {
return err
}
*v = Uint64String(n)
return nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment