Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions op-node/rollup/attributes/engine_consolidate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package attributes

import (
"math/big"
"math/rand" // nosemgrep
"testing"

Expand All @@ -14,7 +15,9 @@ import (
"github.com/ethereum-optimism/optimism/op-core/forks"
opparams "github.com/ethereum-optimism/optimism/op-core/params"
"github.com/ethereum-optimism/optimism/op-core/predeploys"
optypes "github.com/ethereum-optimism/optimism/op-core/types"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/testlog"
Expand Down Expand Up @@ -744,3 +747,42 @@ func depositTxToBytes(t *testing.T, tx *types.Transaction) hexutil.Bytes {

return txBytes
}

// Characterize the existing safe/unsafe comparison: expected derivation
// already authenticates the group, and consolidation must compare every step.
func TestCgtV2SafeAttributesRejectMissingOrMisplacedGroup(t *testing.T) {
cfg := &rollup.Config{ChainOpConfig: defaultOpConfig}
cfg.ActivateAtGenesis(forks.Jovian)
for _, name := range []string{"valid", "missing", "misplaced", "wrong amount"} {
t.Run(name, func(t *testing.T) {
args := jovianArgs()
group, _, err := derive.CgtV2UpgradeTransactions(big.NewInt(1337), big.NewInt(600))
require.NoError(t, err)
// Origin deposits precede the complete upgrade group.
before, deposit := args.attrs.Transactions[0], eth.Data{0xc0}
args.attrs.Transactions = append([]eth.Data{before, deposit}, group...)
txs := append([]eth.Data(nil), args.attrs.Transactions...)
switch name {
case "missing":
txs = txs[:len(txs)-1]
case "misplaced":
txs[1], txs[2] = txs[2], txs[1]
case "wrong amount":
funding, err := optypes.UnmarshalDepositTx(txs[len(txs)-1])
require.NoError(t, err)
funding.Mint.Add(funding.Mint, big.NewInt(1))
funding.Value.Add(funding.Value, big.NewInt(1))
changed, err := funding.MarshalBinary()
require.NoError(t, err)
txs[len(txs)-1] = changed
}
args.envelope.ExecutionPayload.Transactions = txs
err = AttributesMatchBlock(cfg, args.attrs, args.parentHash, args.envelope, testlog.Logger(t, log.LevelCrit))
if name == "valid" {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
22 changes: 21 additions & 1 deletion op-node/rollup/derive/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
var l1Info eth.BlockInfo
var depositTxs []hexutil.Bytes
var seqNumber uint64
var cgtUpgrades []hexutil.Bytes

sysConfig, err := ba.l2.SystemConfigByL2Hash(ctx, l2Parent.Hash)
if err != nil {
Expand All @@ -84,12 +85,16 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
if err != nil {
return nil, NewTemporaryError(fmt.Errorf("failed to fetch L1 block info and receipts: %w", err))
}

if l2Parent.L1Origin.Hash != info.ParentHash() {
return nil, NewResetError(
fmt.Errorf("cannot create new block with L1 origin %s (parent %s) on top of L1 origin %s",
epoch, info.ParentHash(), l2Parent.L1Origin))
}

cgtUpgrades, err = cgtV2UpgradeTransactionsFromReceipts(ba.rollupCfg, receipts)
if err != nil {
return nil, NewCriticalError(err)
}
deposits, err := DeriveDeposits(receipts.Geth(), ba.rollupCfg.DepositContractAddress)
if err != nil {
// deposits may never be ignored. Failing to process them is a critical error.
Expand Down Expand Up @@ -182,6 +187,18 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
upgradeGas += lagoonGas
}

if len(cgtUpgrades) > 0 {
upgradeTxs = append(upgradeTxs, cgtUpgrades...)
gas, err := CgtV2UpgradeGas(ba.rollupCfg.L2ChainID)
if err != nil {
return nil, NewCriticalError(err)
}
if gas > ^uint64(0)-upgradeGas {
return nil, NewCriticalError(fmt.Errorf("CGTv2 upgrade gas overflow"))
}
upgradeGas += gas
}

l1InfoTx, err := L1InfoDepositBytes(ba.rollupCfg, ba.l1ChainConfig, sysConfig, seqNumber, l1Info, nextL2Time)
if err != nil {
return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err))
Expand All @@ -206,6 +223,9 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
}

gasLimit := sysConfig.GasLimit + upgradeGas
if gasLimit < sysConfig.GasLimit {
return nil, NewCriticalError(fmt.Errorf("CGTv2 upgrade gas overflows block gas limit"))
}

r := &eth.PayloadAttributes{
Timestamp: hexutil.Uint64(nextL2Time),
Expand Down
8 changes: 8 additions & 0 deletions op-node/rollup/derive/attributes_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ func (aq *AttributesQueue) createNextAttributes(ctx context.Context, batch *Sing
return nil, err
}

if ContainsCgtV2Activation(attrs.Transactions) && len(batch.Transactions) > 0 {
aq.log.Warn("Dropping batch with user transactions in CGTv2 activation block")
aq.prev.FlushChannel()
aq.batch = nil
aq.concluding = false
return nil, NotEnoughData
}

// we are verifying, not sequencing, we've got all transactions and do not pull from the tx-pool
// (that would make the block derivation non-deterministic)
attrs.NoTxPool = true
Expand Down
43 changes: 43 additions & 0 deletions op-node/rollup/derive/attributes_queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package derive

import (
"context"
"errors"
"io"
"math/big"
"math/rand"
"testing"

"github.com/stretchr/testify/require"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/params"

"github.com/ethereum-optimism/optimism/op-core/predeploys"
Expand Down Expand Up @@ -87,3 +90,43 @@ func TestAttributesQueue(t *testing.T) {
require.NoError(t, err)
require.Equal(t, attrs, *actual)
}

func TestCgtV2AttributesQueueDropsBatchAfterReceiptRetry(t *testing.T) {
for _, retry := range []bool{false, true} {
t.Run(map[bool]string{false: "immediate", true: "RPC retry"}[retry], func(t *testing.T) {
receipts := cgtSeededReceipts()
header, parent, origin := cgtTestOrigin(t, receipts)
cfg := cgtTestConfig()
cfg.HoloceneTime = new(uint64)
cfg.MaxSequencerDrift = 600
cfg.SeqWindowSize = 10
base := eth.L1BlockRef{Hash: parent.L1Origin.Hash, Number: parent.L1Origin.Number, Time: 88}
head := eth.L1BlockRef{Hash: origin.Hash, ParentHash: base.Hash, Number: origin.Number, Time: 100}
batch := &SingularBatch{ParentHash: parent.Hash, EpochHash: origin.Hash, EpochNum: rollup.Epoch(origin.Number), Timestamp: 102, Transactions: []hexutil.Bytes{{0xc0}}}
prev := &fakeBatchQueueInput{batches: []Batch{batch}, errors: []error{nil}, origin: head}
mux := NewBatchMux(testlog.Logger(t, log.LevelError), cfg, prev, nil)
require.ErrorIs(t, mux.Reset(context.Background(), base, eth.SystemConfig{}), io.EOF)
l1 := &testutils.MockL1Source{}
defer l1.AssertExpectations(t)
if retry {
l1.ExpectFetchReceipts(origin.Hash, nil, nil, errors.New("offline"))
}
l1.ExpectFetchReceipts(origin.Hash, eth.HeaderBlockInfo(header), receipts, nil)
builder := NewFetchingAttributesBuilder(cfg, params.MergedTestChainConfig, nil, l1, cgtL2Stub{})
queue := NewAttributesQueue(testlog.Logger(t, log.LevelError), cfg, builder, mux)
if retry {
_, err := queue.NextAttributes(context.Background(), parent)
require.ErrorIs(t, err, ErrTemporary)
require.Same(t, batch, queue.batch)
require.Equal(t, 1, prev.i)
}
attrs, err := queue.NextAttributes(context.Background(), parent)
require.ErrorIs(t, err, NotEnoughData)
require.Nil(t, attrs)
require.Nil(t, queue.batch)
require.Empty(t, prev.batches)
_, err = queue.NextAttributes(context.Background(), parent)
require.ErrorIs(t, err, io.EOF, "the rejected batch must not be retried")
})
}
}
10 changes: 10 additions & 0 deletions op-node/rollup/derive/batch_mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,13 @@ func (b *BatchMux) TransformHolocene() {
panic(fmt.Sprintf("unknown batch stage type: %T", bp))
}
}

// FlushChannel discards buffered span blocks in either batch stage. The legacy
// queue has no channel-flush operation; its unrelated queued batches remain valid.
func (b *BatchMux) FlushChannel() {
if queue, ok := b.SingularBatchProvider.(*BatchQueue); ok {
queue.nextSpan = nil
return
}
b.SingularBatchProvider.FlushChannel()
}
81 changes: 81 additions & 0 deletions op-node/rollup/derive/cgt_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package derive

import (
"fmt"
"math/big"

optypes "github.com/ethereum-optimism/optimism/op-core/types"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/bigs"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)

// ContainsCgtV2Activation recognizes any reserved group source. Execution checks
// every required transaction field, order, success and group completeness.
func ContainsCgtV2Activation(txs []hexutil.Bytes) bool { return containsCgtV2Group(txs) }

// FetchReceipts authenticates the receipt root and log metadata. These additional
// checks ensure the post-Byzantium receipt fields used by the event scan are canonical.
func cgtV2UpgradeTransactionsFromReceipts(cfg *rollup.Config, receipts optypes.Receipts) ([]hexutil.Bytes, error) {
if cfg.L2ChainID == nil || !cfg.L2ChainID.IsUint64() {
return nil, nil
}
network, ok := cgtV2Params(bigs.Uint64Strict(cfg.L2ChainID))
if !ok {
return nil, nil
}
for _, receipt := range receipts {
if receipt == nil {
return nil, fmt.Errorf("CGTv2 missing origin receipt")
}
if (receipt.Status != types.ReceiptStatusFailed && receipt.Status != types.ReceiptStatusSuccessful) || len(receipt.PostState) != 0 {
return nil, fmt.Errorf("CGTv2 origin receipt has noncanonical status or poststate")
}
for _, log := range receipt.Logs {
if log == nil {
return nil, fmt.Errorf("CGTv2 missing origin log")
}
}
}
amount, err := cgtV2EscrowAmount(receipts, network.bridge, cfg.DepositContractAddress)
if err != nil || amount == nil {
return nil, err
}
upgrades, _, err := CgtV2UpgradeTransactions(cfg.L2ChainID, amount)
return upgrades, err
}

var cgtV2EscrowEvent = crypto.Keccak256Hash([]byte("EscrowSeeded(address,uint256)"))

// Receipts must be authenticated before using this result. RPC log metadata is irrelevant.
// seedEscrowGenesis emits a different indexed caller, so only the configured Portal matches.
// The approved L1 migration seeds once; the first matching event supplies A.
func cgtV2EscrowAmount(receipts optypes.Receipts, bridge, portal common.Address) (*big.Int, error) {
expectedPortal := common.BytesToHash(portal.Bytes())
for _, receipt := range receipts {
if receipt == nil {
return nil, fmt.Errorf("CGTv2 missing origin receipt")
}
if receipt.Status != types.ReceiptStatusSuccessful {
continue
}
for _, log := range receipt.Logs {
if log == nil {
return nil, fmt.Errorf("CGTv2 missing origin log")
}
if log.Address != bridge || len(log.Topics) == 0 || log.Topics[0] != cgtV2EscrowEvent {
continue
}
if len(log.Topics) != 2 || len(log.Data) != 32 {
return nil, fmt.Errorf("malformed CGTv2 EscrowSeeded event")
}
if log.Topics[1] == expectedPortal {
return new(big.Int).SetBytes(log.Data), nil
}
}
}
return nil, nil
}
Loading
Loading