Commit b9ae70a0 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into qbzzt/230321-stop-start

parents 2ecfb76e f088859a
---
'@eth-optimism/sdk': patch
---
Have SDK automatically create Standard and ETH bridges when L1StandardBridge is provided.
---
'@eth-optimism/batch-submitter-service': patch
---
Allow deposit only batches
---
'@eth-optimism/contracts-bedrock': patch
---
Added a contsructor to the System Dictator
...@@ -518,6 +518,10 @@ jobs: ...@@ -518,6 +518,10 @@ jobs:
patterns: packages patterns: packages
# Note: The below needs to be manually configured whenever we # Note: The below needs to be manually configured whenever we
# add a new package to CI. # add a new package to CI.
- run:
name: Check common-ts
command: npx depcheck
working_directory: packages/common-ts
- run: - run:
name: Check contracts name: Check contracts
command: npx depcheck command: npx depcheck
......
...@@ -66,6 +66,7 @@ You'll need the following: ...@@ -66,6 +66,7 @@ You'll need the following:
* [Yarn](https://classic.yarnpkg.com/en/docs/install) * [Yarn](https://classic.yarnpkg.com/en/docs/install)
* [Docker](https://docs.docker.com/get-docker/) * [Docker](https://docs.docker.com/get-docker/)
* [Docker Compose](https://docs.docker.com/compose/install/) * [Docker Compose](https://docs.docker.com/compose/install/)
* [Go](https://go.dev/dl/)
* [Foundry](https://getfoundry.sh) * [Foundry](https://getfoundry.sh)
### Setup ### Setup
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/batch-submitter/drivers/sequencer" "github.com/ethereum-optimism/optimism/batch-submitter/drivers/sequencer"
l2common "github.com/ethereum-optimism/optimism/l2geth/common" l2common "github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum-optimism/optimism/l2geth/core/types"
l2types "github.com/ethereum-optimism/optimism/l2geth/core/types" l2types "github.com/ethereum-optimism/optimism/l2geth/core/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -47,3 +48,76 @@ func TestBatchElementFromBlock(t *testing.T) { ...@@ -47,3 +48,76 @@ func TestBatchElementFromBlock(t *testing.T) {
require.False(t, element.IsSequencerTx()) require.False(t, element.IsSequencerTx())
require.Nil(t, element.Tx) require.Nil(t, element.Tx)
} }
func TestGenSequencerParams(t *testing.T) {
tx := types.NewTransaction(0, l2common.Address{}, big.NewInt(0), 0, big.NewInt(0), []byte{})
shouldStartAtElement := uint64(1)
blockOffset := uint64(1)
batches := []sequencer.BatchElement{
{Timestamp: 1, BlockNumber: 1},
{Timestamp: 1, BlockNumber: 1, Tx: sequencer.NewCachedTx(tx)},
}
params, err := sequencer.GenSequencerBatchParams(shouldStartAtElement, blockOffset, batches)
require.NoError(t, err)
require.Equal(t, uint64(0), params.ShouldStartAtElement)
require.Equal(t, uint64(len(batches)), params.TotalElementsToAppend)
require.Equal(t, len(batches), len(params.Contexts))
// There is only 1 sequencer tx
require.Equal(t, 1, len(params.Txs))
// There are 2 contexts
// The first context contains the deposit
context1 := params.Contexts[0]
require.Equal(t, uint64(0), context1.NumSequencedTxs)
require.Equal(t, uint64(1), context1.NumSubsequentQueueTxs)
require.Equal(t, uint64(1), context1.Timestamp)
require.Equal(t, uint64(1), context1.BlockNumber)
// The second context contains the sequencer tx
context2 := params.Contexts[1]
require.Equal(t, uint64(1), context2.NumSequencedTxs)
require.Equal(t, uint64(0), context2.NumSubsequentQueueTxs)
require.Equal(t, uint64(1), context2.Timestamp)
require.Equal(t, uint64(1), context2.BlockNumber)
}
func TestGenSequencerParamsOnlyDeposits(t *testing.T) {
shouldStartAtElement := uint64(1)
blockOffset := uint64(1)
batches := []sequencer.BatchElement{
{Timestamp: 1, BlockNumber: 1},
{Timestamp: 1, BlockNumber: 1},
{Timestamp: 2, BlockNumber: 2},
}
params, err := sequencer.GenSequencerBatchParams(shouldStartAtElement, blockOffset, batches)
require.NoError(t, err)
// The batches will pack deposits into the same context when their
// timestamps and blocknumbers are the same
require.Equal(t, uint64(0), params.ShouldStartAtElement)
require.Equal(t, uint64(len(batches)), params.TotalElementsToAppend)
// 2 deposits have the same timestamp + blocknumber, they go in the
// same context. 1 deposit has a different timestamp + blocknumber,
// it goes into a different context. Therefore there are 2 contexts
require.Equal(t, 2, len(params.Contexts))
// No sequencer txs
require.Equal(t, 0, len(params.Txs))
// There are 2 contexts
// The first context contains the deposit
context1 := params.Contexts[0]
require.Equal(t, uint64(0), context1.NumSequencedTxs)
require.Equal(t, uint64(2), context1.NumSubsequentQueueTxs)
require.Equal(t, uint64(1), context1.Timestamp)
require.Equal(t, uint64(1), context1.BlockNumber)
context2 := params.Contexts[1]
require.Equal(t, uint64(0), context2.NumSequencedTxs)
require.Equal(t, uint64(1), context2.NumSubsequentQueueTxs)
require.Equal(t, uint64(2), context2.Timestamp)
require.Equal(t, uint64(2), context2.BlockNumber)
}
...@@ -222,11 +222,6 @@ func (p *AppendSequencerBatchParams) Write( ...@@ -222,11 +222,6 @@ func (p *AppendSequencerBatchParams) Write(
return ErrMalformedBatch return ErrMalformedBatch
} }
// There must be transactions if there are contexts
if len(p.Txs) == 0 && len(p.Contexts) != 0 {
return ErrMalformedBatch
}
// copy the contexts as to not malleate the struct // copy the contexts as to not malleate the struct
// when it is a typed batch // when it is a typed batch
contexts := make([]BatchContext, 0, len(p.Contexts)+1) contexts := make([]BatchContext, 0, len(p.Contexts)+1)
...@@ -361,9 +356,6 @@ func (p *AppendSequencerBatchParams) Read(r io.Reader) error { ...@@ -361,9 +356,6 @@ func (p *AppendSequencerBatchParams) Read(r io.Reader) error {
if len(p.Contexts) == 0 && len(p.Txs) != 0 { if len(p.Contexts) == 0 && len(p.Txs) != 0 {
return ErrMalformedBatch return ErrMalformedBatch
} }
if len(p.Txs) == 0 && len(p.Contexts) != 0 {
return ErrMalformedBatch
}
return closeReader() return closeReader()
} else if err != nil { } else if err != nil {
return err return err
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
} }
], ],
"txs": [], "txs": [],
"error": true "error": false
}, },
{ {
"name": "multiple-contexts-no-txs", "name": "multiple-contexts-no-txs",
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
} }
], ],
"txs": [], "txs": [],
"error": true "error": false
}, },
{ {
"name": "complex", "name": "complex",
......
...@@ -166,6 +166,7 @@ module.exports = { ...@@ -166,6 +166,7 @@ module.exports = {
children: [ children: [
"/docs/build/tutorials/add-attr.md", "/docs/build/tutorials/add-attr.md",
"/docs/build/tutorials/new-precomp.md", "/docs/build/tutorials/new-precomp.md",
"/docs/build/tutorials/predeploys.md"
] ]
} // End of tutorials } // End of tutorials
], ],
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<i class="fab fa-discord"></i> Discord community <i class="fab fa-discord"></i> Discord community
</a> </a>
</li> </li>
<li>
<a href="https://wkf.ms/3XTdpLl" target="_blank" rel="noopener noreferrer">
<i class="far fa-comment-dots"></i> Get support for going live
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -201,7 +201,7 @@ Once you’ve built both repositories, you’ll need head back to the Optimism M ...@@ -201,7 +201,7 @@ Once you’ve built both repositories, you’ll need head back to the Optimism M
- Replace `"BATCHER"` with the address of the Batcher account you generated earlier. - Replace `"BATCHER"` with the address of the Batcher account you generated earlier.
- Replace `"SEQUENCER"` with the address of the Sequencer account you generated earlier. - Replace `"SEQUENCER"` with the address of the Sequencer account you generated earlier.
- Replace `"BLOCKHASH"` with the blockhash you got from the `cast` command. - Replace `"BLOCKHASH"` with the blockhash you got from the `cast` command.
- Replace `"TIMESTAMP"` with the timestamp you got from the `cast` command. Note that although all the other fields are strings, this field is a number! Don’t include the quotation marks. - Replace `TIMESTAMP` with the timestamp you got from the `cast` command. Note that although all the other fields are strings, this field is a number! Don’t include the quotation marks.
## Deploy the L1 contracts ## Deploy the L1 contracts
...@@ -634,4 +634,4 @@ You should *not* add an `op-bathcer`, there should be only one. ...@@ -634,4 +634,4 @@ You should *not* add an `op-bathcer`, there should be only one.
## What’s next? ## What’s next?
You can use this rollup the same way you’d use any other test blockchain. Once the superchain is available, this blockchain should be able to join the test version. Alternatively, you could [modify the blockchain in various ways](./hacks.md). **Please note that OP Stack Hacks are unofficial and are not explicitly supported by the OP Stack.** You will not be able to receive significant developer support for any modifications you make to the OP Stack. You can use this rollup the same way you’d use any other test blockchain. Once the superchain is available, this blockchain should be able to join the test version. Alternatively, you could [modify the blockchain in various ways](./hacks.md). **Please note that OP Stack Hacks are unofficial and are not explicitly supported by the OP Stack.** You will not be able to receive significant developer support for any modifications you make to the OP Stack.
\ No newline at end of file
---
title: Modifying Predeployed Contracts
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
OP Stack blockchains have a number of [predeployed contracts](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/constants.ts) that provide important functionality.
Most of those contracts are proxies that can be upgraded using the `proxyAdminOwner` which was configured when the network was initially deployed.
The predeploys are controlled from a predeploy called [`ProxyAdmin`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/contracts/universal/ProxyAdmin.sol), whose address is `0x4200000000000000000000000000000000000018`.
The function to call is [`upgrade(address,address)`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/contracts/universal/ProxyAdmin.sol#L211-L229).
The first parameter is the proxy to upgrade, and the second is the address of a new implementation.
For example, the legacy `L1BlockNumber` contract is at `0x420...013`.
To disable this function, we'll set the implementation to `0x00...00`.
We do this using the [Foundry](https://book.getfoundry.sh/) command `cast`.
1. We'll need several constants.
- Set these addresses as variables in your terminal.
```sh
L1BLOCKNUM=0x4200000000000000000000000000000000000013
PROXY_ADMIN=0x4200000000000000000000000000000000000018
ZERO_ADDR=0x0000000000000000000000000000000000000000
```
- Set `PRIVKEY` to the private key of your ADMIN account.
- Set `ETH_RPC_URL`. If you're on the computer that runs the blockchain, use this command.
```sh
export ETH_RPC_URL=http://localhost:8545
```
1. Verify `L1BlockNumber` works correctly.
See that when you call the contract you get a block number, and twelve seconds later you get the next one (block time on L1 is twelve seconds).
```sh
cast call $L1BLOCKNUM 'number()' | cast --to-dec
sleep 12 && cast call $L1BLOCKNUM 'number()' | cast --to-dec
```
1. Get the current implementation for the contract.
```sh
L1BLOCKNUM_IMPLEMENTATION=`cast call $L1BLOCKNUM "implementation()" | sed 's/000000000000000000000000//'`
echo $L1BLOCKNUM_IMPLEMENTATION
```
1. Change the implementation to the zero address
```sh
cast send --private-key $PRIVKEY $PROXY_ADMIN "upgrade(address,address)" $L1BLOCKNUM $ZERO_ADDR
```
1. See that the implementation is address zero, and that calling it fails.
```sh
cast call $L1BLOCKNUM 'implementation()'
cast call $L1BLOCKNUM 'number()'
```
1. Fix the predeploy by returning it to the previous implementation, and verify it works.
```sh
cast send --private-key $PRIVKEY $PROXY_ADMIN "upgrade(address,address)" $L1BLOCKNUM $L1BLOCKNUM_IMPLEMENTATION
cast call $L1BLOCKNUM 'number()' | cast --to-dec
```
\ No newline at end of file
...@@ -20,6 +20,7 @@ lint: ...@@ -20,6 +20,7 @@ lint:
golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint -e "errors.As" -e "errors.Is" golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint -e "errors.As" -e "errors.Is"
fuzz: fuzz:
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzChannelConfig_CheckTimeout ./batcher
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDurationZero ./batcher go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDurationZero ./batcher
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDurationTimeoutMaxChannelDuration ./batcher go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDurationTimeoutMaxChannelDuration ./batcher
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDurationTimeoutZeroMaxChannelDuration ./batcher go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDurationTimeoutZeroMaxChannelDuration ./batcher
......
...@@ -12,8 +12,6 @@ import ( ...@@ -12,8 +12,6 @@ import (
) )
var ( var (
ErrZeroMaxFrameSize = errors.New("max frame size cannot be zero")
ErrSmallMaxFrameSize = errors.New("max frame size cannot be less than 23")
ErrInvalidChannelTimeout = errors.New("channel timeout is less than the safety margin") ErrInvalidChannelTimeout = errors.New("channel timeout is less than the safety margin")
ErrInputTargetReached = errors.New("target amount of input data reached") ErrInputTargetReached = errors.New("target amount of input data reached")
ErrMaxFrameIndex = errors.New("max frame index reached (uint16)") ErrMaxFrameIndex = errors.New("max frame index reached (uint16)")
...@@ -83,15 +81,15 @@ func (cc *ChannelConfig) Check() error { ...@@ -83,15 +81,15 @@ func (cc *ChannelConfig) Check() error {
// will infinitely loop when trying to create frames in the // will infinitely loop when trying to create frames in the
// [channelBuilder.OutputFrames] function. // [channelBuilder.OutputFrames] function.
if cc.MaxFrameSize == 0 { if cc.MaxFrameSize == 0 {
return ErrZeroMaxFrameSize return errors.New("max frame size cannot be zero")
} }
// If the [MaxFrameSize] is set to < 23, the channel out // If the [MaxFrameSize] is less than [FrameV0OverHeadSize], the channel
// will underflow the maxSize variable in the [derive.ChannelOut]. // out will underflow the maxSize variable in the [derive.ChannelOut].
// Since it is of type uint64, it will wrap around to a very large // Since it is of type uint64, it will wrap around to a very large
// number, making the frame size extremely large. // number, making the frame size extremely large.
if cc.MaxFrameSize < 23 { if cc.MaxFrameSize < derive.FrameV0OverHeadSize {
return ErrSmallMaxFrameSize return fmt.Errorf("max frame size %d is less than the minimum 23", cc.MaxFrameSize)
} }
return nil return nil
......
...@@ -3,6 +3,7 @@ package batcher ...@@ -3,6 +3,7 @@ package batcher
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"math" "math"
"math/big" "math/big"
"math/rand" "math/rand"
...@@ -32,27 +33,79 @@ var defaultTestChannelConfig = ChannelConfig{ ...@@ -32,27 +33,79 @@ var defaultTestChannelConfig = ChannelConfig{
ApproxComprRatio: 0.4, ApproxComprRatio: 0.4,
} }
// TestConfigValidation tests the validation of the [ChannelConfig] struct. // TestChannelConfig_Check tests the [ChannelConfig] [Check] function.
func TestConfigValidation(t *testing.T) { func TestChannelConfig_Check(t *testing.T) {
// Construct a valid config. type test struct {
validChannelConfig := defaultTestChannelConfig input ChannelConfig
require.NoError(t, validChannelConfig.Check()) assertion func(error)
}
// Set the config to have a zero max frame size.
validChannelConfig.MaxFrameSize = 0 // Construct test cases that test the boundary conditions
require.ErrorIs(t, validChannelConfig.Check(), ErrZeroMaxFrameSize) zeroChannelConfig := defaultTestChannelConfig
zeroChannelConfig.MaxFrameSize = 0
// Set the config to have a max frame size less than 23. timeoutChannelConfig := defaultTestChannelConfig
validChannelConfig.MaxFrameSize = 22 timeoutChannelConfig.ChannelTimeout = 0
require.ErrorIs(t, validChannelConfig.Check(), ErrSmallMaxFrameSize) timeoutChannelConfig.SubSafetyMargin = 1
tests := []test{
// Reset the config and test the Timeout error. {
// NOTE: We should be fuzzing these values with the constraint that input: defaultTestChannelConfig,
// SubSafetyMargin > ChannelTimeout to ensure validation. assertion: func(output error) {
validChannelConfig = defaultTestChannelConfig require.NoError(t, output)
validChannelConfig.ChannelTimeout = 0 },
validChannelConfig.SubSafetyMargin = 1 },
require.ErrorIs(t, validChannelConfig.Check(), ErrInvalidChannelTimeout) {
input: timeoutChannelConfig,
assertion: func(output error) {
require.ErrorIs(t, output, ErrInvalidChannelTimeout)
},
},
{
input: zeroChannelConfig,
assertion: func(output error) {
require.EqualError(t, output, "max frame size cannot be zero")
},
},
}
for i := 1; i < derive.FrameV0OverHeadSize; i++ {
smallChannelConfig := defaultTestChannelConfig
smallChannelConfig.MaxFrameSize = uint64(i)
expectedErr := fmt.Sprintf("max frame size %d is less than the minimum 23", i)
tests = append(tests, test{
input: smallChannelConfig,
assertion: func(output error) {
require.EqualError(t, output, expectedErr)
},
})
}
// Run the table tests
for _, test := range tests {
test.assertion(test.input.Check())
}
}
// FuzzChannelConfig_CheckTimeout tests the [ChannelConfig] [Check] function
// with fuzzing to make sure that a [ErrInvalidChannelTimeout] is thrown when
// the [ChannelTimeout] is less than the [SubSafetyMargin].
func FuzzChannelConfig_CheckTimeout(f *testing.F) {
for i := range [10]int{} {
f.Add(uint64(i+1), uint64(i))
}
f.Fuzz(func(t *testing.T, channelTimeout uint64, subSafetyMargin uint64) {
// We only test where [ChannelTimeout] is less than the [SubSafetyMargin]
// So we cannot have [ChannelTimeout] be [math.MaxUint64]
if channelTimeout == math.MaxUint64 {
channelTimeout = math.MaxUint64 - 1
}
if subSafetyMargin <= channelTimeout {
subSafetyMargin = channelTimeout + 1
}
channelConfig := defaultTestChannelConfig
channelConfig.ChannelTimeout = channelTimeout
channelConfig.SubSafetyMargin = subSafetyMargin
require.ErrorIs(t, channelConfig.Check(), ErrInvalidChannelTimeout)
})
} }
// addMiniBlock adds a minimal valid L2 block to the channel builder using the // addMiniBlock adds a minimal valid L2 block to the channel builder using the
...@@ -132,10 +185,10 @@ func FuzzDurationTimeoutZeroMaxChannelDuration(f *testing.F) { ...@@ -132,10 +185,10 @@ func FuzzDurationTimeoutZeroMaxChannelDuration(f *testing.F) {
}) })
} }
// FuzzDurationZero ensures that when whenever the MaxChannelDuration // FuzzChannelBuilder_DurationZero ensures that when whenever the MaxChannelDuration
// is not set to 0, the channel builder will always have a duration timeout // is not set to 0, the channel builder will always have a duration timeout
// as long as the channel builder's timeout is set to 0. // as long as the channel builder's timeout is set to 0.
func FuzzDurationZero(f *testing.F) { func FuzzChannelBuilder_DurationZero(f *testing.F) {
for i := range [10]int{} { for i := range [10]int{} {
f.Add(uint64(i), uint64(i)) f.Add(uint64(i), uint64(i))
} }
...@@ -313,8 +366,8 @@ func FuzzSeqWindowZeroTimeoutClose(f *testing.F) { ...@@ -313,8 +366,8 @@ func FuzzSeqWindowZeroTimeoutClose(f *testing.F) {
}) })
} }
// TestBuilderNextFrame tests calling NextFrame on a ChannelBuilder with only one frame // TestChannelBuilder_NextFrame tests calling NextFrame on a ChannelBuilder with only one frame
func TestBuilderNextFrame(t *testing.T) { func TestChannelBuilder_NextFrame(t *testing.T) {
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
// Create a new channel builder // Create a new channel builder
...@@ -353,8 +406,8 @@ func TestBuilderNextFrame(t *testing.T) { ...@@ -353,8 +406,8 @@ func TestBuilderNextFrame(t *testing.T) {
require.PanicsWithValue(t, "no next frame", func() { cb.NextFrame() }) require.PanicsWithValue(t, "no next frame", func() { cb.NextFrame() })
} }
// TestBuilderInvalidFrameId tests that a panic is thrown when a frame is pushed with an invalid frame id // TestChannelBuilder_OutputWrongFramePanic tests that a panic is thrown when a frame is pushed with an invalid frame id
func TestBuilderWrongFramePanic(t *testing.T) { func TestChannelBuilder_OutputWrongFramePanic(t *testing.T) {
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
// Construct a channel builder // Construct a channel builder
...@@ -383,15 +436,14 @@ func TestBuilderWrongFramePanic(t *testing.T) { ...@@ -383,15 +436,14 @@ func TestBuilderWrongFramePanic(t *testing.T) {
}) })
} }
// TestOutputFrames tests the OutputFrames function // TestChannelBuilder_OutputFramesWorks tests the [ChannelBuilder] OutputFrames is successful.
func TestOutputFrames(t *testing.T) { func TestChannelBuilder_OutputFramesWorks(t *testing.T) {
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
channelConfig.MaxFrameSize = 2 channelConfig.MaxFrameSize = 24
// Construct the channel builder // Construct the channel builder
cb, err := newChannelBuilder(channelConfig) cb, err := newChannelBuilder(channelConfig)
require.NoError(t, err) require.NoError(t, err)
require.False(t, cb.IsFull()) require.False(t, cb.IsFull())
require.Equal(t, 0, cb.NumFrames()) require.Equal(t, 0, cb.NumFrames())
...@@ -400,40 +452,34 @@ func TestOutputFrames(t *testing.T) { ...@@ -400,40 +452,34 @@ func TestOutputFrames(t *testing.T) {
require.NoError(t, cb.OutputFrames()) require.NoError(t, cb.OutputFrames())
// There should be no ready bytes yet // There should be no ready bytes yet
readyBytes := cb.co.ReadyBytes() require.Equal(t, 0, cb.co.ReadyBytes())
require.Equal(t, 0, readyBytes)
// Let's add a block // Let's add a block
err = addMiniBlock(cb) require.NoError(t, addMiniBlock(cb))
require.NoError(t, err) require.NoError(t, cb.co.Flush())
// Check how many ready bytes // Check how many ready bytes
readyBytes = cb.co.ReadyBytes() // There should be more than the max frame size ready
require.Equal(t, 2, readyBytes) require.Greater(t, uint64(cb.co.ReadyBytes()), channelConfig.MaxFrameSize)
require.Equal(t, 0, cb.NumFrames()) require.Equal(t, 0, cb.NumFrames())
// The channel should not be full // The channel should not be full
// but we want to output the frames for testing anyways // but we want to output the frames for testing anyways
isFull := cb.IsFull() require.False(t, cb.IsFull())
require.False(t, isFull)
// Since we manually set the max frame size to 2,
// we should be able to compress the two frames now
err = cb.OutputFrames()
require.NoError(t, err)
// There should be one frame in the channel builder now // We should be able to output the frames
require.Equal(t, 1, cb.NumFrames()) require.NoError(t, cb.OutputFrames())
// There should no longer be any ready bytes // There should be many frames in the channel builder now
readyBytes = cb.co.ReadyBytes() require.Greater(t, cb.NumFrames(), 1)
require.Equal(t, 0, readyBytes) for _, frame := range cb.frames {
require.Len(t, frame.data, int(channelConfig.MaxFrameSize))
}
} }
// TestMaxRLPBytesPerChannel tests the [channelBuilder.OutputFrames] // TestChannelBuilder_MaxRLPBytesPerChannel tests the [channelBuilder.OutputFrames]
// function errors when the max RLP bytes per channel is reached. // function errors when the max RLP bytes per channel is reached.
func TestMaxRLPBytesPerChannel(t *testing.T) { func TestChannelBuilder_MaxRLPBytesPerChannel(t *testing.T) {
t.Parallel() t.Parallel()
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
channelConfig.MaxFrameSize = derive.MaxRLPBytesPerChannel * 2 channelConfig.MaxFrameSize = derive.MaxRLPBytesPerChannel * 2
...@@ -449,13 +495,13 @@ func TestMaxRLPBytesPerChannel(t *testing.T) { ...@@ -449,13 +495,13 @@ func TestMaxRLPBytesPerChannel(t *testing.T) {
require.ErrorIs(t, err, derive.ErrTooManyRLPBytes) require.ErrorIs(t, err, derive.ErrTooManyRLPBytes)
} }
// TestOutputFramesMaxFrameIndex tests the [channelBuilder.OutputFrames] // TestChannelBuilder_OutputFramesMaxFrameIndex tests the [ChannelBuilder.OutputFrames]
// function errors when the max frame index is reached. // function errors when the max frame index is reached.
func TestOutputFramesMaxFrameIndex(t *testing.T) { func TestChannelBuilder_OutputFramesMaxFrameIndex(t *testing.T) {
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
channelConfig.MaxFrameSize = 1 channelConfig.MaxFrameSize = 24
channelConfig.TargetNumFrames = math.MaxInt channelConfig.TargetNumFrames = math.MaxInt
channelConfig.TargetFrameSize = 1 channelConfig.TargetFrameSize = 24
channelConfig.ApproxComprRatio = 0 channelConfig.ApproxComprRatio = 0
// Continuously add blocks until the max frame index is reached // Continuously add blocks until the max frame index is reached
...@@ -477,6 +523,7 @@ func TestOutputFramesMaxFrameIndex(t *testing.T) { ...@@ -477,6 +523,7 @@ func TestOutputFramesMaxFrameIndex(t *testing.T) {
Number: big.NewInt(0), Number: big.NewInt(0),
}, txs, nil, nil, trie.NewStackTrie(nil)) }, txs, nil, nil, trie.NewStackTrie(nil))
_, err = cb.AddBlock(a) _, err = cb.AddBlock(a)
require.NoError(t, cb.co.Flush())
if cb.IsFull() { if cb.IsFull() {
fullErr := cb.FullErr() fullErr := cb.FullErr()
require.ErrorIs(t, fullErr, ErrMaxFrameIndex) require.ErrorIs(t, fullErr, ErrMaxFrameIndex)
...@@ -489,17 +536,15 @@ func TestOutputFramesMaxFrameIndex(t *testing.T) { ...@@ -489,17 +536,15 @@ func TestOutputFramesMaxFrameIndex(t *testing.T) {
} }
} }
// TestBuilderAddBlock tests the AddBlock function // TestChannelBuilder_AddBlock tests the AddBlock function
func TestBuilderAddBlock(t *testing.T) { func TestChannelBuilder_AddBlock(t *testing.T) {
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
// Lower the max frame size so that we can batch // Lower the max frame size so that we can batch
channelConfig.MaxFrameSize = 2 channelConfig.MaxFrameSize = 30
// Configure the Input Threshold params so we observe a full channel // Configure the Input Threshold params so we observe a full channel
// In reality, we only need the input bytes (74) below to be greater than channelConfig.TargetFrameSize = 30
// or equal to the input threshold (3 * 2) / 1 = 6
channelConfig.TargetFrameSize = 3
channelConfig.TargetNumFrames = 2 channelConfig.TargetNumFrames = 2
channelConfig.ApproxComprRatio = 1 channelConfig.ApproxComprRatio = 1
...@@ -508,8 +553,8 @@ func TestBuilderAddBlock(t *testing.T) { ...@@ -508,8 +553,8 @@ func TestBuilderAddBlock(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Add a nonsense block to the channel builder // Add a nonsense block to the channel builder
err = addMiniBlock(cb) require.NoError(t, addMiniBlock(cb))
require.NoError(t, err) require.NoError(t, cb.co.Flush())
// Check the fields reset in the AddBlock function // Check the fields reset in the AddBlock function
require.Equal(t, 74, cb.co.InputBytes()) require.Equal(t, 74, cb.co.InputBytes())
...@@ -519,23 +564,22 @@ func TestBuilderAddBlock(t *testing.T) { ...@@ -519,23 +564,22 @@ func TestBuilderAddBlock(t *testing.T) {
// Since the channel output is full, the next call to AddBlock // Since the channel output is full, the next call to AddBlock
// should return the channel out full error // should return the channel out full error
err = addMiniBlock(cb) require.ErrorIs(t, addMiniBlock(cb), ErrInputTargetReached)
require.ErrorIs(t, err, ErrInputTargetReached)
} }
// TestBuilderReset tests the Reset function // TestChannelBuilder_Reset tests the [Reset] function
func TestBuilderReset(t *testing.T) { func TestChannelBuilder_Reset(t *testing.T) {
channelConfig := defaultTestChannelConfig channelConfig := defaultTestChannelConfig
// Lower the max frame size so that we can batch // Lower the max frame size so that we can batch
channelConfig.MaxFrameSize = 2 channelConfig.MaxFrameSize = 24
cb, err := newChannelBuilder(channelConfig) cb, err := newChannelBuilder(channelConfig)
require.NoError(t, err) require.NoError(t, err)
// Add a nonsense block to the channel builder // Add a nonsense block to the channel builder
err = addMiniBlock(cb) require.NoError(t, addMiniBlock(cb))
require.NoError(t, err) require.NoError(t, cb.co.Flush())
// Check the fields reset in the Reset function // Check the fields reset in the Reset function
require.Equal(t, 1, len(cb.blocks)) require.Equal(t, 1, len(cb.blocks))
...@@ -546,22 +590,20 @@ func TestBuilderReset(t *testing.T) { ...@@ -546,22 +590,20 @@ func TestBuilderReset(t *testing.T) {
require.NoError(t, cb.fullErr) require.NoError(t, cb.fullErr)
// Output frames so we can set the channel builder frames // Output frames so we can set the channel builder frames
err = cb.OutputFrames() require.NoError(t, cb.OutputFrames())
require.NoError(t, err)
// Add another block to increment the block count // Add another block to increment the block count
err = addMiniBlock(cb) require.NoError(t, addMiniBlock(cb))
require.NoError(t, err) require.NoError(t, cb.co.Flush())
// Check the fields reset in the Reset function // Check the fields reset in the Reset function
require.Equal(t, 2, len(cb.blocks)) require.Equal(t, 2, len(cb.blocks))
require.Equal(t, 1, len(cb.frames)) require.Greater(t, len(cb.frames), 1)
require.Equal(t, timeout, cb.timeout) require.Equal(t, timeout, cb.timeout)
require.NoError(t, cb.fullErr) require.NoError(t, cb.fullErr)
// Reset the channel builder // Reset the channel builder
err = cb.Reset() require.NoError(t, cb.Reset())
require.NoError(t, err)
// Check the fields reset in the Reset function // Check the fields reset in the Reset function
require.Equal(t, 0, len(cb.blocks)) require.Equal(t, 0, len(cb.blocks))
......
...@@ -32,8 +32,7 @@ func TestPendingChannelTimeout(t *testing.T) { ...@@ -32,8 +32,7 @@ func TestPendingChannelTimeout(t *testing.T) {
require.False(t, timeout) require.False(t, timeout)
// Set the pending channel // Set the pending channel
err := m.ensurePendingChannel(eth.BlockID{}) require.NoError(t, m.ensurePendingChannel(eth.BlockID{}))
require.NoError(t, err)
// There are no confirmed transactions so // There are no confirmed transactions so
// the pending channel cannot be timed out // the pending channel cannot be timed out
...@@ -85,14 +84,10 @@ func TestChannelManagerReturnsErrReorg(t *testing.T) { ...@@ -85,14 +84,10 @@ func TestChannelManagerReturnsErrReorg(t *testing.T) {
ParentHash: common.Hash{0xff}, ParentHash: common.Hash{0xff},
}, nil, nil, nil, nil) }, nil, nil, nil, nil)
err := m.AddL2Block(a) require.NoError(t, m.AddL2Block(a))
require.NoError(t, err) require.NoError(t, m.AddL2Block(b))
err = m.AddL2Block(b) require.NoError(t, m.AddL2Block(c))
require.NoError(t, err) require.ErrorIs(t, m.AddL2Block(x), ErrReorg)
err = m.AddL2Block(c)
require.NoError(t, err)
err = m.AddL2Block(x)
require.ErrorIs(t, err, ErrReorg)
require.Equal(t, []*types.Block{a, b, c}, m.blocks) require.Equal(t, []*types.Block{a, b, c}, m.blocks)
} }
...@@ -111,16 +106,14 @@ func TestChannelManagerReturnsErrReorgWhenDrained(t *testing.T) { ...@@ -111,16 +106,14 @@ func TestChannelManagerReturnsErrReorgWhenDrained(t *testing.T) {
a := newMiniL2Block(0) a := newMiniL2Block(0)
x := newMiniL2BlockWithNumberParent(0, big.NewInt(1), common.Hash{0xff}) x := newMiniL2BlockWithNumberParent(0, big.NewInt(1), common.Hash{0xff})
err := m.AddL2Block(a) require.NoError(t, m.AddL2Block(a))
require.NoError(t, err)
_, err = m.TxData(eth.BlockID{}) _, err := m.TxData(eth.BlockID{})
require.NoError(t, err) require.NoError(t, err)
_, err = m.TxData(eth.BlockID{}) _, err = m.TxData(eth.BlockID{})
require.ErrorIs(t, err, io.EOF) require.ErrorIs(t, err, io.EOF)
err = m.AddL2Block(x) require.ErrorIs(t, m.AddL2Block(x), ErrReorg)
require.ErrorIs(t, err, ErrReorg)
} }
// TestChannelManagerNextTxData checks the nextTxData function. // TestChannelManagerNextTxData checks the nextTxData function.
...@@ -136,8 +129,7 @@ func TestChannelManagerNextTxData(t *testing.T) { ...@@ -136,8 +129,7 @@ func TestChannelManagerNextTxData(t *testing.T) {
// Set the pending channel // Set the pending channel
// The nextTxData function should still return EOF // The nextTxData function should still return EOF
// since the pending channel has no frames // since the pending channel has no frames
err = m.ensurePendingChannel(eth.BlockID{}) require.NoError(t, m.ensurePendingChannel(eth.BlockID{}))
require.NoError(t, err)
returnedTxData, err = m.nextTxData() returnedTxData, err = m.nextTxData()
require.ErrorIs(t, err, io.EOF) require.ErrorIs(t, err, io.EOF)
require.Equal(t, txData{}, returnedTxData) require.Equal(t, txData{}, returnedTxData)
...@@ -164,8 +156,10 @@ func TestChannelManagerNextTxData(t *testing.T) { ...@@ -164,8 +156,10 @@ func TestChannelManagerNextTxData(t *testing.T) {
require.Equal(t, expectedTxData, m.pendingTransactions[expectedChannelID]) require.Equal(t, expectedTxData, m.pendingTransactions[expectedChannelID])
} }
// TestClearChannelManager tests clearing the channel manager. // TestChannelManager_Clear tests clearing the channel manager.
func TestClearChannelManager(t *testing.T) { func TestChannelManager_Clear(t *testing.T) {
require := require.New(t)
// Create a channel manager // Create a channel manager
log := testlog.Logger(t, log.LvlCrit) log := testlog.Logger(t, log.LvlCrit)
rng := rand.New(rand.NewSource(time.Now().UnixNano())) rng := rand.New(rand.NewSource(time.Now().UnixNano()))
...@@ -176,15 +170,17 @@ func TestClearChannelManager(t *testing.T) { ...@@ -176,15 +170,17 @@ func TestClearChannelManager(t *testing.T) {
ChannelTimeout: 10, ChannelTimeout: 10,
// Have to set the max frame size here otherwise the channel builder would not // Have to set the max frame size here otherwise the channel builder would not
// be able to output any frames // be able to output any frames
MaxFrameSize: 1, MaxFrameSize: 24,
TargetFrameSize: 24,
ApproxComprRatio: 1.0,
}) })
// Channel Manager state should be empty by default // Channel Manager state should be empty by default
require.Empty(t, m.blocks) require.Empty(m.blocks)
require.Equal(t, common.Hash{}, m.tip) require.Equal(common.Hash{}, m.tip)
require.Nil(t, m.pendingChannel) require.Nil(m.pendingChannel)
require.Empty(t, m.pendingTransactions) require.Empty(m.pendingTransactions)
require.Empty(t, m.confirmedTransactions) require.Empty(m.confirmedTransactions)
// Add a block to the channel manager // Add a block to the channel manager
a, _ := derivetest.RandomL2Block(rng, 4) a, _ := derivetest.RandomL2Block(rng, 4)
...@@ -193,28 +189,25 @@ func TestClearChannelManager(t *testing.T) { ...@@ -193,28 +189,25 @@ func TestClearChannelManager(t *testing.T) {
Hash: a.Hash(), Hash: a.Hash(),
Number: a.NumberU64(), Number: a.NumberU64(),
} }
err := m.AddL2Block(a) require.NoError(m.AddL2Block(a))
require.NoError(t, err)
// Make sure there is a channel builder // Make sure there is a channel builder
err = m.ensurePendingChannel(l1BlockID) require.NoError(m.ensurePendingChannel(l1BlockID))
require.NoError(t, err) require.NotNil(m.pendingChannel)
require.NotNil(t, m.pendingChannel) require.Len(m.confirmedTransactions, 0)
require.Equal(t, 0, len(m.confirmedTransactions))
// Process the blocks // Process the blocks
// We should have a pending channel with 1 frame // We should have a pending channel with 1 frame
// and no more blocks since processBlocks consumes // and no more blocks since processBlocks consumes
// the list // the list
err = m.processBlocks() require.NoError(m.processBlocks())
require.NoError(t, err) require.NoError(m.pendingChannel.co.Flush())
err = m.pendingChannel.OutputFrames() require.NoError(m.pendingChannel.OutputFrames())
require.NoError(t, err) _, err := m.nextTxData()
_, err = m.nextTxData() require.NoError(err)
require.NoError(t, err) require.Len(m.blocks, 0)
require.Equal(t, 0, len(m.blocks)) require.Equal(newL1Tip, m.tip)
require.Equal(t, newL1Tip, m.tip) require.Len(m.pendingTransactions, 1)
require.Equal(t, 1, len(m.pendingTransactions))
// Add a new block so we can test clearing // Add a new block so we can test clearing
// the channel manager with a full state // the channel manager with a full state
...@@ -222,20 +215,19 @@ func TestClearChannelManager(t *testing.T) { ...@@ -222,20 +215,19 @@ func TestClearChannelManager(t *testing.T) {
Number: big.NewInt(1), Number: big.NewInt(1),
ParentHash: a.Hash(), ParentHash: a.Hash(),
}, nil, nil, nil, nil) }, nil, nil, nil, nil)
err = m.AddL2Block(b) require.NoError(m.AddL2Block(b))
require.NoError(t, err) require.Len(m.blocks, 1)
require.Equal(t, 1, len(m.blocks)) require.Equal(b.Hash(), m.tip)
require.Equal(t, b.Hash(), m.tip)
// Clear the channel manager // Clear the channel manager
m.Clear() m.Clear()
// Check that the entire channel manager state cleared // Check that the entire channel manager state cleared
require.Empty(t, m.blocks) require.Empty(m.blocks)
require.Equal(t, common.Hash{}, m.tip) require.Equal(common.Hash{}, m.tip)
require.Nil(t, m.pendingChannel) require.Nil(m.pendingChannel)
require.Empty(t, m.pendingTransactions) require.Empty(m.pendingTransactions)
require.Empty(t, m.confirmedTransactions) require.Empty(m.confirmedTransactions)
} }
// TestChannelManagerTxConfirmed checks the [ChannelManager.TxConfirmed] function. // TestChannelManagerTxConfirmed checks the [ChannelManager.TxConfirmed] function.
...@@ -251,8 +243,7 @@ func TestChannelManagerTxConfirmed(t *testing.T) { ...@@ -251,8 +243,7 @@ func TestChannelManagerTxConfirmed(t *testing.T) {
// Let's add a valid pending transaction to the channel manager // Let's add a valid pending transaction to the channel manager
// So we can demonstrate that TxConfirmed's correctness // So we can demonstrate that TxConfirmed's correctness
err := m.ensurePendingChannel(eth.BlockID{}) require.NoError(t, m.ensurePendingChannel(eth.BlockID{}))
require.NoError(t, err)
channelID := m.pendingChannel.ID() channelID := m.pendingChannel.ID()
frame := frameData{ frame := frameData{
data: []byte{}, data: []byte{},
...@@ -270,7 +261,7 @@ func TestChannelManagerTxConfirmed(t *testing.T) { ...@@ -270,7 +261,7 @@ func TestChannelManagerTxConfirmed(t *testing.T) {
require.Equal(t, expectedTxData, returnedTxData) require.Equal(t, expectedTxData, returnedTxData)
require.Equal(t, 0, m.pendingChannel.NumFrames()) require.Equal(t, 0, m.pendingChannel.NumFrames())
require.Equal(t, expectedTxData, m.pendingTransactions[expectedChannelID]) require.Equal(t, expectedTxData, m.pendingTransactions[expectedChannelID])
require.Equal(t, 1, len(m.pendingTransactions)) require.Len(t, m.pendingTransactions, 1)
// An unknown pending transaction should not be marked as confirmed // An unknown pending transaction should not be marked as confirmed
// and should not be removed from the pending transactions map // and should not be removed from the pending transactions map
...@@ -281,14 +272,14 @@ func TestChannelManagerTxConfirmed(t *testing.T) { ...@@ -281,14 +272,14 @@ func TestChannelManagerTxConfirmed(t *testing.T) {
blockID := eth.BlockID{Number: 0, Hash: common.Hash{0x69}} blockID := eth.BlockID{Number: 0, Hash: common.Hash{0x69}}
m.TxConfirmed(unknownTxID, blockID) m.TxConfirmed(unknownTxID, blockID)
require.Empty(t, m.confirmedTransactions) require.Empty(t, m.confirmedTransactions)
require.Equal(t, 1, len(m.pendingTransactions)) require.Len(t, m.pendingTransactions, 1)
// Now let's mark the pending transaction as confirmed // Now let's mark the pending transaction as confirmed
// and check that it is removed from the pending transactions map // and check that it is removed from the pending transactions map
// and added to the confirmed transactions map // and added to the confirmed transactions map
m.TxConfirmed(expectedChannelID, blockID) m.TxConfirmed(expectedChannelID, blockID)
require.Empty(t, m.pendingTransactions) require.Empty(t, m.pendingTransactions)
require.Equal(t, 1, len(m.confirmedTransactions)) require.Len(t, m.confirmedTransactions, 1)
require.Equal(t, blockID, m.confirmedTransactions[expectedChannelID]) require.Equal(t, blockID, m.confirmedTransactions[expectedChannelID])
} }
...@@ -300,8 +291,7 @@ func TestChannelManagerTxFailed(t *testing.T) { ...@@ -300,8 +291,7 @@ func TestChannelManagerTxFailed(t *testing.T) {
// Let's add a valid pending transaction to the channel // Let's add a valid pending transaction to the channel
// manager so we can demonstrate correctness // manager so we can demonstrate correctness
err := m.ensurePendingChannel(eth.BlockID{}) require.NoError(t, m.ensurePendingChannel(eth.BlockID{}))
require.NoError(t, err)
channelID := m.pendingChannel.ID() channelID := m.pendingChannel.ID()
frame := frameData{ frame := frameData{
data: []byte{}, data: []byte{},
...@@ -319,7 +309,7 @@ func TestChannelManagerTxFailed(t *testing.T) { ...@@ -319,7 +309,7 @@ func TestChannelManagerTxFailed(t *testing.T) {
require.Equal(t, expectedTxData, returnedTxData) require.Equal(t, expectedTxData, returnedTxData)
require.Equal(t, 0, m.pendingChannel.NumFrames()) require.Equal(t, 0, m.pendingChannel.NumFrames())
require.Equal(t, expectedTxData, m.pendingTransactions[expectedChannelID]) require.Equal(t, expectedTxData, m.pendingTransactions[expectedChannelID])
require.Equal(t, 1, len(m.pendingTransactions)) require.Len(t, m.pendingTransactions, 1)
// Trying to mark an unknown pending transaction as failed // Trying to mark an unknown pending transaction as failed
// shouldn't modify state // shouldn't modify state
...@@ -348,8 +338,7 @@ func TestChannelManager_TxResend(t *testing.T) { ...@@ -348,8 +338,7 @@ func TestChannelManager_TxResend(t *testing.T) {
a, _ := derivetest.RandomL2Block(rng, 4) a, _ := derivetest.RandomL2Block(rng, 4)
err := m.AddL2Block(a) require.NoError(m.AddL2Block(a))
require.NoError(err)
txdata0, err := m.TxData(eth.BlockID{}) txdata0, err := m.TxData(eth.BlockID{})
require.NoError(err) require.NoError(err)
......
...@@ -110,6 +110,14 @@ type CLIConfig struct { ...@@ -110,6 +110,14 @@ type CLIConfig struct {
/* Optional Params */ /* Optional Params */
// TxManagerTimeout is the max amount of time to wait for the [txmgr].
// This will default to: 10 * time.Minute.
TxManagerTimeout time.Duration
// OfflineGasEstimation specifies whether the batcher should calculate
// gas estimations offline using the [core.IntrinsicGas] function.
OfflineGasEstimation bool
// MaxL1TxSize is the maximum size of a batch tx submitted to L1. // MaxL1TxSize is the maximum size of a batch tx submitted to L1.
MaxL1TxSize uint64 MaxL1TxSize uint64
...@@ -168,19 +176,21 @@ func NewConfig(ctx *cli.Context) CLIConfig { ...@@ -168,19 +176,21 @@ func NewConfig(ctx *cli.Context) CLIConfig {
ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name), ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name),
/* Optional Flags */ /* Optional Flags */
MaxChannelDuration: ctx.GlobalUint64(flags.MaxChannelDurationFlag.Name), OfflineGasEstimation: ctx.GlobalBool(flags.OfflineGasEstimationFlag.Name),
MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeBytesFlag.Name), TxManagerTimeout: ctx.GlobalDuration(flags.TxManagerTimeoutFlag.Name),
TargetL1TxSize: ctx.GlobalUint64(flags.TargetL1TxSizeBytesFlag.Name), MaxChannelDuration: ctx.GlobalUint64(flags.MaxChannelDurationFlag.Name),
TargetNumFrames: ctx.GlobalInt(flags.TargetNumFramesFlag.Name), MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeBytesFlag.Name),
ApproxComprRatio: ctx.GlobalFloat64(flags.ApproxComprRatioFlag.Name), TargetL1TxSize: ctx.GlobalUint64(flags.TargetL1TxSizeBytesFlag.Name),
Stopped: ctx.GlobalBool(flags.StoppedFlag.Name), TargetNumFrames: ctx.GlobalInt(flags.TargetNumFramesFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name), ApproxComprRatio: ctx.GlobalFloat64(flags.ApproxComprRatioFlag.Name),
SequencerHDPath: ctx.GlobalString(flags.SequencerHDPathFlag.Name), Stopped: ctx.GlobalBool(flags.StoppedFlag.Name),
PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name), Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
RPCConfig: rpc.ReadCLIConfig(ctx), SequencerHDPath: ctx.GlobalString(flags.SequencerHDPathFlag.Name),
LogConfig: oplog.ReadCLIConfig(ctx), PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name),
MetricsConfig: opmetrics.ReadCLIConfig(ctx), RPCConfig: rpc.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx), LogConfig: oplog.ReadCLIConfig(ctx),
SignerConfig: opsigner.ReadCLIConfig(ctx), MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
SignerConfig: opsigner.ReadCLIConfig(ctx),
} }
} }
...@@ -15,6 +15,7 @@ import ( ...@@ -15,6 +15,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -24,7 +25,7 @@ import ( ...@@ -24,7 +25,7 @@ import (
type BatchSubmitter struct { type BatchSubmitter struct {
Config // directly embed the config + sources Config // directly embed the config + sources
txMgr *TransactionManager txMgr txmgr.TxManager
wg sync.WaitGroup wg sync.WaitGroup
done chan struct{} done chan struct{}
...@@ -79,6 +80,7 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger, m metrics.Metri ...@@ -79,6 +80,7 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger, m metrics.Metri
NumConfirmations: cfg.NumConfirmations, NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount, SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
From: fromAddress, From: fromAddress,
ChainID: rcfg.L1ChainID,
Signer: signer(rcfg.L1ChainID), Signer: signer(rcfg.L1ChainID),
} }
...@@ -125,10 +127,8 @@ func NewBatchSubmitter(ctx context.Context, cfg Config, l log.Logger, m metrics. ...@@ -125,10 +127,8 @@ func NewBatchSubmitter(ctx context.Context, cfg Config, l log.Logger, m metrics.
return &BatchSubmitter{ return &BatchSubmitter{
Config: cfg, Config: cfg,
txMgr: NewTransactionManager(l, txMgr: txmgr.NewSimpleTxManager("batcher", l, cfg.TxManagerConfig, cfg.L1Client),
cfg.TxManagerConfig, cfg.Rollup.BatchInboxAddress, cfg.Rollup.L1ChainID, state: NewChannelManager(l, m, cfg.Channel),
cfg.From, cfg.L1Client),
state: NewChannelManager(l, m, cfg.Channel),
}, nil }, nil
} }
...@@ -226,7 +226,7 @@ func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context) { ...@@ -226,7 +226,7 @@ func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context) {
// loadBlockIntoState fetches & stores a single block into `state`. It returns the block it loaded. // loadBlockIntoState fetches & stores a single block into `state`. It returns the block it loaded.
func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uint64) (*types.Block, error) { func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uint64) (*types.Block, error) {
ctx, cancel := context.WithTimeout(ctx, networkTimeout) ctx, cancel := context.WithTimeout(ctx, txManagerTimeout)
defer cancel() defer cancel()
block, err := l.L2Client.BlockByNumber(ctx, new(big.Int).SetUint64(blockNumber)) block, err := l.L2Client.BlockByNumber(ctx, new(big.Int).SetUint64(blockNumber))
if err != nil { if err != nil {
...@@ -244,7 +244,7 @@ func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uin ...@@ -244,7 +244,7 @@ func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uin
// calculateL2BlockRangeToStore determines the range (start,end] that should be loaded into the local state. // calculateL2BlockRangeToStore determines the range (start,end] that should be loaded into the local state.
// It also takes care of initializing some local state (i.e. will modify l.lastStoredBlock in certain conditions) // It also takes care of initializing some local state (i.e. will modify l.lastStoredBlock in certain conditions)
func (l *BatchSubmitter) calculateL2BlockRangeToStore(ctx context.Context) (eth.BlockID, eth.BlockID, error) { func (l *BatchSubmitter) calculateL2BlockRangeToStore(ctx context.Context) (eth.BlockID, eth.BlockID, error) {
childCtx, cancel := context.WithTimeout(ctx, networkTimeout) childCtx, cancel := context.WithTimeout(ctx, txManagerTimeout)
defer cancel() defer cancel()
syncStatus, err := l.RollupNode.SyncStatus(childCtx) syncStatus, err := l.RollupNode.SyncStatus(childCtx)
// Ensure that we have the sync status // Ensure that we have the sync status
...@@ -312,8 +312,9 @@ func (l *BatchSubmitter) loop() { ...@@ -312,8 +312,9 @@ func (l *BatchSubmitter) loop() {
l.log.Error("unable to get tx data", "err", err) l.log.Error("unable to get tx data", "err", err)
break break
} }
// Record TX Status // Record TX Status
if receipt, err := l.txMgr.SendTransaction(l.ctx, txdata.Bytes()); err != nil { if receipt, err := l.SendTransaction(l.ctx, txdata.Bytes()); err != nil {
l.recordFailedTx(txdata.ID(), err) l.recordFailedTx(txdata.ID(), err)
} else { } else {
l.recordConfirmedTx(txdata.ID(), receipt) l.recordConfirmedTx(txdata.ID(), receipt)
...@@ -335,6 +336,46 @@ func (l *BatchSubmitter) loop() { ...@@ -335,6 +336,46 @@ func (l *BatchSubmitter) loop() {
} }
} }
const networkTimeout = 2 * time.Second // How long a single network request can take. TODO: put in a config somewhere
// fix(refcell):
// combined with above, these config variables should also be replicated in the op-proposer
// along with op-proposer changes to include the updated tx manager
const txManagerTimeout = 2 * time.Minute // How long the tx manager can take to send a transaction.
// SendTransaction creates & submits a transaction to the batch inbox address with the given `data`.
// It currently uses the underlying `txmgr` to handle transaction sending & price management.
// This is a blocking method. It should not be called concurrently.
func (l *BatchSubmitter) SendTransaction(ctx context.Context, data []byte) (*types.Receipt, error) {
// Do the gas estimation offline. A value of 0 will cause the [txmgr] to estimate the gas limit.
intrinsicGas, err := core.IntrinsicGas(data, nil, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to calculate intrinsic gas: %w", err)
}
// Create the transaction
tx, err := l.txMgr.CraftTx(ctx, txmgr.TxCandidate{
To: l.Rollup.BatchInboxAddress,
TxData: data,
From: l.From,
GasLimit: intrinsicGas,
})
if err != nil {
return nil, fmt.Errorf("failed to create tx: %w", err)
}
// Send the transaction through the txmgr
ctx, cancel := context.WithTimeout(ctx, txManagerTimeout)
defer cancel()
if receipt, err := l.txMgr.Send(ctx, tx); err != nil {
l.log.Warn("unable to publish tx", "err", err, "data_size", len(data))
return nil, err
} else {
l.log.Info("tx successfully published", "tx_hash", receipt.TxHash, "data_size", len(data))
return receipt, nil
}
}
func (l *BatchSubmitter) recordL1Tip(l1tip eth.L1BlockRef) { func (l *BatchSubmitter) recordL1Tip(l1tip eth.L1BlockRef) {
if l.lastL1Tip == l1tip { if l.lastL1Tip == l1tip {
return return
......
package batcher
import (
"context"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txmgr/mocks"
)
// TestBatchSubmitter_SendTransaction tests the driver's
// [SendTransaction] external facing function.
func TestBatchSubmitter_SendTransaction(t *testing.T) {
log := testlog.Logger(t, log.LvlCrit)
txMgr := mocks.TxManager{}
batcherInboxAddress := common.HexToAddress("0x42000000000000000000000000000000000000ff")
chainID := big.NewInt(1)
sender := common.HexToAddress("0xdeadbeef")
bs := BatchSubmitter{
Config: Config{
log: log,
From: sender,
Rollup: &rollup.Config{
L1ChainID: chainID,
BatchInboxAddress: batcherInboxAddress,
},
},
txMgr: &txMgr,
}
txData := []byte{0x00, 0x01, 0x02}
gasTipCap := big.NewInt(136)
gasFeeCap := big.NewInt(137)
gas := uint64(1337)
// Candidate gas should be calculated with [core.IntrinsicGas]
intrinsicGas, err := core.IntrinsicGas(txData, nil, false, true, true, false)
require.NoError(t, err)
candidate := txmgr.TxCandidate{
To: batcherInboxAddress,
TxData: txData,
From: sender,
GasLimit: intrinsicGas,
}
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainID,
Nonce: 0,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: gas,
To: &batcherInboxAddress,
Data: txData,
})
txHash := tx.Hash()
expectedReceipt := types.Receipt{
Type: 1,
PostState: []byte{},
Status: uint64(1),
CumulativeGasUsed: gas,
TxHash: txHash,
GasUsed: gas,
}
txMgr.On("CraftTx", mock.Anything, candidate).Return(tx, nil)
txMgr.On("Send", mock.Anything, tx).Return(&expectedReceipt, nil)
receipt, err := bs.SendTransaction(context.Background(), tx.Data())
require.NoError(t, err)
require.Equal(t, receipt, &expectedReceipt)
}
package batcher
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
)
const networkTimeout = 2 * time.Second // How long a single network request can take. TODO: put in a config somewhere
// TransactionManager wraps the simple txmgr package to make it easy to send & wait for transactions
type TransactionManager struct {
// Config
batchInboxAddress common.Address
senderAddress common.Address
chainID *big.Int
// Outside world
txMgr txmgr.TxManager
l1Client *ethclient.Client
signerFn opcrypto.SignerFn
log log.Logger
}
func NewTransactionManager(log log.Logger, txMgrConfg txmgr.Config, batchInboxAddress common.Address, chainID *big.Int, senderAddress common.Address, l1Client *ethclient.Client) *TransactionManager {
t := &TransactionManager{
batchInboxAddress: batchInboxAddress,
senderAddress: senderAddress,
chainID: chainID,
txMgr: txmgr.NewSimpleTxManager("batcher", log, txMgrConfg, l1Client),
l1Client: l1Client,
signerFn: txMgrConfg.Signer,
log: log,
}
return t
}
// SendTransaction creates & submits a transaction to the batch inbox address with the given `data`.
// It currently uses the underlying `txmgr` to handle transaction sending & price management.
// This is a blocking method. It should not be called concurrently.
// TODO: where to put concurrent transaction handling logic.
func (t *TransactionManager) SendTransaction(ctx context.Context, data []byte) (*types.Receipt, error) {
tx, err := t.CraftTx(ctx, data)
if err != nil {
return nil, fmt.Errorf("failed to create tx: %w", err)
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) // TODO: Select a timeout that makes sense here.
defer cancel()
if receipt, err := t.txMgr.Send(ctx, tx); err != nil {
t.log.Warn("unable to publish tx", "err", err, "data_size", len(data))
return nil, err
} else {
t.log.Info("tx successfully published", "tx_hash", receipt.TxHash, "data_size", len(data))
return receipt, nil
}
}
// calcGasTipAndFeeCap queries L1 to determine what a suitable miner tip & basefee limit would be for timely inclusion
func (t *TransactionManager) calcGasTipAndFeeCap(ctx context.Context) (gasTipCap *big.Int, gasFeeCap *big.Int, err error) {
childCtx, cancel := context.WithTimeout(ctx, networkTimeout)
gasTipCap, err = t.l1Client.SuggestGasTipCap(childCtx)
cancel()
if err != nil {
return nil, nil, fmt.Errorf("failed to get suggested gas tip cap: %w", err)
}
if gasTipCap == nil {
t.log.Warn("unexpected unset gasTipCap, using default 2 gwei")
gasTipCap = new(big.Int).SetUint64(params.GWei * 2)
}
childCtx, cancel = context.WithTimeout(ctx, networkTimeout)
head, err := t.l1Client.HeaderByNumber(childCtx, nil)
cancel()
if err != nil || head == nil {
return nil, nil, fmt.Errorf("failed to get L1 head block for fee cap: %w", err)
}
if head.BaseFee == nil {
return nil, nil, fmt.Errorf("failed to get L1 basefee in block %d for fee cap", head.Number)
}
gasFeeCap = txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
return gasTipCap, gasFeeCap, nil
}
// CraftTx creates the signed transaction to the batchInboxAddress.
// It queries L1 for the current fee market conditions as well as for the nonce.
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (t *TransactionManager) CraftTx(ctx context.Context, data []byte) (*types.Transaction, error) {
gasTipCap, gasFeeCap, err := t.calcGasTipAndFeeCap(ctx)
if err != nil {
return nil, err
}
childCtx, cancel := context.WithTimeout(ctx, networkTimeout)
nonce, err := t.l1Client.NonceAt(childCtx, t.senderAddress, nil)
cancel()
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}
rawTx := &types.DynamicFeeTx{
ChainID: t.chainID,
Nonce: nonce,
To: &t.batchInboxAddress,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: data,
}
t.log.Info("creating tx", "to", rawTx.To, "from", t.senderAddress)
gas, err := core.IntrinsicGas(rawTx.Data, nil, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to calculate intrinsic gas: %w", err)
}
rawTx.Gas = gas
ctx, cancel = context.WithTimeout(ctx, networkTimeout)
defer cancel()
tx := types.NewTx(rawTx)
return t.signerFn(ctx, t.senderAddress, tx)
}
package flags package flags
import ( import (
"time"
"github.com/urfave/cli" "github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-batcher/rpc" "github.com/ethereum-optimism/optimism/op-batcher/rpc"
...@@ -74,7 +76,17 @@ var ( ...@@ -74,7 +76,17 @@ var (
} }
/* Optional flags */ /* Optional flags */
OfflineGasEstimationFlag = cli.BoolFlag{
Name: "offline-gas-estimation",
Usage: "Whether to use offline gas estimation",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "OFFLINE_GAS_ESTIMATION"),
}
TxManagerTimeoutFlag = cli.DurationFlag{
Name: "tx-manager-timeout",
Usage: "Maximum duration to wait for L1 transactions, including resubmissions",
Value: 10 * time.Minute,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "TX_MANAGER_TIMEOUT"),
}
MaxChannelDurationFlag = cli.Uint64Flag{ MaxChannelDurationFlag = cli.Uint64Flag{
Name: "max-channel-duration", Name: "max-channel-duration",
Usage: "The maximum duration of L1-blocks to keep a channel open. 0 to disable.", Usage: "The maximum duration of L1-blocks to keep a channel open. 0 to disable.",
...@@ -141,6 +153,8 @@ var requiredFlags = []cli.Flag{ ...@@ -141,6 +153,8 @@ var requiredFlags = []cli.Flag{
} }
var optionalFlags = []cli.Flag{ var optionalFlags = []cli.Flag{
OfflineGasEstimationFlag,
TxManagerTimeoutFlag,
MaxChannelDurationFlag, MaxChannelDurationFlag,
MaxL1TxSizeBytesFlag, MaxL1TxSizeBytesFlag,
TargetL1TxSizeBytesFlag, TargetL1TxSizeBytesFlag,
......
...@@ -185,7 +185,7 @@ func (m *Metrics) RecordLatestL1Block(l1ref eth.L1BlockRef) { ...@@ -185,7 +185,7 @@ func (m *Metrics) RecordLatestL1Block(l1ref eth.L1BlockRef) {
m.RecordL1Ref("latest", l1ref) m.RecordL1Ref("latest", l1ref)
} }
// RecordL2BlockLoaded should be called when a new L2 block was loaded into the // RecordL2BlocksLoaded should be called when a new L2 block was loaded into the
// channel manager (but not processed yet). // channel manager (but not processed yet).
func (m *Metrics) RecordL2BlocksLoaded(l2ref eth.L2BlockRef) { func (m *Metrics) RecordL2BlocksLoaded(l2ref eth.L2BlockRef) {
m.RecordL2Ref(StageLoaded, l2ref) m.RecordL2Ref(StageLoaded, l2ref)
......
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package legacy_bindings
import (
"errors"
"math/big"
"strings"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
)
// Reference imports to suppress errors if they are not otherwise used.
var (
_ = errors.New
_ = big.NewInt
_ = strings.NewReader
_ = ethereum.NotFound
_ = bind.Bind
_ = common.Big1
_ = types.BloomLookup
_ = event.NewSubscription
)
// LibOVMCodecQueueElement is an auto generated low-level Go binding around an user-defined struct.
type LibOVMCodecQueueElement struct {
TransactionHash [32]byte
Timestamp *big.Int
BlockNumber *big.Int
}
// CanonicalTransactionChainMetaData contains all meta data concerning the CanonicalTransactionChain contract.
var CanonicalTransactionChainMetaData = &bind.MetaData{
ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_libAddressManager\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_maxTransactionGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_l2GasDiscountDivisor\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_enqueueGasCost\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"l2GasDiscountDivisor\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"enqueueGasCost\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"enqueueL2GasPrepaid\",\"type\":\"uint256\"}],\"name\":\"L2GasParamsUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_startingQueueIndex\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_numQueueElements\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_totalElements\",\"type\":\"uint256\"}],\"name\":\"QueueBatchAppended\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_startingQueueIndex\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_numQueueElements\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_totalElements\",\"type\":\"uint256\"}],\"name\":\"SequencerBatchAppended\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"_batchIndex\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"_batchRoot\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_batchSize\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_prevTotalElements\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"_extraData\",\"type\":\"bytes\"}],\"name\":\"TransactionBatchAppended\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_l1TxOrigin\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_gasLimit\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"_queueIndex\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_timestamp\",\"type\":\"uint256\"}],\"name\":\"TransactionEnqueued\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"MAX_ROLLUP_TX_SIZE\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MIN_ROLLUP_TX_GAS\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"appendSequencerBatch\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"batches\",\"outputs\":[{\"internalType\":\"contractIChainStorageContainer\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_gasLimit\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"enqueue\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"enqueueGasCost\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"enqueueL2GasPrepaid\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockNumber\",\"outputs\":[{\"internalType\":\"uint40\",\"name\":\"\",\"type\":\"uint40\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastTimestamp\",\"outputs\":[{\"internalType\":\"uint40\",\"name\":\"\",\"type\":\"uint40\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getNextQueueIndex\",\"outputs\":[{\"internalType\":\"uint40\",\"name\":\"\",\"type\":\"uint40\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getNumPendingQueueElements\",\"outputs\":[{\"internalType\":\"uint40\",\"name\":\"\",\"type\":\"uint40\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_index\",\"type\":\"uint256\"}],\"name\":\"getQueueElement\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"transactionHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint40\",\"name\":\"timestamp\",\"type\":\"uint40\"},{\"internalType\":\"uint40\",\"name\":\"blockNumber\",\"type\":\"uint40\"}],\"internalType\":\"structLib_OVMCodec.QueueElement\",\"name\":\"_element\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getQueueLength\",\"outputs\":[{\"internalType\":\"uint40\",\"name\":\"\",\"type\":\"uint40\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTotalBatches\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"_totalBatches\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTotalElements\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"_totalElements\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l2GasDiscountDivisor\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"libAddressManager\",\"outputs\":[{\"internalType\":\"contractLib_AddressManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"maxTransactionGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"_name\",\"type\":\"string\"}],\"name\":\"resolve\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_l2GasDiscountDivisor\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_enqueueGasCost\",\"type\":\"uint256\"}],\"name\":\"setGasParams\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]",
Bin: "0x608060405234801561001057600080fd5b5060405162001a9838038062001a9883398101604081905261003191610072565b600080546001600160a01b0319166001600160a01b03861617905560048390556002829055600181905561006581836100bd565b600355506100ea92505050565b6000806000806080858703121561008857600080fd5b84516001600160a01b038116811461009f57600080fd5b60208601516040870151606090970151919890975090945092505050565b60008160001904831182151516156100e557634e487b7160e01b600052601160045260246000fd5b500290565b61199e80620000fa6000396000f3fe608060405234801561001057600080fd5b506004361061016c5760003560e01c8063876ed5cb116100cd578063d0f8934411610081578063e654b1fb11610066578063e654b1fb146102c0578063edcc4a45146102c9578063f722b41a146102dc57600080fd5b8063d0f89344146102b0578063e561dddc146102b857600080fd5b8063b8f77005116100b2578063b8f7700514610297578063ccf987c81461029f578063cfdf677e146102a857600080fd5b8063876ed5cb146102855780638d38c6c11461028e57600080fd5b80635ae6256d1161012457806378f4b2f21161010957806378f4b2f2146102645780637a167a8a1461026e5780637aa63a861461027d57600080fd5b80635ae6256d146102475780636fee07e01461024f57600080fd5b80632a7f18be116101555780632a7f18be146101d25780633789977014610216578063461a44781461023457600080fd5b80630b3dfa9714610171578063299ca4781461018d575b600080fd5b61017a60035481565b6040519081526020015b60405180910390f35b6000546101ad9073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610184565b6101e56101e03660046113e5565b6102e4565b604080518251815260208084015164ffffffffff908116918301919091529282015190921690820152606001610184565b61021e610362565b60405164ffffffffff9091168152602001610184565b6101ad6102423660046114c1565b610376565b61021e610423565b61026261025d366004611537565b610437565b005b61017a620186a081565b60055464ffffffffff1661021e565b61017a610899565b61017a61c35081565b61017a60045481565b60065461021e565b61017a60025481565b6101ad6108b4565b6102626108dc565b61017a610df8565b61017a60015481565b6102626102d73660046115a4565b610e7f565b61021e611016565b604080516060810182526000808252602082018190529181019190915260068281548110610314576103146115c6565b6000918252602091829020604080516060810182526002909302909101805483526001015464ffffffffff808216948401949094526501000000000090049092169181019190915292915050565b60008061036d611032565b50949350505050565b600080546040517fbf40fac100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091169063bf40fac1906103cd908590600401611660565b60206040518083038186803b1580156103e557600080fd5b505afa1580156103f9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061041d919061167a565b92915050565b60008061042e611032565b95945050505050565b61c350815111156104cf576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603d60248201527f5472616e73616374696f6e20646174612073697a652065786365656473206d6160448201527f78696d756d20666f7220726f6c6c7570207472616e73616374696f6e2e00000060648201526084015b60405180910390fd5b600454821115610561576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603d60248201527f5472616e73616374696f6e20676173206c696d69742065786365656473206d6160448201527f78696d756d20666f7220726f6c6c7570207472616e73616374696f6e2e00000060648201526084016104c6565b620186a08210156105f4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602960248201527f5472616e73616374696f6e20676173206c696d697420746f6f206c6f7720746f60448201527f20656e71756575652e000000000000000000000000000000000000000000000060648201526084016104c6565b6003548211156106dc5760006002546003548461061191906116c6565b61061b91906116dd565b905060005a90508181116106b1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e73756666696369656e742067617320666f72204c322072617465206c696d60448201527f6974696e67206275726e2e00000000000000000000000000000000000000000060648201526084016104c6565b60005b825a6106c090846116c6565b10156106d857806106d081611718565b9150506106b4565b5050505b6000333214156106ed575033610706565b5033731111000000000000000000000000000000001111015b60008185858560405160200161071f9493929190611751565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0818403018152828252805160209182012060608401835280845264ffffffffff42811692850192835243811693850193845260068054600181810183556000838152975160029092027ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f81019290925594517ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d4090910180549651841665010000000000027fffffffffffffffffffffffffffffffffffffffffffff0000000000000000000090971691909316179490941790559154919350610825916116c6565b9050808673ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f4b388aecf9fa6cc92253704e5975a6129a4f735bdbd99567df4ed0094ee4ceb58888426040516108899392919061179a565b60405180910390a4505050505050565b6000806108a4611032565b50505064ffffffffff1692915050565b60006108d760405180606001604052806021815260200161194860219139610376565b905090565b60043560d81c60093560e890811c90600c35901c6108f8610899565b8364ffffffffff161461098d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603d60248201527f41637475616c20626174636820737461727420696e64657820646f6573206e6f60448201527f74206d6174636820657870656374656420737461727420696e6465782e00000060648201526084016104c6565b6109cb6040518060400160405280600d81526020017f4f564d5f53657175656e63657200000000000000000000000000000000000000815250610376565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610a85576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f46756e6374696f6e2063616e206f6e6c792062652063616c6c6564206279207460448201527f68652053657175656e6365722e0000000000000000000000000000000000000060648201526084016104c6565b6000610a9762ffffff831660106117c3565b610aa290600f611800565b905064ffffffffff8116361015610b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f4e6f7420656e6f756768204261746368436f6e74657874732070726f7669646560448201527f642e00000000000000000000000000000000000000000000000000000000000060648201526084016104c6565b6005546040805160808101825260008082526020820181905291810182905260608101829052909164ffffffffff169060005b8562ffffff168163ffffffff161015610bcc576000610b928263ffffffff166110ed565b8051909350839150610ba49086611818565b9450826020015184610bb69190611840565b9350508080610bc490611860565b915050610b6e565b5060065464ffffffffff83161115610c8c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f417474656d7074656420746f20617070656e64206d6f726520656c656d656e7460448201527f73207468616e2061726520617661696c61626c6520696e20746865207175657560648201527f652e000000000000000000000000000000000000000000000000000000000000608482015260a4016104c6565b6000610c9d8462ffffff8916611884565b63ffffffff169050600080836020015160001415610cc657505060408201516060830151610d37565b60006006610cd56001886118a9565b64ffffffffff1681548110610cec57610cec6115c6565b6000918252602091829020604080516060810182526002909302909101805483526001015464ffffffffff808216948401859052650100000000009091041691018190529093509150505b610d5b610d456001436116c6565b408a62ffffff168564ffffffffff168585611174565b7f602f1aeac0ca2e7a13e281a9ef0ad7838542712ce16780fa2ecffd351f05f899610d8684876118a9565b84610d8f610899565b6040805164ffffffffff94851681529390921660208401529082015260600160405180910390a15050600580547fffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000001664ffffffffff949094169390931790925550505050505050565b6000610e026108b4565b73ffffffffffffffffffffffffffffffffffffffff16631f7b6d326040518163ffffffff1660e01b815260040160206040518083038186803b158015610e4757600080fd5b505afa158015610e5b573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108d791906118c7565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b815260040160206040518083038186803b158015610ee557600080fd5b505afa158015610ef9573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f1d919061167a565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610fb1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f6e6c792063616c6c61626c6520627920746865204275726e2041646d696e2e60448201526064016104c6565b60018190556002829055610fc581836117c3565b60038190556002546001546040805192835260208301919091528101919091527fc6ed75e96b8b18b71edc1a6e82a9d677f8268c774a262c624eeb2cf0a8b3e07e9060600160405180910390a15050565b6005546006546000916108d79164ffffffffff909116906118a9565b60008060008060006110426108b4565b73ffffffffffffffffffffffffffffffffffffffff1663ccf8f9696040518163ffffffff1660e01b815260040160206040518083038186803b15801561108757600080fd5b505afa15801561109b573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110bf91906118e0565b64ffffffffff602882901c811697605083901c82169750607883901c8216965060a09290921c169350915050565b6111186040518060800160405280600081526020016000815260200160008152602001600081525090565b60006111256010846117c3565b61113090600f611800565b60408051608081018252823560e890811c82526003840135901c6020820152600683013560d890811c92820192909252600b90920135901c60608201529392505050565b600061117e6108b4565b905060008061118b611032565b50509150915060006040518060a001604052808573ffffffffffffffffffffffffffffffffffffffff16631f7b6d326040518163ffffffff1660e01b815260040160206040518083038186803b1580156111e457600080fd5b505afa1580156111f8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061121c91906118c7565b81526020018a81526020018981526020018464ffffffffff16815260200160405180602001604052806000815250815250905080600001517f127186556e7be68c7e31263195225b4de02820707889540969f62c05cf73525e82602001518360400151846060015185608001516040516112999493929190611922565b60405180910390a260006112ac8261139f565b905060006112e78360400151866112c39190611840565b6112cd8b87611840565b602890811b9190911760508b901b1760788a901b17901b90565b6040517f2015276c000000000000000000000000000000000000000000000000000000008152600481018490527fffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000008216602482015290915073ffffffffffffffffffffffffffffffffffffffff871690632015276c90604401600060405180830381600087803b15801561137a57600080fd5b505af115801561138e573d6000803e3d6000fd5b505050505050505050505050505050565b600081602001518260400151836060015184608001516040516020016113c89493929190611922565b604051602081830303815290604052805190602001209050919050565b6000602082840312156113f757600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600067ffffffffffffffff80841115611448576114486113fe565b604051601f85017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190828211818310171561148e5761148e6113fe565b816040528093508581528686860111156114a757600080fd5b858560208301376000602087830101525050509392505050565b6000602082840312156114d357600080fd5b813567ffffffffffffffff8111156114ea57600080fd5b8201601f810184136114fb57600080fd5b61150a8482356020840161142d565b949350505050565b73ffffffffffffffffffffffffffffffffffffffff8116811461153457600080fd5b50565b60008060006060848603121561154c57600080fd5b833561155781611512565b925060208401359150604084013567ffffffffffffffff81111561157a57600080fd5b8401601f8101861361158b57600080fd5b61159a8682356020840161142d565b9150509250925092565b600080604083850312156115b757600080fd5b50508035926020909101359150565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6000815180845260005b8181101561161b576020818501810151868301820152016115ff565b8181111561162d576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b60208152600061167360208301846115f5565b9392505050565b60006020828403121561168c57600080fd5b815161167381611512565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000828210156116d8576116d8611697565b500390565b600082611713577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82141561174a5761174a611697565b5060010190565b600073ffffffffffffffffffffffffffffffffffffffff80871683528086166020840152508360408301526080606083015261179060808301846115f5565b9695505050505050565b8381526060602082015260006117b360608301856115f5565b9050826040830152949350505050565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04831182151516156117fb576117fb611697565b500290565b6000821982111561181357611813611697565b500190565b600063ffffffff80831681851680830382111561183757611837611697565b01949350505050565b600064ffffffffff80831681851680830382111561183757611837611697565b600063ffffffff8083168181141561187a5761187a611697565b6001019392505050565b600063ffffffff838116908316818110156118a1576118a1611697565b039392505050565b600064ffffffffff838116908316818110156118a1576118a1611697565b6000602082840312156118d957600080fd5b5051919050565b6000602082840312156118f257600080fd5b81517fffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000008116811461167357600080fd5b84815283602082015282604082015260806060820152600061179060808301846115f556fe436861696e53746f72616765436f6e7461696e65722d4354432d62617463686573a264697066735822122071f9046c41835cfaa3b888bb4aa8b907bdd46588ad69741847a96bf3fcaad90264736f6c63430008090033",
}
// CanonicalTransactionChainABI is the input ABI used to generate the binding from.
// Deprecated: Use CanonicalTransactionChainMetaData.ABI instead.
var CanonicalTransactionChainABI = CanonicalTransactionChainMetaData.ABI
// CanonicalTransactionChainBin is the compiled bytecode used for deploying new contracts.
// Deprecated: Use CanonicalTransactionChainMetaData.Bin instead.
var CanonicalTransactionChainBin = CanonicalTransactionChainMetaData.Bin
// DeployCanonicalTransactionChain deploys a new Ethereum contract, binding an instance of CanonicalTransactionChain to it.
func DeployCanonicalTransactionChain(auth *bind.TransactOpts, backend bind.ContractBackend, _libAddressManager common.Address, _maxTransactionGasLimit *big.Int, _l2GasDiscountDivisor *big.Int, _enqueueGasCost *big.Int) (common.Address, *types.Transaction, *CanonicalTransactionChain, error) {
parsed, err := CanonicalTransactionChainMetaData.GetAbi()
if err != nil {
return common.Address{}, nil, nil, err
}
if parsed == nil {
return common.Address{}, nil, nil, errors.New("GetABI returned nil")
}
address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(CanonicalTransactionChainBin), backend, _libAddressManager, _maxTransactionGasLimit, _l2GasDiscountDivisor, _enqueueGasCost)
if err != nil {
return common.Address{}, nil, nil, err
}
return address, tx, &CanonicalTransactionChain{CanonicalTransactionChainCaller: CanonicalTransactionChainCaller{contract: contract}, CanonicalTransactionChainTransactor: CanonicalTransactionChainTransactor{contract: contract}, CanonicalTransactionChainFilterer: CanonicalTransactionChainFilterer{contract: contract}}, nil
}
// CanonicalTransactionChain is an auto generated Go binding around an Ethereum contract.
type CanonicalTransactionChain struct {
CanonicalTransactionChainCaller // Read-only binding to the contract
CanonicalTransactionChainTransactor // Write-only binding to the contract
CanonicalTransactionChainFilterer // Log filterer for contract events
}
// CanonicalTransactionChainCaller is an auto generated read-only Go binding around an Ethereum contract.
type CanonicalTransactionChainCaller struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// CanonicalTransactionChainTransactor is an auto generated write-only Go binding around an Ethereum contract.
type CanonicalTransactionChainTransactor struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// CanonicalTransactionChainFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type CanonicalTransactionChainFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// CanonicalTransactionChainSession is an auto generated Go binding around an Ethereum contract,
// with pre-set call and transact options.
type CanonicalTransactionChainSession struct {
Contract *CanonicalTransactionChain // Generic contract binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// CanonicalTransactionChainCallerSession is an auto generated read-only Go binding around an Ethereum contract,
// with pre-set call options.
type CanonicalTransactionChainCallerSession struct {
Contract *CanonicalTransactionChainCaller // Generic contract caller binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
}
// CanonicalTransactionChainTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
// with pre-set transact options.
type CanonicalTransactionChainTransactorSession struct {
Contract *CanonicalTransactionChainTransactor // Generic contract transactor binding to set the session for
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// CanonicalTransactionChainRaw is an auto generated low-level Go binding around an Ethereum contract.
type CanonicalTransactionChainRaw struct {
Contract *CanonicalTransactionChain // Generic contract binding to access the raw methods on
}
// CanonicalTransactionChainCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
type CanonicalTransactionChainCallerRaw struct {
Contract *CanonicalTransactionChainCaller // Generic read-only contract binding to access the raw methods on
}
// CanonicalTransactionChainTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type CanonicalTransactionChainTransactorRaw struct {
Contract *CanonicalTransactionChainTransactor // Generic write-only contract binding to access the raw methods on
}
// NewCanonicalTransactionChain creates a new instance of CanonicalTransactionChain, bound to a specific deployed contract.
func NewCanonicalTransactionChain(address common.Address, backend bind.ContractBackend) (*CanonicalTransactionChain, error) {
contract, err := bindCanonicalTransactionChain(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &CanonicalTransactionChain{CanonicalTransactionChainCaller: CanonicalTransactionChainCaller{contract: contract}, CanonicalTransactionChainTransactor: CanonicalTransactionChainTransactor{contract: contract}, CanonicalTransactionChainFilterer: CanonicalTransactionChainFilterer{contract: contract}}, nil
}
// NewCanonicalTransactionChainCaller creates a new read-only instance of CanonicalTransactionChain, bound to a specific deployed contract.
func NewCanonicalTransactionChainCaller(address common.Address, caller bind.ContractCaller) (*CanonicalTransactionChainCaller, error) {
contract, err := bindCanonicalTransactionChain(address, caller, nil, nil)
if err != nil {
return nil, err
}
return &CanonicalTransactionChainCaller{contract: contract}, nil
}
// NewCanonicalTransactionChainTransactor creates a new write-only instance of CanonicalTransactionChain, bound to a specific deployed contract.
func NewCanonicalTransactionChainTransactor(address common.Address, transactor bind.ContractTransactor) (*CanonicalTransactionChainTransactor, error) {
contract, err := bindCanonicalTransactionChain(address, nil, transactor, nil)
if err != nil {
return nil, err
}
return &CanonicalTransactionChainTransactor{contract: contract}, nil
}
// NewCanonicalTransactionChainFilterer creates a new log filterer instance of CanonicalTransactionChain, bound to a specific deployed contract.
func NewCanonicalTransactionChainFilterer(address common.Address, filterer bind.ContractFilterer) (*CanonicalTransactionChainFilterer, error) {
contract, err := bindCanonicalTransactionChain(address, nil, nil, filterer)
if err != nil {
return nil, err
}
return &CanonicalTransactionChainFilterer{contract: contract}, nil
}
// bindCanonicalTransactionChain binds a generic wrapper to an already deployed contract.
func bindCanonicalTransactionChain(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
parsed, err := abi.JSON(strings.NewReader(CanonicalTransactionChainABI))
if err != nil {
return nil, err
}
return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_CanonicalTransactionChain *CanonicalTransactionChainRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _CanonicalTransactionChain.Contract.CanonicalTransactionChainCaller.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_CanonicalTransactionChain *CanonicalTransactionChainRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.CanonicalTransactionChainTransactor.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_CanonicalTransactionChain *CanonicalTransactionChainRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.CanonicalTransactionChainTransactor.contract.Transact(opts, method, params...)
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _CanonicalTransactionChain.Contract.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.contract.Transact(opts, method, params...)
}
// MAXROLLUPTXSIZE is a free data retrieval call binding the contract method 0x876ed5cb.
//
// Solidity: function MAX_ROLLUP_TX_SIZE() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) MAXROLLUPTXSIZE(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "MAX_ROLLUP_TX_SIZE")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// MAXROLLUPTXSIZE is a free data retrieval call binding the contract method 0x876ed5cb.
//
// Solidity: function MAX_ROLLUP_TX_SIZE() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) MAXROLLUPTXSIZE() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.MAXROLLUPTXSIZE(&_CanonicalTransactionChain.CallOpts)
}
// MAXROLLUPTXSIZE is a free data retrieval call binding the contract method 0x876ed5cb.
//
// Solidity: function MAX_ROLLUP_TX_SIZE() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) MAXROLLUPTXSIZE() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.MAXROLLUPTXSIZE(&_CanonicalTransactionChain.CallOpts)
}
// MINROLLUPTXGAS is a free data retrieval call binding the contract method 0x78f4b2f2.
//
// Solidity: function MIN_ROLLUP_TX_GAS() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) MINROLLUPTXGAS(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "MIN_ROLLUP_TX_GAS")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// MINROLLUPTXGAS is a free data retrieval call binding the contract method 0x78f4b2f2.
//
// Solidity: function MIN_ROLLUP_TX_GAS() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) MINROLLUPTXGAS() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.MINROLLUPTXGAS(&_CanonicalTransactionChain.CallOpts)
}
// MINROLLUPTXGAS is a free data retrieval call binding the contract method 0x78f4b2f2.
//
// Solidity: function MIN_ROLLUP_TX_GAS() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) MINROLLUPTXGAS() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.MINROLLUPTXGAS(&_CanonicalTransactionChain.CallOpts)
}
// Batches is a free data retrieval call binding the contract method 0xcfdf677e.
//
// Solidity: function batches() view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) Batches(opts *bind.CallOpts) (common.Address, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "batches")
if err != nil {
return *new(common.Address), err
}
out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address)
return out0, err
}
// Batches is a free data retrieval call binding the contract method 0xcfdf677e.
//
// Solidity: function batches() view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) Batches() (common.Address, error) {
return _CanonicalTransactionChain.Contract.Batches(&_CanonicalTransactionChain.CallOpts)
}
// Batches is a free data retrieval call binding the contract method 0xcfdf677e.
//
// Solidity: function batches() view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) Batches() (common.Address, error) {
return _CanonicalTransactionChain.Contract.Batches(&_CanonicalTransactionChain.CallOpts)
}
// EnqueueGasCost is a free data retrieval call binding the contract method 0xe654b1fb.
//
// Solidity: function enqueueGasCost() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) EnqueueGasCost(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "enqueueGasCost")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// EnqueueGasCost is a free data retrieval call binding the contract method 0xe654b1fb.
//
// Solidity: function enqueueGasCost() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) EnqueueGasCost() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.EnqueueGasCost(&_CanonicalTransactionChain.CallOpts)
}
// EnqueueGasCost is a free data retrieval call binding the contract method 0xe654b1fb.
//
// Solidity: function enqueueGasCost() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) EnqueueGasCost() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.EnqueueGasCost(&_CanonicalTransactionChain.CallOpts)
}
// EnqueueL2GasPrepaid is a free data retrieval call binding the contract method 0x0b3dfa97.
//
// Solidity: function enqueueL2GasPrepaid() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) EnqueueL2GasPrepaid(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "enqueueL2GasPrepaid")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// EnqueueL2GasPrepaid is a free data retrieval call binding the contract method 0x0b3dfa97.
//
// Solidity: function enqueueL2GasPrepaid() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) EnqueueL2GasPrepaid() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.EnqueueL2GasPrepaid(&_CanonicalTransactionChain.CallOpts)
}
// EnqueueL2GasPrepaid is a free data retrieval call binding the contract method 0x0b3dfa97.
//
// Solidity: function enqueueL2GasPrepaid() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) EnqueueL2GasPrepaid() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.EnqueueL2GasPrepaid(&_CanonicalTransactionChain.CallOpts)
}
// GetLastBlockNumber is a free data retrieval call binding the contract method 0x5ae6256d.
//
// Solidity: function getLastBlockNumber() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetLastBlockNumber(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getLastBlockNumber")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetLastBlockNumber is a free data retrieval call binding the contract method 0x5ae6256d.
//
// Solidity: function getLastBlockNumber() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetLastBlockNumber() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetLastBlockNumber(&_CanonicalTransactionChain.CallOpts)
}
// GetLastBlockNumber is a free data retrieval call binding the contract method 0x5ae6256d.
//
// Solidity: function getLastBlockNumber() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetLastBlockNumber() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetLastBlockNumber(&_CanonicalTransactionChain.CallOpts)
}
// GetLastTimestamp is a free data retrieval call binding the contract method 0x37899770.
//
// Solidity: function getLastTimestamp() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetLastTimestamp(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getLastTimestamp")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetLastTimestamp is a free data retrieval call binding the contract method 0x37899770.
//
// Solidity: function getLastTimestamp() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetLastTimestamp() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetLastTimestamp(&_CanonicalTransactionChain.CallOpts)
}
// GetLastTimestamp is a free data retrieval call binding the contract method 0x37899770.
//
// Solidity: function getLastTimestamp() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetLastTimestamp() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetLastTimestamp(&_CanonicalTransactionChain.CallOpts)
}
// GetNextQueueIndex is a free data retrieval call binding the contract method 0x7a167a8a.
//
// Solidity: function getNextQueueIndex() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetNextQueueIndex(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getNextQueueIndex")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetNextQueueIndex is a free data retrieval call binding the contract method 0x7a167a8a.
//
// Solidity: function getNextQueueIndex() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetNextQueueIndex() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetNextQueueIndex(&_CanonicalTransactionChain.CallOpts)
}
// GetNextQueueIndex is a free data retrieval call binding the contract method 0x7a167a8a.
//
// Solidity: function getNextQueueIndex() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetNextQueueIndex() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetNextQueueIndex(&_CanonicalTransactionChain.CallOpts)
}
// GetNumPendingQueueElements is a free data retrieval call binding the contract method 0xf722b41a.
//
// Solidity: function getNumPendingQueueElements() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetNumPendingQueueElements(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getNumPendingQueueElements")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetNumPendingQueueElements is a free data retrieval call binding the contract method 0xf722b41a.
//
// Solidity: function getNumPendingQueueElements() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetNumPendingQueueElements() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetNumPendingQueueElements(&_CanonicalTransactionChain.CallOpts)
}
// GetNumPendingQueueElements is a free data retrieval call binding the contract method 0xf722b41a.
//
// Solidity: function getNumPendingQueueElements() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetNumPendingQueueElements() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetNumPendingQueueElements(&_CanonicalTransactionChain.CallOpts)
}
// GetQueueElement is a free data retrieval call binding the contract method 0x2a7f18be.
//
// Solidity: function getQueueElement(uint256 _index) view returns((bytes32,uint40,uint40) _element)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetQueueElement(opts *bind.CallOpts, _index *big.Int) (LibOVMCodecQueueElement, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getQueueElement", _index)
if err != nil {
return *new(LibOVMCodecQueueElement), err
}
out0 := *abi.ConvertType(out[0], new(LibOVMCodecQueueElement)).(*LibOVMCodecQueueElement)
return out0, err
}
// GetQueueElement is a free data retrieval call binding the contract method 0x2a7f18be.
//
// Solidity: function getQueueElement(uint256 _index) view returns((bytes32,uint40,uint40) _element)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetQueueElement(_index *big.Int) (LibOVMCodecQueueElement, error) {
return _CanonicalTransactionChain.Contract.GetQueueElement(&_CanonicalTransactionChain.CallOpts, _index)
}
// GetQueueElement is a free data retrieval call binding the contract method 0x2a7f18be.
//
// Solidity: function getQueueElement(uint256 _index) view returns((bytes32,uint40,uint40) _element)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetQueueElement(_index *big.Int) (LibOVMCodecQueueElement, error) {
return _CanonicalTransactionChain.Contract.GetQueueElement(&_CanonicalTransactionChain.CallOpts, _index)
}
// GetQueueLength is a free data retrieval call binding the contract method 0xb8f77005.
//
// Solidity: function getQueueLength() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetQueueLength(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getQueueLength")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetQueueLength is a free data retrieval call binding the contract method 0xb8f77005.
//
// Solidity: function getQueueLength() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetQueueLength() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetQueueLength(&_CanonicalTransactionChain.CallOpts)
}
// GetQueueLength is a free data retrieval call binding the contract method 0xb8f77005.
//
// Solidity: function getQueueLength() view returns(uint40)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetQueueLength() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetQueueLength(&_CanonicalTransactionChain.CallOpts)
}
// GetTotalBatches is a free data retrieval call binding the contract method 0xe561dddc.
//
// Solidity: function getTotalBatches() view returns(uint256 _totalBatches)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetTotalBatches(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getTotalBatches")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetTotalBatches is a free data retrieval call binding the contract method 0xe561dddc.
//
// Solidity: function getTotalBatches() view returns(uint256 _totalBatches)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetTotalBatches() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetTotalBatches(&_CanonicalTransactionChain.CallOpts)
}
// GetTotalBatches is a free data retrieval call binding the contract method 0xe561dddc.
//
// Solidity: function getTotalBatches() view returns(uint256 _totalBatches)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetTotalBatches() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetTotalBatches(&_CanonicalTransactionChain.CallOpts)
}
// GetTotalElements is a free data retrieval call binding the contract method 0x7aa63a86.
//
// Solidity: function getTotalElements() view returns(uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) GetTotalElements(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "getTotalElements")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// GetTotalElements is a free data retrieval call binding the contract method 0x7aa63a86.
//
// Solidity: function getTotalElements() view returns(uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) GetTotalElements() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetTotalElements(&_CanonicalTransactionChain.CallOpts)
}
// GetTotalElements is a free data retrieval call binding the contract method 0x7aa63a86.
//
// Solidity: function getTotalElements() view returns(uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) GetTotalElements() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.GetTotalElements(&_CanonicalTransactionChain.CallOpts)
}
// L2GasDiscountDivisor is a free data retrieval call binding the contract method 0xccf987c8.
//
// Solidity: function l2GasDiscountDivisor() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) L2GasDiscountDivisor(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "l2GasDiscountDivisor")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// L2GasDiscountDivisor is a free data retrieval call binding the contract method 0xccf987c8.
//
// Solidity: function l2GasDiscountDivisor() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) L2GasDiscountDivisor() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.L2GasDiscountDivisor(&_CanonicalTransactionChain.CallOpts)
}
// L2GasDiscountDivisor is a free data retrieval call binding the contract method 0xccf987c8.
//
// Solidity: function l2GasDiscountDivisor() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) L2GasDiscountDivisor() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.L2GasDiscountDivisor(&_CanonicalTransactionChain.CallOpts)
}
// LibAddressManager is a free data retrieval call binding the contract method 0x299ca478.
//
// Solidity: function libAddressManager() view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) LibAddressManager(opts *bind.CallOpts) (common.Address, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "libAddressManager")
if err != nil {
return *new(common.Address), err
}
out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address)
return out0, err
}
// LibAddressManager is a free data retrieval call binding the contract method 0x299ca478.
//
// Solidity: function libAddressManager() view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) LibAddressManager() (common.Address, error) {
return _CanonicalTransactionChain.Contract.LibAddressManager(&_CanonicalTransactionChain.CallOpts)
}
// LibAddressManager is a free data retrieval call binding the contract method 0x299ca478.
//
// Solidity: function libAddressManager() view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) LibAddressManager() (common.Address, error) {
return _CanonicalTransactionChain.Contract.LibAddressManager(&_CanonicalTransactionChain.CallOpts)
}
// MaxTransactionGasLimit is a free data retrieval call binding the contract method 0x8d38c6c1.
//
// Solidity: function maxTransactionGasLimit() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) MaxTransactionGasLimit(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "maxTransactionGasLimit")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
// MaxTransactionGasLimit is a free data retrieval call binding the contract method 0x8d38c6c1.
//
// Solidity: function maxTransactionGasLimit() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) MaxTransactionGasLimit() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.MaxTransactionGasLimit(&_CanonicalTransactionChain.CallOpts)
}
// MaxTransactionGasLimit is a free data retrieval call binding the contract method 0x8d38c6c1.
//
// Solidity: function maxTransactionGasLimit() view returns(uint256)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) MaxTransactionGasLimit() (*big.Int, error) {
return _CanonicalTransactionChain.Contract.MaxTransactionGasLimit(&_CanonicalTransactionChain.CallOpts)
}
// Resolve is a free data retrieval call binding the contract method 0x461a4478.
//
// Solidity: function resolve(string _name) view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainCaller) Resolve(opts *bind.CallOpts, _name string) (common.Address, error) {
var out []interface{}
err := _CanonicalTransactionChain.contract.Call(opts, &out, "resolve", _name)
if err != nil {
return *new(common.Address), err
}
out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address)
return out0, err
}
// Resolve is a free data retrieval call binding the contract method 0x461a4478.
//
// Solidity: function resolve(string _name) view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) Resolve(_name string) (common.Address, error) {
return _CanonicalTransactionChain.Contract.Resolve(&_CanonicalTransactionChain.CallOpts, _name)
}
// Resolve is a free data retrieval call binding the contract method 0x461a4478.
//
// Solidity: function resolve(string _name) view returns(address)
func (_CanonicalTransactionChain *CanonicalTransactionChainCallerSession) Resolve(_name string) (common.Address, error) {
return _CanonicalTransactionChain.Contract.Resolve(&_CanonicalTransactionChain.CallOpts, _name)
}
// AppendSequencerBatch is a paid mutator transaction binding the contract method 0xd0f89344.
//
// Solidity: function appendSequencerBatch() returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactor) AppendSequencerBatch(opts *bind.TransactOpts) (*types.Transaction, error) {
return _CanonicalTransactionChain.contract.Transact(opts, "appendSequencerBatch")
}
// AppendSequencerBatch is a paid mutator transaction binding the contract method 0xd0f89344.
//
// Solidity: function appendSequencerBatch() returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) AppendSequencerBatch() (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.AppendSequencerBatch(&_CanonicalTransactionChain.TransactOpts)
}
// AppendSequencerBatch is a paid mutator transaction binding the contract method 0xd0f89344.
//
// Solidity: function appendSequencerBatch() returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactorSession) AppendSequencerBatch() (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.AppendSequencerBatch(&_CanonicalTransactionChain.TransactOpts)
}
// Enqueue is a paid mutator transaction binding the contract method 0x6fee07e0.
//
// Solidity: function enqueue(address _target, uint256 _gasLimit, bytes _data) returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactor) Enqueue(opts *bind.TransactOpts, _target common.Address, _gasLimit *big.Int, _data []byte) (*types.Transaction, error) {
return _CanonicalTransactionChain.contract.Transact(opts, "enqueue", _target, _gasLimit, _data)
}
// Enqueue is a paid mutator transaction binding the contract method 0x6fee07e0.
//
// Solidity: function enqueue(address _target, uint256 _gasLimit, bytes _data) returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) Enqueue(_target common.Address, _gasLimit *big.Int, _data []byte) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.Enqueue(&_CanonicalTransactionChain.TransactOpts, _target, _gasLimit, _data)
}
// Enqueue is a paid mutator transaction binding the contract method 0x6fee07e0.
//
// Solidity: function enqueue(address _target, uint256 _gasLimit, bytes _data) returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactorSession) Enqueue(_target common.Address, _gasLimit *big.Int, _data []byte) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.Enqueue(&_CanonicalTransactionChain.TransactOpts, _target, _gasLimit, _data)
}
// SetGasParams is a paid mutator transaction binding the contract method 0xedcc4a45.
//
// Solidity: function setGasParams(uint256 _l2GasDiscountDivisor, uint256 _enqueueGasCost) returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactor) SetGasParams(opts *bind.TransactOpts, _l2GasDiscountDivisor *big.Int, _enqueueGasCost *big.Int) (*types.Transaction, error) {
return _CanonicalTransactionChain.contract.Transact(opts, "setGasParams", _l2GasDiscountDivisor, _enqueueGasCost)
}
// SetGasParams is a paid mutator transaction binding the contract method 0xedcc4a45.
//
// Solidity: function setGasParams(uint256 _l2GasDiscountDivisor, uint256 _enqueueGasCost) returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainSession) SetGasParams(_l2GasDiscountDivisor *big.Int, _enqueueGasCost *big.Int) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.SetGasParams(&_CanonicalTransactionChain.TransactOpts, _l2GasDiscountDivisor, _enqueueGasCost)
}
// SetGasParams is a paid mutator transaction binding the contract method 0xedcc4a45.
//
// Solidity: function setGasParams(uint256 _l2GasDiscountDivisor, uint256 _enqueueGasCost) returns()
func (_CanonicalTransactionChain *CanonicalTransactionChainTransactorSession) SetGasParams(_l2GasDiscountDivisor *big.Int, _enqueueGasCost *big.Int) (*types.Transaction, error) {
return _CanonicalTransactionChain.Contract.SetGasParams(&_CanonicalTransactionChain.TransactOpts, _l2GasDiscountDivisor, _enqueueGasCost)
}
// CanonicalTransactionChainL2GasParamsUpdatedIterator is returned from FilterL2GasParamsUpdated and is used to iterate over the raw logs and unpacked data for L2GasParamsUpdated events raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainL2GasParamsUpdatedIterator struct {
Event *CanonicalTransactionChainL2GasParamsUpdated // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *CanonicalTransactionChainL2GasParamsUpdatedIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainL2GasParamsUpdated)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainL2GasParamsUpdated)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *CanonicalTransactionChainL2GasParamsUpdatedIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *CanonicalTransactionChainL2GasParamsUpdatedIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// CanonicalTransactionChainL2GasParamsUpdated represents a L2GasParamsUpdated event raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainL2GasParamsUpdated struct {
L2GasDiscountDivisor *big.Int
EnqueueGasCost *big.Int
EnqueueL2GasPrepaid *big.Int
Raw types.Log // Blockchain specific contextual infos
}
// FilterL2GasParamsUpdated is a free log retrieval operation binding the contract event 0xc6ed75e96b8b18b71edc1a6e82a9d677f8268c774a262c624eeb2cf0a8b3e07e.
//
// Solidity: event L2GasParamsUpdated(uint256 l2GasDiscountDivisor, uint256 enqueueGasCost, uint256 enqueueL2GasPrepaid)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) FilterL2GasParamsUpdated(opts *bind.FilterOpts) (*CanonicalTransactionChainL2GasParamsUpdatedIterator, error) {
logs, sub, err := _CanonicalTransactionChain.contract.FilterLogs(opts, "L2GasParamsUpdated")
if err != nil {
return nil, err
}
return &CanonicalTransactionChainL2GasParamsUpdatedIterator{contract: _CanonicalTransactionChain.contract, event: "L2GasParamsUpdated", logs: logs, sub: sub}, nil
}
// WatchL2GasParamsUpdated is a free log subscription operation binding the contract event 0xc6ed75e96b8b18b71edc1a6e82a9d677f8268c774a262c624eeb2cf0a8b3e07e.
//
// Solidity: event L2GasParamsUpdated(uint256 l2GasDiscountDivisor, uint256 enqueueGasCost, uint256 enqueueL2GasPrepaid)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) WatchL2GasParamsUpdated(opts *bind.WatchOpts, sink chan<- *CanonicalTransactionChainL2GasParamsUpdated) (event.Subscription, error) {
logs, sub, err := _CanonicalTransactionChain.contract.WatchLogs(opts, "L2GasParamsUpdated")
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(CanonicalTransactionChainL2GasParamsUpdated)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "L2GasParamsUpdated", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseL2GasParamsUpdated is a log parse operation binding the contract event 0xc6ed75e96b8b18b71edc1a6e82a9d677f8268c774a262c624eeb2cf0a8b3e07e.
//
// Solidity: event L2GasParamsUpdated(uint256 l2GasDiscountDivisor, uint256 enqueueGasCost, uint256 enqueueL2GasPrepaid)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) ParseL2GasParamsUpdated(log types.Log) (*CanonicalTransactionChainL2GasParamsUpdated, error) {
event := new(CanonicalTransactionChainL2GasParamsUpdated)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "L2GasParamsUpdated", log); err != nil {
return nil, err
}
event.Raw = log
return event, nil
}
// CanonicalTransactionChainQueueBatchAppendedIterator is returned from FilterQueueBatchAppended and is used to iterate over the raw logs and unpacked data for QueueBatchAppended events raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainQueueBatchAppendedIterator struct {
Event *CanonicalTransactionChainQueueBatchAppended // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *CanonicalTransactionChainQueueBatchAppendedIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainQueueBatchAppended)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainQueueBatchAppended)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *CanonicalTransactionChainQueueBatchAppendedIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *CanonicalTransactionChainQueueBatchAppendedIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// CanonicalTransactionChainQueueBatchAppended represents a QueueBatchAppended event raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainQueueBatchAppended struct {
StartingQueueIndex *big.Int
NumQueueElements *big.Int
TotalElements *big.Int
Raw types.Log // Blockchain specific contextual infos
}
// FilterQueueBatchAppended is a free log retrieval operation binding the contract event 0x64d7f508348c70dea42d5302a393987e4abc20e45954ab3f9d320207751956f0.
//
// Solidity: event QueueBatchAppended(uint256 _startingQueueIndex, uint256 _numQueueElements, uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) FilterQueueBatchAppended(opts *bind.FilterOpts) (*CanonicalTransactionChainQueueBatchAppendedIterator, error) {
logs, sub, err := _CanonicalTransactionChain.contract.FilterLogs(opts, "QueueBatchAppended")
if err != nil {
return nil, err
}
return &CanonicalTransactionChainQueueBatchAppendedIterator{contract: _CanonicalTransactionChain.contract, event: "QueueBatchAppended", logs: logs, sub: sub}, nil
}
// WatchQueueBatchAppended is a free log subscription operation binding the contract event 0x64d7f508348c70dea42d5302a393987e4abc20e45954ab3f9d320207751956f0.
//
// Solidity: event QueueBatchAppended(uint256 _startingQueueIndex, uint256 _numQueueElements, uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) WatchQueueBatchAppended(opts *bind.WatchOpts, sink chan<- *CanonicalTransactionChainQueueBatchAppended) (event.Subscription, error) {
logs, sub, err := _CanonicalTransactionChain.contract.WatchLogs(opts, "QueueBatchAppended")
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(CanonicalTransactionChainQueueBatchAppended)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "QueueBatchAppended", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseQueueBatchAppended is a log parse operation binding the contract event 0x64d7f508348c70dea42d5302a393987e4abc20e45954ab3f9d320207751956f0.
//
// Solidity: event QueueBatchAppended(uint256 _startingQueueIndex, uint256 _numQueueElements, uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) ParseQueueBatchAppended(log types.Log) (*CanonicalTransactionChainQueueBatchAppended, error) {
event := new(CanonicalTransactionChainQueueBatchAppended)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "QueueBatchAppended", log); err != nil {
return nil, err
}
event.Raw = log
return event, nil
}
// CanonicalTransactionChainSequencerBatchAppendedIterator is returned from FilterSequencerBatchAppended and is used to iterate over the raw logs and unpacked data for SequencerBatchAppended events raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainSequencerBatchAppendedIterator struct {
Event *CanonicalTransactionChainSequencerBatchAppended // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *CanonicalTransactionChainSequencerBatchAppendedIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainSequencerBatchAppended)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainSequencerBatchAppended)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *CanonicalTransactionChainSequencerBatchAppendedIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *CanonicalTransactionChainSequencerBatchAppendedIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// CanonicalTransactionChainSequencerBatchAppended represents a SequencerBatchAppended event raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainSequencerBatchAppended struct {
StartingQueueIndex *big.Int
NumQueueElements *big.Int
TotalElements *big.Int
Raw types.Log // Blockchain specific contextual infos
}
// FilterSequencerBatchAppended is a free log retrieval operation binding the contract event 0x602f1aeac0ca2e7a13e281a9ef0ad7838542712ce16780fa2ecffd351f05f899.
//
// Solidity: event SequencerBatchAppended(uint256 _startingQueueIndex, uint256 _numQueueElements, uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) FilterSequencerBatchAppended(opts *bind.FilterOpts) (*CanonicalTransactionChainSequencerBatchAppendedIterator, error) {
logs, sub, err := _CanonicalTransactionChain.contract.FilterLogs(opts, "SequencerBatchAppended")
if err != nil {
return nil, err
}
return &CanonicalTransactionChainSequencerBatchAppendedIterator{contract: _CanonicalTransactionChain.contract, event: "SequencerBatchAppended", logs: logs, sub: sub}, nil
}
// WatchSequencerBatchAppended is a free log subscription operation binding the contract event 0x602f1aeac0ca2e7a13e281a9ef0ad7838542712ce16780fa2ecffd351f05f899.
//
// Solidity: event SequencerBatchAppended(uint256 _startingQueueIndex, uint256 _numQueueElements, uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) WatchSequencerBatchAppended(opts *bind.WatchOpts, sink chan<- *CanonicalTransactionChainSequencerBatchAppended) (event.Subscription, error) {
logs, sub, err := _CanonicalTransactionChain.contract.WatchLogs(opts, "SequencerBatchAppended")
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(CanonicalTransactionChainSequencerBatchAppended)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "SequencerBatchAppended", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseSequencerBatchAppended is a log parse operation binding the contract event 0x602f1aeac0ca2e7a13e281a9ef0ad7838542712ce16780fa2ecffd351f05f899.
//
// Solidity: event SequencerBatchAppended(uint256 _startingQueueIndex, uint256 _numQueueElements, uint256 _totalElements)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) ParseSequencerBatchAppended(log types.Log) (*CanonicalTransactionChainSequencerBatchAppended, error) {
event := new(CanonicalTransactionChainSequencerBatchAppended)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "SequencerBatchAppended", log); err != nil {
return nil, err
}
event.Raw = log
return event, nil
}
// CanonicalTransactionChainTransactionBatchAppendedIterator is returned from FilterTransactionBatchAppended and is used to iterate over the raw logs and unpacked data for TransactionBatchAppended events raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainTransactionBatchAppendedIterator struct {
Event *CanonicalTransactionChainTransactionBatchAppended // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *CanonicalTransactionChainTransactionBatchAppendedIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainTransactionBatchAppended)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainTransactionBatchAppended)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *CanonicalTransactionChainTransactionBatchAppendedIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *CanonicalTransactionChainTransactionBatchAppendedIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// CanonicalTransactionChainTransactionBatchAppended represents a TransactionBatchAppended event raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainTransactionBatchAppended struct {
BatchIndex *big.Int
BatchRoot [32]byte
BatchSize *big.Int
PrevTotalElements *big.Int
ExtraData []byte
Raw types.Log // Blockchain specific contextual infos
}
// FilterTransactionBatchAppended is a free log retrieval operation binding the contract event 0x127186556e7be68c7e31263195225b4de02820707889540969f62c05cf73525e.
//
// Solidity: event TransactionBatchAppended(uint256 indexed _batchIndex, bytes32 _batchRoot, uint256 _batchSize, uint256 _prevTotalElements, bytes _extraData)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) FilterTransactionBatchAppended(opts *bind.FilterOpts, _batchIndex []*big.Int) (*CanonicalTransactionChainTransactionBatchAppendedIterator, error) {
var _batchIndexRule []interface{}
for _, _batchIndexItem := range _batchIndex {
_batchIndexRule = append(_batchIndexRule, _batchIndexItem)
}
logs, sub, err := _CanonicalTransactionChain.contract.FilterLogs(opts, "TransactionBatchAppended", _batchIndexRule)
if err != nil {
return nil, err
}
return &CanonicalTransactionChainTransactionBatchAppendedIterator{contract: _CanonicalTransactionChain.contract, event: "TransactionBatchAppended", logs: logs, sub: sub}, nil
}
// WatchTransactionBatchAppended is a free log subscription operation binding the contract event 0x127186556e7be68c7e31263195225b4de02820707889540969f62c05cf73525e.
//
// Solidity: event TransactionBatchAppended(uint256 indexed _batchIndex, bytes32 _batchRoot, uint256 _batchSize, uint256 _prevTotalElements, bytes _extraData)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) WatchTransactionBatchAppended(opts *bind.WatchOpts, sink chan<- *CanonicalTransactionChainTransactionBatchAppended, _batchIndex []*big.Int) (event.Subscription, error) {
var _batchIndexRule []interface{}
for _, _batchIndexItem := range _batchIndex {
_batchIndexRule = append(_batchIndexRule, _batchIndexItem)
}
logs, sub, err := _CanonicalTransactionChain.contract.WatchLogs(opts, "TransactionBatchAppended", _batchIndexRule)
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(CanonicalTransactionChainTransactionBatchAppended)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "TransactionBatchAppended", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseTransactionBatchAppended is a log parse operation binding the contract event 0x127186556e7be68c7e31263195225b4de02820707889540969f62c05cf73525e.
//
// Solidity: event TransactionBatchAppended(uint256 indexed _batchIndex, bytes32 _batchRoot, uint256 _batchSize, uint256 _prevTotalElements, bytes _extraData)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) ParseTransactionBatchAppended(log types.Log) (*CanonicalTransactionChainTransactionBatchAppended, error) {
event := new(CanonicalTransactionChainTransactionBatchAppended)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "TransactionBatchAppended", log); err != nil {
return nil, err
}
event.Raw = log
return event, nil
}
// CanonicalTransactionChainTransactionEnqueuedIterator is returned from FilterTransactionEnqueued and is used to iterate over the raw logs and unpacked data for TransactionEnqueued events raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainTransactionEnqueuedIterator struct {
Event *CanonicalTransactionChainTransactionEnqueued // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *CanonicalTransactionChainTransactionEnqueuedIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainTransactionEnqueued)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(CanonicalTransactionChainTransactionEnqueued)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *CanonicalTransactionChainTransactionEnqueuedIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *CanonicalTransactionChainTransactionEnqueuedIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// CanonicalTransactionChainTransactionEnqueued represents a TransactionEnqueued event raised by the CanonicalTransactionChain contract.
type CanonicalTransactionChainTransactionEnqueued struct {
L1TxOrigin common.Address
Target common.Address
GasLimit *big.Int
Data []byte
QueueIndex *big.Int
Timestamp *big.Int
Raw types.Log // Blockchain specific contextual infos
}
// FilterTransactionEnqueued is a free log retrieval operation binding the contract event 0x4b388aecf9fa6cc92253704e5975a6129a4f735bdbd99567df4ed0094ee4ceb5.
//
// Solidity: event TransactionEnqueued(address indexed _l1TxOrigin, address indexed _target, uint256 _gasLimit, bytes _data, uint256 indexed _queueIndex, uint256 _timestamp)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) FilterTransactionEnqueued(opts *bind.FilterOpts, _l1TxOrigin []common.Address, _target []common.Address, _queueIndex []*big.Int) (*CanonicalTransactionChainTransactionEnqueuedIterator, error) {
var _l1TxOriginRule []interface{}
for _, _l1TxOriginItem := range _l1TxOrigin {
_l1TxOriginRule = append(_l1TxOriginRule, _l1TxOriginItem)
}
var _targetRule []interface{}
for _, _targetItem := range _target {
_targetRule = append(_targetRule, _targetItem)
}
var _queueIndexRule []interface{}
for _, _queueIndexItem := range _queueIndex {
_queueIndexRule = append(_queueIndexRule, _queueIndexItem)
}
logs, sub, err := _CanonicalTransactionChain.contract.FilterLogs(opts, "TransactionEnqueued", _l1TxOriginRule, _targetRule, _queueIndexRule)
if err != nil {
return nil, err
}
return &CanonicalTransactionChainTransactionEnqueuedIterator{contract: _CanonicalTransactionChain.contract, event: "TransactionEnqueued", logs: logs, sub: sub}, nil
}
// WatchTransactionEnqueued is a free log subscription operation binding the contract event 0x4b388aecf9fa6cc92253704e5975a6129a4f735bdbd99567df4ed0094ee4ceb5.
//
// Solidity: event TransactionEnqueued(address indexed _l1TxOrigin, address indexed _target, uint256 _gasLimit, bytes _data, uint256 indexed _queueIndex, uint256 _timestamp)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) WatchTransactionEnqueued(opts *bind.WatchOpts, sink chan<- *CanonicalTransactionChainTransactionEnqueued, _l1TxOrigin []common.Address, _target []common.Address, _queueIndex []*big.Int) (event.Subscription, error) {
var _l1TxOriginRule []interface{}
for _, _l1TxOriginItem := range _l1TxOrigin {
_l1TxOriginRule = append(_l1TxOriginRule, _l1TxOriginItem)
}
var _targetRule []interface{}
for _, _targetItem := range _target {
_targetRule = append(_targetRule, _targetItem)
}
var _queueIndexRule []interface{}
for _, _queueIndexItem := range _queueIndex {
_queueIndexRule = append(_queueIndexRule, _queueIndexItem)
}
logs, sub, err := _CanonicalTransactionChain.contract.WatchLogs(opts, "TransactionEnqueued", _l1TxOriginRule, _targetRule, _queueIndexRule)
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(CanonicalTransactionChainTransactionEnqueued)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "TransactionEnqueued", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseTransactionEnqueued is a log parse operation binding the contract event 0x4b388aecf9fa6cc92253704e5975a6129a4f735bdbd99567df4ed0094ee4ceb5.
//
// Solidity: event TransactionEnqueued(address indexed _l1TxOrigin, address indexed _target, uint256 _gasLimit, bytes _data, uint256 indexed _queueIndex, uint256 _timestamp)
func (_CanonicalTransactionChain *CanonicalTransactionChainFilterer) ParseTransactionEnqueued(log types.Log) (*CanonicalTransactionChainTransactionEnqueued, error) {
event := new(CanonicalTransactionChainTransactionEnqueued)
if err := _CanonicalTransactionChain.contract.UnpackLog(event, "TransactionEnqueued", log); err != nil {
return nil, err
}
event.Raw = log
return event, nil
}
...@@ -3,10 +3,6 @@ package ether ...@@ -3,10 +3,6 @@ package ether
import ( import (
"fmt" "fmt"
"math/big" "math/big"
"sync"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/util" "github.com/ethereum-optimism/optimism/op-chain-ops/util"
...@@ -46,9 +42,6 @@ var ( ...@@ -46,9 +42,6 @@ var (
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000006"): true, common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000006"): true,
} }
// maxSlot is the maximum possible storage slot.
maxSlot = common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
// sequencerEntrypointAddr is the address of the OVM sequencer entrypoint contract. // sequencerEntrypointAddr is the address of the OVM sequencer entrypoint contract.
sequencerEntrypointAddr = common.HexToAddress("0x4200000000000000000000000000000000000005") sequencerEntrypointAddr = common.HexToAddress("0x4200000000000000000000000000000000000005")
) )
...@@ -61,11 +54,9 @@ type accountData struct { ...@@ -61,11 +54,9 @@ type accountData struct {
address common.Address address common.Address
} }
type DBFactory func() (*state.StateDB, error)
// MigrateBalances migrates all balances in the LegacyERC20ETH contract into state. It performs checks // MigrateBalances migrates all balances in the LegacyERC20ETH contract into state. It performs checks
// in parallel with mutations in order to reduce overall migration time. // in parallel with mutations in order to reduce overall migration time.
func MigrateBalances(mutableDB *state.StateDB, dbFactory DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool) error { func MigrateBalances(mutableDB *state.StateDB, dbFactory util.DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool) error {
// Chain params to use for integrity checking. // Chain params to use for integrity checking.
params := crossdomain.ParamsByChainID[chainID] params := crossdomain.ParamsByChainID[chainID]
if params == nil { if params == nil {
...@@ -75,7 +66,7 @@ func MigrateBalances(mutableDB *state.StateDB, dbFactory DBFactory, addresses [] ...@@ -75,7 +66,7 @@ func MigrateBalances(mutableDB *state.StateDB, dbFactory DBFactory, addresses []
return doMigration(mutableDB, dbFactory, addresses, allowances, params.ExpectedSupplyDelta, noCheck) return doMigration(mutableDB, dbFactory, addresses, allowances, params.ExpectedSupplyDelta, noCheck)
} }
func doMigration(mutableDB *state.StateDB, dbFactory DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, expDiff *big.Int, noCheck bool) error { func doMigration(mutableDB *state.StateDB, dbFactory util.DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, expDiff *big.Int, noCheck bool) error {
// We'll need to maintain a list of all addresses that we've seen along with all of the storage // We'll need to maintain a list of all addresses that we've seen along with all of the storage
// slots based on the witness data. // slots based on the witness data.
slotsAddrs := make(map[common.Hash]common.Address) slotsAddrs := make(map[common.Hash]common.Address)
...@@ -103,226 +94,109 @@ func doMigration(mutableDB *state.StateDB, dbFactory DBFactory, addresses []comm ...@@ -103,226 +94,109 @@ func doMigration(mutableDB *state.StateDB, dbFactory DBFactory, addresses []comm
slotsAddrs[entrySK] = sequencerEntrypointAddr slotsAddrs[entrySK] = sequencerEntrypointAddr
slotsInp[entrySK] = BalanceSlot slotsInp[entrySK] = BalanceSlot
// WaitGroup to wait on each iteration job to finish.
var wg sync.WaitGroup
// Channel to receive storage slot keys and values from each iteration job. // Channel to receive storage slot keys and values from each iteration job.
outCh := make(chan accountData) outCh := make(chan accountData)
// Channel to receive errors from each iteration job.
errCh := make(chan error, checkJobs)
// Channel to cancel all iteration jobs.
cancelCh := make(chan struct{})
// Define a worker function to iterate over each partition.
worker := func(start, end common.Hash) {
// Decrement the WaitGroup when the function returns.
defer wg.Done()
db, err := dbFactory()
if err != nil {
log.Crit("cannot get database", "err", err)
}
// Create a new storage trie. Each trie returned by db.StorageTrie // Channel that gets closed when the collector is done.
// is a copy, so this is safe for concurrent use. doneCh := make(chan struct{})
st, err := db.StorageTrie(predeploys.LegacyERC20ETHAddr)
if err != nil {
// Should never happen, so explode if it does.
log.Crit("cannot get storage trie for LegacyERC20ETHAddr", "err", err)
}
if st == nil {
// Should never happen, so explode if it does.
log.Crit("nil storage trie for LegacyERC20ETHAddr")
}
it := trie.NewIterator(st.NodeIterator(start.Bytes()))
// Below code is largely based on db.ForEachStorage. We can't use that
// because it doesn't allow us to specify a start and end key.
for it.Next() {
select {
case <-cancelCh:
// If one of the workers encounters an error, cancel all of them.
return
default:
break
}
// Use the raw (i.e., secure hashed) key to check if we've reached // Create a map of accounts we've seen so that we can filter out duplicates.
// the end of the partition. Use > rather than >= here to account for seenAccounts := make(map[common.Address]bool)
// the fact that the values returned by PartitionKeys are inclusive.
// Duplicate addresses that may be returned by this iteration are
// filtered out in the collector.
if new(big.Int).SetBytes(it.Key).Cmp(end.Big()) > 0 {
return
}
// Skip if the value is empty. // Keep track of the total migrated supply.
rawValue := it.Value totalFound := new(big.Int)
if len(rawValue) == 0 {
continue
}
// Get the preimage. // Kick off a background process to collect
rawKey := st.GetKey(it.Key) // values from the channel and add them to the map.
if rawKey == nil { var count int
// Should never happen, so explode if it does. progress := util.ProgressLogger(1000, "Migrated OVM_ETH storage slot")
log.Crit("cannot get preimage for storage key", "key", it.Key) go func() {
} defer func() { doneCh <- struct{}{} }()
key := common.BytesToHash(rawKey)
// Parse the raw value. for account := range outCh {
_, content, _, err := rlp.Split(rawValue) progress()
if err != nil {
// Should never happen, so explode if it does.
log.Crit("mal-formed data in state: %v", err)
}
// We can safely ignore specific slots (totalSupply, name, symbol). // Filter out duplicate accounts. See the below note about keyspace iteration for
if ignoredSlots[key] { // why we may have to filter out duplicates.
if seenAccounts[account.address] {
log.Info("skipping duplicate account during iteration", "addr", account.address)
continue continue
} }
slotType, ok := slotsInp[key] // Accumulate addresses and total supply.
if !ok { totalFound = new(big.Int).Add(totalFound, account.balance)
if noCheck {
log.Error("ignoring unknown storage slot in state", "slot", key.String())
} else {
errCh <- fmt.Errorf("unknown storage slot in state: %s", key.String())
return
}
}
// No accounts should have a balance in state. If they do, bail.
addr, ok := slotsAddrs[key]
if !ok {
log.Crit("could not find address in map - should never happen")
}
bal := db.GetBalance(addr)
if bal.Sign() != 0 {
log.Error(
"account has non-zero balance in state - should never happen",
"addr", addr,
"balance", bal.String(),
)
if !noCheck {
errCh <- fmt.Errorf("account has non-zero balance in state - should never happen: %s", addr.String())
return
}
}
// Add balances to the total found. mutableDB.SetBalance(account.address, account.balance)
switch slotType { mutableDB.SetState(predeploys.LegacyERC20ETHAddr, account.legacySlot, common.Hash{})
case BalanceSlot: count++
// Convert the value to a common.Hash, then send to the channel. seenAccounts[account.address] = true
value := common.BytesToHash(content)
outCh <- accountData{
balance: value.Big(),
legacySlot: key,
address: addr,
}
case AllowanceSlot:
// Allowance slot.
continue
default:
// Should never happen.
if noCheck {
log.Error("unknown slot type", "slot", key, "type", slotType)
} else {
log.Crit("unknown slot type %d, should never happen", slotType)
}
}
} }
} }()
for i := 0; i < checkJobs; i++ {
wg.Add(1)
// Partition the keyspace per worker.
start, end := PartitionKeyspace(i, checkJobs)
// Kick off our worker.
go worker(start, end)
}
// Make a channel to track when collector process completes.
collectorClosedCh := make(chan struct{})
// Make a channel to cancel the collector process.
collectorCancelCh := make(chan struct{})
// Keep track of the last error seen.
var lastErr error
// The cancel channel can be closed if any of the workers returns an error.
// We wrap the close in a sync.Once to ensure that it's only closed once.
var cancelOnce sync.Once
// Create a map of accounts we've seen so that we can filter out duplicates. err := util.IterateState(dbFactory, predeploys.LegacyERC20ETHAddr, func(db *state.StateDB, key, value common.Hash) error {
seenAccounts := make(map[common.Address]bool) // We can safely ignore specific slots (totalSupply, name, symbol).
if ignoredSlots[key] {
return nil
}
// Keep track of the total migrated supply. slotType, ok := slotsInp[key]
totalFound := new(big.Int) if !ok {
log.Error("unknown storage slot in state", "slot", key.String())
if !noCheck {
return fmt.Errorf("unknown storage slot in state: %s", key.String())
}
}
// Kick off another background process to collect // No accounts should have a balance in state. If they do, bail.
// values from the channel and add them to the map. addr, ok := slotsAddrs[key]
var count int if !ok {
progress := util.ProgressLogger(1000, "Migrated OVM_ETH storage slot") log.Crit("could not find address in map - should never happen")
go func() { }
defer func() { bal := db.GetBalance(addr)
collectorClosedCh <- struct{}{} if bal.Sign() != 0 {
}() log.Error(
for { "account has non-zero balance in state - should never happen",
select { "addr", addr,
case account := <-outCh: "balance", bal.String(),
progress() )
if !noCheck {
// Filter out duplicate accounts. See the below note about keyspace iteration for return fmt.Errorf("account has non-zero balance in state - should never happen: %s", addr.String())
// why we may have to filter out duplicates.
if seenAccounts[account.address] {
log.Info("skipping duplicate account during iteration", "addr", account.address)
continue
}
// Accumulate addresses and total supply.
totalFound = new(big.Int).Add(totalFound, account.balance)
mutableDB.SetBalance(account.address, account.balance)
mutableDB.SetState(predeploys.LegacyERC20ETHAddr, account.legacySlot, common.Hash{})
count++
seenAccounts[account.address] = true
case err := <-errCh:
cancelOnce.Do(func() {
close(cancelCh)
lastErr = err
})
case <-collectorCancelCh:
// Explicitly drain the error channel. Since the error channel is buffered, it's possible
// for the wg.Wait() call below to unblock and cancel this goroutine before the error gets
// processed by the case statement above.
for len(errCh) > 0 {
err := <-errCh
if lastErr == nil {
lastErr = err
}
}
return
} }
} }
}()
// Wait for the workers to finish. // Add balances to the total found.
wg.Wait() switch slotType {
case BalanceSlot:
// Send the data to the channel.
outCh <- accountData{
balance: value.Big(),
legacySlot: key,
address: addr,
}
case AllowanceSlot:
// Allowance slot. Do nothing here.
default:
// Should never happen.
if noCheck {
log.Error("unknown slot type", "slot", key, "type", slotType)
} else {
log.Crit("unknown slot type %d, should never happen", slotType)
}
}
// Close the collector, and wait for it to finish. return nil
close(collectorCancelCh) }, checkJobs)
<-collectorClosedCh
// If we saw an error, return it. if err != nil {
if lastErr != nil { return err
return lastErr
} }
// Close the outCh to cancel the collector. The collector will signal that it's done
// using doneCh. Any values waiting to be read from outCh will be read before the
// collector exits.
close(outCh)
<-doneCh
// Log how many slots were iterated over. // Log how many slots were iterated over.
log.Info("Iterated legacy balances", "count", count) log.Info("Iterated legacy balances", "count", count)
...@@ -368,33 +242,3 @@ func doMigration(mutableDB *state.StateDB, dbFactory DBFactory, addresses []comm ...@@ -368,33 +242,3 @@ func doMigration(mutableDB *state.StateDB, dbFactory DBFactory, addresses []comm
return nil return nil
} }
// PartitionKeyspace divides the key space into partitions by dividing the maximum keyspace
// by count then multiplying by i. This will leave some slots left over, which we handle below. It
// returns the start and end keys for the partition as a common.Hash. Note that the returned range
// of keys is inclusive, i.e., [start, end] NOT [start, end).
func PartitionKeyspace(i int, count int) (common.Hash, common.Hash) {
if i < 0 || count < 0 {
panic("i and count must be greater than 0")
}
if i > count-1 {
panic("i must be less than count - 1")
}
// Divide the key space into partitions by dividing the key space by the number
// of jobs. This will leave some slots left over, which we handle below.
partSize := new(big.Int).Div(maxSlot.Big(), big.NewInt(int64(count)))
start := common.BigToHash(new(big.Int).Mul(big.NewInt(int64(i)), partSize))
var end common.Hash
if i < count-1 {
// If this is not the last partition, use the next partition's start key as the end.
end = common.BigToHash(new(big.Int).Mul(big.NewInt(int64(i+1)), partSize))
} else {
// If this is the last partition, use the max slot as the end.
end = maxSlot
}
return start, end
}
package ether package ether
import ( import (
"fmt"
"math/big" "math/big"
"math/rand" "math/rand"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
...@@ -190,7 +191,7 @@ func TestMigrateBalances(t *testing.T) { ...@@ -190,7 +191,7 @@ func TestMigrateBalances(t *testing.T) {
} }
} }
func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Address]*big.Int, allowances map[common.Address]common.Address) (*state.StateDB, DBFactory) { func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Address]*big.Int, allowances map[common.Address]common.Address) (*state.StateDB, util.DBFactory) {
memDB := rawdb.NewMemoryDatabase() memDB := rawdb.NewMemoryDatabase()
db, err := state.New(common.Hash{}, state.NewDatabaseWithConfig(memDB, &trie.Config{ db, err := state.New(common.Hash{}, state.NewDatabaseWithConfig(memDB, &trie.Config{
Preimages: true, Preimages: true,
...@@ -283,85 +284,6 @@ func TestMigrateBalancesRandomMissing(t *testing.T) { ...@@ -283,85 +284,6 @@ func TestMigrateBalancesRandomMissing(t *testing.T) {
} }
} }
func TestPartitionKeyspace(t *testing.T) {
tests := []struct {
i int
count int
expected [2]common.Hash
}{
{
i: 0,
count: 1,
expected: [2]common.Hash{
common.HexToHash("0x00"),
common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
{
i: 0,
count: 2,
expected: [2]common.Hash{
common.HexToHash("0x00"),
common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
{
i: 1,
count: 2,
expected: [2]common.Hash{
common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
{
i: 0,
count: 3,
expected: [2]common.Hash{
common.HexToHash("0x00"),
common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"),
},
},
{
i: 1,
count: 3,
expected: [2]common.Hash{
common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"),
common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
},
{
i: 2,
count: 3,
expected: [2]common.Hash{
common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("i %d, count %d", tt.i, tt.count), func(t *testing.T) {
start, end := PartitionKeyspace(tt.i, tt.count)
require.Equal(t, tt.expected[0], start)
require.Equal(t, tt.expected[1], end)
})
}
t.Run("panics on invalid i or count", func(t *testing.T) {
require.Panics(t, func() {
PartitionKeyspace(1, 1)
})
require.Panics(t, func() {
PartitionKeyspace(-1, 1)
})
require.Panics(t, func() {
PartitionKeyspace(0, -1)
})
require.Panics(t, func() {
PartitionKeyspace(-1, -1)
})
})
}
func randAddr(t *testing.T) common.Address { func randAddr(t *testing.T) common.Address {
var addr common.Address var addr common.Address
_, err := rand.Read(addr[:]) _, err := rand.Read(addr[:])
......
package util
import (
"fmt"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
var (
// maxSlot is the maximum possible storage slot.
maxSlot = common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
)
type DBFactory func() (*state.StateDB, error)
type StateCallback func(db *state.StateDB, key, value common.Hash) error
func IterateState(dbFactory DBFactory, address common.Address, cb StateCallback, workers int) error {
if workers <= 0 {
panic("workers must be greater than 0")
}
// WaitGroup to wait for all workers to finish.
var wg sync.WaitGroup
// Channel to receive errors from each iteration job.
errCh := make(chan error, workers)
// Channel to cancel all iteration jobs.
cancelCh := make(chan struct{})
worker := func(start, end common.Hash) {
// Decrement the WaitGroup when the function returns.
defer wg.Done()
db, err := dbFactory()
if err != nil {
// Should never happen, so explode if it does.
log.Crit("cannot create state db", "err", err)
}
st, err := db.StorageTrie(address)
if err != nil {
// Should never happen, so explode if it does.
log.Crit("cannot get storage trie", "address", address, "err", err)
}
// st can be nil if the account doesn't exist.
if st == nil {
errCh <- fmt.Errorf("account does not exist: %s", address.Hex())
return
}
it := trie.NewIterator(st.NodeIterator(start.Bytes()))
// Below code is largely based on db.ForEachStorage. We can't use that
// because it doesn't allow us to specify a start and end key.
for it.Next() {
select {
case <-cancelCh:
// If one of the workers encounters an error, cancel all of them.
return
default:
break
}
// Use the raw (i.e., secure hashed) key to check if we've reached
// the end of the partition. Use > rather than >= here to account for
// the fact that the values returned by PartitionKeys are inclusive.
// Duplicate addresses that may be returned by this iteration are
// filtered out in the collector.
if new(big.Int).SetBytes(it.Key).Cmp(end.Big()) > 0 {
return
}
// Skip if the value is empty.
rawValue := it.Value
if len(rawValue) == 0 {
continue
}
// Get the preimage.
rawKey := st.GetKey(it.Key)
if rawKey == nil {
// Should never happen, so explode if it does.
log.Crit("cannot get preimage for storage key", "key", it.Key)
}
key := common.BytesToHash(rawKey)
// Parse the raw value.
_, content, _, err := rlp.Split(rawValue)
if err != nil {
// Should never happen, so explode if it does.
log.Crit("mal-formed data in state: %v", err)
}
value := common.BytesToHash(content)
// Call the callback with the DB, key, and value. Errors get
// bubbled up to the errCh.
if err := cb(db, key, value); err != nil {
errCh <- err
return
}
}
}
for i := 0; i < workers; i++ {
wg.Add(1)
// Partition the keyspace per worker.
start, end := PartitionKeyspace(i, workers)
// Kick off our worker.
go worker(start, end)
}
wg.Wait()
for len(errCh) > 0 {
err := <-errCh
if err != nil {
return err
}
}
return nil
}
// PartitionKeyspace divides the key space into partitions by dividing the maximum keyspace
// by count then multiplying by i. This will leave some slots left over, which we handle below. It
// returns the start and end keys for the partition as a common.Hash. Note that the returned range
// of keys is inclusive, i.e., [start, end] NOT [start, end).
func PartitionKeyspace(i int, count int) (common.Hash, common.Hash) {
if i < 0 || count < 0 {
panic("i and count must be greater than 0")
}
if i > count-1 {
panic("i must be less than count - 1")
}
// Divide the key space into partitions by dividing the key space by the number
// of jobs. This will leave some slots left over, which we handle below.
partSize := new(big.Int).Div(maxSlot.Big(), big.NewInt(int64(count)))
start := common.BigToHash(new(big.Int).Mul(big.NewInt(int64(i)), partSize))
var end common.Hash
if i < count-1 {
// If this is not the last partition, use the next partition's start key as the end.
end = common.BigToHash(new(big.Int).Mul(big.NewInt(int64(i+1)), partSize))
} else {
// If this is the last partition, use the max slot as the end.
end = maxSlot
}
return start, end
}
package util
import (
crand "crypto/rand"
"fmt"
"math/rand"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/trie"
"github.com/stretchr/testify/require"
)
var testAddr = common.Address{0: 0xff}
func TestStateIteratorWorkers(t *testing.T) {
_, factory, _ := setupRandTest(t)
for i := -1; i <= 0; i++ {
require.Panics(t, func() {
_ = IterateState(factory, testAddr, func(db *state.StateDB, key, value common.Hash) error {
return nil
}, i)
})
}
}
func TestStateIteratorNonexistentAccount(t *testing.T) {
_, factory, _ := setupRandTest(t)
require.ErrorContains(t, IterateState(factory, common.Address{}, func(db *state.StateDB, key, value common.Hash) error {
return nil
}, 1), "account does not exist")
}
func TestStateIteratorRandomOK(t *testing.T) {
for i := 0; i < 100; i++ {
hashes, factory, workerCount := setupRandTest(t)
seenHashes := make(map[common.Hash]bool)
hashCh := make(chan common.Hash)
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
for hash := range hashCh {
seenHashes[hash] = true
}
}()
require.NoError(t, IterateState(factory, testAddr, func(db *state.StateDB, key, value common.Hash) error {
hashCh <- key
return nil
}, workerCount))
close(hashCh)
<-doneCh
// Perform a less or equal check here in case of duplicates. The map check below will assert
// that all of the hashes are accounted for.
require.LessOrEqual(t, len(seenHashes), len(hashes))
// Every hash we put into state should have been iterated over.
for _, hash := range hashes {
require.Contains(t, seenHashes, hash)
}
}
}
func TestStateIteratorRandomError(t *testing.T) {
for i := 0; i < 100; i++ {
hashes, factory, workerCount := setupRandTest(t)
failHash := hashes[rand.Intn(len(hashes))]
require.ErrorContains(t, IterateState(factory, testAddr, func(db *state.StateDB, key, value common.Hash) error {
if key == failHash {
return fmt.Errorf("test error")
}
return nil
}, workerCount), "test error")
}
}
func TestPartitionKeyspace(t *testing.T) {
tests := []struct {
i int
count int
expected [2]common.Hash
}{
{
i: 0,
count: 1,
expected: [2]common.Hash{
common.HexToHash("0x00"),
common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
{
i: 0,
count: 2,
expected: [2]common.Hash{
common.HexToHash("0x00"),
common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
{
i: 1,
count: 2,
expected: [2]common.Hash{
common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
{
i: 0,
count: 3,
expected: [2]common.Hash{
common.HexToHash("0x00"),
common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"),
},
},
{
i: 1,
count: 3,
expected: [2]common.Hash{
common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"),
common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
},
{
i: 2,
count: 3,
expected: [2]common.Hash{
common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("i %d, count %d", tt.i, tt.count), func(t *testing.T) {
start, end := PartitionKeyspace(tt.i, tt.count)
require.Equal(t, tt.expected[0], start)
require.Equal(t, tt.expected[1], end)
})
}
t.Run("panics on invalid i or count", func(t *testing.T) {
require.Panics(t, func() {
PartitionKeyspace(1, 1)
})
require.Panics(t, func() {
PartitionKeyspace(-1, 1)
})
require.Panics(t, func() {
PartitionKeyspace(0, -1)
})
require.Panics(t, func() {
PartitionKeyspace(-1, -1)
})
})
}
func setupRandTest(t *testing.T) ([]common.Hash, DBFactory, int) {
memDB := rawdb.NewMemoryDatabase()
db, err := state.New(common.Hash{}, state.NewDatabaseWithConfig(memDB, &trie.Config{
Preimages: true,
Cache: 1024,
}), nil)
require.NoError(t, err)
hashCount := rand.Intn(100)
if hashCount == 0 {
hashCount = 1
}
hashes := make([]common.Hash, hashCount)
db.CreateAccount(testAddr)
for j := 0; j < hashCount; j++ {
hashes[j] = randHash(t)
db.SetState(testAddr, hashes[j], hashes[j])
}
root, err := db.Commit(false)
require.NoError(t, err)
err = db.Database().TrieDB().Commit(root, true)
require.NoError(t, err)
factory := func() (*state.StateDB, error) {
return state.New(root, state.NewDatabaseWithConfig(memDB, &trie.Config{
Preimages: true,
Cache: 1024,
}), nil)
}
workerCount := rand.Intn(64)
if workerCount == 0 {
workerCount = 1
}
return hashes, factory, workerCount
}
func randHash(t *testing.T) common.Hash {
var h common.Hash
_, err := crand.Read(h[:])
require.NoError(t, err)
return h
}
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/metrics"
"github.com/ethereum-optimism/optimism/op-proposer/proposer" "github.com/ethereum-optimism/optimism/op-proposer/proposer"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
...@@ -51,6 +52,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl ...@@ -51,6 +52,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
NumConfirmations: 1, NumConfirmations: 1,
SafeAbortNonceTooLowCount: 4, SafeAbortNonceTooLowCount: 4,
From: from, From: from,
ChainID: big.NewInt(420),
// Signer is loaded in `proposer.NewL2OutputSubmitter` // Signer is loaded in `proposer.NewL2OutputSubmitter`
}, },
L1Client: l1, L1Client: l1,
...@@ -60,7 +62,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl ...@@ -60,7 +62,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
SignerFnFactory: signer, SignerFnFactory: signer,
} }
dr, err := proposer.NewL2OutputSubmitter(proposerCfg, log) dr, err := proposer.NewL2OutputSubmitter(proposerCfg, log, metrics.NoopMetrics)
require.NoError(t, err) require.NoError(t, err)
return &L2Proposer{ return &L2Proposer{
......
...@@ -15,6 +15,7 @@ import ( ...@@ -15,6 +15,7 @@ import (
batchermetrics "github.com/ethereum-optimism/optimism/op-batcher/metrics" batchermetrics "github.com/ethereum-optimism/optimism/op-batcher/metrics"
"github.com/ethereum-optimism/optimism/op-node/chaincfg" "github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
proposermetrics "github.com/ethereum-optimism/optimism/op-proposer/metrics"
l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer" l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
...@@ -277,6 +278,7 @@ func TestMigration(t *testing.T) { ...@@ -277,6 +278,7 @@ func TestMigration(t *testing.T) {
L2EngineAddr: gethNode.HTTPAuthEndpoint(), L2EngineAddr: gethNode.HTTPAuthEndpoint(),
L2EngineJWTSecret: testingJWTSecret, L2EngineJWTSecret: testingJWTSecret,
}, },
L2Sync: &node.PreparedL2SyncEndpoint{Client: nil, TrustRPC: false},
Driver: driver.Config{ Driver: driver.Config{
VerifierConfDepth: 0, VerifierConfDepth: 0,
SequencerConfDepth: 0, SequencerConfDepth: 0,
...@@ -327,6 +329,8 @@ func TestMigration(t *testing.T) { ...@@ -327,6 +329,8 @@ func TestMigration(t *testing.T) {
L1EthRpc: forkedL1URL, L1EthRpc: forkedL1URL,
L2EthRpc: gethNode.WSEndpoint(), L2EthRpc: gethNode.WSEndpoint(),
RollupRpc: rollupNode.HTTPEndpoint(), RollupRpc: rollupNode.HTTPEndpoint(),
TxManagerTimeout: 10 * time.Minute,
OfflineGasEstimation: true,
MaxChannelDuration: 1, MaxChannelDuration: 1,
MaxL1TxSize: 120_000, MaxL1TxSize: 120_000,
TargetL1TxSize: 100_000, TargetL1TxSize: 100_000,
...@@ -362,7 +366,7 @@ func TestMigration(t *testing.T) { ...@@ -362,7 +366,7 @@ func TestMigration(t *testing.T) {
Format: "text", Format: "text",
}, },
PrivateKey: hexPriv(secrets.Proposer), PrivateKey: hexPriv(secrets.Proposer),
}, lgr.New("module", "proposer")) }, lgr.New("module", "proposer"), proposermetrics.NoopMetrics)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
proposer.Stop() proposer.Stop()
......
...@@ -37,6 +37,7 @@ import ( ...@@ -37,6 +37,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup/driver" "github.com/ethereum-optimism/optimism/op-node/rollup/driver"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
proposermetrics "github.com/ethereum-optimism/optimism/op-proposer/metrics"
l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer" l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
) )
...@@ -193,6 +194,9 @@ type SystemConfig struct { ...@@ -193,6 +194,9 @@ type SystemConfig struct {
// If the proposer can make proposals for L2 blocks derived from L1 blocks which are not finalized on L1 yet. // If the proposer can make proposals for L2 blocks derived from L1 blocks which are not finalized on L1 yet.
NonFinalizedProposals bool NonFinalizedProposals bool
// Explicitly disable batcher, for tests that rely on unsafe L2 payloads
DisableBatcher bool
} }
type System struct { type System struct {
...@@ -417,6 +421,10 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -417,6 +421,10 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
L2EngineAddr: l2EndpointConfig, L2EngineAddr: l2EndpointConfig,
L2EngineJWTSecret: cfg.JWTSecret, L2EngineJWTSecret: cfg.JWTSecret,
} }
rollupCfg.L2Sync = &rollupNode.PreparedL2SyncEndpoint{
Client: nil,
TrustRPC: false,
}
} }
// Geth Clients // Geth Clients
...@@ -572,7 +580,7 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -572,7 +580,7 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
Format: "text", Format: "text",
}, },
PrivateKey: hexPriv(cfg.Secrets.Proposer), PrivateKey: hexPriv(cfg.Secrets.Proposer),
}, sys.cfg.Loggers["proposer"]) }, sys.cfg.Loggers["proposer"], proposermetrics.NoopMetrics)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to setup l2 output submitter: %w", err) return nil, fmt.Errorf("unable to setup l2 output submitter: %w", err)
} }
...@@ -582,10 +590,13 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -582,10 +590,13 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
} }
// Batch Submitter // Batch Submitter
txManagerTimeout := 10 * time.Minute
sys.BatchSubmitter, err = bss.NewBatchSubmitterFromCLIConfig(bss.CLIConfig{ sys.BatchSubmitter, err = bss.NewBatchSubmitterFromCLIConfig(bss.CLIConfig{
L1EthRpc: sys.Nodes["l1"].WSEndpoint(), L1EthRpc: sys.Nodes["l1"].WSEndpoint(),
L2EthRpc: sys.Nodes["sequencer"].WSEndpoint(), L2EthRpc: sys.Nodes["sequencer"].WSEndpoint(),
RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(), RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(),
TxManagerTimeout: txManagerTimeout,
OfflineGasEstimation: true,
MaxChannelDuration: 1, MaxChannelDuration: 1,
MaxL1TxSize: 120_000, MaxL1TxSize: 120_000,
TargetL1TxSize: 100_000, TargetL1TxSize: 100_000,
...@@ -606,8 +617,11 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -606,8 +617,11 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
return nil, fmt.Errorf("failed to setup batch submitter: %w", err) return nil, fmt.Errorf("failed to setup batch submitter: %w", err)
} }
if err := sys.BatchSubmitter.Start(); err != nil { // Batcher may be enabled later
return nil, fmt.Errorf("unable to start batch submitter: %w", err) if !sys.cfg.DisableBatcher {
if err := sys.BatchSubmitter.Start(); err != nil {
return nil, fmt.Errorf("unable to start batch submitter: %w", err)
}
} }
return sys, nil return sys, nil
......
...@@ -649,7 +649,7 @@ func TestSystemMockP2P(t *testing.T) { ...@@ -649,7 +649,7 @@ func TestSystemMockP2P(t *testing.T) {
require.Contains(t, received, receiptVerif.BlockHash) require.Contains(t, received, receiptVerif.BlockHash)
} }
// TestSystemMockP2P sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that // TestSystemRPCAltSync sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that
// the nodes can sync L2 blocks before they are confirmed on L1. // the nodes can sync L2 blocks before they are confirmed on L1.
// //
// Test steps: // Test steps:
...@@ -660,24 +660,28 @@ func TestSystemMockP2P(t *testing.T) { ...@@ -660,24 +660,28 @@ func TestSystemMockP2P(t *testing.T) {
// 6. Wait for the RPC sync method to grab the block from the sequencer over RPC and insert it into the verifier's unsafe chain. // 6. Wait for the RPC sync method to grab the block from the sequencer over RPC and insert it into the verifier's unsafe chain.
// 7. Wait for the verifier to sync the unsafe chain into the safe chain. // 7. Wait for the verifier to sync the unsafe chain into the safe chain.
// 8. Verify that the TX is included in the verifier's safe chain. // 8. Verify that the TX is included in the verifier's safe chain.
func TestSystemMockAltSync(t *testing.T) { func TestSystemRPCAltSync(t *testing.T) {
parallel(t) parallel(t)
if !verboseGethNodes { if !verboseGethNodes {
log.Root().SetHandler(log.DiscardHandler()) log.Root().SetHandler(log.DiscardHandler())
} }
cfg := DefaultSystemConfig(t) cfg := DefaultSystemConfig(t)
// slow down L1 blocks so we can see the L2 blocks arrive well before the L1 blocks do. // the default is nil, but this may change in the future.
// Keep the seq window small so the L2 chain is started quick // This test must ensure the blocks are not synced via Gossip, but instead via the alt RPC based sync.
cfg.DeployConfig.L1BlockTime = 10 cfg.P2PTopology = nil
// Disable batcher, so there will not be any L1 data to sync from
cfg.DisableBatcher = true
var published, received []common.Hash var published, received []string
seqTracer, verifTracer := new(FnTracer), new(FnTracer) seqTracer, verifTracer := new(FnTracer), new(FnTracer)
// The sequencer still publishes the blocks to the tracer, even if they do not reach the network due to disabled P2P
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) { seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.BlockHash) published = append(published, payload.ID().String())
} }
// Blocks are now received via the RPC based alt-sync method
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) { verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received = append(received, payload.BlockHash) received = append(received, payload.ID().String())
} }
cfg.Nodes["sequencer"].Tracer = seqTracer cfg.Nodes["sequencer"].Tracer = seqTracer
cfg.Nodes["verifier"].Tracer = verifTracer cfg.Nodes["verifier"].Tracer = verifTracer
...@@ -687,8 +691,8 @@ func TestSystemMockAltSync(t *testing.T) { ...@@ -687,8 +691,8 @@ func TestSystemMockAltSync(t *testing.T) {
role: "sequencer", role: "sequencer",
action: func(sCfg *SystemConfig, system *System) { action: func(sCfg *SystemConfig, system *System) {
rpc, _ := system.Nodes["sequencer"].Attach() // never errors rpc, _ := system.Nodes["sequencer"].Attach() // never errors
cfg.Nodes["verifier"].L2Sync = &rollupNode.L2SyncRPCConfig{ cfg.Nodes["verifier"].L2Sync = &rollupNode.PreparedL2SyncEndpoint{
Rpc: client.NewBaseRPCClient(rpc), Client: client.NewBaseRPCClient(rpc),
} }
}, },
}) })
...@@ -726,7 +730,7 @@ func TestSystemMockAltSync(t *testing.T) { ...@@ -726,7 +730,7 @@ func TestSystemMockAltSync(t *testing.T) {
require.Equal(t, receiptSeq, receiptVerif) require.Equal(t, receiptSeq, receiptVerif)
// Verify that the tx was received via RPC sync (P2P is disabled) // Verify that the tx was received via RPC sync (P2P is disabled)
require.Contains(t, received, receiptVerif.BlockHash) require.Contains(t, received, eth.BlockID{Hash: receiptVerif.BlockHash, Number: receiptVerif.BlockNumber.Uint64()}.String())
// Verify that everything that was received was published // Verify that everything that was received was published
require.GreaterOrEqual(t, len(published), len(received)) require.GreaterOrEqual(t, len(published), len(received))
......
...@@ -52,6 +52,12 @@ jq "select(.valid_data == false)|.tx.hash" $TX_DIR ...@@ -52,6 +52,12 @@ jq "select(.valid_data == false)|.tx.hash" $TX_DIR
# Select all channels that are not ready and then get the id and inclusion block & tx hash of the first frame. # Select all channels that are not ready and then get the id and inclusion block & tx hash of the first frame.
jq "select(.is_ready == false)|[.id, .frames[0].inclusion_block, .frames[0].transaction_hash]" $CHANNEL_DIR jq "select(.is_ready == false)|[.id, .frames[0].inclusion_block, .frames[0].transaction_hash]" $CHANNEL_DIR
# Show all of the frames in a channel without seeing the batches or frame data
jq 'del(.batches)|del(.frames[]|.frame.data)' $CHANNEL_FILE
# Show all batches (without timestamps) in a channel
jq '.batches|del(.[]|.Transactions)' $CHANNEL_FILE
``` ```
......
...@@ -17,3 +17,9 @@ const ( ...@@ -17,3 +17,9 @@ const (
// - L2: Derived chain tip from finalized L1 data // - L2: Derived chain tip from finalized L1 data
Finalized = "finalized" Finalized = "finalized"
) )
func (label BlockLabel) Arg() any { return string(label) }
func (BlockLabel) CheckID(id BlockID) error {
return nil
}
...@@ -14,10 +14,10 @@ import ( ...@@ -14,10 +14,10 @@ import (
// Flags // Flags
const envVarPrefix = "OP_NODE_" const envVarPrefix = "OP_NODE"
func prefixEnvVar(name string) string { func prefixEnvVar(name string) string {
return envVarPrefix + name return envVarPrefix + "_" + name
} }
var ( var (
...@@ -175,6 +175,13 @@ var ( ...@@ -175,6 +175,13 @@ var (
EnvVar: prefixEnvVar("L2_BACKUP_UNSAFE_SYNC_RPC"), EnvVar: prefixEnvVar("L2_BACKUP_UNSAFE_SYNC_RPC"),
Required: false, Required: false,
} }
BackupL2UnsafeSyncRPCTrustRPC = cli.StringFlag{
Name: "l2.backup-unsafe-sync-rpc.trustrpc",
Usage: "Like l1.trustrpc, configure if response data from the RPC needs to be verified, e.g. blockhash computation." +
"This does not include checks if the blockhash is part of the canonical chain.",
EnvVar: prefixEnvVar("L2_BACKUP_UNSAFE_SYNC_RPC_TRUST_RPC"),
Required: false,
}
) )
var requiredFlags = []cli.Flag{ var requiredFlags = []cli.Flag{
...@@ -207,6 +214,7 @@ var optionalFlags = []cli.Flag{ ...@@ -207,6 +214,7 @@ var optionalFlags = []cli.Flag{
HeartbeatMonikerFlag, HeartbeatMonikerFlag,
HeartbeatURLFlag, HeartbeatURLFlag,
BackupL2UnsafeSyncRPC, BackupL2UnsafeSyncRPC,
BackupL2UnsafeSyncRPCTrustRPC,
} }
// Flags contains the list of configuration options available to the binary. // Flags contains the list of configuration options available to the binary.
......
...@@ -34,6 +34,15 @@ var ( ...@@ -34,6 +34,15 @@ var (
Value: "none", Value: "none",
EnvVar: p2pEnv("PEER_SCORING"), EnvVar: p2pEnv("PEER_SCORING"),
} }
PeerScoreBands = cli.StringFlag{
Name: "p2p.score.bands",
Usage: "Sets the peer score bands used primarily for peer score metrics. " +
"Should be provided in following format: <threshold>:<label>;<threshold>:<label>;..." +
"For example: -40:graylist;-20:restricted;0:nopx;20:friend;",
Required: false,
Value: "-40:graylist;-20:restricted;0:nopx;20:friend;",
EnvVar: p2pEnv("SCORE_BANDS"),
}
// Banning Flag - whether or not we want to act on the scoring // Banning Flag - whether or not we want to act on the scoring
Banning = cli.BoolFlag{ Banning = cli.BoolFlag{
...@@ -276,6 +285,10 @@ var p2pFlags = []cli.Flag{ ...@@ -276,6 +285,10 @@ var p2pFlags = []cli.Flag{
NoDiscovery, NoDiscovery,
P2PPrivPath, P2PPrivPath,
P2PPrivRaw, P2PPrivRaw,
PeerScoring,
PeerScoreBands,
Banning,
TopicScoring,
ListenIP, ListenIP,
ListenTCPPort, ListenTCPPort,
ListenUDPPort, ListenUDPPort,
......
...@@ -15,7 +15,6 @@ import ( ...@@ -15,7 +15,6 @@ import (
pb "github.com/libp2p/go-libp2p-pubsub/pb" pb "github.com/libp2p/go-libp2p-pubsub/pb"
libp2pmetrics "github.com/libp2p/go-libp2p/core/metrics" libp2pmetrics "github.com/libp2p/go-libp2p/core/metrics"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
...@@ -66,7 +65,7 @@ type Metricer interface { ...@@ -66,7 +65,7 @@ type Metricer interface {
RecordSequencerSealingTime(duration time.Duration) RecordSequencerSealingTime(duration time.Duration)
Document() []metrics.DocumentedMetric Document() []metrics.DocumentedMetric
// P2P Metrics // P2P Metrics
RecordPeerScoring(peerID peer.ID, score float64) SetPeerScores(scores map[string]float64)
} }
// Metrics tracks all the metrics for the op-node. // Metrics tracks all the metrics for the op-node.
...@@ -287,21 +286,24 @@ func NewMetrics(procName string) *Metrics { ...@@ -287,21 +286,24 @@ func NewMetrics(procName string) *Metrics {
Name: "peer_count", Name: "peer_count",
Help: "Count of currently connected p2p peers", Help: "Count of currently connected p2p peers",
}), }),
StreamCount: factory.NewGauge(prometheus.GaugeOpts{ // Notice: We cannot use peer ids as [Labels] in the GaugeVec
Namespace: ns, // since peer ids would open a service attack vector.
Subsystem: "p2p", // Each peer id would be a separate metric, flooding prometheus.
Name: "stream_count", //
Help: "Count of currently connected p2p streams", // [Labels]: https://prometheus.io/docs/practices/naming/#labels
}),
PeerScores: factory.NewGaugeVec(prometheus.GaugeOpts{ PeerScores: factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns, Namespace: ns,
Subsystem: "p2p", Subsystem: "p2p",
Name: "peer_scores", Name: "peer_scores",
Help: "Peer scoring", Help: "Count of peer scores grouped by score",
}, []string{ }, []string{
// No label names here since peer ids would open a service attack vector. "band",
// Each peer id would be a separate metric, flooding prometheus. }),
// See: https://prometheus.io/docs/practices/naming/#labels StreamCount: factory.NewGauge(prometheus.GaugeOpts{
Namespace: ns,
Subsystem: "p2p",
Name: "stream_count",
Help: "Count of currently connected p2p streams",
}), }),
GossipEventsTotal: factory.NewCounterVec(prometheus.CounterOpts{ GossipEventsTotal: factory.NewCounterVec(prometheus.CounterOpts{
Namespace: ns, Namespace: ns,
...@@ -350,6 +352,14 @@ func NewMetrics(procName string) *Metrics { ...@@ -350,6 +352,14 @@ func NewMetrics(procName string) *Metrics {
} }
} }
// SetPeerScores updates the peer score [prometheus.GaugeVec].
// This takes a map of labels to scores.
func (m *Metrics) SetPeerScores(scores map[string]float64) {
for label, score := range scores {
m.PeerScores.WithLabelValues(label).Set(score)
}
}
// RecordInfo sets a pseudo-metric that contains versioning and // RecordInfo sets a pseudo-metric that contains versioning and
// config info for the opnode. // config info for the opnode.
func (m *Metrics) RecordInfo(version string) { func (m *Metrics) RecordInfo(version string) {
...@@ -491,10 +501,6 @@ func (m *Metrics) RecordGossipEvent(evType int32) { ...@@ -491,10 +501,6 @@ func (m *Metrics) RecordGossipEvent(evType int32) {
m.GossipEventsTotal.WithLabelValues(pb.TraceEvent_Type_name[evType]).Inc() m.GossipEventsTotal.WithLabelValues(pb.TraceEvent_Type_name[evType]).Inc()
} }
func (m *Metrics) RecordPeerScoring(peerID peer.ID, score float64) {
m.PeerScores.WithLabelValues(peerID.String()).Set(score)
}
func (m *Metrics) IncPeerCount() { func (m *Metrics) IncPeerCount() {
m.PeerCount.Inc() m.PeerCount.Inc()
} }
...@@ -627,7 +633,7 @@ func (n *noopMetricer) RecordSequencerReset() { ...@@ -627,7 +633,7 @@ func (n *noopMetricer) RecordSequencerReset() {
func (n *noopMetricer) RecordGossipEvent(evType int32) { func (n *noopMetricer) RecordGossipEvent(evType int32) {
} }
func (n *noopMetricer) RecordPeerScoring(peerID peer.ID, score float64) { func (n *noopMetricer) SetPeerScores(scores map[string]float64) {
} }
func (n *noopMetricer) IncPeerCount() { func (n *noopMetricer) IncPeerCount() {
......
...@@ -20,7 +20,9 @@ type L2EndpointSetup interface { ...@@ -20,7 +20,9 @@ type L2EndpointSetup interface {
} }
type L2SyncEndpointSetup interface { type L2SyncEndpointSetup interface {
Setup(ctx context.Context, log log.Logger) (cl client.RPC, err error) // Setup a RPC client to another L2 node to sync L2 blocks from.
// It may return a nil client with nil error if RPC based sync is not enabled.
Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, err error)
Check() error Check() error
} }
...@@ -82,45 +84,45 @@ func (p *PreparedL2Endpoints) Setup(ctx context.Context, log log.Logger) (client ...@@ -82,45 +84,45 @@ func (p *PreparedL2Endpoints) Setup(ctx context.Context, log log.Logger) (client
// L2SyncEndpointConfig contains configuration for the fallback sync endpoint // L2SyncEndpointConfig contains configuration for the fallback sync endpoint
type L2SyncEndpointConfig struct { type L2SyncEndpointConfig struct {
// Address of the L2 RPC to use for backup sync // Address of the L2 RPC to use for backup sync, may be empty if RPC alt-sync is disabled.
L2NodeAddr string L2NodeAddr string
TrustRPC bool
} }
var _ L2SyncEndpointSetup = (*L2SyncEndpointConfig)(nil) var _ L2SyncEndpointSetup = (*L2SyncEndpointConfig)(nil)
func (cfg *L2SyncEndpointConfig) Setup(ctx context.Context, log log.Logger) (client.RPC, error) { // Setup creates an RPC client to sync from.
// It will return nil without error if no sync method is configured.
func (cfg *L2SyncEndpointConfig) Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, err error) {
if cfg.L2NodeAddr == "" {
return nil, false, nil
}
l2Node, err := client.NewRPC(ctx, log, cfg.L2NodeAddr) l2Node, err := client.NewRPC(ctx, log, cfg.L2NodeAddr)
if err != nil { if err != nil {
return nil, err return nil, false, err
} }
return l2Node, nil return l2Node, cfg.TrustRPC, nil
} }
func (cfg *L2SyncEndpointConfig) Check() error { func (cfg *L2SyncEndpointConfig) Check() error {
if cfg.L2NodeAddr == "" { // empty addr is valid, as it is optional.
return errors.New("empty L2 Node Address")
}
return nil return nil
} }
type L2SyncRPCConfig struct { type PreparedL2SyncEndpoint struct {
// RPC endpoint to use for syncing // RPC endpoint to use for syncing, may be nil if RPC alt-sync is disabled.
Rpc client.RPC Client client.RPC
TrustRPC bool
} }
var _ L2SyncEndpointSetup = (*L2SyncRPCConfig)(nil) var _ L2SyncEndpointSetup = (*PreparedL2SyncEndpoint)(nil)
func (cfg *L2SyncRPCConfig) Setup(ctx context.Context, log log.Logger) (client.RPC, error) { func (cfg *PreparedL2SyncEndpoint) Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, err error) {
return cfg.Rpc, nil return cfg.Client, cfg.TrustRPC, nil
} }
func (cfg *L2SyncRPCConfig) Check() error { func (cfg *PreparedL2SyncEndpoint) Check() error {
if cfg.Rpc == nil {
return errors.New("rpc cannot be nil")
}
return nil return nil
} }
......
...@@ -80,6 +80,9 @@ func (cfg *Config) Check() error { ...@@ -80,6 +80,9 @@ func (cfg *Config) Check() error {
if err := cfg.L2.Check(); err != nil { if err := cfg.L2.Check(); err != nil {
return fmt.Errorf("l2 endpoint config error: %w", err) return fmt.Errorf("l2 endpoint config error: %w", err)
} }
if err := cfg.L2Sync.Check(); err != nil {
return fmt.Errorf("sync config error: %w", err)
}
if err := cfg.Rollup.Check(); err != nil { if err := cfg.Rollup.Check(); err != nil {
return fmt.Errorf("rollup config error: %w", err) return fmt.Errorf("rollup config error: %w", err)
} }
......
...@@ -33,6 +33,7 @@ type OpNode struct { ...@@ -33,6 +33,7 @@ type OpNode struct {
l1Source *sources.L1Client // L1 Client to fetch data from l1Source *sources.L1Client // L1 Client to fetch data from
l2Driver *driver.Driver // L2 Engine to Sync l2Driver *driver.Driver // L2 Engine to Sync
l2Source *sources.EngineClient // L2 Execution Engine RPC bindings l2Source *sources.EngineClient // L2 Execution Engine RPC bindings
rpcSync *sources.SyncClient // Alt-sync RPC client, optional (may be nil)
server *rpcServer // RPC server hosting the rollup-node API server *rpcServer // RPC server hosting the rollup-node API
p2pNode *p2p.NodeP2P // P2P node functionality p2pNode *p2p.NodeP2P // P2P node functionality
p2pSigner p2p.Signer // p2p gogssip application messages will be signed with this signer p2pSigner p2p.Signer // p2p gogssip application messages will be signed with this signer
...@@ -86,6 +87,9 @@ func (n *OpNode) init(ctx context.Context, cfg *Config, snapshotLog log.Logger) ...@@ -86,6 +87,9 @@ func (n *OpNode) init(ctx context.Context, cfg *Config, snapshotLog log.Logger)
if err := n.initL2(ctx, cfg, snapshotLog); err != nil { if err := n.initL2(ctx, cfg, snapshotLog); err != nil {
return err return err
} }
if err := n.initRPCSync(ctx, cfg); err != nil {
return err
}
if err := n.initP2PSigner(ctx, cfg); err != nil { if err := n.initP2PSigner(ctx, cfg); err != nil {
return err return err
} }
...@@ -197,29 +201,27 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger ...@@ -197,29 +201,27 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger
return err return err
} }
var syncClient *sources.SyncClient n.l2Driver = driver.NewDriver(&cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source, n, n, n.log, snapshotLog, n.metrics)
// If the L2 sync config is present, use it to create a sync client
if cfg.L2Sync != nil {
if err := cfg.L2Sync.Check(); err != nil {
log.Info("L2 sync config is not present, skipping L2 sync client setup", "err", err)
} else {
rpcSyncClient, err := cfg.L2Sync.Setup(ctx, n.log)
if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err)
}
// The sync client's RPC is always trusted return nil
config := sources.SyncClientDefaultConfig(&cfg.Rollup, true) }
syncClient, err = sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, config) func (n *OpNode) initRPCSync(ctx context.Context, cfg *Config) error {
if err != nil { rpcSyncClient, trustRPC, err := cfg.L2Sync.Setup(ctx, n.log)
return fmt.Errorf("failed to create sync client: %w", err) if err != nil {
} return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err)
} }
if rpcSyncClient == nil { // if no RPC client is configured to sync from, then don't add the RPC sync client
return nil
} }
n.l2Driver = driver.NewDriver(&cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source, syncClient, n, n.log, snapshotLog, n.metrics) config := sources.SyncClientDefaultConfig(&cfg.Rollup, trustRPC)
syncClient, err := sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, config)
if err != nil {
return fmt.Errorf("failed to create sync client: %w", err)
}
n.rpcSync = syncClient
return nil return nil
} }
...@@ -292,11 +294,12 @@ func (n *OpNode) Start(ctx context.Context) error { ...@@ -292,11 +294,12 @@ func (n *OpNode) Start(ctx context.Context) error {
} }
// If the backup unsafe sync client is enabled, start its event loop // If the backup unsafe sync client is enabled, start its event loop
if n.l2Driver.L2SyncCl != nil { if n.rpcSync != nil {
if err := n.l2Driver.L2SyncCl.Start(); err != nil { if err := n.rpcSync.Start(); err != nil {
n.log.Error("Could not start the backup sync client", "err", err) n.log.Error("Could not start the backup sync client", "err", err)
return err return err
} }
n.log.Info("Started L2-RPC sync service")
} }
return nil return nil
...@@ -375,6 +378,14 @@ func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *e ...@@ -375,6 +378,14 @@ func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *e
return nil return nil
} }
func (n *OpNode) RequestL2Range(ctx context.Context, start, end uint64) error {
if n.rpcSync != nil {
return n.rpcSync.RequestL2Range(ctx, start, end)
}
n.log.Debug("ignoring request to sync L2 range, no sync method available")
return nil
}
func (n *OpNode) P2P() p2p.Node { func (n *OpNode) P2P() p2p.Node {
return n.p2pNode return n.p2pNode
} }
...@@ -413,8 +424,8 @@ func (n *OpNode) Close() error { ...@@ -413,8 +424,8 @@ func (n *OpNode) Close() error {
} }
// If the L2 sync client is present & running, close it. // If the L2 sync client is present & running, close it.
if n.l2Driver.L2SyncCl != nil { if n.rpcSync != nil {
if err := n.l2Driver.L2SyncCl.Close(); err != nil { if err := n.rpcSync.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to close L2 engine backup sync client cleanly: %w", err)) result = multierror.Append(result, fmt.Errorf("failed to close L2 engine backup sync client cleanly: %w", err))
} }
} }
......
package p2p
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestBandScorer_ParseDefault tests the [BandScorer.Parse] function
// on the default band scores cli flag value.
func TestBandScorer_ParseDefault(t *testing.T) {
// Create a new band scorer.
bandScorer, err := NewBandScorer("-40:graylist;-20:restricted;0:nopx;20:friend;")
require.NoError(t, err)
// Validate the [BandScorer] internals.
require.ElementsMatch(t, bandScorer.bands, []scorePair{
{band: "graylist", threshold: -40},
{band: "restricted", threshold: -20},
{band: "nopx", threshold: 0},
{band: "friend", threshold: 20},
})
}
// TestBandScorer_BucketCorrectly tests the [BandScorer.Bucket] function
// on a variety of scores.
func TestBandScorer_BucketCorrectly(t *testing.T) {
// Create a new band scorer.
bandScorer, err := NewBandScorer("-40:graylist;-20:restricted;0:nopx;20:friend;")
require.NoError(t, err)
// Validate the [BandScorer] internals.
require.Equal(t, bandScorer.Bucket(-100), "graylist")
require.Equal(t, bandScorer.Bucket(-40), "graylist")
require.Equal(t, bandScorer.Bucket(-39), "restricted")
require.Equal(t, bandScorer.Bucket(-20), "restricted")
require.Equal(t, bandScorer.Bucket(-19), "nopx")
require.Equal(t, bandScorer.Bucket(0), "nopx")
require.Equal(t, bandScorer.Bucket(1), "friend")
require.Equal(t, bandScorer.Bucket(20), "friend")
require.Equal(t, bandScorer.Bucket(21), "friend")
}
// TestBandScorer_BucketInverted tests the [BandScorer.Bucket] function
// on a variety of scores, in descending order.
func TestBandScorer_BucketInverted(t *testing.T) {
// Create a new band scorer.
bandScorer, err := NewBandScorer("20:friend;0:nopx;-20:restricted;-40:graylist;")
require.NoError(t, err)
// Validate the [BandScorer] internals.
require.Equal(t, bandScorer.Bucket(-100), "graylist")
require.Equal(t, bandScorer.Bucket(-40), "graylist")
require.Equal(t, bandScorer.Bucket(-39), "restricted")
require.Equal(t, bandScorer.Bucket(-20), "restricted")
require.Equal(t, bandScorer.Bucket(-19), "nopx")
require.Equal(t, bandScorer.Bucket(0), "nopx")
require.Equal(t, bandScorer.Bucket(1), "friend")
require.Equal(t, bandScorer.Bucket(20), "friend")
require.Equal(t, bandScorer.Bucket(21), "friend")
}
// TestBandScorer_ParseEmpty tests the [BandScorer.Parse] function
// on an empty string.
func TestBandScorer_ParseEmpty(t *testing.T) {
// Create a band scorer on an empty string.
bandScorer, err := NewBandScorer("")
require.NoError(t, err)
// Validate the [BandScorer] internals.
require.Len(t, bandScorer.bands, 0)
}
// TestBandScorer_ParseWhitespace tests the [BandScorer.Parse] function
// on a variety of whitespaced strings.
func TestBandScorer_ParseWhitespace(t *testing.T) {
// Create a band scorer on an empty string.
bandScorer, err := NewBandScorer(" ; ; ; ")
require.NoError(t, err)
// Validate the [BandScorer] internals.
require.Len(t, bandScorer.bands, 0)
}
...@@ -58,6 +58,10 @@ func NewConfig(ctx *cli.Context, blockTime uint64) (*p2p.Config, error) { ...@@ -58,6 +58,10 @@ func NewConfig(ctx *cli.Context, blockTime uint64) (*p2p.Config, error) {
return nil, fmt.Errorf("failed to load p2p peer scoring options: %w", err) return nil, fmt.Errorf("failed to load p2p peer scoring options: %w", err)
} }
if err := loadPeerScoreBands(conf, ctx); err != nil {
return nil, fmt.Errorf("failed to load p2p peer score bands: %w", err)
}
if err := loadBanningOption(conf, ctx); err != nil { if err := loadBanningOption(conf, ctx); err != nil {
return nil, fmt.Errorf("failed to load banning option: %w", err) return nil, fmt.Errorf("failed to load banning option: %w", err)
} }
...@@ -121,6 +125,17 @@ func loadPeerScoringParams(conf *p2p.Config, ctx *cli.Context, blockTime uint64) ...@@ -121,6 +125,17 @@ func loadPeerScoringParams(conf *p2p.Config, ctx *cli.Context, blockTime uint64)
return nil return nil
} }
// loadPeerScoreBands loads [p2p.BandScorer] from the CLI context.
func loadPeerScoreBands(conf *p2p.Config, ctx *cli.Context) error {
scoreBands := ctx.GlobalString(flags.PeerScoreBands.Name)
bandScorer, err := p2p.NewBandScorer(scoreBands)
if err != nil {
return err
}
conf.BandScoreThresholds = *bandScorer
return nil
}
// loadBanningOption loads whether or not to ban peers from the CLI context. // loadBanningOption loads whether or not to ban peers from the CLI context.
func loadBanningOption(conf *p2p.Config, ctx *cli.Context) error { func loadBanningOption(conf *p2p.Config, ctx *cli.Context) error {
ban := ctx.GlobalBool(flags.Banning.Name) ban := ctx.GlobalBool(flags.Banning.Name)
......
...@@ -54,6 +54,9 @@ type Config struct { ...@@ -54,6 +54,9 @@ type Config struct {
PeerScoring pubsub.PeerScoreParams PeerScoring pubsub.PeerScoreParams
TopicScoring pubsub.TopicScoreParams TopicScoring pubsub.TopicScoreParams
// Peer Score Band Thresholds
BandScoreThresholds BandScoreThresholds
// Whether to ban peers based on their [PeerScoring] score. // Whether to ban peers based on their [PeerScoring] score.
BanningEnabled bool BanningEnabled bool
...@@ -151,6 +154,10 @@ func (conf *Config) PeerScoringParams() *pubsub.PeerScoreParams { ...@@ -151,6 +154,10 @@ func (conf *Config) PeerScoringParams() *pubsub.PeerScoreParams {
return &conf.PeerScoring return &conf.PeerScoring
} }
func (conf *Config) PeerBandScorer() *BandScoreThresholds {
return &conf.BandScoreThresholds
}
func (conf *Config) BanPeers() bool { func (conf *Config) BanPeers() bool {
return conf.BanningEnabled return conf.BanningEnabled
} }
......
...@@ -55,6 +55,7 @@ type GossipSetupConfigurables interface { ...@@ -55,6 +55,7 @@ type GossipSetupConfigurables interface {
TopicScoringParams() *pubsub.TopicScoreParams TopicScoringParams() *pubsub.TopicScoreParams
BanPeers() bool BanPeers() bool
ConfigureGossip(params *pubsub.GossipSubParams) []pubsub.Option ConfigureGossip(params *pubsub.GossipSubParams) []pubsub.Option
PeerBandScorer() *BandScoreThresholds
} }
type GossipRuntimeConfig interface { type GossipRuntimeConfig interface {
...@@ -64,7 +65,8 @@ type GossipRuntimeConfig interface { ...@@ -64,7 +65,8 @@ type GossipRuntimeConfig interface {
//go:generate mockery --name GossipMetricer //go:generate mockery --name GossipMetricer
type GossipMetricer interface { type GossipMetricer interface {
RecordGossipEvent(evType int32) RecordGossipEvent(evType int32)
RecordPeerScoring(peerID peer.ID, score float64) // Peer Scoring Metric Funcs
SetPeerScores(map[string]float64)
} }
func blocksTopicV1(cfg *rollup.Config) string { func blocksTopicV1(cfg *rollup.Config) string {
......
// Code generated by mockery v2.14.0. DO NOT EDIT. // Code generated by mockery v2.22.1. DO NOT EDIT.
package mocks package mocks
...@@ -123,13 +123,16 @@ func (_m *ConnectionGater) InterceptUpgraded(_a0 network.Conn) (bool, control.Di ...@@ -123,13 +123,16 @@ func (_m *ConnectionGater) InterceptUpgraded(_a0 network.Conn) (bool, control.Di
ret := _m.Called(_a0) ret := _m.Called(_a0)
var r0 bool var r0 bool
var r1 control.DisconnectReason
if rf, ok := ret.Get(0).(func(network.Conn) (bool, control.DisconnectReason)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(network.Conn) bool); ok { if rf, ok := ret.Get(0).(func(network.Conn) bool); ok {
r0 = rf(_a0) r0 = rf(_a0)
} else { } else {
r0 = ret.Get(0).(bool) r0 = ret.Get(0).(bool)
} }
var r1 control.DisconnectReason
if rf, ok := ret.Get(1).(func(network.Conn) control.DisconnectReason); ok { if rf, ok := ret.Get(1).(func(network.Conn) control.DisconnectReason); ok {
r1 = rf(_a0) r1 = rf(_a0)
} else { } else {
......
// Code generated by mockery v2.14.0. DO NOT EDIT. // Code generated by mockery v2.22.1. DO NOT EDIT.
package mocks package mocks
import ( import mock "github.com/stretchr/testify/mock"
mock "github.com/stretchr/testify/mock"
peer "github.com/libp2p/go-libp2p/core/peer"
)
// GossipMetricer is an autogenerated mock type for the GossipMetricer type // GossipMetricer is an autogenerated mock type for the GossipMetricer type
type GossipMetricer struct { type GossipMetricer struct {
...@@ -18,9 +14,9 @@ func (_m *GossipMetricer) RecordGossipEvent(evType int32) { ...@@ -18,9 +14,9 @@ func (_m *GossipMetricer) RecordGossipEvent(evType int32) {
_m.Called(evType) _m.Called(evType)
} }
// RecordPeerScoring provides a mock function with given fields: peerID, score // SetPeerScores provides a mock function with given fields: _a0
func (_m *GossipMetricer) RecordPeerScoring(peerID peer.ID, score float64) { func (_m *GossipMetricer) SetPeerScores(_a0 map[string]float64) {
_m.Called(peerID, score) _m.Called(_a0)
} }
type mockConstructorTestingTNewGossipMetricer interface { type mockConstructorTestingTNewGossipMetricer interface {
......
// Code generated by mockery v2.14.0. DO NOT EDIT. // Code generated by mockery v2.22.1. DO NOT EDIT.
package mocks package mocks
......
// Code generated by mockery v2.14.0. DO NOT EDIT. // Code generated by mockery v2.22.1. DO NOT EDIT.
package mocks package mocks
......
...@@ -43,11 +43,15 @@ func (s *gater) Update(id peer.ID, score float64) { ...@@ -43,11 +43,15 @@ func (s *gater) Update(id peer.ID, score float64) {
if score < PeerScoreThreshold && s.banEnabled { if score < PeerScoreThreshold && s.banEnabled {
s.log.Warn("peer blocking enabled, blocking peer", "id", id.String(), "score", score) s.log.Warn("peer blocking enabled, blocking peer", "id", id.String(), "score", score)
err := s.connGater.BlockPeer(id) err := s.connGater.BlockPeer(id)
s.log.Warn("connection gater failed to block peer", id.String(), "err", err) if err != nil {
s.log.Warn("connection gater failed to block peer", "id", id.String(), "err", err)
}
} }
// Unblock peers whose score has recovered to an acceptable level // Unblock peers whose score has recovered to an acceptable level
if (score > PeerScoreThreshold) && slices.Contains(s.connGater.ListBlockedPeers(), id) { if (score > PeerScoreThreshold) && slices.Contains(s.connGater.ListBlockedPeers(), id) {
err := s.connGater.UnblockPeer(id) err := s.connGater.UnblockPeer(id)
s.log.Warn("connection gater failed to unblock peer", id.String(), "err", err) if err != nil {
s.log.Warn("connection gater failed to unblock peer", "id", id.String(), "err", err)
}
} }
} }
package p2p package p2p
import ( import (
"fmt"
"sort"
"strconv"
"strings"
log "github.com/ethereum/go-ethereum/log" log "github.com/ethereum/go-ethereum/log"
pubsub "github.com/libp2p/go-libp2p-pubsub" pubsub "github.com/libp2p/go-libp2p-pubsub"
peer "github.com/libp2p/go-libp2p/core/peer" peer "github.com/libp2p/go-libp2p/core/peer"
) )
type scorer struct { type scorer struct {
peerStore Peerstore peerStore Peerstore
metricer GossipMetricer metricer GossipMetricer
log log.Logger log log.Logger
gater PeerGater gater PeerGater
bandScoreThresholds *BandScoreThresholds
}
// scorePair holds a band and its corresponding threshold.
type scorePair struct {
band string
threshold float64
}
// BandScoreThresholds holds the thresholds for classifying peers
// into different score bands.
type BandScoreThresholds struct {
bands []scorePair
}
// NewBandScorer constructs a new [BandScoreThresholds] instance.
func NewBandScorer(str string) (*BandScoreThresholds, error) {
s := &BandScoreThresholds{
bands: make([]scorePair, 0),
}
for _, band := range strings.Split(str, ";") {
// Skip empty band strings.
band := strings.TrimSpace(band)
if band == "" {
continue
}
split := strings.Split(band, ":")
if len(split) != 2 {
return nil, fmt.Errorf("invalid score band: %s", band)
}
threshold, err := strconv.ParseFloat(split[0], 64)
if err != nil {
return nil, err
}
s.bands = append(s.bands, scorePair{
band: split[1],
threshold: threshold,
})
}
// Order the bands by threshold in ascending order.
sort.Slice(s.bands, func(i, j int) bool {
return s.bands[i].threshold < s.bands[j].threshold
})
return s, nil
}
// Bucket returns the appropriate band for a given score.
func (s *BandScoreThresholds) Bucket(score float64) string {
for _, pair := range s.bands {
if score <= pair.threshold {
return pair.band
}
}
// If there is no band threshold higher than the score,
// the peer must be placed in the highest bucket.
if len(s.bands) > 0 {
return s.bands[len(s.bands)-1].band
}
return ""
} }
// Peerstore is a subset of the libp2p peerstore.Peerstore interface. // Peerstore is a subset of the libp2p peerstore.Peerstore interface.
...@@ -34,12 +101,13 @@ type Scorer interface { ...@@ -34,12 +101,13 @@ type Scorer interface {
} }
// NewScorer returns a new peer scorer. // NewScorer returns a new peer scorer.
func NewScorer(peerGater PeerGater, peerStore Peerstore, metricer GossipMetricer, log log.Logger) Scorer { func NewScorer(peerGater PeerGater, peerStore Peerstore, metricer GossipMetricer, bandScoreThresholds *BandScoreThresholds, log log.Logger) Scorer {
return &scorer{ return &scorer{
peerStore: peerStore, peerStore: peerStore,
metricer: metricer, metricer: metricer,
log: log, log: log,
gater: peerGater, gater: peerGater,
bandScoreThresholds: bandScoreThresholds,
} }
} }
...@@ -48,13 +116,13 @@ func NewScorer(peerGater PeerGater, peerStore Peerstore, metricer GossipMetricer ...@@ -48,13 +116,13 @@ func NewScorer(peerGater PeerGater, peerStore Peerstore, metricer GossipMetricer
// The returned [pubsub.ExtendedPeerScoreInspectFn] is called with a mapping of peer IDs to peer score snapshots. // The returned [pubsub.ExtendedPeerScoreInspectFn] is called with a mapping of peer IDs to peer score snapshots.
func (s *scorer) SnapshotHook() pubsub.ExtendedPeerScoreInspectFn { func (s *scorer) SnapshotHook() pubsub.ExtendedPeerScoreInspectFn {
return func(m map[peer.ID]*pubsub.PeerScoreSnapshot) { return func(m map[peer.ID]*pubsub.PeerScoreSnapshot) {
scoreMap := make(map[string]float64)
for id, snap := range m { for id, snap := range m {
// Record peer score in the metricer band := s.bandScoreThresholds.Bucket(snap.Score)
s.metricer.RecordPeerScoring(id, snap.Score) scoreMap[band] += 1
// Update with the peer gater
s.gater.Update(id, snap.Score) s.gater.Update(id, snap.Score)
} }
s.metricer.SetPeerScores(scoreMap)
} }
} }
......
...@@ -20,15 +20,18 @@ type PeerScorerTestSuite struct { ...@@ -20,15 +20,18 @@ type PeerScorerTestSuite struct {
mockGater *p2pMocks.PeerGater mockGater *p2pMocks.PeerGater
mockStore *p2pMocks.Peerstore mockStore *p2pMocks.Peerstore
mockMetricer *p2pMocks.GossipMetricer mockMetricer *p2pMocks.GossipMetricer
bandScorer *p2p.BandScoreThresholds
logger log.Logger logger log.Logger
} }
// SetupTest sets up the test suite. // SetupTest sets up the test suite.
func (testSuite *PeerScorerTestSuite) SetupTest() { func (testSuite *PeerScorerTestSuite) SetupTest() {
testSuite.mockGater = &p2pMocks.PeerGater{} testSuite.mockGater = &p2pMocks.PeerGater{}
// testSuite.mockConnGater = &p2pMocks.ConnectionGater{}
testSuite.mockStore = &p2pMocks.Peerstore{} testSuite.mockStore = &p2pMocks.Peerstore{}
testSuite.mockMetricer = &p2pMocks.GossipMetricer{} testSuite.mockMetricer = &p2pMocks.GossipMetricer{}
bandScorer, err := p2p.NewBandScorer("0:graylist;")
testSuite.NoError(err)
testSuite.bandScorer = bandScorer
testSuite.logger = testlog.Logger(testSuite.T(), log.LvlError) testSuite.logger = testlog.Logger(testSuite.T(), log.LvlError)
} }
...@@ -37,45 +40,49 @@ func TestPeerScorer(t *testing.T) { ...@@ -37,45 +40,49 @@ func TestPeerScorer(t *testing.T) {
suite.Run(t, new(PeerScorerTestSuite)) suite.Run(t, new(PeerScorerTestSuite))
} }
// TestPeerScorerOnConnect ensures we can call the OnConnect method on the peer scorer. // TestScorer_OnConnect ensures we can call the OnConnect method on the peer scorer.
func (testSuite *PeerScorerTestSuite) TestPeerScorerOnConnect() { func (testSuite *PeerScorerTestSuite) TestScorer_OnConnect() {
scorer := p2p.NewScorer( scorer := p2p.NewScorer(
testSuite.mockGater, testSuite.mockGater,
testSuite.mockStore, testSuite.mockStore,
testSuite.mockMetricer, testSuite.mockMetricer,
testSuite.bandScorer,
testSuite.logger, testSuite.logger,
) )
scorer.OnConnect() scorer.OnConnect()
} }
// TestPeerScorerOnDisconnect ensures we can call the OnDisconnect method on the peer scorer. // TestScorer_OnDisconnect ensures we can call the OnDisconnect method on the peer scorer.
func (testSuite *PeerScorerTestSuite) TestPeerScorerOnDisconnect() { func (testSuite *PeerScorerTestSuite) TestScorer_OnDisconnect() {
scorer := p2p.NewScorer( scorer := p2p.NewScorer(
testSuite.mockGater, testSuite.mockGater,
testSuite.mockStore, testSuite.mockStore,
testSuite.mockMetricer, testSuite.mockMetricer,
testSuite.bandScorer,
testSuite.logger, testSuite.logger,
) )
scorer.OnDisconnect() scorer.OnDisconnect()
} }
// TestSnapshotHook tests running the snapshot hook on the peer scorer. // TestScorer_SnapshotHook tests running the snapshot hook on the peer scorer.
func (testSuite *PeerScorerTestSuite) TestSnapshotHook() { func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHook() {
scorer := p2p.NewScorer( scorer := p2p.NewScorer(
testSuite.mockGater, testSuite.mockGater,
testSuite.mockStore, testSuite.mockStore,
testSuite.mockMetricer, testSuite.mockMetricer,
testSuite.bandScorer,
testSuite.logger, testSuite.logger,
) )
inspectFn := scorer.SnapshotHook() inspectFn := scorer.SnapshotHook()
// Mock the snapshot updates
// This doesn't return anything
testSuite.mockMetricer.On("RecordPeerScoring", peer.ID("peer1"), float64(-100)).Return(nil)
// Mock the peer gater call // Mock the peer gater call
testSuite.mockGater.On("Update", peer.ID("peer1"), float64(-100)).Return(nil) testSuite.mockGater.On("Update", peer.ID("peer1"), float64(-100)).Return(nil)
// The metricer should then be called with the peer score band map
testSuite.mockMetricer.On("SetPeerScores", map[string]float64{
"graylist": 1,
}).Return(nil)
// Apply the snapshot // Apply the snapshot
snapshotMap := map[peer.ID]*pubsub.PeerScoreSnapshot{ snapshotMap := map[peer.ID]*pubsub.PeerScoreSnapshot{
peer.ID("peer1"): { peer.ID("peer1"): {
...@@ -85,24 +92,26 @@ func (testSuite *PeerScorerTestSuite) TestSnapshotHook() { ...@@ -85,24 +92,26 @@ func (testSuite *PeerScorerTestSuite) TestSnapshotHook() {
inspectFn(snapshotMap) inspectFn(snapshotMap)
} }
// TestSnapshotHookBlockPeer tests running the snapshot hook on the peer scorer with a peer score below the threshold. // TestScorer_SnapshotHookBlocksPeer tests running the snapshot hook on the peer scorer with a peer score below the threshold.
// This implies that the peer should be blocked. // This implies that the peer should be blocked.
func (testSuite *PeerScorerTestSuite) TestSnapshotHookBlockPeer() { func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHookBlocksPeer() {
scorer := p2p.NewScorer( scorer := p2p.NewScorer(
testSuite.mockGater, testSuite.mockGater,
testSuite.mockStore, testSuite.mockStore,
testSuite.mockMetricer, testSuite.mockMetricer,
testSuite.bandScorer,
testSuite.logger, testSuite.logger,
) )
inspectFn := scorer.SnapshotHook() inspectFn := scorer.SnapshotHook()
// Mock the snapshot updates
// This doesn't return anything
testSuite.mockMetricer.On("RecordPeerScoring", peer.ID("peer1"), float64(-101)).Return(nil)
// Mock the peer gater call // Mock the peer gater call
testSuite.mockGater.On("Update", peer.ID("peer1"), float64(-101)).Return(nil) testSuite.mockGater.On("Update", peer.ID("peer1"), float64(-101)).Return(nil)
// The metricer should then be called with the peer score band map
testSuite.mockMetricer.On("SetPeerScores", map[string]float64{
"graylist": 1,
}).Return(nil)
// Apply the snapshot // Apply the snapshot
snapshotMap := map[peer.ID]*pubsub.PeerScoreSnapshot{ snapshotMap := map[peer.ID]*pubsub.PeerScoreSnapshot{
peer.ID("peer1"): { peer.ID("peer1"): {
......
...@@ -14,7 +14,7 @@ func ConfigurePeerScoring(h host.Host, g ConnectionGater, gossipConf GossipSetup ...@@ -14,7 +14,7 @@ func ConfigurePeerScoring(h host.Host, g ConnectionGater, gossipConf GossipSetup
peerScoreThresholds := NewPeerScoreThresholds() peerScoreThresholds := NewPeerScoreThresholds()
banEnabled := gossipConf.BanPeers() banEnabled := gossipConf.BanPeers()
peerGater := NewPeerGater(g, log, banEnabled) peerGater := NewPeerGater(g, log, banEnabled)
scorer := NewScorer(peerGater, h.Peerstore(), m, log) scorer := NewScorer(peerGater, h.Peerstore(), m, gossipConf.PeerBandScorer(), log)
opts := []pubsub.Option{} opts := []pubsub.Option{}
// Check the app specific score since libp2p doesn't export it's [validate] function :/ // Check the app specific score since libp2p doesn't export it's [validate] function :/
if peerScoreParams != nil && peerScoreParams.AppSpecificScore != nil { if peerScoreParams != nil && peerScoreParams.AppSpecificScore != nil {
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
p2pMocks "github.com/ethereum-optimism/optimism/op-node/p2p/mocks" p2pMocks "github.com/ethereum-optimism/optimism/op-node/p2p/mocks"
testlog "github.com/ethereum-optimism/optimism/op-node/testlog" testlog "github.com/ethereum-optimism/optimism/op-node/testlog"
mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
suite "github.com/stretchr/testify/suite" suite "github.com/stretchr/testify/suite"
log "github.com/ethereum/go-ethereum/log" log "github.com/ethereum/go-ethereum/log"
...@@ -30,6 +30,7 @@ type PeerScoresTestSuite struct { ...@@ -30,6 +30,7 @@ type PeerScoresTestSuite struct {
mockGater *p2pMocks.ConnectionGater mockGater *p2pMocks.ConnectionGater
mockStore *p2pMocks.Peerstore mockStore *p2pMocks.Peerstore
mockMetricer *p2pMocks.GossipMetricer mockMetricer *p2pMocks.GossipMetricer
bandScorer p2p.BandScoreThresholds
logger log.Logger logger log.Logger
} }
...@@ -38,6 +39,9 @@ func (testSuite *PeerScoresTestSuite) SetupTest() { ...@@ -38,6 +39,9 @@ func (testSuite *PeerScoresTestSuite) SetupTest() {
testSuite.mockGater = &p2pMocks.ConnectionGater{} testSuite.mockGater = &p2pMocks.ConnectionGater{}
testSuite.mockStore = &p2pMocks.Peerstore{} testSuite.mockStore = &p2pMocks.Peerstore{}
testSuite.mockMetricer = &p2pMocks.GossipMetricer{} testSuite.mockMetricer = &p2pMocks.GossipMetricer{}
bandScorer, err := p2p.NewBandScorer("0:graylist;")
testSuite.NoError(err)
testSuite.bandScorer = *bandScorer
testSuite.logger = testlog.Logger(testSuite.T(), log.LvlError) testSuite.logger = testlog.Logger(testSuite.T(), log.LvlError)
} }
...@@ -68,6 +72,7 @@ func newGossipSubs(testSuite *PeerScoresTestSuite, ctx context.Context, hosts [] ...@@ -68,6 +72,7 @@ func newGossipSubs(testSuite *PeerScoresTestSuite, ctx context.Context, hosts []
rt := pubsub.DefaultGossipSubRouter(h) rt := pubsub.DefaultGossipSubRouter(h)
opts := []pubsub.Option{} opts := []pubsub.Option{}
opts = append(opts, p2p.ConfigurePeerScoring(h, testSuite.mockGater, &p2p.Config{ opts = append(opts, p2p.ConfigurePeerScoring(h, testSuite.mockGater, &p2p.Config{
BandScoreThresholds: testSuite.bandScorer,
PeerScoring: pubsub.PeerScoreParams{ PeerScoring: pubsub.PeerScoreParams{
AppSpecificScore: func(p peer.ID) float64 { AppSpecificScore: func(p peer.ID) float64 {
if p == hosts[0].ID() { if p == hosts[0].ID() {
...@@ -118,8 +123,7 @@ func (testSuite *PeerScoresTestSuite) TestNegativeScores() { ...@@ -118,8 +123,7 @@ func (testSuite *PeerScoresTestSuite) TestNegativeScores() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
testSuite.mockMetricer.On("RecordPeerScoring", mock.Anything, float64(0)).Return(nil) testSuite.mockMetricer.On("SetPeerScores", mock.Anything).Return(nil)
testSuite.mockMetricer.On("RecordPeerScoring", mock.Anything, float64(-1000)).Return(nil)
testSuite.mockGater.On("ListBlockedPeers").Return([]peer.ID{}) testSuite.mockGater.On("ListBlockedPeers").Return([]peer.ID{})
......
...@@ -68,6 +68,10 @@ func (p *Prepared) PeerScoringParams() *pubsub.PeerScoreParams { ...@@ -68,6 +68,10 @@ func (p *Prepared) PeerScoringParams() *pubsub.PeerScoreParams {
return nil return nil
} }
func (p *Prepared) PeerBandScorer() *BandScoreThresholds {
return nil
}
func (p *Prepared) BanPeers() bool { func (p *Prepared) BanPeers() bool {
return false return false
} }
......
...@@ -14,9 +14,17 @@ import ( ...@@ -14,9 +14,17 @@ import (
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
var ErrMaxFrameSizeTooSmall = errors.New("maxSize is too small to fit the fixed frame overhead")
var ErrNotDepositTx = errors.New("first transaction in block is not a deposit tx") var ErrNotDepositTx = errors.New("first transaction in block is not a deposit tx")
var ErrTooManyRLPBytes = errors.New("batch would cause RLP bytes to go over limit") var ErrTooManyRLPBytes = errors.New("batch would cause RLP bytes to go over limit")
// FrameV0OverHeadSize is the absolute minimum size of a frame.
// This is the fixed overhead frame size, calculated as specified
// in the [Frame Format] specs: 16 + 2 + 4 + 1 = 23 bytes.
//
// [Frame Format]: https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#frame-format
const FrameV0OverHeadSize = 23
type ChannelOut struct { type ChannelOut struct {
id ChannelID id ChannelID
// Frame ID of the next frame to emit. Increment after emitting // Frame ID of the next frame to emit. Increment after emitting
...@@ -141,19 +149,23 @@ func (co *ChannelOut) Close() error { ...@@ -141,19 +149,23 @@ func (co *ChannelOut) Close() error {
// OutputFrame writes a frame to w with a given max size and returns the frame // OutputFrame writes a frame to w with a given max size and returns the frame
// number. // number.
// Use `ReadyBytes`, `Flush`, and `Close` to modify the ready buffer. // Use `ReadyBytes`, `Flush`, and `Close` to modify the ready buffer.
// Returns io.EOF when the channel is closed & there are no more frames // Returns an error if the `maxSize` < FrameV0OverHeadSize.
// Returns io.EOF when the channel is closed & there are no more frames.
// Returns nil if there is still more buffered data. // Returns nil if there is still more buffered data.
// Returns and error if it ran into an error during processing. // Returns an error if it ran into an error during processing.
func (co *ChannelOut) OutputFrame(w *bytes.Buffer, maxSize uint64) (uint16, error) { func (co *ChannelOut) OutputFrame(w *bytes.Buffer, maxSize uint64) (uint16, error) {
f := Frame{ f := Frame{
ID: co.id, ID: co.id,
FrameNumber: uint16(co.frame), FrameNumber: uint16(co.frame),
} }
// Check that the maxSize is large enough for the frame overhead size.
if maxSize < FrameV0OverHeadSize {
return 0, ErrMaxFrameSizeTooSmall
}
// Copy data from the local buffer into the frame data buffer // Copy data from the local buffer into the frame data buffer
// Don't go past the maxSize with the fixed frame overhead. maxDataSize := maxSize - FrameV0OverHeadSize
// Fixed overhead: 16 + 2 + 4 + 1 = 23 bytes.
maxDataSize := maxSize - 23
if maxDataSize > uint64(co.buf.Len()) { if maxDataSize > uint64(co.buf.Len()) {
maxDataSize = uint64(co.buf.Len()) maxDataSize = uint64(co.buf.Len())
// If we are closed & will not spill past the current frame // If we are closed & will not spill past the current frame
......
...@@ -29,6 +29,22 @@ func TestChannelOutAddBlock(t *testing.T) { ...@@ -29,6 +29,22 @@ func TestChannelOutAddBlock(t *testing.T) {
}) })
} }
// TestOutputFrameSmallMaxSize tests that calling [OutputFrame] with a small
// max size that is below the fixed frame size overhead of 23, will return
// an error.
func TestOutputFrameSmallMaxSize(t *testing.T) {
cout, err := NewChannelOut()
require.NoError(t, err)
// Call OutputFrame with the range of small max size values that err
var w bytes.Buffer
for i := 0; i < 23; i++ {
fid, err := cout.OutputFrame(&w, uint64(i))
require.ErrorIs(t, err, ErrMaxFrameSizeTooSmall)
require.Zero(t, fid)
}
}
// TestRLPByteLimit ensures that stream encoder is properly limiting the length. // TestRLPByteLimit ensures that stream encoder is properly limiting the length.
// It will decode the input if `len(input) <= inputLimit`. // It will decode the input if `len(input) <= inputLimit`.
func TestRLPByteLimit(t *testing.T) { func TestRLPByteLimit(t *testing.T) {
......
...@@ -107,7 +107,7 @@ type EngineQueue struct { ...@@ -107,7 +107,7 @@ type EngineQueue struct {
// The queued-up attributes // The queued-up attributes
safeAttributesParent eth.L2BlockRef safeAttributesParent eth.L2BlockRef
safeAttributes *eth.PayloadAttributes safeAttributes *eth.PayloadAttributes
unsafePayloads PayloadsQueue // queue of unsafe payloads, ordered by ascending block number, may have gaps unsafePayloads *PayloadsQueue // queue of unsafe payloads, ordered by ascending block number, may have gaps and duplicates
// Tracks which L2 blocks where last derived from which L1 block. At most finalityLookback large. // Tracks which L2 blocks where last derived from which L1 block. At most finalityLookback large.
finalityData []FinalityData finalityData []FinalityData
...@@ -127,18 +127,14 @@ var _ EngineControl = (*EngineQueue)(nil) ...@@ -127,18 +127,14 @@ var _ EngineControl = (*EngineQueue)(nil)
// NewEngineQueue creates a new EngineQueue, which should be Reset(origin) before use. // NewEngineQueue creates a new EngineQueue, which should be Reset(origin) before use.
func NewEngineQueue(log log.Logger, cfg *rollup.Config, engine Engine, metrics Metrics, prev NextAttributesProvider, l1Fetcher L1Fetcher) *EngineQueue { func NewEngineQueue(log log.Logger, cfg *rollup.Config, engine Engine, metrics Metrics, prev NextAttributesProvider, l1Fetcher L1Fetcher) *EngineQueue {
return &EngineQueue{ return &EngineQueue{
log: log, log: log,
cfg: cfg, cfg: cfg,
engine: engine, engine: engine,
metrics: metrics, metrics: metrics,
finalityData: make([]FinalityData, 0, finalityLookback), finalityData: make([]FinalityData, 0, finalityLookback),
unsafePayloads: PayloadsQueue{ unsafePayloads: NewPayloadsQueue(maxUnsafePayloadsMemory, payloadMemSize),
MaxSize: maxUnsafePayloadsMemory, prev: prev,
SizeFn: payloadMemSize, l1Fetcher: l1Fetcher,
blockNos: make(map[uint64]bool),
},
prev: prev,
l1Fetcher: l1Fetcher,
} }
} }
...@@ -682,7 +678,8 @@ func (eq *EngineQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.System ...@@ -682,7 +678,8 @@ func (eq *EngineQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.System
return io.EOF return io.EOF
} }
// GetUnsafeQueueGap retrieves the current [start, end] range of the gap between the tip of the unsafe priority queue and the unsafe head. // GetUnsafeQueueGap retrieves the current [start, end) range (incl. start, excl. end)
// of the gap between the tip of the unsafe priority queue and the unsafe head.
// If there is no gap, the difference between end and start will be 0. // If there is no gap, the difference between end and start will be 0.
func (eq *EngineQueue) GetUnsafeQueueGap(expectedNumber uint64) (start uint64, end uint64) { func (eq *EngineQueue) GetUnsafeQueueGap(expectedNumber uint64) (start uint64, end uint64) {
// The start of the gap is always the unsafe head + 1 // The start of the gap is always the unsafe head + 1
...@@ -691,9 +688,11 @@ func (eq *EngineQueue) GetUnsafeQueueGap(expectedNumber uint64) (start uint64, e ...@@ -691,9 +688,11 @@ func (eq *EngineQueue) GetUnsafeQueueGap(expectedNumber uint64) (start uint64, e
// If the priority queue is empty, the end is the first block number at the top of the priority queue // If the priority queue is empty, the end is the first block number at the top of the priority queue
// Otherwise, the end is the expected block number // Otherwise, the end is the expected block number
if first := eq.unsafePayloads.Peek(); first != nil { if first := eq.unsafePayloads.Peek(); first != nil {
// Don't include the payload we already have in the sync range
end = first.ID().Number end = first.ID().Number
} else { } else {
end = expectedNumber // Include the expected payload in the sync range
end = expectedNumber + 1
} }
return start, end return start, end
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
) )
...@@ -48,8 +50,8 @@ func (pq *payloadsByNumber) Pop() any { ...@@ -48,8 +50,8 @@ func (pq *payloadsByNumber) Pop() any {
} }
const ( const (
// ~580 bytes per payload, with some margin for overhead // ~580 bytes per payload, with some margin for overhead like map data
payloadMemFixedCost uint64 = 600 payloadMemFixedCost uint64 = 800
// 24 bytes per tx overhead (size of slice header in memory) // 24 bytes per tx overhead (size of slice header in memory)
payloadTxMemOverhead uint64 = 24 payloadTxMemOverhead uint64 = 24
) )
...@@ -72,15 +74,25 @@ func payloadMemSize(p *eth.ExecutionPayload) uint64 { ...@@ -72,15 +74,25 @@ func payloadMemSize(p *eth.ExecutionPayload) uint64 {
// without the need to use heap.Push/heap.Pop as caller. // without the need to use heap.Push/heap.Pop as caller.
// PayloadsQueue maintains a MaxSize by counting and tracking sizes of added eth.ExecutionPayload entries. // PayloadsQueue maintains a MaxSize by counting and tracking sizes of added eth.ExecutionPayload entries.
// When the size grows too large, the first (lowest block-number) payload is removed from the queue. // When the size grows too large, the first (lowest block-number) payload is removed from the queue.
// PayloadsQueue allows entries with same block number, or even full duplicates. // PayloadsQueue allows entries with same block number, but does not allow duplicate blocks
type PayloadsQueue struct { type PayloadsQueue struct {
pq payloadsByNumber pq payloadsByNumber
currentSize uint64 currentSize uint64
MaxSize uint64 MaxSize uint64
blockNos map[uint64]bool blockHashes map[common.Hash]struct{}
SizeFn func(p *eth.ExecutionPayload) uint64 SizeFn func(p *eth.ExecutionPayload) uint64
} }
func NewPayloadsQueue(maxSize uint64, sizeFn func(p *eth.ExecutionPayload) uint64) *PayloadsQueue {
return &PayloadsQueue{
pq: nil,
currentSize: 0,
MaxSize: maxSize,
blockHashes: make(map[common.Hash]struct{}),
SizeFn: sizeFn,
}
}
func (upq *PayloadsQueue) Len() int { func (upq *PayloadsQueue) Len() int {
return len(upq.pq) return len(upq.pq)
} }
...@@ -100,8 +112,8 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error { ...@@ -100,8 +112,8 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error {
if p == nil { if p == nil {
return errors.New("cannot add nil payload") return errors.New("cannot add nil payload")
} }
if upq.blockNos[p.ID().Number] { if _, ok := upq.blockHashes[p.BlockHash]; ok {
return errors.New("cannot add duplicate payload") return fmt.Errorf("cannot add duplicate payload %s", p.ID())
} }
size := upq.SizeFn(p) size := upq.SizeFn(p)
if size > upq.MaxSize { if size > upq.MaxSize {
...@@ -115,7 +127,7 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error { ...@@ -115,7 +127,7 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error {
for upq.currentSize > upq.MaxSize { for upq.currentSize > upq.MaxSize {
upq.Pop() upq.Pop()
} }
upq.blockNos[p.ID().Number] = true upq.blockHashes[p.BlockHash] = struct{}{}
return nil return nil
} }
...@@ -137,7 +149,7 @@ func (upq *PayloadsQueue) Pop() *eth.ExecutionPayload { ...@@ -137,7 +149,7 @@ func (upq *PayloadsQueue) Pop() *eth.ExecutionPayload {
} }
ps := heap.Pop(&upq.pq).(payloadAndSize) // nosemgrep ps := heap.Pop(&upq.pq).(payloadAndSize) // nosemgrep
upq.currentSize -= ps.size upq.currentSize -= ps.size
// remove the key from the blockNos map // remove the key from the block hashes map
delete(upq.blockNos, ps.payload.ID().Number) delete(upq.blockHashes, ps.payload.BlockHash)
return ps.payload return ps.payload
} }
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"container/heap" "container/heap"
"testing" "testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
...@@ -74,20 +75,17 @@ func TestPayloadMemSize(t *testing.T) { ...@@ -74,20 +75,17 @@ func TestPayloadMemSize(t *testing.T) {
} }
func TestPayloadsQueue(t *testing.T) { func TestPayloadsQueue(t *testing.T) {
pq := PayloadsQueue{ pq := NewPayloadsQueue(payloadMemFixedCost*3, payloadMemSize)
MaxSize: payloadMemFixedCost * 3,
SizeFn: payloadMemSize,
blockNos: make(map[uint64]bool),
}
require.Equal(t, 0, pq.Len()) require.Equal(t, 0, pq.Len())
require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Peek()) require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Peek())
require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Pop()) require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Pop())
a := &eth.ExecutionPayload{BlockNumber: 3} a := &eth.ExecutionPayload{BlockNumber: 3, BlockHash: common.Hash{3}}
b := &eth.ExecutionPayload{BlockNumber: 4} b := &eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{4}}
c := &eth.ExecutionPayload{BlockNumber: 5} c := &eth.ExecutionPayload{BlockNumber: 5, BlockHash: common.Hash{5}}
d := &eth.ExecutionPayload{BlockNumber: 6} d := &eth.ExecutionPayload{BlockNumber: 6, BlockHash: common.Hash{6}}
bAlt := &eth.ExecutionPayload{BlockNumber: 4} bAlt := &eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{0xff}}
bDup := &eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{4}}
require.NoError(t, pq.Push(b)) require.NoError(t, pq.Push(b))
require.Equal(t, pq.Len(), 1) require.Equal(t, pq.Len(), 1)
require.Equal(t, pq.Peek(), b) require.Equal(t, pq.Peek(), b)
...@@ -130,7 +128,9 @@ func TestPayloadsQueue(t *testing.T) { ...@@ -130,7 +128,9 @@ func TestPayloadsQueue(t *testing.T) {
require.Equal(t, pq.Peek(), a) require.Equal(t, pq.Peek(), a)
// No duplicates allowed // No duplicates allowed
require.Error(t, pq.Push(bAlt)) require.Error(t, pq.Push(bDup))
// But reorg data allowed
require.NoError(t, pq.Push(bAlt))
require.NoError(t, pq.Push(d)) require.NoError(t, pq.Push(d))
require.Equal(t, pq.Len(), 3) require.Equal(t, pq.Len(), 3)
......
...@@ -10,7 +10,6 @@ import ( ...@@ -10,7 +10,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
) )
type Metrics interface { type Metrics interface {
...@@ -82,8 +81,19 @@ type Network interface { ...@@ -82,8 +81,19 @@ type Network interface {
PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error
} }
type AltSync interface {
// RequestL2Range informs the sync source that the given range of L2 blocks is missing,
// and should be retrieved from any available alternative syncing source.
// The start of the range is inclusive, the end is exclusive.
// The sync results should be returned back to the driver via the OnUnsafeL2Payload(ctx, payload) method.
// The latest requested range should always take priority over previous requests.
// There may be overlaps in requested ranges.
// An error may be returned if the scheduling fails immediately, e.g. a context timeout.
RequestL2Range(ctx context.Context, start, end uint64) error
}
// NewDriver composes an events handler that tracks L1 state, triggers L2 derivation, and optionally sequences new L2 blocks. // NewDriver composes an events handler that tracks L1 state, triggers L2 derivation, and optionally sequences new L2 blocks.
func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, syncClient *sources.SyncClient, network Network, log log.Logger, snapshotLog log.Logger, metrics Metrics) *Driver { func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, altSync AltSync, network Network, log log.Logger, snapshotLog log.Logger, metrics Metrics) *Driver {
l1State := NewL1State(log, metrics) l1State := NewL1State(log, metrics)
sequencerConfDepth := NewConfDepth(driverCfg.SequencerConfDepth, l1State.L1Head, l1) sequencerConfDepth := NewConfDepth(driverCfg.SequencerConfDepth, l1State.L1Head, l1)
findL1Origin := NewL1OriginSelector(log, cfg, sequencerConfDepth) findL1Origin := NewL1OriginSelector(log, cfg, sequencerConfDepth)
...@@ -115,6 +125,6 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, sy ...@@ -115,6 +125,6 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, sy
l1SafeSig: make(chan eth.L1BlockRef, 10), l1SafeSig: make(chan eth.L1BlockRef, 10),
l1FinalizedSig: make(chan eth.L1BlockRef, 10), l1FinalizedSig: make(chan eth.L1BlockRef, 10),
unsafeL2Payloads: make(chan *eth.ExecutionPayload, 10), unsafeL2Payloads: make(chan *eth.ExecutionPayload, 10),
L2SyncCl: syncClient, altSync: altSync,
} }
} }
...@@ -16,7 +16,6 @@ import ( ...@@ -16,7 +16,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-service/backoff" "github.com/ethereum-optimism/optimism/op-service/backoff"
) )
...@@ -64,8 +63,8 @@ type Driver struct { ...@@ -64,8 +63,8 @@ type Driver struct {
l1SafeSig chan eth.L1BlockRef l1SafeSig chan eth.L1BlockRef
l1FinalizedSig chan eth.L1BlockRef l1FinalizedSig chan eth.L1BlockRef
// Backup unsafe sync client // Interface to signal the L2 block range to sync.
L2SyncCl *sources.SyncClient altSync AltSync
// L2 Signals: // L2 Signals:
...@@ -200,11 +199,12 @@ func (s *Driver) eventLoop() { ...@@ -200,11 +199,12 @@ func (s *Driver) eventLoop() {
sequencerTimer.Reset(delay) sequencerTimer.Reset(delay)
} }
// Create a ticker to check if there is a gap in the engine queue every 15 seconds // Create a ticker to check if there is a gap in the engine queue. Whenever
// If there is, we send requests to the backup RPC to retrieve the missing payloads // there is, we send requests to sync source to retrieve the missing payloads.
// and add them to the unsafe queue. syncCheckInterval := time.Duration(s.config.BlockTime) * time.Second * 2
altSyncTicker := time.NewTicker(15 * time.Second) altSyncTicker := time.NewTicker(syncCheckInterval)
defer altSyncTicker.Stop() defer altSyncTicker.Stop()
lastUnsafeL2 := s.derivation.UnsafeL2Head()
for { for {
// If we are sequencing, and the L1 state is ready, update the trigger for the next sequencer action. // If we are sequencing, and the L1 state is ready, update the trigger for the next sequencer action.
...@@ -220,6 +220,13 @@ func (s *Driver) eventLoop() { ...@@ -220,6 +220,13 @@ func (s *Driver) eventLoop() {
sequencerCh = nil sequencerCh = nil
} }
// If the engine is not ready, or if the L2 head is actively changing, then reset the alt-sync:
// there is no need to request L2 blocks when we are syncing already.
if head := s.derivation.UnsafeL2Head(); head != lastUnsafeL2 || !s.derivation.EngineReady() {
lastUnsafeL2 = head
altSyncTicker.Reset(syncCheckInterval)
}
select { select {
case <-sequencerCh: case <-sequencerCh:
payload, err := s.sequencer.RunNextSequencerAction(ctx) payload, err := s.sequencer.RunNextSequencerAction(ctx)
...@@ -237,10 +244,12 @@ func (s *Driver) eventLoop() { ...@@ -237,10 +244,12 @@ func (s *Driver) eventLoop() {
} }
planSequencerAction() // schedule the next sequencer action to keep the sequencing looping planSequencerAction() // schedule the next sequencer action to keep the sequencing looping
case <-altSyncTicker.C: case <-altSyncTicker.C:
// Check if there is a gap in the current unsafe payload queue. If there is, attempt to fetch // Check if there is a gap in the current unsafe payload queue.
// missing payloads from the backup RPC (if it is configured). ctx, cancel := context.WithTimeout(ctx, time.Second*2)
if s.L2SyncCl != nil { err := s.checkForGapInUnsafeQueue(ctx)
s.checkForGapInUnsafeQueue(ctx) cancel()
if err != nil {
s.log.Warn("failed to check for unsafe L2 blocks to sync", "err", err)
} }
case payload := <-s.unsafeL2Payloads: case payload := <-s.unsafeL2Payloads:
s.snapshot("New unsafe payload") s.snapshot("New unsafe payload")
...@@ -462,35 +471,29 @@ type hashAndErrorChannel struct { ...@@ -462,35 +471,29 @@ type hashAndErrorChannel struct {
err chan error err chan error
} }
// checkForGapInUnsafeQueue checks if there is a gap in the unsafe queue and attempts to retrieve the missing payloads from the backup RPC. // checkForGapInUnsafeQueue checks if there is a gap in the unsafe queue and attempts to retrieve the missing payloads from an alt-sync method.
// WARNING: The sync client's attempt to retrieve the missing payloads is not guaranteed to succeed, and it will fail silently (besides // WARNING: This is only an outgoing signal, the blocks are not guaranteed to be retrieved.
// emitting warning logs) if the requests fail. // Results are received through OnUnsafeL2Payload.
func (s *Driver) checkForGapInUnsafeQueue(ctx context.Context) { func (s *Driver) checkForGapInUnsafeQueue(ctx context.Context) error {
// subtract genesis time from wall clock to get the time elapsed since genesis, and then divide that // subtract genesis time from wall clock to get the time elapsed since genesis, and then divide that
// difference by the block time to get the expected L2 block number at the current time. If the // difference by the block time to get the expected L2 block number at the current time. If the
// unsafe head does not have this block number, then there is a gap in the queue. // unsafe head does not have this block number, then there is a gap in the queue.
wallClock := uint64(time.Now().Unix()) wallClock := uint64(time.Now().Unix())
genesisTimestamp := s.config.Genesis.L2Time genesisTimestamp := s.config.Genesis.L2Time
if wallClock < genesisTimestamp {
s.log.Debug("nothing to sync, did not reach genesis L2 time yet", "genesis", genesisTimestamp)
return nil
}
wallClockGenesisDiff := wallClock - genesisTimestamp wallClockGenesisDiff := wallClock - genesisTimestamp
expectedL2Block := wallClockGenesisDiff / s.config.BlockTime // Note: round down, we should not request blocks into the future.
blocksSinceGenesis := wallClockGenesisDiff / s.config.BlockTime
expectedL2Block := s.config.Genesis.L2.Number + blocksSinceGenesis
start, end := s.derivation.GetUnsafeQueueGap(expectedL2Block) start, end := s.derivation.GetUnsafeQueueGap(expectedL2Block)
size := end - start
// Check if there is a gap between the unsafe head and the expected L2 block number at the current time. // Check if there is a gap between the unsafe head and the expected L2 block number at the current time.
if size > 0 { if end > start {
s.log.Warn("Gap in payload queue tip and expected unsafe chain detected", "start", start, "end", end, "size", size) s.log.Debug("requesting missing unsafe L2 block range", "start", start, "end", end, "size", end-start)
s.log.Info("Attempting to fetch missing payloads from backup RPC", "start", start, "end", end, "size", size) return s.altSync.RequestL2Range(ctx, start, end)
// Attempt to fetch the missing payloads from the backup unsafe sync RPC concurrently.
// Concurrent requests are safe here due to the engine queue being a priority queue.
for blockNumber := start; blockNumber <= end; blockNumber++ {
select {
case s.L2SyncCl.FetchUnsafeBlock <- blockNumber:
// Do nothing- the block number was successfully sent into the channel
default:
return // If the channel is full, return and wait for the next iteration of the event loop
}
}
} }
return nil
} }
...@@ -109,6 +109,9 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -109,6 +109,9 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
return nil, fmt.Errorf("failed to fetch current L2 forkchoice state: %w", err) return nil, fmt.Errorf("failed to fetch current L2 forkchoice state: %w", err)
} }
lgr.Info("Loaded current L2 heads", "unsafe", result.Unsafe, "safe", result.Safe, "finalized", result.Finalized,
"unsafe_origin", result.Unsafe.L1Origin, "unsafe_origin", result.Safe.L1Origin)
// Remember original unsafe block to determine reorg depth // Remember original unsafe block to determine reorg depth
prevUnsafe := result.Unsafe prevUnsafe := result.Unsafe
...@@ -134,6 +137,7 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -134,6 +137,7 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
// Exit, find-sync start should start over, to move to an available L1 chain with block-by-number / not-found case. // Exit, find-sync start should start over, to move to an available L1 chain with block-by-number / not-found case.
return nil, fmt.Errorf("failed to retrieve L1 block: %w", err) return nil, fmt.Errorf("failed to retrieve L1 block: %w", err)
} }
lgr.Info("Walking back L1Block by hash", "curr", l1Block, "next", b, "l2block", n)
l1Block = b l1Block = b
ahead = false ahead = false
} else if l1Block == (eth.L1BlockRef{}) || n.L1Origin.Hash != l1Block.Hash { } else if l1Block == (eth.L1BlockRef{}) || n.L1Origin.Hash != l1Block.Hash {
...@@ -145,9 +149,10 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -145,9 +149,10 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
} }
l1Block = b l1Block = b
ahead = notFound ahead = notFound
lgr.Info("Walking back L1Block by number", "curr", l1Block, "next", b, "l2block", n)
} }
lgr.Trace("walking sync start", "number", n.Number) lgr.Trace("walking sync start", "l2block", n)
// Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin, // Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin,
// the L2 chain is building on the wrong L1 branch. // the L2 chain is building on the wrong L1 branch.
...@@ -201,6 +206,8 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -201,6 +206,8 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
// Don't traverse further than the finalized head to find a safe head // Don't traverse further than the finalized head to find a safe head
if n.Number == result.Finalized.Number { if n.Number == result.Finalized.Number {
lgr.Info("Hit finalized L2 head, returning immediately", "unsafe", result.Unsafe, "safe", result.Safe,
"finalized", result.Finalized, "unsafe_origin", result.Unsafe.L1Origin, "unsafe_origin", result.Safe.L1Origin)
result.Safe = n result.Safe = n
return result, nil return result, nil
} }
......
...@@ -136,6 +136,7 @@ func NewL2EndpointConfig(ctx *cli.Context, log log.Logger) (*node.L2EndpointConf ...@@ -136,6 +136,7 @@ func NewL2EndpointConfig(ctx *cli.Context, log log.Logger) (*node.L2EndpointConf
func NewL2SyncEndpointConfig(ctx *cli.Context) *node.L2SyncEndpointConfig { func NewL2SyncEndpointConfig(ctx *cli.Context) *node.L2SyncEndpointConfig {
return &node.L2SyncEndpointConfig{ return &node.L2SyncEndpointConfig{
L2NodeAddr: ctx.GlobalString(flags.BackupL2UnsafeSyncRPC.Name), L2NodeAddr: ctx.GlobalString(flags.BackupL2UnsafeSyncRPC.Name),
TrustRPC: ctx.GlobalBool(flags.BackupL2UnsafeSyncRPCTrustRPC.Name),
} }
} }
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/big" "math/big"
"time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -56,6 +57,11 @@ type EthClientConfig struct { ...@@ -56,6 +57,11 @@ type EthClientConfig struct {
// RPCProviderKind is a hint at what type of RPC provider we are dealing with // RPCProviderKind is a hint at what type of RPC provider we are dealing with
RPCProviderKind RPCProviderKind RPCProviderKind RPCProviderKind
// Method reset duration defines how long we stick to available RPC methods,
// till we re-attempt the user-preferred methods.
// If this is 0 then the client does not fall back to less optimal but available methods.
MethodResetDuration time.Duration
} }
func (c *EthClientConfig) Check() error { func (c *EthClientConfig) Check() error {
...@@ -118,9 +124,25 @@ type EthClient struct { ...@@ -118,9 +124,25 @@ type EthClient struct {
// This may be modified concurrently, but we don't lock since it's a single // This may be modified concurrently, but we don't lock since it's a single
// uint64 that's not critical (fine to miss or mix up a modification) // uint64 that's not critical (fine to miss or mix up a modification)
availableReceiptMethods ReceiptsFetchingMethod availableReceiptMethods ReceiptsFetchingMethod
// lastMethodsReset tracks when availableReceiptMethods was last reset.
// When receipt-fetching fails it falls back to available methods,
// but periodically it will try to reset to the preferred optimal methods.
lastMethodsReset time.Time
// methodResetDuration defines how long we take till we reset lastMethodsReset
methodResetDuration time.Duration
} }
func (s *EthClient) PickReceiptsMethod(txCount uint64) ReceiptsFetchingMethod { func (s *EthClient) PickReceiptsMethod(txCount uint64) ReceiptsFetchingMethod {
if now := time.Now(); now.Sub(s.lastMethodsReset) > s.methodResetDuration {
m := AvailableReceiptsFetchingMethods(s.provKind)
if s.availableReceiptMethods != m {
s.log.Warn("resetting back RPC preferences, please review RPC provider kind setting", "kind", s.provKind.String())
}
s.availableReceiptMethods = m
s.lastMethodsReset = now
}
return PickBestReceiptsFetchingMethod(s.provKind, s.availableReceiptMethods, txCount) return PickBestReceiptsFetchingMethod(s.provKind, s.availableReceiptMethods, txCount)
} }
...@@ -128,7 +150,7 @@ func (s *EthClient) OnReceiptsMethodErr(m ReceiptsFetchingMethod, err error) { ...@@ -128,7 +150,7 @@ func (s *EthClient) OnReceiptsMethodErr(m ReceiptsFetchingMethod, err error) {
if unusableMethod(err) { if unusableMethod(err) {
// clear the bit of the method that errored // clear the bit of the method that errored
s.availableReceiptMethods &^= m s.availableReceiptMethods &^= m
s.log.Warn("failed to use selected RPC method for receipt fetching, falling back to alternatives", s.log.Warn("failed to use selected RPC method for receipt fetching, temporarily falling back to alternatives",
"provider_kind", s.provKind, "failed_method", m, "fallback", s.availableReceiptMethods, "err", err) "provider_kind", s.provKind, "failed_method", m, "fallback", s.availableReceiptMethods, "err", err)
} else { } else {
s.log.Debug("failed to use selected RPC method for receipt fetching, but method does appear to be available, so we continue to use it", s.log.Debug("failed to use selected RPC method for receipt fetching, but method does appear to be available, so we continue to use it",
...@@ -155,6 +177,8 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co ...@@ -155,6 +177,8 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co
headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize), headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize),
payloadsCache: caching.NewLRUCache(metrics, "payloads", config.PayloadsCacheSize), payloadsCache: caching.NewLRUCache(metrics, "payloads", config.PayloadsCacheSize),
availableReceiptMethods: AvailableReceiptsFetchingMethods(config.RPCProviderKind), availableReceiptMethods: AvailableReceiptsFetchingMethods(config.RPCProviderKind),
lastMethodsReset: time.Now(),
methodResetDuration: config.MethodResetDuration,
}, nil }, nil
} }
...@@ -165,9 +189,39 @@ func (s *EthClient) SubscribeNewHead(ctx context.Context, ch chan<- *types.Heade ...@@ -165,9 +189,39 @@ func (s *EthClient) SubscribeNewHead(ctx context.Context, ch chan<- *types.Heade
return s.client.EthSubscribe(ctx, ch, "newHeads") return s.client.EthSubscribe(ctx, ch, "newHeads")
} }
func (s *EthClient) headerCall(ctx context.Context, method string, id any) (*HeaderInfo, error) { // rpcBlockID is an internal type to enforce header and block call results match the requested identifier
type rpcBlockID interface {
// Arg translates the object into an RPC argument
Arg() any
// CheckID verifies a block/header result matches the requested block identifier
CheckID(id eth.BlockID) error
}
// hashID implements rpcBlockID for safe block-by-hash fetching
type hashID common.Hash
func (h hashID) Arg() any { return common.Hash(h) }
func (h hashID) CheckID(id eth.BlockID) error {
if common.Hash(h) != id.Hash {
return fmt.Errorf("expected block hash %s but got block %s", common.Hash(h), id)
}
return nil
}
// numberID implements rpcBlockID for safe block-by-number fetching
type numberID uint64
func (n numberID) Arg() any { return hexutil.EncodeUint64(uint64(n)) }
func (n numberID) CheckID(id eth.BlockID) error {
if uint64(n) != id.Number {
return fmt.Errorf("expected block number %d but got block %s", uint64(n), id)
}
return nil
}
func (s *EthClient) headerCall(ctx context.Context, method string, id rpcBlockID) (*HeaderInfo, error) {
var header *rpcHeader var header *rpcHeader
err := s.client.CallContext(ctx, &header, method, id, false) // headers are just blocks without txs err := s.client.CallContext(ctx, &header, method, id.Arg(), false) // headers are just blocks without txs
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -178,13 +232,16 @@ func (s *EthClient) headerCall(ctx context.Context, method string, id any) (*Hea ...@@ -178,13 +232,16 @@ func (s *EthClient) headerCall(ctx context.Context, method string, id any) (*Hea
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := id.CheckID(eth.ToBlockID(info)); err != nil {
return nil, fmt.Errorf("fetched block header does not match requested ID: %w", err)
}
s.headersCache.Add(info.Hash(), info) s.headersCache.Add(info.Hash(), info)
return info, nil return info, nil
} }
func (s *EthClient) blockCall(ctx context.Context, method string, id any) (*HeaderInfo, types.Transactions, error) { func (s *EthClient) blockCall(ctx context.Context, method string, id rpcBlockID) (*HeaderInfo, types.Transactions, error) {
var block *rpcBlock var block *rpcBlock
err := s.client.CallContext(ctx, &block, method, id, true) err := s.client.CallContext(ctx, &block, method, id.Arg(), true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
...@@ -195,14 +252,17 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id any) (*Head ...@@ -195,14 +252,17 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id any) (*Head
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if err := id.CheckID(eth.ToBlockID(info)); err != nil {
return nil, nil, fmt.Errorf("fetched block data does not match requested ID: %w", err)
}
s.headersCache.Add(info.Hash(), info) s.headersCache.Add(info.Hash(), info)
s.transactionsCache.Add(info.Hash(), txs) s.transactionsCache.Add(info.Hash(), txs)
return info, txs, nil return info, txs, nil
} }
func (s *EthClient) payloadCall(ctx context.Context, method string, id any) (*eth.ExecutionPayload, error) { func (s *EthClient) payloadCall(ctx context.Context, method string, id rpcBlockID) (*eth.ExecutionPayload, error) {
var block *rpcBlock var block *rpcBlock
err := s.client.CallContext(ctx, &block, method, id, true) err := s.client.CallContext(ctx, &block, method, id.Arg(), true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -213,6 +273,9 @@ func (s *EthClient) payloadCall(ctx context.Context, method string, id any) (*et ...@@ -213,6 +273,9 @@ func (s *EthClient) payloadCall(ctx context.Context, method string, id any) (*et
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := id.CheckID(payload.ID()); err != nil {
return nil, fmt.Errorf("fetched payload does not match requested ID: %w", err)
}
s.payloadsCache.Add(payload.BlockHash, payload) s.payloadsCache.Add(payload.BlockHash, payload)
return payload, nil return payload, nil
} }
...@@ -231,17 +294,17 @@ func (s *EthClient) InfoByHash(ctx context.Context, hash common.Hash) (eth.Block ...@@ -231,17 +294,17 @@ func (s *EthClient) InfoByHash(ctx context.Context, hash common.Hash) (eth.Block
if header, ok := s.headersCache.Get(hash); ok { if header, ok := s.headersCache.Get(hash); ok {
return header.(*HeaderInfo), nil return header.(*HeaderInfo), nil
} }
return s.headerCall(ctx, "eth_getBlockByHash", hash) return s.headerCall(ctx, "eth_getBlockByHash", hashID(hash))
} }
func (s *EthClient) InfoByNumber(ctx context.Context, number uint64) (eth.BlockInfo, error) { func (s *EthClient) InfoByNumber(ctx context.Context, number uint64) (eth.BlockInfo, error) {
// can't hit the cache when querying by number due to reorgs. // can't hit the cache when querying by number due to reorgs.
return s.headerCall(ctx, "eth_getBlockByNumber", hexutil.EncodeUint64(number)) return s.headerCall(ctx, "eth_getBlockByNumber", numberID(number))
} }
func (s *EthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, error) { func (s *EthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, error) {
// can't hit the cache when querying the head due to reorgs / changes. // can't hit the cache when querying the head due to reorgs / changes.
return s.headerCall(ctx, "eth_getBlockByNumber", string(label)) return s.headerCall(ctx, "eth_getBlockByNumber", label)
} }
func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) { func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
...@@ -250,32 +313,32 @@ func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth ...@@ -250,32 +313,32 @@ func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth
return header.(*HeaderInfo), txs.(types.Transactions), nil return header.(*HeaderInfo), txs.(types.Transactions), nil
} }
} }
return s.blockCall(ctx, "eth_getBlockByHash", hash) return s.blockCall(ctx, "eth_getBlockByHash", hashID(hash))
} }
func (s *EthClient) InfoAndTxsByNumber(ctx context.Context, number uint64) (eth.BlockInfo, types.Transactions, error) { func (s *EthClient) InfoAndTxsByNumber(ctx context.Context, number uint64) (eth.BlockInfo, types.Transactions, error) {
// can't hit the cache when querying by number due to reorgs. // can't hit the cache when querying by number due to reorgs.
return s.blockCall(ctx, "eth_getBlockByNumber", hexutil.EncodeUint64(number)) return s.blockCall(ctx, "eth_getBlockByNumber", numberID(number))
} }
func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, types.Transactions, error) { func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, types.Transactions, error) {
// can't hit the cache when querying the head due to reorgs / changes. // can't hit the cache when querying the head due to reorgs / changes.
return s.blockCall(ctx, "eth_getBlockByNumber", string(label)) return s.blockCall(ctx, "eth_getBlockByNumber", label)
} }
func (s *EthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) { func (s *EthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
if payload, ok := s.payloadsCache.Get(hash); ok { if payload, ok := s.payloadsCache.Get(hash); ok {
return payload.(*eth.ExecutionPayload), nil return payload.(*eth.ExecutionPayload), nil
} }
return s.payloadCall(ctx, "eth_getBlockByHash", hash) return s.payloadCall(ctx, "eth_getBlockByHash", hashID(hash))
} }
func (s *EthClient) PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayload, error) { func (s *EthClient) PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayload, error) {
return s.payloadCall(ctx, "eth_getBlockByNumber", hexutil.EncodeUint64(number)) return s.payloadCall(ctx, "eth_getBlockByNumber", numberID(number))
} }
func (s *EthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayload, error) { func (s *EthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayload, error) {
return s.payloadCall(ctx, "eth_getBlockByNumber", string(label)) return s.payloadCall(ctx, "eth_getBlockByNumber", label)
} }
// FetchReceipts returns a block info and all of the receipts associated with transactions in the block. // FetchReceipts returns a block info and all of the receipts associated with transactions in the block.
......
...@@ -140,3 +140,40 @@ func TestEthClient_InfoByNumber(t *testing.T) { ...@@ -140,3 +140,40 @@ func TestEthClient_InfoByNumber(t *testing.T) {
require.Equal(t, info, expectedInfo) require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t) m.Mock.AssertExpectations(t)
} }
func TestEthClient_WrongInfoByNumber(t *testing.T) {
m := new(mockRPC)
_, rhdr := randHeader()
rhdr2 := *rhdr
rhdr2.Number += 1
n := rhdr.Number
ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader),
"eth_getBlockByNumber", []any{n.String(), false}).Run(func(args mock.Arguments) {
*args[1].(**rpcHeader) = &rhdr2
}).Return([]error{nil})
s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true, RPCKindBasic))
require.NoError(t, err)
_, err = s.InfoByNumber(ctx, uint64(n))
require.Error(t, err, "cannot accept the wrong block")
m.Mock.AssertExpectations(t)
}
func TestEthClient_WrongInfoByHash(t *testing.T) {
m := new(mockRPC)
_, rhdr := randHeader()
rhdr2 := *rhdr
rhdr2.Root[0] += 1
rhdr2.Hash = rhdr2.computeBlockHash()
k := rhdr.Hash
ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader),
"eth_getBlockByHash", []any{k, false}).Run(func(args mock.Arguments) {
*args[1].(**rpcHeader) = &rhdr2
}).Return([]error{nil})
s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true, RPCKindBasic))
require.NoError(t, err)
_, err = s.InfoByHash(ctx, k)
require.Error(t, err, "cannot accept the wrong block")
m.Mock.AssertExpectations(t)
}
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -40,6 +41,7 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool, kind RPCProvide ...@@ -40,6 +41,7 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool, kind RPCProvide
TrustRPC: trustRPC, TrustRPC: trustRPC,
MustBePostMerge: false, MustBePostMerge: false,
RPCProviderKind: kind, RPCProviderKind: kind,
MethodResetDuration: time.Minute,
}, },
// Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors. // Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors.
L1BlockRefsCacheSize: fullSpan, L1BlockRefsCacheSize: fullSpan,
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -50,6 +51,7 @@ func L2ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L2ClientConfig ...@@ -50,6 +51,7 @@ func L2ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L2ClientConfig
TrustRPC: trustRPC, TrustRPC: trustRPC,
MustBePostMerge: true, MustBePostMerge: true,
RPCProviderKind: RPCKindBasic, RPCProviderKind: RPCKindBasic,
MethodResetDuration: time.Minute,
}, },
// Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors. // Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors.
L2BlockRefsCacheSize: fullSpan, L2BlockRefsCacheSize: fullSpan,
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
...@@ -85,6 +86,7 @@ func (e *methodNotFoundError) Error() string { ...@@ -85,6 +86,7 @@ func (e *methodNotFoundError) Error() string {
type ReceiptsTestCase struct { type ReceiptsTestCase struct {
name string name string
providerKind RPCProviderKind providerKind RPCProviderKind
staticMethod bool
setup func(t *testing.T) (*rpcBlock, []ReceiptsRequest) setup func(t *testing.T) (*rpcBlock, []ReceiptsRequest)
} }
...@@ -142,6 +144,10 @@ func (tc *ReceiptsTestCase) Run(t *testing.T) { ...@@ -142,6 +144,10 @@ func (tc *ReceiptsTestCase) Run(t *testing.T) {
TrustRPC: false, TrustRPC: false,
MustBePostMerge: false, MustBePostMerge: false,
RPCProviderKind: tc.providerKind, RPCProviderKind: tc.providerKind,
MethodResetDuration: time.Minute,
}
if tc.staticMethod { // if static, instantly reset, for fast clock-independent testing
testCfg.MethodResetDuration = 0
} }
logger := testlog.Logger(t, log.LvlError) logger := testlog.Logger(t, log.LvlError)
ethCl, err := NewEthClient(client.NewBaseRPCClient(cl), logger, nil, testCfg) ethCl, err := NewEthClient(client.NewBaseRPCClient(cl), logger, nil, testCfg)
...@@ -226,6 +232,12 @@ func TestEthClient_FetchReceipts(t *testing.T) { ...@@ -226,6 +232,12 @@ func TestEthClient_FetchReceipts(t *testing.T) {
providerKind: RPCKindAlchemy, providerKind: RPCKindAlchemy,
setup: fallbackCase(30, AlchemyGetTransactionReceipts), setup: fallbackCase(30, AlchemyGetTransactionReceipts),
}, },
{
name: "alchemy sticky",
providerKind: RPCKindAlchemy,
staticMethod: true,
setup: fallbackCase(30, AlchemyGetTransactionReceipts, AlchemyGetTransactionReceipts),
},
{ {
name: "alchemy fallback 1", name: "alchemy fallback 1",
providerKind: RPCKindAlchemy, providerKind: RPCKindAlchemy,
......
...@@ -3,12 +3,17 @@ package sources ...@@ -3,12 +3,17 @@ package sources
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io"
"sync" "sync"
"time"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/sources/caching" "github.com/ethereum-optimism/optimism/op-node/sources/caching"
"github.com/ethereum-optimism/optimism/op-service/backoff"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
) )
...@@ -18,23 +23,29 @@ var ErrNoUnsafeL2PayloadChannel = errors.New("unsafeL2Payloads channel must not ...@@ -18,23 +23,29 @@ var ErrNoUnsafeL2PayloadChannel = errors.New("unsafeL2Payloads channel must not
// RpcSyncPeer is a mock PeerID for the RPC sync client. // RpcSyncPeer is a mock PeerID for the RPC sync client.
var RpcSyncPeer peer.ID = "ALT_RPC_SYNC" var RpcSyncPeer peer.ID = "ALT_RPC_SYNC"
// receivePayload queues the received payload for processing.
// This may return an error if there's no capacity for the payload.
type receivePayload = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error type receivePayload = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error
type SyncClientInterface interface { type RPCSync interface {
io.Closer
// Start starts an additional worker syncing job
Start() error Start() error
Close() error // RequestL2Range signals that the given range should be fetched, implementing the alt-sync interface.
fetchUnsafeBlockFromRpc(ctx context.Context, blockNumber uint64) RequestL2Range(ctx context.Context, start, end uint64) error
} }
type SyncClient struct { type SyncClient struct {
*L2Client *L2Client
FetchUnsafeBlock chan uint64
done chan struct{}
receivePayload receivePayload
wg sync.WaitGroup
}
var _ SyncClientInterface = (*SyncClient)(nil) requests chan uint64
resCtx context.Context
resCancel context.CancelFunc
receivePayload receivePayload
wg sync.WaitGroup
}
type SyncClientConfig struct { type SyncClientConfig struct {
L2ClientConfig L2ClientConfig
...@@ -51,41 +62,92 @@ func NewSyncClient(receiver receivePayload, client client.RPC, log log.Logger, m ...@@ -51,41 +62,92 @@ func NewSyncClient(receiver receivePayload, client client.RPC, log log.Logger, m
if err != nil { if err != nil {
return nil, err return nil, err
} }
// This resource context is shared between all workers that may be started
resCtx, resCancel := context.WithCancel(context.Background())
return &SyncClient{ return &SyncClient{
L2Client: l2Client, L2Client: l2Client,
FetchUnsafeBlock: make(chan uint64, 128), resCtx: resCtx,
done: make(chan struct{}), resCancel: resCancel,
receivePayload: receiver, requests: make(chan uint64, 128),
receivePayload: receiver,
}, nil }, nil
} }
// Start starts up the state loop. // Start starts the syncing background work. This may not be called after Close().
// The loop will have been started if err is not nil.
func (s *SyncClient) Start() error { func (s *SyncClient) Start() error {
// TODO(CLI-3635): we can start multiple event loop runners as workers, to parallelize the work
s.wg.Add(1) s.wg.Add(1)
go s.eventLoop() go s.eventLoop()
return nil return nil
} }
// Close sends a signal to the event loop to stop. // Close sends a signal to close all concurrent syncing work.
func (s *SyncClient) Close() error { func (s *SyncClient) Close() error {
s.done <- struct{}{} s.resCancel()
s.wg.Wait() s.wg.Wait()
return nil return nil
} }
func (s *SyncClient) RequestL2Range(ctx context.Context, start, end uint64) error {
// Drain previous requests now that we have new information
for len(s.requests) > 0 {
select { // in case requests is being read at the same time, don't block on draining it.
case <-s.requests:
default:
break
}
}
// TODO(CLI-3635): optimize the by-range fetching with the Engine API payloads-by-range method.
s.log.Info("Scheduling to fetch missing payloads from backup RPC", "start", start, "end", end, "size", end-start)
for i := start; i < end; i++ {
select {
case s.requests <- i:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
// eventLoop is the main event loop for the sync client. // eventLoop is the main event loop for the sync client.
func (s *SyncClient) eventLoop() { func (s *SyncClient) eventLoop() {
defer s.wg.Done() defer s.wg.Done()
s.log.Info("Starting sync client event loop") s.log.Info("Starting sync client event loop")
backoffStrategy := &backoff.ExponentialStrategy{
Min: 1000,
Max: 20_000,
MaxJitter: 250,
}
for { for {
select { select {
case <-s.done: case <-s.resCtx.Done():
s.log.Debug("Shutting down RPC sync worker")
return return
case blockNumber := <-s.FetchUnsafeBlock: case reqNum := <-s.requests:
s.fetchUnsafeBlockFromRpc(context.Background(), blockNumber) err := backoff.DoCtx(s.resCtx, 5, backoffStrategy, func() error {
// Limit the maximum time for fetching payloads
ctx, cancel := context.WithTimeout(s.resCtx, time.Second*10)
defer cancel()
// We are only fetching one block at a time here.
return s.fetchUnsafeBlockFromRpc(ctx, reqNum)
})
if err != nil {
if err == s.resCtx.Err() {
return
}
s.log.Error("failed syncing L2 block via RPC", "err", err, "num", reqNum)
// Reschedule at end of queue
select {
case s.requests <- reqNum:
default:
// drop syncing job if we are too busy with sync jobs already.
}
}
} }
} }
} }
...@@ -95,28 +157,22 @@ func (s *SyncClient) eventLoop() { ...@@ -95,28 +157,22 @@ func (s *SyncClient) eventLoop() {
// //
// Post Shanghai hardfork, the engine API's `PayloadBodiesByRange` method will be much more efficient, but for now, // Post Shanghai hardfork, the engine API's `PayloadBodiesByRange` method will be much more efficient, but for now,
// the `eth_getBlockByNumber` method is more widely available. // the `eth_getBlockByNumber` method is more widely available.
func (s *SyncClient) fetchUnsafeBlockFromRpc(ctx context.Context, blockNumber uint64) { func (s *SyncClient) fetchUnsafeBlockFromRpc(ctx context.Context, blockNumber uint64) error {
s.log.Info("Requesting unsafe payload from backup RPC", "block number", blockNumber) s.log.Info("Requesting unsafe payload from backup RPC", "block number", blockNumber)
payload, err := s.PayloadByNumber(ctx, blockNumber) payload, err := s.PayloadByNumber(ctx, blockNumber)
if err != nil { if err != nil {
s.log.Warn("Failed to convert block to execution payload", "block number", blockNumber, "err", err) return fmt.Errorf("failed to fetch payload by number (%d): %w", blockNumber, err)
return
}
// Signature validation is not necessary here since the backup RPC is trusted.
if _, ok := payload.CheckBlockHash(); !ok {
s.log.Warn("Received invalid payload from backup RPC; invalid block hash", "payload", payload.ID())
return
} }
// Note: the underlying RPC client used for syncing verifies the execution payload blockhash, if set to untrusted.
s.log.Info("Received unsafe payload from backup RPC", "payload", payload.ID()) s.log.Info("Received unsafe payload from backup RPC", "payload", payload.ID())
// Send the retrieved payload to the `unsafeL2Payloads` channel. // Send the retrieved payload to the `unsafeL2Payloads` channel.
if err = s.receivePayload(ctx, RpcSyncPeer, payload); err != nil { if err = s.receivePayload(ctx, RpcSyncPeer, payload); err != nil {
s.log.Warn("Failed to send payload into the driver's unsafeL2Payloads channel", "payload", payload.ID(), "err", err) return fmt.Errorf("failed to send payload %s into the driver's unsafeL2Payloads channel: %w", payload.ID(), err)
return
} else { } else {
s.log.Info("Sent received payload into the driver's unsafeL2Payloads channel", "payload", payload.ID()) s.log.Debug("Sent received payload into the driver's unsafeL2Payloads channel", "payload", payload.ID())
return nil
} }
} }
package metrics
import (
"context"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/prometheus/client_golang/prometheus"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
)
const Namespace = "op_proposer"
type Metricer interface {
RecordInfo(version string)
RecordUp()
// Records all L1 and L2 block events
opmetrics.RefMetricer
RecordL2BlocksProposed(l2ref eth.L2BlockRef)
}
type Metrics struct {
ns string
registry *prometheus.Registry
factory opmetrics.Factory
opmetrics.RefMetrics
Info prometheus.GaugeVec
Up prometheus.Gauge
}
var _ Metricer = (*Metrics)(nil)
func NewMetrics(procName string) *Metrics {
if procName == "" {
procName = "default"
}
ns := Namespace + "_" + procName
registry := opmetrics.NewRegistry()
factory := opmetrics.With(registry)
return &Metrics{
ns: ns,
registry: registry,
factory: factory,
RefMetrics: opmetrics.MakeRefMetrics(ns, factory),
Info: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns,
Name: "info",
Help: "Pseudo-metric tracking version and config info",
}, []string{
"version",
}),
Up: factory.NewGauge(prometheus.GaugeOpts{
Namespace: ns,
Name: "up",
Help: "1 if the op-proposer has finished starting up",
}),
}
}
func (m *Metrics) Serve(ctx context.Context, host string, port int) error {
return opmetrics.ListenAndServe(ctx, m.registry, host, port)
}
func (m *Metrics) StartBalanceMetrics(ctx context.Context,
l log.Logger, client *ethclient.Client, account common.Address) {
opmetrics.LaunchBalanceMetrics(ctx, l, m.registry, m.ns, client, account)
}
// RecordInfo sets a pseudo-metric that contains versioning and
// config info for the op-proposer.
func (m *Metrics) RecordInfo(version string) {
m.Info.WithLabelValues(version).Set(1)
}
// RecordUp sets the up metric to 1.
func (m *Metrics) RecordUp() {
prometheus.MustRegister()
m.Up.Set(1)
}
const (
BlockProposed = "proposed"
)
// RecordL2BlocksProposed should be called when new L2 block is proposed
func (m *Metrics) RecordL2BlocksProposed(l2ref eth.L2BlockRef) {
m.RecordL2Ref(BlockProposed, l2ref)
}
package metrics
import (
"github.com/ethereum-optimism/optimism/op-node/eth"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
)
type noopMetrics struct{ opmetrics.NoopRefMetrics }
var NoopMetrics Metricer = new(noopMetrics)
func (*noopMetrics) RecordInfo(version string) {}
func (*noopMetrics) RecordUp() {}
func (*noopMetrics) RecordL2BlocksProposed(l2ref eth.L2BlockRef) {}
...@@ -24,9 +24,9 @@ import ( ...@@ -24,9 +24,9 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/metrics"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof" oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
...@@ -49,9 +49,10 @@ func Main(version string, cliCtx *cli.Context) error { ...@@ -49,9 +49,10 @@ func Main(version string, cliCtx *cli.Context) error {
} }
l := oplog.NewLogger(cfg.LogConfig) l := oplog.NewLogger(cfg.LogConfig)
m := metrics.NewMetrics("default")
l.Info("Initializing L2 Output Submitter") l.Info("Initializing L2 Output Submitter")
l2OutputSubmitter, err := NewL2OutputSubmitterFromCLIConfig(cfg, l) l2OutputSubmitter, err := NewL2OutputSubmitterFromCLIConfig(cfg, l, m)
if err != nil { if err != nil {
l.Error("Unable to create the L2 Output Submitter", "error", err) l.Error("Unable to create the L2 Output Submitter", "error", err)
return err return err
...@@ -78,17 +79,15 @@ func Main(version string, cliCtx *cli.Context) error { ...@@ -78,17 +79,15 @@ func Main(version string, cliCtx *cli.Context) error {
}() }()
} }
registry := opmetrics.NewRegistry()
metricsCfg := cfg.MetricsConfig metricsCfg := cfg.MetricsConfig
if metricsCfg.Enabled { if metricsCfg.Enabled {
l.Info("starting metrics server", "addr", metricsCfg.ListenAddr, "port", metricsCfg.ListenPort) l.Info("starting metrics server", "addr", metricsCfg.ListenAddr, "port", metricsCfg.ListenPort)
go func() { go func() {
if err := opmetrics.ListenAndServe(ctx, registry, metricsCfg.ListenAddr, metricsCfg.ListenPort); err != nil { if err := m.Serve(ctx, metricsCfg.ListenAddr, metricsCfg.ListenPort); err != nil {
l.Error("error starting metrics server", err) l.Error("error starting metrics server", err)
} }
}() }()
addr := l2OutputSubmitter.from m.StartBalanceMetrics(ctx, l, l2OutputSubmitter.l1Client, l2OutputSubmitter.from)
opmetrics.LaunchBalanceMetrics(ctx, l, registry, "", l2OutputSubmitter.l1Client, addr)
} }
rpcCfg := cfg.RPCConfig rpcCfg := cfg.RPCConfig
...@@ -98,6 +97,9 @@ func Main(version string, cliCtx *cli.Context) error { ...@@ -98,6 +97,9 @@ func Main(version string, cliCtx *cli.Context) error {
return fmt.Errorf("error starting RPC server: %w", err) return fmt.Errorf("error starting RPC server: %w", err)
} }
m.RecordInfo(version)
m.RecordUp()
interruptChannel := make(chan os.Signal, 1) interruptChannel := make(chan os.Signal, 1)
signal.Notify(interruptChannel, []os.Signal{ signal.Notify(interruptChannel, []os.Signal{
os.Interrupt, os.Interrupt,
...@@ -117,6 +119,7 @@ type L2OutputSubmitter struct { ...@@ -117,6 +119,7 @@ type L2OutputSubmitter struct {
wg sync.WaitGroup wg sync.WaitGroup
done chan struct{} done chan struct{}
log log.Logger log log.Logger
metr metrics.Metricer
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
...@@ -143,7 +146,7 @@ type L2OutputSubmitter struct { ...@@ -143,7 +146,7 @@ type L2OutputSubmitter struct {
} }
// NewL2OutputSubmitterFromCLIConfig creates a new L2 Output Submitter given the CLI Config // NewL2OutputSubmitterFromCLIConfig creates a new L2 Output Submitter given the CLI Config
func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*L2OutputSubmitter, error) { func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger, m metrics.Metricer) (*L2OutputSubmitter, error) {
signer, fromAddress, err := opcrypto.SignerFactoryFromConfig(l, cfg.PrivateKey, cfg.Mnemonic, cfg.L2OutputHDPath, cfg.SignerConfig) signer, fromAddress, err := opcrypto.SignerFactoryFromConfig(l, cfg.PrivateKey, cfg.Mnemonic, cfg.L2OutputHDPath, cfg.SignerConfig)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -166,12 +169,18 @@ func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*L2OutputSu ...@@ -166,12 +169,18 @@ func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*L2OutputSu
return nil, err return nil, err
} }
chainID, err := l1Client.ChainID(context.Background())
if err != nil {
return nil, err
}
txMgrConfg := txmgr.Config{ txMgrConfg := txmgr.Config{
ResubmissionTimeout: cfg.ResubmissionTimeout, ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second, ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations, NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount, SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
From: fromAddress, From: fromAddress,
ChainID: chainID,
} }
proposerCfg := Config{ proposerCfg := Config{
...@@ -185,11 +194,11 @@ func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*L2OutputSu ...@@ -185,11 +194,11 @@ func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*L2OutputSu
SignerFnFactory: signer, SignerFnFactory: signer,
} }
return NewL2OutputSubmitter(proposerCfg, l) return NewL2OutputSubmitter(proposerCfg, l, m)
} }
// NewL2OutputSubmitter creates a new L2 Output Submitter // NewL2OutputSubmitter creates a new L2 Output Submitter
func NewL2OutputSubmitter(cfg Config, l log.Logger) (*L2OutputSubmitter, error) { func NewL2OutputSubmitter(cfg Config, l log.Logger, m metrics.Metricer) (*L2OutputSubmitter, error) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cCtx, cCancel := context.WithTimeout(ctx, defaultDialTimeout) cCtx, cCancel := context.WithTimeout(ctx, defaultDialTimeout)
...@@ -228,6 +237,7 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger) (*L2OutputSubmitter, error) ...@@ -228,6 +237,7 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger) (*L2OutputSubmitter, error)
log: l, log: l,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
metr: m,
l1Client: cfg.L1Client, l1Client: cfg.L1Client,
rollupClient: cfg.RollupClient, rollupClient: cfg.RollupClient,
...@@ -413,9 +423,9 @@ func (l *L2OutputSubmitter) loop() { ...@@ -413,9 +423,9 @@ func (l *L2OutputSubmitter) loop() {
l.log.Error("Failed to send proposal transaction", "err", err) l.log.Error("Failed to send proposal transaction", "err", err)
cancel() cancel()
break break
} else {
cancel()
} }
l.metr.RecordL2BlocksProposed(output.BlockRef)
cancel()
case <-l.done: case <-l.done:
return return
......
// Code generated by mockery v2.22.1. DO NOT EDIT.
package mocks
import (
context "context"
txmgr "github.com/ethereum-optimism/optimism/op-service/txmgr"
mock "github.com/stretchr/testify/mock"
types "github.com/ethereum/go-ethereum/core/types"
)
// TxManager is an autogenerated mock type for the TxManager type
type TxManager struct {
mock.Mock
}
// CraftTx provides a mock function with given fields: ctx, candidate
func (_m *TxManager) CraftTx(ctx context.Context, candidate txmgr.TxCandidate) (*types.Transaction, error) {
ret := _m.Called(ctx, candidate)
var r0 *types.Transaction
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, txmgr.TxCandidate) (*types.Transaction, error)); ok {
return rf(ctx, candidate)
}
if rf, ok := ret.Get(0).(func(context.Context, txmgr.TxCandidate) *types.Transaction); ok {
r0 = rf(ctx, candidate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.Transaction)
}
}
if rf, ok := ret.Get(1).(func(context.Context, txmgr.TxCandidate) error); ok {
r1 = rf(ctx, candidate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Send provides a mock function with given fields: ctx, tx
func (_m *TxManager) Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) {
ret := _m.Called(ctx, tx)
var r0 *types.Receipt
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) (*types.Receipt, error)); ok {
return rf(ctx, tx)
}
if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) *types.Receipt); ok {
r0 = rf(ctx, tx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.Receipt)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *types.Transaction) error); ok {
r1 = rf(ctx, tx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewTxManager interface {
mock.TestingT
Cleanup(func())
}
// NewTxManager creates a new instance of TxManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewTxManager(t mockConstructorTestingTNewTxManager) *TxManager {
mock := &TxManager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
...@@ -3,14 +3,17 @@ package txmgr ...@@ -3,14 +3,17 @@ package txmgr
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/big" "math/big"
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
) )
...@@ -38,6 +41,15 @@ type Config struct { ...@@ -38,6 +41,15 @@ type Config struct {
// attempted. // attempted.
ResubmissionTimeout time.Duration ResubmissionTimeout time.Duration
// ChainID is the chain ID of the L1 chain.
ChainID *big.Int
// NetworkTimeout is the allowed duration for a single network request.
// This is intended to be used for network requests that can be replayed.
//
// If not set, this will default to 2 seconds.
NetworkTimeout time.Duration
// RequireQueryInterval is the interval at which the tx manager will // RequireQueryInterval is the interval at which the tx manager will
// query the backend to check for confirmations after a tx at a // query the backend to check for confirmations after a tx at a
// specific gas price has been published. // specific gas price has been published.
...@@ -59,6 +71,8 @@ type Config struct { ...@@ -59,6 +71,8 @@ type Config struct {
// TxManager is an interface that allows callers to reliably publish txs, // TxManager is an interface that allows callers to reliably publish txs,
// bumping the gas price if needed, and obtain the receipt of the resulting tx. // bumping the gas price if needed, and obtain the receipt of the resulting tx.
//
//go:generate mockery --name TxManager --output ./mocks
type TxManager interface { type TxManager interface {
// Send is used to publish a transaction with incrementally higher gas // Send is used to publish a transaction with incrementally higher gas
// prices until the transaction eventually confirms. This method blocks // prices until the transaction eventually confirms. This method blocks
...@@ -69,6 +83,9 @@ type TxManager interface { ...@@ -69,6 +83,9 @@ type TxManager interface {
// //
// NOTE: Send should be called by AT MOST one caller at a time. // NOTE: Send should be called by AT MOST one caller at a time.
Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error)
// CraftTx is used to craft a transaction using a [TxCandidate].
CraftTx(ctx context.Context, candidate TxCandidate) (*types.Transaction, error)
} }
// ETHBackend is the set of methods that the transaction manager uses to resubmit gas & determine // ETHBackend is the set of methods that the transaction manager uses to resubmit gas & determine
...@@ -89,18 +106,119 @@ type ETHBackend interface { ...@@ -89,18 +106,119 @@ type ETHBackend interface {
// TODO(CLI-3318): Maybe need a generic interface to support different RPC providers // TODO(CLI-3318): Maybe need a generic interface to support different RPC providers
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
SuggestGasTipCap(ctx context.Context) (*big.Int, error) SuggestGasTipCap(ctx context.Context) (*big.Int, error)
// NonceAt returns the account nonce of the given account.
// The block number can be nil, in which case the nonce is taken from the latest known block.
NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error)
/// EstimateGas returns an estimate of the amount of gas needed to execute the given
/// transaction against the current pending block.
EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
} }
// SimpleTxManager is a implementation of TxManager that performs linear fee // SimpleTxManager is a implementation of TxManager that performs linear fee
// bumping of a tx until it confirms. // bumping of a tx until it confirms.
type SimpleTxManager struct { type SimpleTxManager struct {
Config // embed the config directly Config // embed the config directly
name string name string
chainID *big.Int
backend ETHBackend backend ETHBackend
l log.Logger l log.Logger
} }
// TxCandidate is a transaction candidate that can be submitted to ask the
// [TxManager] to construct a transaction with gas price bounds.
type TxCandidate struct {
// TxData is the transaction data to be used in the constructed tx.
TxData []byte
// To is the recipient of the constructed tx.
To common.Address
// GasLimit is the gas limit to be used in the constructed tx.
GasLimit uint64
// From is the sender (or `from`) of the constructed tx.
From common.Address
}
// calcGasTipAndFeeCap queries L1 to determine what a suitable miner tip & basefee limit would be for timely inclusion
func (m *SimpleTxManager) calcGasTipAndFeeCap(ctx context.Context) (gasTipCap *big.Int, gasFeeCap *big.Int, err error) {
childCtx, cancel := context.WithTimeout(ctx, m.Config.NetworkTimeout)
gasTipCap, err = m.backend.SuggestGasTipCap(childCtx)
cancel()
if err != nil {
return nil, nil, fmt.Errorf("failed to get suggested gas tip cap: %w", err)
}
if gasTipCap == nil {
m.l.Warn("unexpected unset gasTipCap, using default 2 gwei")
gasTipCap = new(big.Int).SetUint64(params.GWei * 2)
}
childCtx, cancel = context.WithTimeout(ctx, m.Config.NetworkTimeout)
head, err := m.backend.HeaderByNumber(childCtx, nil)
cancel()
if err != nil || head == nil {
return nil, nil, fmt.Errorf("failed to get L1 head block for fee cap: %w", err)
}
if head.BaseFee == nil {
return nil, nil, fmt.Errorf("failed to get L1 basefee in block %d for fee cap", head.Number)
}
gasFeeCap = CalcGasFeeCap(head.BaseFee, gasTipCap)
return gasTipCap, gasFeeCap, nil
}
// CraftTx creates the signed transaction to the batchInboxAddress.
// It queries L1 for the current fee market conditions as well as for the nonce.
// NOTE: This method SHOULD NOT publish the resulting transaction.
// NOTE: If the [TxCandidate.GasLimit] is non-zero, it will be used as the transaction's gas.
// NOTE: Otherwise, the [SimpleTxManager] will query the specified backend for an estimate.
func (m *SimpleTxManager) CraftTx(ctx context.Context, candidate TxCandidate) (*types.Transaction, error) {
gasTipCap, gasFeeCap, err := m.calcGasTipAndFeeCap(ctx)
if err != nil {
return nil, err
}
// Fetch the sender's nonce from the latest known block (nil `blockNumber`)
childCtx, cancel := context.WithTimeout(ctx, m.Config.NetworkTimeout)
defer cancel()
nonce, err := m.backend.NonceAt(childCtx, candidate.From, nil)
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}
rawTx := &types.DynamicFeeTx{
ChainID: m.chainID,
Nonce: nonce,
To: &candidate.To,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: candidate.TxData,
}
m.l.Info("creating tx", "to", rawTx.To, "from", candidate.From)
// If the gas limit is set, we can use that as the gas
if candidate.GasLimit != 0 {
rawTx.Gas = candidate.GasLimit
} else {
// Calculate the intrinsic gas for the transaction
gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{
From: candidate.From,
To: &candidate.To,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Data: rawTx.Data,
})
if err != nil {
return nil, fmt.Errorf("failed to estimate gas: %w", err)
}
rawTx.Gas = gas
}
ctx, cancel = context.WithTimeout(ctx, m.Config.NetworkTimeout)
defer cancel()
return m.Signer(ctx, candidate.From, types.NewTx(rawTx))
}
// IncreaseGasPrice takes the previous transaction & potentially clones then signs it with a higher tip. // IncreaseGasPrice takes the previous transaction & potentially clones then signs it with a higher tip.
// If the tip + basefee suggested by the network are not greater than the previous values, the same transaction // If the tip + basefee suggested by the network are not greater than the previous values, the same transaction
// will be returned. If they are greater, this function will ensure that they are at least greater by 15% than // will be returned. If they are greater, this function will ensure that they are at least greater by 15% than
...@@ -196,8 +314,12 @@ func NewSimpleTxManager(name string, l log.Logger, cfg Config, backend ETHBacken ...@@ -196,8 +314,12 @@ func NewSimpleTxManager(name string, l log.Logger, cfg Config, backend ETHBacken
if cfg.NumConfirmations == 0 { if cfg.NumConfirmations == 0 {
panic("txmgr: NumConfirmations cannot be zero") panic("txmgr: NumConfirmations cannot be zero")
} }
if cfg.NetworkTimeout == 0 {
cfg.NetworkTimeout = 2 * time.Second
}
return &SimpleTxManager{ return &SimpleTxManager{
chainID: cfg.ChainID,
name: name, name: name,
Config: cfg, Config: cfg,
backend: backend, backend: backend,
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-node/testutils"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum/go-ethereum"
"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/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -50,6 +51,18 @@ func newTestHarness(t *testing.T) *testHarness { ...@@ -50,6 +51,18 @@ func newTestHarness(t *testing.T) *testHarness {
return newTestHarnessWithConfig(t, configWithNumConfs(1)) return newTestHarnessWithConfig(t, configWithNumConfs(1))
} }
// createTxCandidate creates a mock [TxCandidate].
func (h testHarness) createTxCandidate() TxCandidate {
inbox := common.HexToAddress("0x42000000000000000000000000000000000000ff")
sender := common.HexToAddress("0xdeadbeef")
return TxCandidate{
To: inbox,
TxData: []byte{0x00, 0x01, 0x02},
From: sender,
GasLimit: uint64(1337),
}
}
func configWithNumConfs(numConfirmations uint64) Config { func configWithNumConfs(numConfirmations uint64) Config {
return Config{ return Config{
ResubmissionTimeout: time.Second, ResubmissionTimeout: time.Second,
...@@ -175,6 +188,10 @@ func (b *mockBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*typ ...@@ -175,6 +188,10 @@ func (b *mockBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*typ
}, nil }, nil
} }
func (b *mockBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
return b.g.basefee().Uint64(), nil
}
func (b *mockBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { func (b *mockBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
tip, _ := b.g.sample() tip, _ := b.g.sample()
return tip, nil return tip, nil
...@@ -185,7 +202,14 @@ func (b *mockBackend) SendTransaction(ctx context.Context, tx *types.Transaction ...@@ -185,7 +202,14 @@ func (b *mockBackend) SendTransaction(ctx context.Context, tx *types.Transaction
panic("set sender function was not set") panic("set sender function was not set")
} }
return b.send(ctx, tx) return b.send(ctx, tx)
}
func (b *mockBackend) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
return 0, nil
}
func (*mockBackend) ChainID(ctx context.Context) (*big.Int, error) {
return big.NewInt(1), nil
} }
// TransactionReceipt queries the mockBackend for a mined txHash. If none is // TransactionReceipt queries the mockBackend for a mined txHash. If none is
...@@ -330,6 +354,51 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) { ...@@ -330,6 +354,51 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
require.Nil(t, receipt) require.Nil(t, receipt)
} }
// TestTxMgr_CraftTx ensures that the tx manager will create transactions as expected.
func TestTxMgr_CraftTx(t *testing.T) {
t.Parallel()
h := newTestHarness(t)
candidate := h.createTxCandidate()
// Craft the transaction.
gasTipCap, gasFeeCap := h.gasPricer.feesForEpoch(h.gasPricer.epoch + 1)
tx, err := h.mgr.CraftTx(context.Background(), candidate)
require.Nil(t, err)
require.NotNil(t, tx)
// Validate the gas tip cap and fee cap.
require.Equal(t, gasTipCap, tx.GasTipCap())
require.Equal(t, gasFeeCap, tx.GasFeeCap())
// Validate the nonce was set correctly using the backend.
require.Zero(t, tx.Nonce())
// Check that the gas was set using the gas limit.
require.Equal(t, candidate.GasLimit, tx.Gas())
}
// TestTxMgr_EstimateGas ensures that the tx manager will estimate
// the gas when candidate gas limit is zero in [CraftTx].
func TestTxMgr_EstimateGas(t *testing.T) {
t.Parallel()
h := newTestHarness(t)
candidate := h.createTxCandidate()
// Set the gas limit to zero to trigger gas estimation.
candidate.GasLimit = 0
// Gas estimate
gasEstimate := h.gasPricer.baseBaseFee.Uint64()
// Craft the transaction.
tx, err := h.mgr.CraftTx(context.Background(), candidate)
require.Nil(t, err)
require.NotNil(t, tx)
// Check that the gas was estimated correctly.
require.Equal(t, gasEstimate, tx.Gas())
}
// TestTxMgrOnlyOnePublicationSucceeds asserts that the tx manager will return a // TestTxMgrOnlyOnePublicationSucceeds asserts that the tx manager will return a
// receipt so long as at least one of the publications is able to succeed with a // receipt so long as at least one of the publications is able to succeed with a
// simulated rpc failure. // simulated rpc failure.
...@@ -577,6 +646,18 @@ func (b *failingBackend) SuggestGasTipCap(_ context.Context) (*big.Int, error) { ...@@ -577,6 +646,18 @@ func (b *failingBackend) SuggestGasTipCap(_ context.Context) (*big.Int, error) {
return b.gasTip, nil return b.gasTip, nil
} }
func (b *failingBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
return b.baseFee.Uint64(), nil
}
func (b *failingBackend) NonceAt(_ context.Context, _ common.Address, _ *big.Int) (uint64, error) {
return 0, errors.New("unimplemented")
}
func (b *failingBackend) ChainID(ctx context.Context) (*big.Int, error) {
return nil, errors.New("unimplemented")
}
// TestWaitMinedReturnsReceiptAfterFailure asserts that WaitMined is able to // TestWaitMinedReturnsReceiptAfterFailure asserts that WaitMined is able to
// recover from failed calls to the backend. It uses the failedBackend to // recover from failed calls to the backend. It uses the failedBackend to
// simulate an rpc call failure, followed by the successful return of a receipt. // simulate an rpc call failure, followed by the successful return of a receipt.
......
...@@ -61,6 +61,8 @@ services: ...@@ -61,6 +61,8 @@ services:
--p2p.listen.ip=0.0.0.0 --p2p.listen.ip=0.0.0.0
--p2p.listen.tcp=9003 --p2p.listen.tcp=9003
--p2p.listen.udp=9003 --p2p.listen.udp=9003
--p2p.scoring.peers=light
--p2p.ban.peers=true
--snapshotlog.file=/op_log/snapshot.log --snapshotlog.file=/op_log/snapshot.log
--p2p.priv.path=/config/p2p-node-key.txt --p2p.priv.path=/config/p2p-node-key.txt
--metrics.enabled --metrics.enabled
...@@ -121,6 +123,8 @@ services: ...@@ -121,6 +123,8 @@ services:
OP_BATCHER_L1_ETH_RPC: http://l1:8545 OP_BATCHER_L1_ETH_RPC: http://l1:8545
OP_BATCHER_L2_ETH_RPC: http://l2:8545 OP_BATCHER_L2_ETH_RPC: http://l2:8545
OP_BATCHER_ROLLUP_RPC: http://op-node:8545 OP_BATCHER_ROLLUP_RPC: http://op-node:8545
TX_MANAGER_TIMEOUT: 10m
OFFLINE_GAS_ESTIMATION: false
OP_BATCHER_MAX_CHANNEL_DURATION: 1 OP_BATCHER_MAX_CHANNEL_DURATION: 1
OP_BATCHER_MAX_L1_TX_SIZE_BYTES: 120000 OP_BATCHER_MAX_L1_TX_SIZE_BYTES: 120000
OP_BATCHER_TARGET_L1_TX_SIZE_BYTES: 100000 OP_BATCHER_TARGET_L1_TX_SIZE_BYTES: 100000
......
...@@ -16,7 +16,7 @@ WORKDIR /opt/foundry ...@@ -16,7 +16,7 @@ WORKDIR /opt/foundry
# Only diff from upstream docker image is this clone instead # Only diff from upstream docker image is this clone instead
# of COPY. We select a specific commit to use. # of COPY. We select a specific commit to use.
RUN git clone https://github.com/foundry-rs/foundry.git . \ RUN git clone https://github.com/foundry-rs/foundry.git . \
&& git checkout 8f3fca9c608d58981daaffe11e7f8076644cb753 && git checkout da2392e58bb8a7fefeba46b40c4df1afad8ccd22
RUN source $HOME/.profile && \ RUN source $HOME/.profile && \
cargo build --release && \ cargo build --release && \
......
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier"
]
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
"lint": "yarn lint:fix && yarn lint:check", "lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"test": "ts-mocha test/*.spec.ts", "test": "ts-mocha test/*.spec.ts",
"test:coverage": "echo 'no coverage'" "test:coverage": "nyc ts-mocha test/*.spec.ts && nyc merge .nyc_output coverage.json"
}, },
"keywords": [ "keywords": [
"optimism", "optimism",
...@@ -48,8 +48,7 @@ ...@@ -48,8 +48,7 @@
"pino": "^6.11.3", "pino": "^6.11.3",
"pino-multi-stream": "^5.3.0", "pino-multi-stream": "^5.3.0",
"pino-sentry": "^0.7.0", "pino-sentry": "^0.7.0",
"prom-client": "^13.1.0", "prom-client": "^13.1.0"
"qs": "^6.10.5"
}, },
"devDependencies": { "devDependencies": {
"@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0",
......
...@@ -3,11 +3,11 @@ import request from 'supertest' ...@@ -3,11 +3,11 @@ import request from 'supertest'
import chai = require('chai') import chai = require('chai')
const expect = chai.expect const expect = chai.expect
import { Logger, Metrics, createMetricsServer } from '../src' import { Logger, LegacyMetrics, createMetricsServer } from '../src'
describe('Metrics', () => { describe('Metrics', () => {
it('shoud serve metrics', async () => { it('shoud serve metrics', async () => {
const metrics = new Metrics({ const metrics = new LegacyMetrics({
prefix: 'test_metrics', prefix: 'test_metrics',
}) })
const registry = metrics.registry const registry = metrics.registry
......
...@@ -155,6 +155,33 @@ contract SystemDictator is OwnableUpgradeable { ...@@ -155,6 +155,33 @@ contract SystemDictator is OwnableUpgradeable {
currentStep++; currentStep++;
} }
/**
* @notice Constructor required to ensure that the implementation of the SystemDictator is
* initialized upon deployment.
*/
constructor() {
// Using this shorter variable as an alias for address(0) just prevents us from having to
// to use a new line for every single parameter.
address zero = address(0);
initialize(
DeployConfig(
GlobalConfig(AddressManager(zero), ProxyAdmin(zero), zero, zero),
ProxyAddressConfig(zero, zero, zero, zero, zero, zero, zero),
ImplementationAddressConfig(
L2OutputOracle(zero),
OptimismPortal(payable(zero)),
L1CrossDomainMessenger(zero),
L1StandardBridge(payable(zero)),
OptimismMintableERC20Factory(zero),
L1ERC721Bridge(zero),
PortalSender(zero),
SystemConfig(zero)
),
SystemConfigConfig(zero, 0, 0, bytes32(0), 0, zero)
)
);
}
/** /**
* @param _config System configuration. * @param _config System configuration.
*/ */
......
...@@ -139,16 +139,20 @@ contract Bytes_slice_Test is Test { ...@@ -139,16 +139,20 @@ contract Bytes_slice_Test is Test {
vm.assume(_length <= _input.length - _start); vm.assume(_length <= _input.length - _start);
// Grab the free memory pointer before the slice operation // Grab the free memory pointer before the slice operation
uint256 initPtr; uint64 initPtr;
assembly { assembly {
initPtr := mload(0x40) initPtr := mload(0x40)
} }
uint64 expectedPtr = uint64(initPtr + 0x20 + ((_length + 0x1f) & ~uint256(0x1f)));
// Ensure that all memory outside of the expected range is safe.
vm.expectSafeMemory(initPtr, expectedPtr);
// Slice the input bytes array from `_start` to `_start + _length` // Slice the input bytes array from `_start` to `_start + _length`
bytes memory slice = Bytes.slice(_input, _start, _length); bytes memory slice = Bytes.slice(_input, _start, _length);
// Grab the free memory pointer after the slice operation // Grab the free memory pointer after the slice operation
uint256 finalPtr; uint64 finalPtr;
assembly { assembly {
finalPtr := mload(0x40) finalPtr := mload(0x40)
} }
...@@ -165,10 +169,11 @@ contract Bytes_slice_Test is Test { ...@@ -165,10 +169,11 @@ contract Bytes_slice_Test is Test {
// Note that we use a slightly less efficient, but equivalent method of rounding // Note that we use a slightly less efficient, but equivalent method of rounding
// up `_length` to the next multiple of 32 than is used in the `slice` function. // up `_length` to the next multiple of 32 than is used in the `slice` function.
// This is to diff test the method used in `slice`. // This is to diff test the method used in `slice`.
assertEq(finalPtr, initPtr + 0x20 + (((_length + 0x1F) >> 5) << 5)); uint64 _expectedPtr = uint64(initPtr + 0x20 + (((_length + 0x1F) >> 5) << 5));
assertEq(finalPtr, _expectedPtr);
// Sanity check for equivalence of the rounding methods. // Sanity check for equivalence of the rounding methods.
assertEq(((_length + 0x1F) >> 5) << 5, (_length + 0x1F) & ~uint256(0x1F)); assertEq(_expectedPtr, expectedPtr);
} }
// The slice length should be equal to `_length` // The slice length should be equal to `_length`
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
"l2OutputOracleStartingTimestamp": -1, "l2OutputOracleStartingTimestamp": -1,
"l2OutputOracleProposer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "l2OutputOracleProposer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"l2OutputOracleChallenger": "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", "l2OutputOracleChallenger": "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
"l2GenesisBlockGasLimit": "0xE4E1C0", "l2GenesisBlockGasLimit": "0x1c9c380",
"l1BlockTime": 3, "l1BlockTime": 3,
"cliqueSignerAddress": "0xca062b0fd91172d89bcd4bb084ac4e21972cc467", "cliqueSignerAddress": "0xca062b0fd91172d89bcd4bb084ac4e21972cc467",
"baseFeeVaultRecipient": "0xBcd4042DE499D14e55001CcbB24a551F3b954096", "baseFeeVaultRecipient": "0xBcd4042DE499D14e55001CcbB24a551F3b954096",
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
"governanceTokenSymbol": "OP", "governanceTokenSymbol": "OP",
"governanceTokenOwner": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76", "governanceTokenOwner": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76",
"l2GenesisBlockGasLimit": "0x17D7840", "l2GenesisBlockGasLimit": "0x1c9c380",
"l2GenesisBlockCoinbase": "0x4200000000000000000000000000000000000011", "l2GenesisBlockCoinbase": "0x4200000000000000000000000000000000000011",
"l2GenesisBlockBaseFeePerGas": "0x3b9aca00", "l2GenesisBlockBaseFeePerGas": "0x3b9aca00",
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
"l2OutputOracleSubmissionInterval": 120, "l2OutputOracleSubmissionInterval": 120,
"l2OutputOracleStartingBlockNumber": 0, "l2OutputOracleStartingBlockNumber": 0,
"l2OutputOracleStartingTimestamp": "TIMESTAMP", "l2OutputOracleStartingTimestamp": TIMESTAMP,
"l2OutputOracleProposer": "PROPOSER", "l2OutputOracleProposer": "PROPOSER",
"l2OutputOracleChallenger": "ADMIN", "l2OutputOracleChallenger": "ADMIN",
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
"governanceTokenName": "Optimism", "governanceTokenName": "Optimism",
"governanceTokenOwner": "ADMIN", "governanceTokenOwner": "ADMIN",
"l2GenesisBlockGasLimit": "0x17D7840", "l2GenesisBlockGasLimit": "0x1c9c380",
"l2GenesisBlockBaseFeePerGas": "0x3b9aca00", "l2GenesisBlockBaseFeePerGas": "0x3b9aca00",
"l2GenesisRegolithTimeOffset": "0x0", "l2GenesisRegolithTimeOffset": "0x0",
......
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"ds-test": "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5", "ds-test": "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5",
"ethereum-waffle": "^3.0.0", "ethereum-waffle": "^3.0.0",
"forge-std": "https://github.com/foundry-rs/forge-std.git#fd86115ed6aba8e234ee0fb86c12fe35eff0b2a0", "forge-std": "https://github.com/foundry-rs/forge-std.git#46264e9788017fc74f9f58b7efa0bc6e1df6d410",
"glob": "^7.1.6", "glob": "^7.1.6",
"hardhat-deploy": "^0.11.4", "hardhat-deploy": "^0.11.4",
"solhint": "^3.3.7", "solhint": "^3.3.7",
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"scripts": { "scripts": {
"build": "yarn build:contracts && yarn copy:contracts && yarn autogen:artifacts && yarn build:typescript", "build": "yarn build:contracts && yarn copy:contracts && yarn autogen:artifacts && yarn build:typescript",
"build:typescript": "tsc -p ./tsconfig.json", "build:typescript": "tsc -p ./tsconfig.json",
"build:bindings": "./scripts/legacy-bindings.sh ./../../op-bindings/legacy-bindings",
"build:contracts": "hardhat compile --show-stack-traces", "build:contracts": "hardhat compile --show-stack-traces",
"autogen:markdown": "ts-node scripts/generate-markdown.ts", "autogen:markdown": "ts-node scripts/generate-markdown.ts",
"autogen:artifacts": "ts-node scripts/generate-artifacts.ts && ts-node scripts/generate-deployed-artifacts.ts", "autogen:artifacts": "ts-node scripts/generate-artifacts.ts && ts-node scripts/generate-deployed-artifacts.ts",
......
#!/bin/bash
OUTDIR="$1"
if [ ! -d "$OUTDIR" ]; then
echo "Must pass output directory"
exit 1
fi
CONTRACTS=("CanonicalTransactionChain")
PKG=legacy_bindings
for contract in ${CONTRACTS[@]}; do
TMPFILE=$(mktemp)
npx hardhat inspect $contract bytecode > "$TMPFILE"
ABI=$(npx hardhat inspect $contract abi)
outfile="$OUTDIR/$contract.go"
echo "$ABI" | abigen --abi - --pkg "$PKG" --bin "$TMPFILE" --type $contract --out "$outfile"
rm "$TMPFILE"
done
...@@ -165,19 +165,19 @@ export const getBridgeAdapters = ( ...@@ -165,19 +165,19 @@ export const getBridgeAdapters = (
} }
): BridgeAdapters => { ): BridgeAdapters => {
const adapterData: BridgeAdapterData = { const adapterData: BridgeAdapterData = {
...(CONTRACT_ADDRESSES[l2ChainId] ...(CONTRACT_ADDRESSES[l2ChainId] || opts?.contracts?.l1?.L1StandardBridge
? { ? {
Standard: { Standard: {
Adapter: StandardBridgeAdapter, Adapter: StandardBridgeAdapter,
l1Bridge: l1Bridge:
opts.contracts?.l1?.L1StandardBridge || opts?.contracts?.l1?.L1StandardBridge ||
CONTRACT_ADDRESSES[l2ChainId].l1.L1StandardBridge, CONTRACT_ADDRESSES[l2ChainId].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge, l2Bridge: predeploys.L2StandardBridge,
}, },
ETH: { ETH: {
Adapter: ETHBridgeAdapter, Adapter: ETHBridgeAdapter,
l1Bridge: l1Bridge:
opts.contracts?.l1?.L1StandardBridge || opts?.contracts?.l1?.L1StandardBridge ||
CONTRACT_ADDRESSES[l2ChainId].l1.L1StandardBridge, CONTRACT_ADDRESSES[l2ChainId].l1.L1StandardBridge,
l2Bridge: predeploys.L2StandardBridge, l2Bridge: predeploys.L2StandardBridge,
}, },
......
...@@ -11471,14 +11471,14 @@ forever-agent@~0.6.1: ...@@ -11471,14 +11471,14 @@ forever-agent@~0.6.1:
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
"forge-std@https://github.com/foundry-rs/forge-std.git#46264e9788017fc74f9f58b7efa0bc6e1df6d410":
version "1.5.2"
resolved "https://github.com/foundry-rs/forge-std.git#46264e9788017fc74f9f58b7efa0bc6e1df6d410"
"forge-std@https://github.com/foundry-rs/forge-std.git#53331f4cb2e313466f72440f3e73af048c454d02": "forge-std@https://github.com/foundry-rs/forge-std.git#53331f4cb2e313466f72440f3e73af048c454d02":
version "1.2.0" version "1.2.0"
resolved "https://github.com/foundry-rs/forge-std.git#53331f4cb2e313466f72440f3e73af048c454d02" resolved "https://github.com/foundry-rs/forge-std.git#53331f4cb2e313466f72440f3e73af048c454d02"
"forge-std@https://github.com/foundry-rs/forge-std.git#fd86115ed6aba8e234ee0fb86c12fe35eff0b2a0":
version "1.4.0"
resolved "https://github.com/foundry-rs/forge-std.git#fd86115ed6aba8e234ee0fb86c12fe35eff0b2a0"
form-data@^2.2.0: form-data@^2.2.0:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
...@@ -17871,13 +17871,6 @@ qs@^6.10.3, qs@^6.9.4: ...@@ -17871,13 +17871,6 @@ qs@^6.10.3, qs@^6.9.4:
dependencies: dependencies:
side-channel "^1.0.4" side-channel "^1.0.4"
qs@^6.10.5:
version "6.10.5"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4"
integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ==
dependencies:
side-channel "^1.0.4"
qs@^6.4.0, qs@^6.7.0: qs@^6.4.0, qs@^6.7.0:
version "6.10.1" version "6.10.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
......
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