Commit 627f7afe authored by Skeletor Spaceman's avatar Skeletor Spaceman Committed by GitHub

feat: ban deposits interop (#11712)

* interop: disable interop messages in force-deposits
Co-authored-by: default avatarDisco <131301107+0xDiscotech@users.noreply.github.com>

* interop: clean up config helpers

* op-node: fix/improve L1 info tx tests

* op-node: fix attributes test

* op-node: fix sequence-number usage in post-interop system deposit, fix tests

* op-contracts: L1Block interop: fix diff, remove duplicate test from renaming

* contracts: update metadata

* contracts: fix build warnings

* contracts: fix interface build

* script: ignore

* lint: fix

* test: .testdata directory

* tests: fix revert

---------
Co-authored-by: default avatarDisco <131301107+0xDiscotech@users.noreply.github.com>
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
Co-authored-by: default avatarMark Tyneway <mark.tyneway@gmail.com>
parent 4ffc7cf3
......@@ -121,9 +121,19 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err))
}
txs := make([]hexutil.Bytes, 0, 1+len(depositTxs)+len(upgradeTxs))
var afterForceIncludeTxs []hexutil.Bytes
if ba.rollupCfg.IsInterop(nextL2Time) {
depositsCompleteTx, err := DepositsCompleteBytes(seqNumber, l1Info)
if err != nil {
return nil, NewCriticalError(fmt.Errorf("failed to create depositsCompleteTx: %w", err))
}
afterForceIncludeTxs = append(afterForceIncludeTxs, depositsCompleteTx)
}
txs := make([]hexutil.Bytes, 0, 1+len(depositTxs)+len(afterForceIncludeTxs)+len(upgradeTxs))
txs = append(txs, l1InfoTx)
txs = append(txs, depositTxs...)
txs = append(txs, afterForceIncludeTxs...)
txs = append(txs, upgradeTxs...)
var withdrawals *types.Withdrawals
......
......@@ -195,6 +195,98 @@ func TestPreparePayloadAttributes(t *testing.T) {
require.Equal(t, l1InfoTx, []byte(attrs.Transactions[0]))
require.True(t, attrs.NoTxPool)
})
t.Run("new origin with deposits on post-Isthmus", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
l1Fetcher := &testutils.MockL1Source{}
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l1CfgFetcher := &testutils.MockL2Client{}
l1CfgFetcher.ExpectSystemConfigByL2Hash(l2Parent.Hash, testSysCfg, nil)
defer l1CfgFetcher.AssertExpectations(t)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoParentHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number + 1 // next origin, where deposits may be
receipts, depositTxs, err := makeReceipts(rng, l1Info.InfoHash, cfg.DepositContractAddress, []receiptData{
{goodReceipt: true, DepositLogs: []bool{true, false}},
{goodReceipt: true, DepositLogs: []bool{true}},
{goodReceipt: false, DepositLogs: []bool{true}},
{goodReceipt: false, DepositLogs: []bool{false}},
})
require.NoError(t, err)
userDepositTxs, err := encodeDeposits(depositTxs)
require.NoError(t, err)
// sets config to post-interop
cfg.ActivateAtGenesis(rollup.Interop)
seqNumber := uint64(0)
epoch := l1Info.ID()
l1InfoTx, err := L1InfoDepositBytes(cfg, testSysCfg, seqNumber, l1Info, 0)
require.NoError(t, err)
depositsComplete, err := DepositsCompleteBytes(seqNumber, l1Info)
require.NoError(t, err)
var l2Txs []eth.Data
l2Txs = append(l2Txs, l1InfoTx)
l2Txs = append(l2Txs, userDepositTxs...)
l2Txs = append(l2Txs, depositsComplete)
l1Fetcher.ExpectFetchReceipts(epoch.Hash, l1Info, receipts, nil)
attrBuilder := NewFetchingAttributesBuilder(cfg, l1Fetcher, l1CfgFetcher)
attrs, err := attrBuilder.PreparePayloadAttributes(context.Background(), l2Parent, epoch)
require.NoError(t, err)
require.NotNil(t, attrs)
require.Equal(t, l2Parent.Time+cfg.BlockTime, uint64(attrs.Timestamp))
require.Equal(t, eth.Bytes32(l1Info.InfoMixDigest), attrs.PrevRandao)
require.Equal(t, predeploys.SequencerFeeVaultAddr, attrs.SuggestedFeeRecipient)
require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs + DepositsComplete")
require.Equal(t, eth.Data(depositsComplete).String(), attrs.Transactions[len(l2Txs)-1].String())
require.Equal(t, l2Txs, attrs.Transactions)
require.True(t, attrs.NoTxPool)
})
t.Run("same origin without deposits on post-Isthmus", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
l1Fetcher := &testutils.MockL1Source{}
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l1CfgFetcher := &testutils.MockL2Client{}
l1CfgFetcher.ExpectSystemConfigByL2Hash(l2Parent.Hash, testSysCfg, nil)
defer l1CfgFetcher.AssertExpectations(t)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number // same origin again, so the sequence number is not reset
// sets config to post-interop
cfg.ActivateAtGenesis(rollup.Interop)
seqNumber := l2Parent.SequenceNumber + 1
epoch := l1Info.ID()
l1InfoTx, err := L1InfoDepositBytes(cfg, testSysCfg, seqNumber, l1Info, 0)
require.NoError(t, err)
depositsComplete, err := DepositsCompleteBytes(seqNumber, l1Info)
require.NoError(t, err)
var l2Txs []eth.Data
l2Txs = append(l2Txs, l1InfoTx)
l2Txs = append(l2Txs, depositsComplete)
l1Fetcher.ExpectInfoByHash(epoch.Hash, l1Info, nil)
attrBuilder := NewFetchingAttributesBuilder(cfg, l1Fetcher, l1CfgFetcher)
attrs, err := attrBuilder.PreparePayloadAttributes(context.Background(), l2Parent, epoch)
require.NoError(t, err)
require.NotNil(t, attrs)
require.Equal(t, l2Parent.Time+cfg.BlockTime, uint64(attrs.Timestamp))
require.Equal(t, eth.Bytes32(l1Info.InfoMixDigest), attrs.PrevRandao)
require.Equal(t, predeploys.SequencerFeeVaultAddr, attrs.SuggestedFeeRecipient)
require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs + DepositsComplete")
require.Equal(t, eth.Data(depositsComplete).String(), attrs.Transactions[len(l2Txs)-1].String())
require.Equal(t, l2Txs, attrs.Transactions)
require.True(t, attrs.NoTxPool)
})
// Test that the payload attributes builder changes the deposit format based on L2-time-based regolith activation
t.Run("regolith", func(t *testing.T) {
testCases := []struct {
......
......@@ -16,6 +16,7 @@ const (
UserDepositSourceDomain = 0
L1InfoDepositSourceDomain = 1
UpgradeDepositSourceDomain = 2
AfterForceIncludeSourceDomain = 3
)
func (dep *UserDepositSource) SourceHash() common.Hash {
......@@ -63,3 +64,21 @@ func (dep *UpgradeDepositSource) SourceHash() common.Hash {
copy(domainInput[32:], intentHash[:])
return crypto.Keccak256Hash(domainInput[:])
}
// AfterForceIncludeSource identifies the DepositsComplete post-user-deposits deposit-transaction.
type AfterForceIncludeSource struct {
L1BlockHash common.Hash
SeqNumber uint64 // without this the Deposit tx would have the same tx hash for every time the L1 info repeats.
}
func (dep *AfterForceIncludeSource) SourceHash() common.Hash {
var input [32 * 2]byte
copy(input[:32], dep.L1BlockHash[:])
binary.BigEndian.PutUint64(input[32*2-8:], dep.SeqNumber)
depositIDHash := crypto.Keccak256Hash(input[:])
var domainInput [32 * 2]byte
binary.BigEndian.PutUint64(domainInput[32-8:32], AfterForceIncludeSourceDomain)
copy(domainInput[32:], depositIDHash[:])
return crypto.Keccak256Hash(domainInput[:])
}
......@@ -3,6 +3,7 @@ package derive
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
)
......@@ -34,3 +35,33 @@ func TestEcotone4788ContractSourceHash(t *testing.T) {
assert.Equal(t, expected, actual.Hex())
}
// TestL1InfoDepositSource
// cast keccak $(cast concat-hex 0x0000000000000000000000000000000000000000000000000000000000000001 $(cast keccak $(cast concat-hex 0xc00e5d67c2755389aded7d8b151cbd5bcdf7ed275ad5e028b664880fc7581c77 0x0000000000000000000000000000000000000000000000000000000000000004)))
// # 0x0586c503340591999b8b38bc9834bb16aec7d5bc00eb5587ab139c9ddab81977
func TestL1InfoDepositSource(t *testing.T) {
source := L1InfoDepositSource{
L1BlockHash: common.HexToHash("0xc00e5d67c2755389aded7d8b151cbd5bcdf7ed275ad5e028b664880fc7581c77"),
SeqNumber: 4,
}
actual := source.SourceHash()
expected := "0x0586c503340591999b8b38bc9834bb16aec7d5bc00eb5587ab139c9ddab81977"
assert.Equal(t, expected, actual.Hex())
}
// TestAfterForceIncludeSourceHash
// cast keccak $(cast concat-hex 0x0000000000000000000000000000000000000000000000000000000000000003 $(cast keccak $(cast concat-hex 0xc00e5d67c2755389aded7d8b151cbd5bcdf7ed275ad5e028b664880fc7581c77 0x0000000000000000000000000000000000000000000000000000000000000004)))
// # 0x0d165c391384b29c29f655e3f32315755b8c1e4c1147d1824d1243420dda5ec3
func TestAfterForceIncludeSource(t *testing.T) {
source := AfterForceIncludeSource{
L1BlockHash: common.HexToHash("0xc00e5d67c2755389aded7d8b151cbd5bcdf7ed275ad5e028b664880fc7581c77"),
SeqNumber: 4,
}
actual := source.SourceHash()
expected := "0x0d165c391384b29c29f655e3f32315755b8c1e4c1147d1824d1243420dda5ec3"
assert.Equal(t, expected, actual.Hex())
}
......@@ -83,15 +83,26 @@ func FuzzL1InfoEcotoneRoundTrip(f *testing.F) {
}
enc, err := in.marshalBinaryEcotone()
if err != nil {
t.Fatalf("Failed to marshal binary: %v", err)
t.Fatalf("Failed to marshal Ecotone binary: %v", err)
}
var out L1BlockInfo
err = out.unmarshalBinaryEcotone(enc)
if err != nil {
t.Fatalf("Failed to unmarshal binary: %v", err)
t.Fatalf("Failed to unmarshal Ecotone binary: %v", err)
}
if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out)
t.Fatalf("The Ecotone data did not round trip correctly. in: %v. out: %v", in, out)
}
enc, err = in.marshalBinaryIsthmus()
if err != nil {
t.Fatalf("Failed to marshal Isthmus binary: %v", err)
}
err = out.unmarshalBinaryIsthmus(enc)
if err != nil {
t.Fatalf("Failed to unmarshal Isthmus binary: %v", err)
}
if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
t.Fatalf("The Isthmus data did not round trip correctly. in: %v. out: %v", in, out)
}
})
......
......@@ -20,14 +20,25 @@ import (
const (
L1InfoFuncBedrockSignature = "setL1BlockValues(uint64,uint64,uint256,bytes32,uint64,bytes32,uint256,uint256)"
L1InfoFuncEcotoneSignature = "setL1BlockValuesEcotone()"
L1InfoFuncIsthmusSignature = "setL1BlockValuesIsthmus()"
DepositsCompleteSignature = "depositsComplete()"
L1InfoArguments = 8
L1InfoBedrockLen = 4 + 32*L1InfoArguments
L1InfoEcotoneLen = 4 + 32*5 // after Ecotone upgrade, args are packed into 5 32-byte slots
DepositsCompleteLen = 4 // only the selector
// DepositsCompleteGas allocates 21k gas for intrinsic tx costs, and
// an additional 15k to ensure that the DepositsComplete call does not run out of gas.
// GasBenchMark_L1BlockIsthmus_DepositsComplete:test_depositsComplete_benchmark() (gas: 7768)
// GasBenchMark_L1BlockIsthmus_DepositsComplete_Warm:test_depositsComplete_benchmark() (gas: 5768)
// see `test_depositsComplete_benchmark` at: `/packages/contracts-bedrock/test/BenchmarkTest.t.sol`
DepositsCompleteGas = uint64(21_000 + 15_000)
)
var (
L1InfoFuncBedrockBytes4 = crypto.Keccak256([]byte(L1InfoFuncBedrockSignature))[:4]
L1InfoFuncEcotoneBytes4 = crypto.Keccak256([]byte(L1InfoFuncEcotoneSignature))[:4]
L1InfoFuncIsthmusBytes4 = crypto.Keccak256([]byte(L1InfoFuncIsthmusSignature))[:4]
DepositsCompleteBytes4 = crypto.Keccak256([]byte(DepositsCompleteSignature))[:4]
L1InfoDepositerAddress = common.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001")
L1BlockAddress = predeploys.L1BlockAddr
ErrInvalidFormat = errors.New("invalid ecotone l1 block info format")
......@@ -144,7 +155,7 @@ func (info *L1BlockInfo) unmarshalBinaryBedrock(data []byte) error {
return nil
}
// Ecotone Binary Format
// Isthmus & Ecotone Binary Format
// +---------+--------------------------+
// | Bytes | Field |
// +---------+--------------------------+
......@@ -161,8 +172,24 @@ func (info *L1BlockInfo) unmarshalBinaryBedrock(data []byte) error {
// +---------+--------------------------+
func (info *L1BlockInfo) marshalBinaryEcotone() ([]byte, error) {
w := bytes.NewBuffer(make([]byte, 0, L1InfoEcotoneLen))
if err := solabi.WriteSignature(w, L1InfoFuncEcotoneBytes4); err != nil {
out, err := marshalBinaryWithSignature(info, L1InfoFuncEcotoneBytes4)
if err != nil {
return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err)
}
return out, nil
}
func (info *L1BlockInfo) marshalBinaryIsthmus() ([]byte, error) {
out, err := marshalBinaryWithSignature(info, L1InfoFuncIsthmusBytes4)
if err != nil {
return nil, fmt.Errorf("failed to marshal Isthmus l1 block info: %w", err)
}
return out, nil
}
func marshalBinaryWithSignature(info *L1BlockInfo, signature []byte) ([]byte, error) {
w := bytes.NewBuffer(make([]byte, 0, L1InfoEcotoneLen)) // Ecotone and Isthmus have the same length
if err := solabi.WriteSignature(w, signature); err != nil {
return nil, err
}
if err := binary.Write(w, binary.BigEndian, info.BaseFeeScalar); err != nil {
......@@ -201,13 +228,21 @@ func (info *L1BlockInfo) marshalBinaryEcotone() ([]byte, error) {
}
func (info *L1BlockInfo) unmarshalBinaryEcotone(data []byte) error {
return unmarshalBinaryWithSignatureAndData(info, L1InfoFuncEcotoneBytes4, data)
}
func (info *L1BlockInfo) unmarshalBinaryIsthmus(data []byte) error {
return unmarshalBinaryWithSignatureAndData(info, L1InfoFuncIsthmusBytes4, data)
}
func unmarshalBinaryWithSignatureAndData(info *L1BlockInfo, signature []byte, data []byte) error {
if len(data) != L1InfoEcotoneLen {
return fmt.Errorf("data is unexpected length: %d", len(data))
}
r := bytes.NewReader(data)
var err error
if _, err := solabi.ReadAndValidateSignature(r, L1InfoFuncEcotoneBytes4); err != nil {
if _, err := solabi.ReadAndValidateSignature(r, signature); err != nil {
return err
}
if err := binary.Read(r, binary.BigEndian, &info.BaseFeeScalar); err != nil {
......@@ -245,14 +280,28 @@ func (info *L1BlockInfo) unmarshalBinaryEcotone(data []byte) error {
}
// isEcotoneButNotFirstBlock returns whether the specified block is subject to the Ecotone upgrade,
// but is not the actiation block itself.
func isEcotoneButNotFirstBlock(rollupCfg *rollup.Config, l2BlockTime uint64) bool {
return rollupCfg.IsEcotone(l2BlockTime) && !rollupCfg.IsEcotoneActivationBlock(l2BlockTime)
// but is not the activation block itself.
func isEcotoneButNotFirstBlock(rollupCfg *rollup.Config, l2Timestamp uint64) bool {
return rollupCfg.IsEcotone(l2Timestamp) && !rollupCfg.IsEcotoneActivationBlock(l2Timestamp)
}
// isInteropButNotFirstBlock returns whether the specified block is subject to the Isthmus upgrade,
// but is not the activation block itself.
func isInteropButNotFirstBlock(rollupCfg *rollup.Config, l2Timestamp uint64) bool {
// Since we use the pre-interop L1 tx one last time during the upgrade block,
// we must disallow the deposit-txs from using the CrossL2Inbox during this block.
// If the CrossL2Inbox does not exist yet, then it is safe,
// but we have to ensure that the spec and code puts any Interop upgrade-txs after the user deposits.
return rollupCfg.IsInterop(l2Timestamp) && !rollupCfg.IsInteropActivationBlock(l2Timestamp)
}
// L1BlockInfoFromBytes is the inverse of L1InfoDeposit, to see where the L2 chain is derived from
func L1BlockInfoFromBytes(rollupCfg *rollup.Config, l2BlockTime uint64, data []byte) (*L1BlockInfo, error) {
var info L1BlockInfo
// Important, this should be ordered from most recent to oldest
if isInteropButNotFirstBlock(rollupCfg, l2BlockTime) {
return &info, info.unmarshalBinaryIsthmus(data)
}
if isEcotoneButNotFirstBlock(rollupCfg, l2BlockTime) {
return &info, info.unmarshalBinaryEcotone(data)
}
......@@ -261,7 +310,7 @@ func L1BlockInfoFromBytes(rollupCfg *rollup.Config, l2BlockTime uint64, data []b
// L1InfoDeposit creates a L1 Info deposit transaction based on the L1 block,
// and the L2 block-height difference with the start of the epoch.
func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber uint64, block eth.BlockInfo, l2BlockTime uint64) (*types.DepositTx, error) {
func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber uint64, block eth.BlockInfo, l2Timestamp uint64) (*types.DepositTx, error) {
l1BlockInfo := L1BlockInfo{
Number: block.NumberU64(),
Time: block.Time(),
......@@ -271,7 +320,7 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber
BatcherAddr: sysCfg.BatcherAddr,
}
var data []byte
if isEcotoneButNotFirstBlock(rollupCfg, l2BlockTime) {
if isEcotoneButNotFirstBlock(rollupCfg, l2Timestamp) {
l1BlockInfo.BlobBaseFee = block.BlobBaseFee()
if l1BlockInfo.BlobBaseFee == nil {
// The L2 spec states to use the MIN_BLOB_GASPRICE from EIP-4844 if not yet active on L1.
......@@ -283,11 +332,19 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber
}
l1BlockInfo.BlobBaseFeeScalar = scalars.BlobBaseFeeScalar
l1BlockInfo.BaseFeeScalar = scalars.BaseFeeScalar
if isInteropButNotFirstBlock(rollupCfg, l2Timestamp) {
out, err := l1BlockInfo.marshalBinaryIsthmus()
if err != nil {
return nil, fmt.Errorf("failed to marshal Isthmus l1 block info: %w", err)
}
data = out
} else {
out, err := l1BlockInfo.marshalBinaryEcotone()
if err != nil {
return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err)
}
data = out
}
} else {
l1BlockInfo.L1FeeOverhead = sysCfg.Overhead
l1BlockInfo.L1FeeScalar = sysCfg.Scalar
......@@ -315,7 +372,7 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber
Data: data,
}
// With the regolith fork we disable the IsSystemTx functionality, and allocate real gas
if rollupCfg.IsRegolith(l2BlockTime) {
if rollupCfg.IsRegolith(l2Timestamp) {
out.IsSystemTransaction = false
out.Gas = RegolithSystemTxGas
}
......@@ -323,8 +380,8 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber
}
// L1InfoDepositBytes returns a serialized L1-info attributes transaction.
func L1InfoDepositBytes(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber uint64, l1Info eth.BlockInfo, l2BlockTime uint64) ([]byte, error) {
dep, err := L1InfoDeposit(rollupCfg, sysCfg, seqNumber, l1Info, l2BlockTime)
func L1InfoDepositBytes(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber uint64, l1Info eth.BlockInfo, l2Timestamp uint64) ([]byte, error) {
dep, err := L1InfoDeposit(rollupCfg, sysCfg, seqNumber, l1Info, l2Timestamp)
if err != nil {
return nil, fmt.Errorf("failed to create L1 info tx: %w", err)
}
......@@ -335,3 +392,34 @@ func L1InfoDepositBytes(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNu
}
return opaqueL1Tx, nil
}
func DepositsCompleteDeposit(seqNumber uint64, block eth.BlockInfo) (*types.DepositTx, error) {
source := AfterForceIncludeSource{
L1BlockHash: block.Hash(),
SeqNumber: seqNumber,
}
out := &types.DepositTx{
SourceHash: source.SourceHash(),
From: L1InfoDepositerAddress,
To: &L1BlockAddress,
Mint: nil,
Value: big.NewInt(0),
Gas: DepositsCompleteGas,
IsSystemTransaction: false,
Data: DepositsCompleteBytes4,
}
return out, nil
}
func DepositsCompleteBytes(seqNumber uint64, l1Info eth.BlockInfo) ([]byte, error) {
dep, err := DepositsCompleteDeposit(seqNumber, l1Info)
if err != nil {
return nil, fmt.Errorf("failed to create DepositsComplete tx: %w", err)
}
depositsCompleteTx := types.NewTx(dep)
opaqueDepositsCompleteTx, err := depositsCompleteTx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to encode DepositsComplete tx: %w", err)
}
return opaqueDepositsCompleteTx, nil
}
......@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
......@@ -109,10 +110,8 @@ func TestParseL1InfoDepositTxData(t *testing.T) {
t.Run("regolith", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
zero := uint64(0)
rollupCfg := rollup.Config{
RegolithTime: &zero,
}
rollupCfg := rollup.Config{}
rollupCfg.ActivateAtGenesis(rollup.Regolith)
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 0)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
......@@ -121,27 +120,24 @@ func TestParseL1InfoDepositTxData(t *testing.T) {
t.Run("ecotone", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
zero := uint64(0)
rollupCfg := rollup.Config{
RegolithTime: &zero,
EcotoneTime: &zero,
}
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 1)
rollupCfg := rollup.Config{BlockTime: 2, Genesis: rollup.Genesis{L2Time: 1000}}
rollupCfg.ActivateAtGenesis(rollup.Ecotone)
// run 1 block after ecotone transition
timestamp := rollupCfg.Genesis.L2Time + rollupCfg.BlockTime
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, timestamp)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas))
require.Equal(t, L1InfoEcotoneLen, len(depTx.Data))
})
t.Run("first-block ecotone", func(t *testing.T) {
t.Run("activation-block ecotone", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
zero := uint64(2)
rollupCfg := rollup.Config{
RegolithTime: &zero,
EcotoneTime: &zero,
BlockTime: 2,
}
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 2)
rollupCfg := rollup.Config{BlockTime: 2, Genesis: rollup.Genesis{L2Time: 1000}}
rollupCfg.ActivateAtGenesis(rollup.Delta)
ecotoneTime := rollupCfg.Genesis.L2Time + rollupCfg.BlockTime // activate ecotone just after genesis
rollupCfg.EcotoneTime = &ecotoneTime
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, ecotoneTime)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas))
......@@ -150,16 +146,88 @@ func TestParseL1InfoDepositTxData(t *testing.T) {
t.Run("genesis-block ecotone", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
zero := uint64(0)
rollupCfg := rollup.Config{
RegolithTime: &zero,
EcotoneTime: &zero,
BlockTime: 2,
}
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 0)
rollupCfg := rollup.Config{BlockTime: 2, Genesis: rollup.Genesis{L2Time: 1000}}
rollupCfg.ActivateAtGenesis(rollup.Ecotone)
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, rollupCfg.Genesis.L2Time)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas))
require.Equal(t, L1InfoEcotoneLen, len(depTx.Data))
})
t.Run("isthmus", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
rollupCfg := rollup.Config{BlockTime: 2, Genesis: rollup.Genesis{L2Time: 1000}}
rollupCfg.ActivateAtGenesis(rollup.Interop)
// run 1 block after interop transition
timestamp := rollupCfg.Genesis.L2Time + rollupCfg.BlockTime
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, timestamp)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas))
require.Equal(t, L1InfoEcotoneLen, len(depTx.Data), "the length is same in isthmus")
require.Equal(t, L1InfoFuncIsthmusBytes4, depTx.Data[:4], "upgrade is active, need isthmus signature")
})
t.Run("activation-block isthmus", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
rollupCfg := rollup.Config{BlockTime: 2, Genesis: rollup.Genesis{L2Time: 1000}}
rollupCfg.ActivateAtGenesis(rollup.Fjord)
isthmusTime := rollupCfg.Genesis.L2Time + rollupCfg.BlockTime // activate isthmus just after genesis
rollupCfg.InteropTime = &isthmusTime
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, isthmusTime)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas))
// Isthmus activates, but ecotone L1 info is still used at this upgrade block
require.Equal(t, L1InfoEcotoneLen, len(depTx.Data))
require.Equal(t, L1InfoFuncEcotoneBytes4, depTx.Data[:4])
})
t.Run("genesis-block isthmus", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
rollupCfg := rollup.Config{BlockTime: 2, Genesis: rollup.Genesis{L2Time: 1000}}
rollupCfg.ActivateAtGenesis(rollup.Interop)
depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, rollupCfg.Genesis.L2Time)
require.NoError(t, err)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas))
require.Equal(t, L1InfoEcotoneLen, len(depTx.Data))
})
}
func TestDepositsCompleteBytes(t *testing.T) {
randomSeqNr := func(rng *rand.Rand) uint64 {
return rng.Uint64()
}
t.Run("valid return bytes", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
depTxByes, err := DepositsCompleteBytes(randomSeqNr(rng), info)
require.NoError(t, err)
var depTx types.Transaction
require.NoError(t, depTx.UnmarshalBinary(depTxByes))
require.Equal(t, uint8(types.DepositTxType), depTx.Type())
require.Equal(t, depTx.Data(), DepositsCompleteBytes4)
require.Equal(t, DepositsCompleteLen, len(depTx.Data()))
require.Equal(t, DepositsCompleteGas, depTx.Gas())
require.False(t, depTx.IsSystemTx())
require.Equal(t, depTx.Value(), big.NewInt(0))
signer := types.LatestSignerForChainID(depTx.ChainId())
sender, err := signer.Sender(&depTx)
require.NoError(t, err)
require.Equal(t, L1InfoDepositerAddress, sender)
})
t.Run("valid return Transaction", func(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
info := testutils.MakeBlockInfo(nil)(rng)
depTx, err := DepositsCompleteDeposit(randomSeqNr(rng), info)
require.NoError(t, err)
require.Equal(t, depTx.Data, DepositsCompleteBytes4)
require.Equal(t, DepositsCompleteLen, len(depTx.Data))
require.Equal(t, DepositsCompleteGas, depTx.Gas)
require.False(t, depTx.IsSystemTransaction)
require.Equal(t, depTx.Value, big.NewInt(0))
require.Equal(t, L1InfoDepositerAddress, depTx.From)
})
}
......@@ -463,6 +463,40 @@ func (c *Config) IsInteropActivationBlock(l2BlockTime uint64) bool {
!c.IsInterop(l2BlockTime-c.BlockTime)
}
func (c *Config) ActivateAtGenesis(hardfork ForkName) {
// IMPORTANT! ordered from newest to oldest
switch hardfork {
case Interop:
c.InteropTime = new(uint64)
fallthrough
case Holocene:
c.HoloceneTime = new(uint64)
fallthrough
case Granite:
c.GraniteTime = new(uint64)
fallthrough
case Fjord:
c.FjordTime = new(uint64)
fallthrough
case Ecotone:
c.EcotoneTime = new(uint64)
fallthrough
case Delta:
c.DeltaTime = new(uint64)
fallthrough
case Canyon:
c.CanyonTime = new(uint64)
fallthrough
case Regolith:
c.RegolithTime = new(uint64)
fallthrough
case Bedrock:
// default
case None:
break
}
}
// ForkchoiceUpdatedVersion returns the EngineAPIMethod suitable for the chain hard fork version.
func (c *Config) ForkchoiceUpdatedVersion(attr *eth.PayloadAttributes) eth.EngineAPIMethod {
if attr == nil {
......
GasBenchMark_L1BlockIsthmus_DepositsComplete:test_depositsComplete_benchmark() (gas: 7567)
GasBenchMark_L1BlockIsthmus_DepositsComplete_Warm:test_depositsComplete_benchmark() (gas: 5567)
GasBenchMark_L1BlockIsthmus_SetValuesIsthmus:test_setL1BlockValuesIsthmus_benchmark() (gas: 175657)
GasBenchMark_L1BlockIsthmus_SetValuesIsthmus_Warm:test_setL1BlockValuesIsthmus_benchmark() (gas: 5121)
GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 158531)
GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7597)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496)
GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483)
......
......@@ -357,7 +357,7 @@ contract L2Genesis is Deployer {
/// @notice This predeploy is following the safety invariant #1.
function setL1Block() public {
if (cfg.useInterop()) {
string memory cname = "L1BlockInterop";
string memory cname = "L1BlockIsthmus";
address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_BLOCK_ATTRIBUTES);
console.log("Setting %s implementation at: %s", cname, impl);
vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname)));
......
......@@ -67,6 +67,7 @@ EXCLUDE_CONTRACTS=(
"IL1StandardBridge"
"ISuperchainConfig"
"IOptimismPortal"
"IL1BlockIsthmus"
)
# Find all JSON files in the forge-artifacts folder
......
......@@ -45,7 +45,7 @@
},
"src/L1/OptimismPortalInterop.sol": {
"initCodeHash": "0x9222fba222d1ab66898eef09ecea3ea757e64c8ae98def4da7808cd7cc8f39a8",
"sourceCodeHash": "0x3fc9d9fc1143bec92801f8a18ad3527533923dc26c4820d03d6905f519d735b4"
"sourceCodeHash": "0x57353b84bbcb05634f135ad8090d96f07467dd3dbf159650714449d0667efc62"
},
"src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x8f033874dd8b36615b2209d553660dcff1ff91ca2bad3ca1de7b441dbfba4842",
......@@ -61,15 +61,15 @@
},
"src/L1/SystemConfigInterop.sol": {
"initCodeHash": "0xc5a3ffc59dd7bf1ef238087414cfa04b37f0d83fc9a4f5e6d62a1059a23359f3",
"sourceCodeHash": "0x71606c81ff4e69bac78d04731287c34dfb20a648ad384646926a62c16344e0d7"
"sourceCodeHash": "0x272bcfafab62516609250c85cd73815bf0a243fdb9d34fc603f71e801299b57c"
},
"src/L2/BaseFeeVault.sol": {
"initCodeHash": "0x3bfcd57e25ad54b66c374f63e24e33a6cf107044aa8f5f69ef21202c380b5c5b",
"sourceCodeHash": "0x2dc2284cf7c68e743da50e4113e96ffeab435de2390aeba2eab2f1e8ca411ce9"
},
"src/L2/CrossL2Inbox.sol": {
"initCodeHash": "0x926ec5b92a5ff032c00ae13f1156332cb43b98b89573467e9ddfab6fce9f3e95",
"sourceCodeHash": "0xd5d3f9f1ff7d15367e200832d3257514c6e8cf3cf64703111111982c0ea4840b"
"initCodeHash": "0x79c5deb404605b42ef917b5e7308a9015dacfb71225d957a634e6d0a3a5bc621",
"sourceCodeHash": "0xd219408d99f627770dfcdb3243a183dec7429372787f0aec3bdbff5b3c294f2a"
},
"src/L2/ETHLiquidity.sol": {
"initCodeHash": "0x1d9958d75fd502f018408ed5585d541b156435ac9c163009135b866d66f8f6ee",
......@@ -80,12 +80,12 @@
"sourceCodeHash": "0xaef30eab756a3804a241f57c8bb787179376477f6344a6ae49771d532153c9d3"
},
"src/L2/L1Block.sol": {
"initCodeHash": "0xb12c0560e4e0aed12df5f65a1bc2b302afd183601c149285a26eafe5e4c20c0e",
"sourceCodeHash": "0x30aef5ac102e3655651ff821ce560ecf0da0914456379b784f5652fe09c37aa3"
"initCodeHash": "0x21a09e366c69cae22f8fa3f3e1ddbbfed19408dee19f482a3d60ae699bebf462",
"sourceCodeHash": "0x254a5709e04e5a3b0a3e73253525457d956fde5299b22da4033012a44070ea09"
},
"src/L2/L1BlockInterop.sol": {
"initCodeHash": "0xd2afdf64b0232264e4996e0557523c108c2f12a9b6d2de45dfd961f7a1c927e3",
"sourceCodeHash": "0xfd2283b341239be76b0b8fa067e8ccdf71ef5b29eb75df6783d9f004a9203530"
"src/L2/L1BlockIsthmus.sol": {
"initCodeHash": "0x93b0a4bc7a0990e5c4e7081fce10f729f4d1c8e9128a7931a610152b786dc461",
"sourceCodeHash": "0x6e4927fb8c26273694257696ff286ed0382d8bd1ecaf02abec51aa4b53922c68"
},
"src/L2/L1FeeVault.sol": {
"initCodeHash": "0x3bfcd57e25ad54b66c374f63e24e33a6cf107044aa8f5f69ef21202c380b5c5b",
......
......@@ -253,6 +253,11 @@
"name": "InvalidTimestamp",
"type": "error"
},
{
"inputs": [],
"name": "NoExecutingDeposits",
"type": "error"
},
{
"inputs": [],
"name": "NotDepositor",
......
......@@ -90,6 +90,13 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "depositsComplete",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "gasPayingToken",
......@@ -160,6 +167,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isDeposit",
"outputs": [
{
"internalType": "bool",
"name": "isDeposit_",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -332,6 +352,13 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "setL1BlockValuesIsthmus",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "timestamp",
......@@ -430,6 +457,11 @@
"name": "DependencySetSizeTooLarge",
"type": "error"
},
{
"inputs": [],
"name": "NotCrossL2Inbox",
"type": "error"
},
{
"inputs": [],
"name": "NotDependency",
......
......@@ -2,7 +2,7 @@
pragma solidity 0.8.15;
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { L1BlockInterop, ConfigType } from "src/L2/L1BlockInterop.sol";
import { L1BlockIsthmus, ConfigType } from "src/L2/L1BlockIsthmus.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { Constants } from "src/libraries/Constants.sol";
......@@ -47,7 +47,7 @@ contract OptimismPortalInterop is OptimismPortal2 {
uint256(0), // value
uint64(SYSTEM_DEPOSIT_GAS_LIMIT), // gasLimit
false, // isCreation,
abi.encodeCall(L1BlockInterop.setConfig, (_type, _value))
abi.encodeCall(L1BlockIsthmus.setConfig, (_type, _value))
)
);
}
......
......@@ -6,7 +6,7 @@ import { OptimismPortalInterop as OptimismPortal } from "src/L1/OptimismPortalIn
import { GasPayingToken } from "src/libraries/GasPayingToken.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { ConfigType } from "src/L2/L1BlockInterop.sol";
import { ConfigType } from "src/L2/L1BlockIsthmus.sol";
import { StaticConfig } from "src/libraries/StaticConfig.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { Storage } from "src/libraries/Storage.sol";
......
......@@ -7,6 +7,7 @@ import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { ICrossL2Inbox } from "src/L2/interfaces/ICrossL2Inbox.sol";
import { SafeCall } from "src/libraries/SafeCall.sol";
import { IDependencySet } from "src/L2/interfaces/IDependencySet.sol";
import { IL1BlockIsthmus } from "src/L2/interfaces/IL1BlockIsthmus.sol";
/// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()`
error NotDepositor();
......@@ -26,6 +27,9 @@ error InvalidChainId();
/// @notice Thrown when trying to execute a cross chain message and the target call fails.
error TargetCallFailed();
/// @notice Thrown when trying to execute a cross chain message on a deposit transaction.
error NoExecutingDeposits();
/// @custom:proxied true
/// @custom:predeploy 0x4200000000000000000000000000000000000022
/// @title CrossL2Inbox
......@@ -135,6 +139,9 @@ contract CrossL2Inbox is ICrossL2Inbox, ISemver, TransientReentrancyAware {
payable
reentrantAware
{
// We need to know if this is being called on a depositTx
if (IL1BlockIsthmus(Predeploys.L1_BLOCK_ATTRIBUTES).isDeposit()) revert NoExecutingDeposits();
// Check the Identifier.
_checkIdentifier(_id);
......
......@@ -57,9 +57,9 @@ contract L1Block is ISemver, IGasToken {
/// @notice The latest L1 blob base fee.
uint256 public blobBaseFee;
/// @custom:semver 1.4.1-beta.2
/// @custom:semver 1.5.1-beta.1
function version() public pure virtual returns (string memory) {
return "1.4.1-beta.2";
return "1.5.1-beta.1";
}
/// @notice Returns the gas paying token, its decimals, name and symbol.
......@@ -133,7 +133,23 @@ contract L1Block is ISemver, IGasToken {
/// 7. _blobBaseFee L1 blob base fee.
/// 8. _hash L1 blockhash.
/// 9. _batcherHash Versioned hash to authenticate batcher by.
function setL1BlockValuesEcotone() external {
function setL1BlockValuesEcotone() public {
_setL1BlockValuesEcotone();
}
/// @notice Updates the L1 block values for an Ecotone upgraded chain.
/// Params are packed and passed in as raw msg.data instead of ABI to reduce calldata size.
/// Params are expected to be in the following order:
/// 1. _baseFeeScalar L1 base fee scalar
/// 2. _blobBaseFeeScalar L1 blob base fee scalar
/// 3. _sequenceNumber Number of L2 blocks since epoch start.
/// 4. _timestamp L1 timestamp.
/// 5. _number L1 blocknumber.
/// 6. _basefee L1 base fee.
/// 7. _blobBaseFee L1 blob base fee.
/// 8. _hash L1 blockhash.
/// 9. _batcherHash Versioned hash to authenticate batcher by.
function _setL1BlockValuesEcotone() internal {
address depositor = DEPOSITOR_ACCOUNT();
assembly {
// Revert if the caller is not the depositor account.
......
......@@ -5,9 +5,10 @@ import { L1Block } from "src/L2/L1Block.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import { GasPayingToken } from "src/libraries/GasPayingToken.sol";
import { StaticConfig } from "src/libraries/StaticConfig.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import "src/libraries/L1BlockErrors.sol";
/// @notice Enum representing different types of configurations that can be set on L1BlockInterop.
/// @notice Enum representing different types of configurations that can be set on L1BlockIsthmus.
/// @custom:value SET_GAS_PAYING_TOKEN Represents the config type for setting the gas paying token.
/// @custom:value ADD_DEPENDENCY Represents the config type for adding a chain to the interop dependency set.
/// @custom:value REMOVE_DEPENDENCY Represents the config type for removing a chain from the interop dependency set.
......@@ -19,9 +20,9 @@ enum ConfigType {
/// @custom:proxied true
/// @custom:predeploy 0x4200000000000000000000000000000000000015
/// @title L1BlockInterop
/// @notice Interop extenstions of L1Block.
contract L1BlockInterop is L1Block {
/// @title L1BlockIsthmus
/// @notice Isthmus extenstions of L1Block.
contract L1BlockIsthmus is L1Block {
using EnumerableSet for EnumerableSet.UintSet;
/// @notice Event emitted when a new dependency is added to the interop dependency set.
......@@ -33,9 +34,23 @@ contract L1BlockInterop is L1Block {
/// @notice The interop dependency set, containing the chain IDs in it.
EnumerableSet.UintSet dependencySet;
/// @custom:semver +interop
/// @notice Storage slot that the isDeposit is stored at.
/// This is a custom slot that is not part of the standard storage layout.
/// keccak256(abi.encode(uint256(keccak256("l1Block.identifier.isDeposit")) - 1)) & ~bytes32(uint256(0xff))
uint256 internal constant IS_DEPOSIT_SLOT = 0x921bd3a089295c6e5540e8fba8195448d253efd6f2e3e495b499b627dc36a300;
/// @custom:semver +isthmus
function version() public pure override returns (string memory) {
return string.concat(super.version(), "+interop");
return string.concat(super.version(), "+isthmus");
}
/// @notice Returns whether the call was triggered from a a deposit or not.
/// @notice This function is only callable by the CrossL2Inbox contract.
function isDeposit() external view returns (bool isDeposit_) {
if (msg.sender != Predeploys.CROSS_L2_INBOX) revert NotCrossL2Inbox();
assembly {
isDeposit_ := sload(IS_DEPOSIT_SLOT)
}
}
/// @notice Returns true if a chain ID is in the interop dependency set and false otherwise.
......@@ -52,6 +67,29 @@ contract L1BlockInterop is L1Block {
return uint8(dependencySet.length());
}
/// @notice Updates the `isDeposit` flag and sets the L1 block values for an Isthmus upgraded chain.
/// It updates the L1 block values through the `setL1BlockValuesEcotone` function.
/// It forwards the calldata to the internally-used `setL1BlockValuesEcotone` function.
function setL1BlockValuesIsthmus() external {
// Set the isDeposit flag to true.
assembly {
sstore(IS_DEPOSIT_SLOT, 1)
}
_setL1BlockValuesEcotone();
}
/// @notice Resets the isDeposit flag.
/// Should only be called by the depositor account after the deposits are complete.
function depositsComplete() external {
if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor();
// Set the isDeposit flag to false.
assembly {
sstore(IS_DEPOSIT_SLOT, 0)
}
}
/// @notice Sets static configuration options for the L2 system. Can only be called by the special
/// depositor account.
/// @param _type The type of configuration to set.
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IL1BlockIsthmus {
type ConfigType is uint8;
error AlreadyDependency();
error CantRemovedDependency();
error DependencySetSizeTooLarge();
error NotCrossL2Inbox();
error NotDependency();
error NotDepositor();
event DependencyAdded(uint256 indexed chainId);
event DependencyRemoved(uint256 indexed chainId);
event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol);
function DEPOSITOR_ACCOUNT() external pure returns (address addr_);
function baseFeeScalar() external view returns (uint32);
function basefee() external view returns (uint256);
function batcherHash() external view returns (bytes32);
function blobBaseFee() external view returns (uint256);
function blobBaseFeeScalar() external view returns (uint32);
function dependencySetSize() external view returns (uint8);
function depositsComplete() external;
function gasPayingToken() external view returns (address addr_, uint8 decimals_);
function gasPayingTokenName() external view returns (string memory name_);
function gasPayingTokenSymbol() external view returns (string memory symbol_);
function hash() external view returns (bytes32);
function isCustomGasToken() external view returns (bool);
function isDeposit() external view returns (bool isDeposit_);
function isInDependencySet(uint256 _chainId) external view returns (bool);
function l1FeeOverhead() external view returns (uint256);
function l1FeeScalar() external view returns (uint256);
function number() external view returns (uint64);
function sequenceNumber() external view returns (uint64);
function setConfig(ConfigType _type, bytes memory _value) external;
function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external;
function setL1BlockValues(
uint64 _number,
uint64 _timestamp,
uint256 _basefee,
bytes32 _hash,
uint64 _sequenceNumber,
bytes32 _batcherHash,
uint256 _l1FeeOverhead,
uint256 _l1FeeScalar
)
external;
function setL1BlockValuesEcotone() external;
function setL1BlockValuesIsthmus() external;
function timestamp() external view returns (uint64);
function version() external pure returns (string memory);
}
......@@ -184,8 +184,7 @@ library Encoding {
/// @param _blobBaseFee L1 blob base fee.
/// @param _hash L1 blockhash.
/// @param _batcherHash Versioned hash to authenticate batcher by.
/// @param _dependencySet Array of the chain IDs in the interop dependency set.
function encodeSetL1BlockValuesInterop(
function encodeSetL1BlockValuesIsthmus(
uint32 _baseFeeScalar,
uint32 _blobBaseFeeScalar,
uint64 _sequenceNumber,
......@@ -194,18 +193,13 @@ library Encoding {
uint256 _baseFee,
uint256 _blobBaseFee,
bytes32 _hash,
bytes32 _batcherHash,
uint256[] memory _dependencySet
bytes32 _batcherHash
)
internal
pure
returns (bytes memory)
{
require(_dependencySet.length <= type(uint8).max, "Encoding: dependency set length is too large");
// Check that the batcher hash is just the address with 0 padding to the left for version 0.
require(uint160(uint256(_batcherHash)) == uint256(_batcherHash), "Encoding: invalid batcher hash");
bytes4 functionSignature = bytes4(keccak256("setL1BlockValuesInterop()"));
bytes4 functionSignature = bytes4(keccak256("setL1BlockValuesIsthmus()"));
return abi.encodePacked(
functionSignature,
_baseFeeScalar,
......@@ -216,9 +210,7 @@ library Encoding {
_baseFee,
_blobBaseFee,
_hash,
_batcherHash,
uint8(_dependencySet.length),
_dependencySet
_batcherHash
);
}
}
......@@ -4,6 +4,9 @@ pragma solidity ^0.8.0;
/// @notice Error returns when a non-depositor account tries to set L1 block values.
error NotDepositor();
/// @notice Error when a non-cross L2 Inbox sender tries to call the `isDeposit()` method.
error NotCrossL2Inbox();
/// @notice Error when a chain ID is not in the interop dependency set.
error NotDependency();
......
......@@ -67,6 +67,13 @@ library SafeCall {
success_ = call({ _target: _target, _gas: gasleft(), _value: _value, _calldata: _calldata });
}
/// @notice Perform a low level call without copying any returndata
/// @param _target Address to call
/// @param _calldata Calldata to pass to the call
function call(address _target, bytes memory _calldata) internal returns (bool success_) {
success_ = call({ _target: _target, _gas: gasleft(), _value: 0, _calldata: _calldata });
}
/// @notice Helper function to determine if there is sufficient gas remaining within the context
/// to guarantee that the minimum gas requirement for a call will be met as well as
/// optionally reserving a specified amount of gas for after the call has concluded.
......
......@@ -9,6 +9,9 @@ import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol";
import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { Types } from "src/libraries/Types.sol";
import { SafeCall } from "src/libraries/SafeCall.sol";
import { L1BlockIsthmus } from "src/L2/L1BlockIsthmus.sol";
import { Encoding } from "src/libraries/Encoding.sol";
// Free function for setting the prevBaseFee param in the OptimismPortal.
function setPrevBaseFee(Vm _vm, address _op, uint128 _prevBaseFee) {
......@@ -209,3 +212,101 @@ contract GasBenchMark_L2OutputOracle is CommonTest {
l2OutputOracle.proposeL2Output(nonZeroHash, nextBlockNumber, 0, 0);
}
}
contract GasBenchMark_L1Block is CommonTest {
address depositor;
bytes setValuesCalldata;
function setUp() public virtual override {
super.setUp();
depositor = l1Block.DEPOSITOR_ACCOUNT();
setValuesCalldata = Encoding.encodeSetL1BlockValuesEcotone(
type(uint32).max,
type(uint32).max,
type(uint64).max,
type(uint64).max,
type(uint64).max,
type(uint256).max,
type(uint256).max,
keccak256(abi.encode(1)),
bytes32(type(uint256).max)
);
vm.startPrank(depositor);
}
}
contract GasBenchMark_L1Block_SetValuesEcotone is GasBenchMark_L1Block {
function test_setL1BlockValuesEcotone_benchmark() external {
SafeCall.call({ _target: address(l1Block), _calldata: setValuesCalldata });
}
}
contract GasBenchMark_L1Block_SetValuesEcotone_Warm is GasBenchMark_L1Block {
function setUp() public virtual override {
SafeCall.call({ _target: address(l1Block), _calldata: setValuesCalldata });
}
function test_setL1BlockValuesEcotone_benchmark() external {
SafeCall.call({ _target: address(l1Block), _calldata: setValuesCalldata });
}
}
contract GasBenchMark_L1BlockIsthmus is GasBenchMark_L1Block {
L1BlockIsthmus l1BlockIsthmus;
function setUp() public virtual override {
super.setUp();
l1BlockIsthmus = new L1BlockIsthmus();
setValuesCalldata = Encoding.encodeSetL1BlockValuesIsthmus(
type(uint32).max,
type(uint32).max,
type(uint64).max,
type(uint64).max,
type(uint64).max,
type(uint256).max,
type(uint256).max,
keccak256(abi.encode(1)),
bytes32(type(uint256).max)
);
}
}
contract GasBenchMark_L1BlockIsthmus_SetValuesIsthmus is GasBenchMark_L1BlockIsthmus {
function test_setL1BlockValuesIsthmus_benchmark() external {
SafeCall.call({ _target: address(l1BlockIsthmus), _calldata: setValuesCalldata });
}
}
contract GasBenchMark_L1BlockIsthmus_SetValuesIsthmus_Warm is GasBenchMark_L1BlockIsthmus {
function setUp() public virtual override {
SafeCall.call({ _target: address(l1BlockIsthmus), _calldata: setValuesCalldata });
}
function test_setL1BlockValuesIsthmus_benchmark() external {
SafeCall.call({ _target: address(l1BlockIsthmus), _calldata: setValuesCalldata });
}
}
contract GasBenchMark_L1BlockIsthmus_DepositsComplete is GasBenchMark_L1BlockIsthmus {
function test_depositsComplete_benchmark() external {
SafeCall.call({
_target: address(l1BlockIsthmus),
_calldata: abi.encodeWithSelector(l1BlockIsthmus.depositsComplete.selector)
});
}
}
contract GasBenchMark_L1BlockIsthmus_DepositsComplete_Warm is GasBenchMark_L1BlockIsthmus {
function setUp() public virtual override {
super.setUp();
// Set the isDeposit flag to true so then we can benchmark when it is reset.
SafeCall.call({ _target: address(l1BlockIsthmus), _calldata: setValuesCalldata });
}
function test_depositsComplete_benchmark() external {
SafeCall.call({
_target: address(l1BlockIsthmus),
_calldata: abi.encodeWithSelector(l1BlockIsthmus.depositsComplete.selector)
});
}
}
......@@ -11,7 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol";
// Target contract dependencies
import "src/libraries/PortalErrors.sol";
import { OptimismPortalInterop } from "src/L1/OptimismPortalInterop.sol";
import { L1BlockInterop, ConfigType } from "src/L2/L1BlockInterop.sol";
import { L1BlockIsthmus, ConfigType } from "src/L2/L1BlockIsthmus.sol";
contract OptimismPortalInterop_Test is CommonTest {
/// @notice Marked virtual to be overridden in
......@@ -31,7 +31,7 @@ contract OptimismPortalInterop_Test is CommonTest {
_mint: 0,
_gasLimit: 200_000,
_isCreation: false,
_data: abi.encodeCall(L1BlockInterop.setConfig, (ConfigType.SET_GAS_PAYING_TOKEN, _value))
_data: abi.encodeCall(L1BlockIsthmus.setConfig, (ConfigType.SET_GAS_PAYING_TOKEN, _value))
});
vm.prank(address(_optimismPortalInterop().systemConfig()));
......@@ -54,7 +54,7 @@ contract OptimismPortalInterop_Test is CommonTest {
_mint: 0,
_gasLimit: 200_000,
_isCreation: false,
_data: abi.encodeCall(L1BlockInterop.setConfig, (ConfigType.ADD_DEPENDENCY, _value))
_data: abi.encodeCall(L1BlockIsthmus.setConfig, (ConfigType.ADD_DEPENDENCY, _value))
});
vm.prank(address(_optimismPortalInterop().systemConfig()));
......@@ -77,7 +77,7 @@ contract OptimismPortalInterop_Test is CommonTest {
_mint: 0,
_gasLimit: 200_000,
_isCreation: false,
_data: abi.encodeCall(L1BlockInterop.setConfig, (ConfigType.REMOVE_DEPENDENCY, _value))
_data: abi.encodeCall(L1BlockIsthmus.setConfig, (ConfigType.REMOVE_DEPENDENCY, _value))
});
vm.prank(address(_optimismPortalInterop().systemConfig()));
......
......@@ -14,7 +14,7 @@ import { SystemConfig } from "src/L1/SystemConfig.sol";
import { SystemConfigInterop } from "src/L1/SystemConfigInterop.sol";
import { OptimismPortalInterop } from "src/L1/OptimismPortalInterop.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ConfigType } from "src/L2/L1BlockInterop.sol";
import { ConfigType } from "src/L2/L1BlockIsthmus.sol";
contract SystemConfigInterop_Test is CommonTest {
/// @notice Marked virtual to be overridden in
......
......@@ -12,12 +12,14 @@ import { TransientContext } from "src/libraries/TransientContext.sol";
import {
CrossL2Inbox,
NotEntered,
NoExecutingDeposits,
InvalidTimestamp,
InvalidChainId,
TargetCallFailed,
NotDepositor,
InteropStartAlreadySet
} from "src/L2/CrossL2Inbox.sol";
import { IL1BlockIsthmus } from "src/L2/interfaces/IL1BlockIsthmus.sol";
import { ICrossL2Inbox } from "src/L2/interfaces/ICrossL2Inbox.sol";
/// @title CrossL2InboxWithModifiableTransientStorage
......@@ -155,6 +157,13 @@ contract CrossL2InboxTest is Test {
// Ensure that the target call is payable if value is sent
if (_value > 0) assumePayable(_target);
// Ensure is not a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(false)
});
// Ensure that the target call does not revert
vm.mockCall({ callee: _target, msgValue: _value, data: _message, returnData: abi.encode(true) });
......@@ -210,6 +219,13 @@ contract CrossL2InboxTest is Test {
_id1.timestamp = bound(_id1.timestamp, interopStartTime + 1, block.timestamp);
_id2.timestamp = bound(_id2.timestamp, interopStartTime + 1, block.timestamp);
// Ensure is not a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(false)
});
// Ensure that id1's chain ID is in the dependency set
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
......@@ -254,6 +270,32 @@ contract CrossL2InboxTest is Test {
assertEq(crossL2Inbox.chainId(), _id2.chainId);
}
/// @dev Tests that the `executeMessage` function reverts if the transaction comes from a deposit.
function testFuzz_executeMessage_isDeposit_reverts(
ICrossL2Inbox.Identifier calldata _id,
address _target,
bytes calldata _message,
uint256 _value
)
external
{
// Ensure it is a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(true)
});
// Ensure that the contract has enough balance to send with value
vm.deal(address(this), _value);
// Expect a revert with the NoExecutingDeposits selector
vm.expectRevert(NoExecutingDeposits.selector);
// Call the executeMessage function
crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message });
}
/// @dev Tests that the `executeMessage` function reverts when called with an identifier with an invalid timestamp.
function testFuzz_executeMessage_invalidTimestamp_reverts(
ICrossL2Inbox.Identifier calldata _id,
......@@ -267,6 +309,13 @@ contract CrossL2InboxTest is Test {
// Ensure that the id's timestamp is invalid (greater than the current block timestamp)
vm.assume(_id.timestamp > block.timestamp);
// Ensure is not a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(false)
});
// Ensure that the contract has enough balance to send with value
vm.deal(address(this), _value);
......@@ -294,6 +343,13 @@ contract CrossL2InboxTest is Test {
// Ensure that the contract has enough balance to send with value
vm.deal(address(this), _value);
// Ensure is not a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(false)
});
// Expect a revert with the InvalidTimestamp selector
vm.expectRevert(InvalidTimestamp.selector);
......@@ -316,6 +372,13 @@ contract CrossL2InboxTest is Test {
// interop start time)
_id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp);
// Ensure is not a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(false)
});
// Ensure that the chain ID is NOT in the dependency set
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
......@@ -353,6 +416,13 @@ contract CrossL2InboxTest is Test {
// Ensure that the target call reverts
vm.mockCallRevert({ callee: _target, msgValue: _value, data: _message, revertData: abi.encode(false) });
// Ensure is not a deposit transaction
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
data: abi.encodeWithSelector(IL1BlockIsthmus.isDeposit.selector),
returnData: abi.encode(false)
});
// Ensure that the chain ID is in the dependency set
vm.mockCall({
callee: Predeploys.L1_BLOCK_ATTRIBUTES,
......
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