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

Merge branch 'develop' into inphi/eq-log

parents 10d72a0a b7f161c4
...@@ -346,6 +346,7 @@ func TestMigration(t *testing.T) { ...@@ -346,6 +346,7 @@ func TestMigration(t *testing.T) {
NumConfirmations: 1, NumConfirmations: 1,
ResubmissionTimeout: 5 * time.Second, ResubmissionTimeout: 5 * time.Second,
SafeAbortNonceTooLowCount: 3, SafeAbortNonceTooLowCount: 3,
TxNotInMempoolTimeout: 2 * time.Minute,
}, },
LogConfig: oplog.CLIConfig{ LogConfig: oplog.CLIConfig{
Level: "info", Level: "info",
...@@ -371,6 +372,7 @@ func TestMigration(t *testing.T) { ...@@ -371,6 +372,7 @@ func TestMigration(t *testing.T) {
NumConfirmations: 1, NumConfirmations: 1,
ResubmissionTimeout: 3 * time.Second, ResubmissionTimeout: 3 * time.Second,
SafeAbortNonceTooLowCount: 3, SafeAbortNonceTooLowCount: 3,
TxNotInMempoolTimeout: 2 * time.Minute,
}, },
LogConfig: oplog.CLIConfig{ LogConfig: oplog.CLIConfig{
Level: "info", Level: "info",
......
...@@ -580,6 +580,7 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -580,6 +580,7 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
ResubmissionTimeout: 3 * time.Second, ResubmissionTimeout: 3 * time.Second,
ReceiptQueryInterval: 50 * time.Millisecond, ReceiptQueryInterval: 50 * time.Millisecond,
NetworkTimeout: 2 * time.Second, NetworkTimeout: 2 * time.Second,
TxNotInMempoolTimeout: 2 * time.Minute,
}, },
AllowNonFinalized: cfg.NonFinalizedProposals, AllowNonFinalized: cfg.NonFinalizedProposals,
LogConfig: oplog.CLIConfig{ LogConfig: oplog.CLIConfig{
...@@ -615,6 +616,7 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -615,6 +616,7 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
ResubmissionTimeout: 3 * time.Second, ResubmissionTimeout: 3 * time.Second,
ReceiptQueryInterval: 50 * time.Millisecond, ReceiptQueryInterval: 50 * time.Millisecond,
NetworkTimeout: 2 * time.Second, NetworkTimeout: 2 * time.Second,
TxNotInMempoolTimeout: 2 * time.Minute,
}, },
LogConfig: oplog.CLIConfig{ LogConfig: oplog.CLIConfig{
Level: "info", Level: "info",
......
...@@ -28,6 +28,7 @@ const ( ...@@ -28,6 +28,7 @@ const (
ResubmissionTimeoutFlagName = "resubmission-timeout" ResubmissionTimeoutFlagName = "resubmission-timeout"
NetworkTimeoutFlagName = "network-timeout" NetworkTimeoutFlagName = "network-timeout"
TxSendTimeoutFlagName = "txmgr.send-timeout" TxSendTimeoutFlagName = "txmgr.send-timeout"
TxNotInMempoolTimeoutFlagName = "txmgr.not-in-mempool-timeout"
ReceiptQueryIntervalFlagName = "txmgr.receipt-query-interval" ReceiptQueryIntervalFlagName = "txmgr.receipt-query-interval"
) )
...@@ -95,6 +96,12 @@ func CLIFlags(envPrefix string) []cli.Flag { ...@@ -95,6 +96,12 @@ func CLIFlags(envPrefix string) []cli.Flag {
Value: 0, Value: 0,
EnvVar: opservice.PrefixEnvVar(envPrefix, "TXMGR_TX_SEND_TIMEOUT"), EnvVar: opservice.PrefixEnvVar(envPrefix, "TXMGR_TX_SEND_TIMEOUT"),
}, },
cli.DurationFlag{
Name: TxNotInMempoolTimeoutFlagName,
Usage: "Timeout for aborting a tx send if the tx does not make it to the mempool.",
Value: 2 * time.Minute,
EnvVar: opservice.PrefixEnvVar(envPrefix, "TXMGR_TX_NOT_IN_MEMPOOL_TIMEOUT"),
},
cli.DurationFlag{ cli.DurationFlag{
Name: ReceiptQueryIntervalFlagName, Name: ReceiptQueryIntervalFlagName,
Usage: "Frequency to poll for receipts", Usage: "Frequency to poll for receipts",
...@@ -118,6 +125,7 @@ type CLIConfig struct { ...@@ -118,6 +125,7 @@ type CLIConfig struct {
ReceiptQueryInterval time.Duration ReceiptQueryInterval time.Duration
NetworkTimeout time.Duration NetworkTimeout time.Duration
TxSendTimeout time.Duration TxSendTimeout time.Duration
TxNotInMempoolTimeout time.Duration
} }
func (m CLIConfig) Check() error { func (m CLIConfig) Check() error {
...@@ -125,16 +133,22 @@ func (m CLIConfig) Check() error { ...@@ -125,16 +133,22 @@ func (m CLIConfig) Check() error {
return errors.New("must provide a L1 RPC url") return errors.New("must provide a L1 RPC url")
} }
if m.NumConfirmations == 0 { if m.NumConfirmations == 0 {
return errors.New("num confirmations must not be 0") return errors.New("NumConfirmations must not be 0")
} }
if m.NetworkTimeout == 0 { if m.NetworkTimeout == 0 {
return errors.New("must provide a network timeout") return errors.New("must provide NetworkTimeout")
} }
if m.ResubmissionTimeout == 0 { if m.ResubmissionTimeout == 0 {
return errors.New("must provide a resumbission interval") return errors.New("must provide ResubmissionTimeout")
} }
if m.ReceiptQueryInterval == 0 { if m.ReceiptQueryInterval == 0 {
return errors.New("must provide a receipt query interval") return errors.New("must provide ReceiptQueryInterval")
}
if m.TxNotInMempoolTimeout == 0 {
return errors.New("must provide TxNotInMempoolTimeout")
}
if m.SafeAbortNonceTooLowCount == 0 {
return errors.New("SafeAbortNonceTooLowCount must not be 0")
} }
if err := m.SignerCLIConfig.Check(); err != nil { if err := m.SignerCLIConfig.Check(); err != nil {
return err return err
...@@ -157,6 +171,7 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig { ...@@ -157,6 +171,7 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig {
ReceiptQueryInterval: ctx.GlobalDuration(ReceiptQueryIntervalFlagName), ReceiptQueryInterval: ctx.GlobalDuration(ReceiptQueryIntervalFlagName),
NetworkTimeout: ctx.GlobalDuration(NetworkTimeoutFlagName), NetworkTimeout: ctx.GlobalDuration(NetworkTimeoutFlagName),
TxSendTimeout: ctx.GlobalDuration(TxSendTimeoutFlagName), TxSendTimeout: ctx.GlobalDuration(TxSendTimeoutFlagName),
TxNotInMempoolTimeout: ctx.GlobalDuration(TxNotInMempoolTimeoutFlagName),
} }
} }
...@@ -197,6 +212,7 @@ func NewConfig(cfg CLIConfig, l log.Logger) (Config, error) { ...@@ -197,6 +212,7 @@ func NewConfig(cfg CLIConfig, l log.Logger) (Config, error) {
ResubmissionTimeout: cfg.ResubmissionTimeout, ResubmissionTimeout: cfg.ResubmissionTimeout,
ChainID: chainID, ChainID: chainID,
TxSendTimeout: cfg.TxSendTimeout, TxSendTimeout: cfg.TxSendTimeout,
TxNotInMempoolTimeout: cfg.TxNotInMempoolTimeout,
NetworkTimeout: cfg.NetworkTimeout, NetworkTimeout: cfg.NetworkTimeout,
ReceiptQueryInterval: cfg.ReceiptQueryInterval, ReceiptQueryInterval: cfg.ReceiptQueryInterval,
NumConfirmations: cfg.NumConfirmations, NumConfirmations: cfg.NumConfirmations,
...@@ -222,6 +238,10 @@ type Config struct { ...@@ -222,6 +238,10 @@ type Config struct {
// By default it is unbounded. If set, this is recommended to be at least 20 minutes. // By default it is unbounded. If set, this is recommended to be at least 20 minutes.
TxSendTimeout time.Duration TxSendTimeout time.Duration
// TxNotInMempoolTimeout is how long to wait before aborting a transaction send if the transaction does not
// make it to the mempool. If the tx is in the mempool, TxSendTimeout is used instead.
TxNotInMempoolTimeout time.Duration
// NetworkTimeout is the allowed duration for a single network request. // NetworkTimeout is the allowed duration for a single network request.
// This is intended to be used for network requests that can be replayed. // This is intended to be used for network requests that can be replayed.
NetworkTimeout time.Duration NetworkTimeout time.Duration
......
...@@ -3,6 +3,7 @@ package txmgr ...@@ -3,6 +3,7 @@ package txmgr
import ( import (
"strings" "strings"
"sync" "sync"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
...@@ -12,48 +13,53 @@ import ( ...@@ -12,48 +13,53 @@ import (
// this context, a txn may correspond to multiple different txn hashes due to // this context, a txn may correspond to multiple different txn hashes due to
// varying gas prices, though we treat them all as the same logical txn. This // varying gas prices, though we treat them all as the same logical txn. This
// struct is primarily used to determine whether or not the txmgr should abort a // struct is primarily used to determine whether or not the txmgr should abort a
// given txn and retry with a higher nonce. // given txn.
type SendState struct { type SendState struct {
minedTxs map[common.Hash]struct{} minedTxs map[common.Hash]struct{}
nonceTooLowCount uint64
mu sync.RWMutex mu sync.RWMutex
now func() time.Time
// Config
nonceTooLowCount uint64
txInMempoolDeadline time.Time // deadline to abort at if no transactions are in the mempool
safeAbortNonceTooLowCount uint64 // Counts of the different types of errors
successFullPublishCount uint64 // nil error => tx made it to the mempool
safeAbortNonceTooLowCount uint64 // nonce too low error
} }
// NewSendState parameterizes a new SendState from the passed // NewSendStateWithNow creates a new send state with the provided clock.
// safeAbortNonceTooLowCount. func NewSendStateWithNow(safeAbortNonceTooLowCount uint64, unableToSendTimeout time.Duration, now func() time.Time) *SendState {
func NewSendState(safeAbortNonceTooLowCount uint64) *SendState {
if safeAbortNonceTooLowCount == 0 { if safeAbortNonceTooLowCount == 0 {
panic("txmgr: safeAbortNonceTooLowCount cannot be zero") panic("txmgr: safeAbortNonceTooLowCount cannot be zero")
} }
return &SendState{ return &SendState{
minedTxs: make(map[common.Hash]struct{}), minedTxs: make(map[common.Hash]struct{}),
nonceTooLowCount: 0,
safeAbortNonceTooLowCount: safeAbortNonceTooLowCount, safeAbortNonceTooLowCount: safeAbortNonceTooLowCount,
txInMempoolDeadline: now().Add(unableToSendTimeout),
now: now,
} }
} }
// NewSendState creates a new send state
func NewSendState(safeAbortNonceTooLowCount uint64, unableToSendTimeout time.Duration) *SendState {
return NewSendStateWithNow(safeAbortNonceTooLowCount, unableToSendTimeout, time.Now)
}
// ProcessSendError should be invoked with the error returned for each // ProcessSendError should be invoked with the error returned for each
// publication. It is safe to call this method with nil or arbitrary errors. // publication. It is safe to call this method with nil or arbitrary errors.
// Currently it only acts on errors containing the ErrNonceTooLow message.
func (s *SendState) ProcessSendError(err error) { func (s *SendState) ProcessSendError(err error) {
// Nothing to do.
if err == nil {
return
}
// Only concerned with ErrNonceTooLow.
if !strings.Contains(err.Error(), core.ErrNonceTooLow.Error()) {
return
}
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
// Record this nonce too low observation. // Record the type of error
switch {
case err == nil:
s.successFullPublishCount++
case strings.Contains(err.Error(), core.ErrNonceTooLow.Error()):
s.nonceTooLowCount++ s.nonceTooLowCount++
}
} }
// TxMined records that the txn with txnHash has been mined and is await // TxMined records that the txn with txnHash has been mined and is await
...@@ -85,8 +91,9 @@ func (s *SendState) TxNotMined(txHash common.Hash) { ...@@ -85,8 +91,9 @@ func (s *SendState) TxNotMined(txHash common.Hash) {
} }
// ShouldAbortImmediately returns true if the txmgr should give up on trying a // ShouldAbortImmediately returns true if the txmgr should give up on trying a
// given txn with the target nonce. For now, this only happens if we see an // given txn with the target nonce.
// extended period of getting ErrNonceTooLow without having a txn mined. // This occurs when the set of errors recorded indicates that no further progress can be made
// on this transaction.
func (s *SendState) ShouldAbortImmediately() bool { func (s *SendState) ShouldAbortImmediately() bool {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
...@@ -96,9 +103,14 @@ func (s *SendState) ShouldAbortImmediately() bool { ...@@ -96,9 +103,14 @@ func (s *SendState) ShouldAbortImmediately() bool {
return false return false
} }
// Only abort if we've observed enough ErrNonceTooLow to meet our safe abort // If we have exceeded the nonce too low count, abort
// threshold. if s.nonceTooLowCount >= s.safeAbortNonceTooLowCount ||
return s.nonceTooLowCount >= s.safeAbortNonceTooLowCount // If we have not published a transaction in the allotted time, abort
(s.successFullPublishCount == 0 && s.now().After(s.txInMempoolDeadline)) {
return true
}
return false
} }
// IsWaitingForConfirmation returns true if we have at least one confirmation on // IsWaitingForConfirmation returns true if we have at least one confirmation on
......
...@@ -3,6 +3,7 @@ package txmgr_test ...@@ -3,6 +3,7 @@ package txmgr_test
import ( import (
"errors" "errors"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -11,14 +12,18 @@ import ( ...@@ -11,14 +12,18 @@ import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
) )
const testSafeAbortNonceTooLowCount = 3
var ( var (
testHash = common.HexToHash("0x01") testHash = common.HexToHash("0x01")
) )
const testSafeAbortNonceTooLowCount = 3
func newSendState() *txmgr.SendState { func newSendState() *txmgr.SendState {
return txmgr.NewSendState(testSafeAbortNonceTooLowCount) return newSendStateWithTimeout(time.Hour, time.Now)
}
func newSendStateWithTimeout(t time.Duration, now func() time.Time) *txmgr.SendState {
return txmgr.NewSendStateWithNow(testSafeAbortNonceTooLowCount, t, now)
} }
func processNSendErrors(sendState *txmgr.SendState, err error, n int) { func processNSendErrors(sendState *txmgr.SendState, err error, n int) {
...@@ -160,3 +165,27 @@ func TestSendStateIsNotWaitingForConfirmationAfterTxUnmined(t *testing.T) { ...@@ -160,3 +165,27 @@ func TestSendStateIsNotWaitingForConfirmationAfterTxUnmined(t *testing.T) {
sendState.TxNotMined(testHash) sendState.TxNotMined(testHash)
require.False(t, sendState.IsWaitingForConfirmation()) require.False(t, sendState.IsWaitingForConfirmation())
} }
func stepClock(step time.Duration) func() time.Time {
i := 0
return func() time.Time {
var start time.Time
i += 1
return start.Add(time.Duration(i) * step)
}
}
// TestSendStateTimeoutAbort ensure that this will abort if it passes the tx pool timeout
// when no successful transactions have been recorded
func TestSendStateTimeoutAbort(t *testing.T) {
sendState := newSendStateWithTimeout(10*time.Millisecond, stepClock(20*time.Millisecond))
require.True(t, sendState.ShouldAbortImmediately(), "Should abort after timing out")
}
// TestSendStateNoTimeoutAbortIfPublishedTx ensure that this will not abort if there is
// a successful transaction send.
func TestSendStateNoTimeoutAbortIfPublishedTx(t *testing.T) {
sendState := newSendStateWithTimeout(10*time.Millisecond, stepClock(20*time.Millisecond))
sendState.ProcessSendError(nil)
require.False(t, sendState.ShouldAbortImmediately(), "Should not abort if published transcation successfully")
}
This diff is collapsed.
...@@ -3,6 +3,7 @@ package txmgr ...@@ -3,6 +3,7 @@ package txmgr
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/big" "math/big"
"sync" "sync"
"testing" "testing"
...@@ -20,6 +21,10 @@ import ( ...@@ -20,6 +21,10 @@ import (
type sendTransactionFunc func(ctx context.Context, tx *types.Transaction) error type sendTransactionFunc func(ctx context.Context, tx *types.Transaction) error
func testSendState() *SendState {
return NewSendState(100, time.Hour)
}
// testHarness houses the necessary resources to test the SimpleTxManager. // testHarness houses the necessary resources to test the SimpleTxManager.
type testHarness struct { type testHarness struct {
cfg Config cfg Config
...@@ -68,6 +73,7 @@ func configWithNumConfs(numConfirmations uint64) Config { ...@@ -68,6 +73,7 @@ func configWithNumConfs(numConfirmations uint64) Config {
ReceiptQueryInterval: 50 * time.Millisecond, ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations, NumConfirmations: numConfirmations,
SafeAbortNonceTooLowCount: 3, SafeAbortNonceTooLowCount: 3,
TxNotInMempoolTimeout: 1 * time.Hour,
Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
return tx, nil return tx, nil
}, },
...@@ -530,7 +536,7 @@ func TestWaitMinedReturnsReceiptOnFirstSuccess(t *testing.T) { ...@@ -530,7 +536,7 @@ func TestWaitMinedReturnsReceiptOnFirstSuccess(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
receipt, err := h.mgr.waitMined(ctx, tx, nil) receipt, err := h.mgr.waitMined(ctx, tx, testSendState())
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
require.Equal(t, receipt.TxHash, txHash) require.Equal(t, receipt.TxHash, txHash)
...@@ -549,7 +555,7 @@ func TestWaitMinedCanBeCanceled(t *testing.T) { ...@@ -549,7 +555,7 @@ func TestWaitMinedCanBeCanceled(t *testing.T) {
// Create an unimined tx. // Create an unimined tx.
tx := types.NewTx(&types.LegacyTx{}) tx := types.NewTx(&types.LegacyTx{})
receipt, err := h.mgr.waitMined(ctx, tx, nil) receipt, err := h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour))
require.Equal(t, err, context.DeadlineExceeded) require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt) require.Nil(t, receipt)
} }
...@@ -570,7 +576,7 @@ func TestWaitMinedMultipleConfs(t *testing.T) { ...@@ -570,7 +576,7 @@ func TestWaitMinedMultipleConfs(t *testing.T) {
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, new(big.Int)) h.backend.mine(&txHash, new(big.Int))
receipt, err := h.mgr.waitMined(ctx, tx, nil) receipt, err := h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour))
require.Equal(t, err, context.DeadlineExceeded) require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt) require.Nil(t, receipt)
...@@ -579,7 +585,7 @@ func TestWaitMinedMultipleConfs(t *testing.T) { ...@@ -579,7 +585,7 @@ func TestWaitMinedMultipleConfs(t *testing.T) {
// Mine an empty block, tx should now be confirmed. // Mine an empty block, tx should now be confirmed.
h.backend.mine(nil, nil) h.backend.mine(nil, nil)
receipt, err = h.mgr.waitMined(ctx, tx, nil) receipt, err = h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour))
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
require.Equal(t, txHash, receipt.TxHash) require.Equal(t, txHash, receipt.TxHash)
...@@ -692,7 +698,7 @@ func TestWaitMinedReturnsReceiptAfterFailure(t *testing.T) { ...@@ -692,7 +698,7 @@ func TestWaitMinedReturnsReceiptAfterFailure(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
receipt, err := mgr.waitMined(ctx, tx, nil) receipt, err := mgr.waitMined(ctx, tx, testSendState())
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
require.Equal(t, receipt.TxHash, txHash) require.Equal(t, receipt.TxHash, txHash)
...@@ -724,8 +730,7 @@ func doGasPriceIncrease(t *testing.T, txTipCap, txFeeCap, newTip, newBaseFee int ...@@ -724,8 +730,7 @@ func doGasPriceIncrease(t *testing.T, txTipCap, txFeeCap, newTip, newBaseFee int
GasTipCap: big.NewInt(txTipCap), GasTipCap: big.NewInt(txTipCap),
GasFeeCap: big.NewInt(txFeeCap), GasFeeCap: big.NewInt(txFeeCap),
}) })
newTx, err := mgr.increaseGasPrice(context.Background(), tx) newTx := mgr.increaseGasPrice(context.Background(), tx)
require.NoError(t, err)
return tx, newTx return tx, newTx
} }
...@@ -831,11 +836,32 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) { ...@@ -831,11 +836,32 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) {
// Run IncreaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop. // Run IncreaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop.
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {
ctx := context.Background() ctx := context.Background()
newTx, err := mgr.increaseGasPrice(ctx, tx) newTx := mgr.increaseGasPrice(ctx, tx)
require.NoError(t, err)
require.True(t, newTx.GasFeeCap().Cmp(feeCap) == 0, "new tx fee cap must be equal L1") require.True(t, newTx.GasFeeCap().Cmp(feeCap) == 0, "new tx fee cap must be equal L1")
require.True(t, newTx.GasTipCap().Cmp(borkedBackend.gasTip) == 0, "new tx tip must be equal L1") require.True(t, newTx.GasTipCap().Cmp(borkedBackend.gasTip) == 0, "new tx tip must be equal L1")
tx = newTx tx = newTx
} }
} }
func TestErrStringMatch(t *testing.T) {
tests := []struct {
err error
target error
match bool
}{
{err: nil, target: nil, match: true},
{err: errors.New("exists"), target: nil, match: false},
{err: nil, target: errors.New("exists"), match: false},
{err: errors.New("exact match"), target: errors.New("exact match"), match: true},
{err: errors.New("partial: match"), target: errors.New("match"), match: true},
}
for i, test := range tests {
i := i
test := test
t.Run(fmt.Sprint(i), func(t *testing.T) {
require.Equal(t, test.match, errStringMatch(test.err, test.target))
})
}
}
...@@ -123,7 +123,6 @@ services: ...@@ -123,7 +123,6 @@ 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 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
......
...@@ -92,8 +92,10 @@ yarn echidna:aliasing ...@@ -92,8 +92,10 @@ yarn echidna:aliasing
#### Configuration #### Configuration
1. Create or modify a file `<network-name>.json` inside of the [`deploy-config`](./deploy-config/) folder. 1. Create or modify a file `<network-name>.ts` inside of the [`deploy-config`](./deploy-config/) folder.
2. Fill out this file according to the `deployConfigSpec` located inside of the [`hardhat.config.ts](./hardhat.config.ts) 2. Fill out this file according to the `deployConfigSpec` located inside of the [`hardhat.config.ts](./hardhat.config.ts)
3. Optionally: Run `npx hardhat generate-deploy-config --network <network-name>` to generate the associated JSON
file. This is required if using `op-chain-ops`.
#### Execution #### Execution
......
{
"finalSystemOwner": "0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A",
"controller": "0x78339d822c23d943e4a2d4c3dd5408f66e6d662d",
"portalGuardian": "0x78339d822c23d943e4a2d4c3dd5408f66e6d662d",
"l1StartingBlockTag": "0x126e52a0cc0ae18948f567ee9443f4a8f0db67c437706e35baee424eb314a0d0",
"l1ChainID": 1,
"l2ChainID": 10,
"l2BlockTime": 2,
"maxSequencerDrift": 600,
"sequencerWindowSize": 3600,
"channelTimeout": 300,
"p2pSequencerAddress": "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
"batchInboxAddress": "0xff00000000000000000000000000000000000010",
"batchSenderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"l2OutputOracleSubmissionInterval": 20,
"l2OutputOracleStartingTimestamp": 1679069195,
"l2OutputOracleStartingBlockNumber": 79149704,
"l2OutputOracleProposer": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"l2OutputOracleChallenger": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"finalizationPeriodSeconds": 2,
"proxyAdminOwner": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"baseFeeVaultRecipient": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"l1FeeVaultRecipient": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"sequencerFeeVaultRecipient": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"governanceTokenName": "Optimism",
"governanceTokenSymbol": "OP",
"governanceTokenOwner": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"l2GenesisBlockGasLimit": "0x17D7840",
"l2GenesisBlockCoinbase": "0x4200000000000000000000000000000000000011",
"l2GenesisBlockBaseFeePerGas": "0x3b9aca00",
"gasPriceOracleOverhead": 2100,
"gasPriceOracleScalar": 1000000,
"eip1559Denominator": 50,
"eip1559Elasticity": 10,
"l2GenesisRegolithTimeOffset": "0x0"
}
\ No newline at end of file
import { DeployConfig } from '../src/deploy-config'
// NOTE: The 'mainnet' network is currently being used for bedrock migration rehearsals.
// The system configured below is not yet live on mainnet, and many of the addresses used are
// unsafe for a production system.
const config: DeployConfig = {
finalSystemOwner: '0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A',
controller: '0x78339d822c23d943e4a2d4c3dd5408f66e6d662d',
portalGuardian: '0x78339d822c23d943e4a2d4c3dd5408f66e6d662d',
l1StartingBlockTag:
'0x126e52a0cc0ae18948f567ee9443f4a8f0db67c437706e35baee424eb314a0d0',
l1ChainID: 1,
l2ChainID: 10,
l2BlockTime: 2,
maxSequencerDrift: 600,
sequencerWindowSize: 3600,
channelTimeout: 300,
p2pSequencerAddress: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65',
batchInboxAddress: '0xff00000000000000000000000000000000000010',
batchSenderAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
l2OutputOracleSubmissionInterval: 20,
l2OutputOracleStartingTimestamp: 1679069195,
l2OutputOracleStartingBlockNumber: 79149704,
l2OutputOracleProposer: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
l2OutputOracleChallenger: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
finalizationPeriodSeconds: 2,
proxyAdminOwner: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
baseFeeVaultRecipient: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
l1FeeVaultRecipient: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
sequencerFeeVaultRecipient: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
governanceTokenName: 'Optimism',
governanceTokenSymbol: 'OP',
governanceTokenOwner: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
l2GenesisBlockGasLimit: '0x17D7840',
l2GenesisBlockCoinbase: '0x4200000000000000000000000000000000000011',
l2GenesisBlockBaseFeePerGas: '0x3b9aca00',
gasPriceOracleOverhead: 2100,
gasPriceOracleScalar: 1000000,
eip1559Denominator: 50,
eip1559Elasticity: 10,
l2GenesisRegolithTimeOffset: '0x0',
}
export default config
...@@ -22,8 +22,10 @@ const config: HardhatUserConfig = { ...@@ -22,8 +22,10 @@ const config: HardhatUserConfig = {
hardhat: { hardhat: {
live: false, live: false,
}, },
// NOTE: The 'mainnet' network is currently being used for mainnet rehearsals.
mainnet: { mainnet: {
url: process.env.L1_RPC || 'http://localhost:8545', url: process.env.L1_RPC || 'https://mainnet-l1-rehearsal.optimism.io',
accounts: [process.env.PRIVATE_KEY_DEPLOYER || ethers.constants.HashZero],
}, },
devnetL1: { devnetL1: {
live: false, live: false,
......
...@@ -129,6 +129,26 @@ interface RequiredDeployConfig { ...@@ -129,6 +129,26 @@ interface RequiredDeployConfig {
* Output finalization period in seconds. * Output finalization period in seconds.
*/ */
finalizationPeriodSeconds: number finalizationPeriodSeconds: number
/**
* Owner of the ProxyAdmin contract.
*/
proxyAdminOwner: string
/**
* L1 address which receives the base fee for the L2 network.
*/
baseFeeVaultRecipient: string
/**
* L1 address which receives data fees for the L2 network.
*/
l1FeeVaultRecipient: string
/**
* L1 address which receives tip fees for the L2 network.
*/
sequencerFeeVaultRecipient: string
} }
/** /**
...@@ -160,6 +180,10 @@ interface OptionalL2DeployConfig { ...@@ -160,6 +180,10 @@ interface OptionalL2DeployConfig {
l2GenesisBlockGasUsed: string l2GenesisBlockGasUsed: string
l2GenesisBlockParentHash: string l2GenesisBlockParentHash: string
l2GenesisBlockBaseFeePerGas: string l2GenesisBlockBaseFeePerGas: string
l2GenesisBlockCoinbase: string
l2GenesisRegolithTimeOffset: string
eip1559Denominator: number
eip1559Elasticity: number
gasPriceOracleOverhead: number gasPriceOracleOverhead: number
gasPriceOracleScalar: number gasPriceOracleScalar: number
} }
...@@ -243,6 +267,18 @@ export const deployConfigSpec: { ...@@ -243,6 +267,18 @@ export const deployConfigSpec: {
type: 'number', type: 'number',
default: 2, default: 2,
}, },
proxyAdminOwner: {
type: 'address',
},
baseFeeVaultRecipient: {
type: 'address',
},
l1FeeVaultRecipient: {
type: 'address',
},
sequencerFeeVaultRecipient: {
type: 'address',
},
cliqueSignerAddress: { cliqueSignerAddress: {
type: 'address', type: 'address',
default: ethers.constants.AddressZero, default: ethers.constants.AddressZero,
......
import fs from 'fs'
import path from 'path'
import { task } from 'hardhat/config'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
task(
'generate-deploy-config',
'generates a json config file for the current network'
).setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
try {
const base = path.join(hre.config.paths.deployConfig, hre.network.name)
if (fs.existsSync(`${base}.ts`)) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const config = require(`${base}.ts`).default
fs.writeFileSync(`${base}.json`, JSON.stringify(config, null, 2), 'utf8')
} else {
throw new Error('not found')
}
} catch (err) {
throw new Error(
`error while loading deploy config for network: ${hre.network.name}, ${err}`
)
}
})
...@@ -10,3 +10,4 @@ import './check-l2' ...@@ -10,3 +10,4 @@ import './check-l2'
import './update-dynamic-oracle-config' import './update-dynamic-oracle-config'
import './wait-for-final-batch' import './wait-for-final-batch'
import './wait-for-final-deposit' import './wait-for-final-deposit'
import './generate-deploy-config'
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { OptimistInviter } from "../../universal/op-nft/OptimistInviter.sol";
/**
* Simple helper contract that helps with testing flow and signature for OptimistInviter contract.
* Made this a separate contract instead of including in OptimistInviter.t.sol for reusability.
*/
contract OptimistInviterHelper {
/**
* @notice EIP712 typehash for the ClaimableInvite type.
*/
bytes32 public constant CLAIMABLE_INVITE_TYPEHASH =
keccak256("ClaimableInvite(address issuer,bytes32 nonce)");
/**
* @notice EIP712 typehash for the EIP712Domain type that is included as part of the signature.
*/
bytes32 public constant EIP712_DOMAIN_TYPEHASH =
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
/**
* @notice Address of OptimistInviter contract we are testing.
*/
OptimistInviter public optimistInviter;
/**
* @notice OptimistInviter contract name. Used to construct the EIP-712 domain.
*/
string public name;
/**
* @notice Keeps track of current nonce to generate new nonces for each invite.
*/
uint256 public currentNonce;
constructor(OptimistInviter _optimistInviter, string memory _name) {
optimistInviter = _optimistInviter;
name = _name;
}
/**
* @notice Returns the hash of the struct ClaimableInvite.
*
* @param _claimableInvite ClaimableInvite struct to hash.
*
* @return EIP-712 typed struct hash.
*/
function getClaimableInviteStructHash(OptimistInviter.ClaimableInvite memory _claimableInvite)
public
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
CLAIMABLE_INVITE_TYPEHASH,
_claimableInvite.issuer,
_claimableInvite.nonce
)
);
}
/**
* @notice Returns a bytes32 nonce that should change everytime. In practice, people should use
* pseudorandom nonces.
*
* @return Nonce that should be used as part of ClaimableInvite.
*/
function consumeNonce() public returns (bytes32) {
return bytes32(keccak256(abi.encode(currentNonce++)));
}
/**
* @notice Returns a ClaimableInvite with the issuer and current nonce.
*
* @param _issuer Issuer to include in the ClaimableInvite.
*
* @return ClaimableInvite that can be hashed & signed.
*/
function getClaimableInviteWithNewNonce(address _issuer)
public
returns (OptimistInviter.ClaimableInvite memory)
{
return OptimistInviter.ClaimableInvite(_issuer, consumeNonce());
}
/**
* @notice Computes the EIP712 digest with default correct parameters.
*
* @param _claimableInvite ClaimableInvite struct to hash.
*
* @return EIP-712 compatible digest.
*/
function getDigest(OptimistInviter.ClaimableInvite calldata _claimableInvite)
public
view
returns (bytes32)
{
return
getDigestWithEIP712Domain(
_claimableInvite,
bytes(name),
bytes(optimistInviter.EIP712_VERSION()),
block.chainid,
address(optimistInviter)
);
}
/**
* @notice Computes the EIP712 digest with the given domain parameters.
* Used for testing that different domain parameters fail.
*
* @param _claimableInvite ClaimableInvite struct to hash.
* @param _name Contract name to use in the EIP712 domain.
* @param _version Contract version to use in the EIP712 domain.
* @param _chainid Chain ID to use in the EIP712 domain.
* @param _verifyingContract Address to use in the EIP712 domain.
*
* @return EIP-712 compatible digest.
*/
function getDigestWithEIP712Domain(
OptimistInviter.ClaimableInvite calldata _claimableInvite,
bytes memory _name,
bytes memory _version,
uint256 _chainid,
address _verifyingContract
) public pure returns (bytes32) {
bytes32 domainSeparator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(_name),
keccak256(_version),
_chainid,
_verifyingContract
)
);
return
ECDSA.toTypedDataHash(domainSeparator, getClaimableInviteStructHash(_claimableInvite));
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
// solhint-disable max-line-length
/**
* Simple ERC1271 wallet that can be used to test the ERC1271 signature checker.
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/mocks/ERC1271WalletMock.sol
*/
contract TestERC1271Wallet is Ownable, IERC1271 {
constructor(address originalOwner) {
transferOwnership(originalOwner);
}
function isValidSignature(bytes32 hash, bytes memory signature)
public
view
override
returns (bytes4 magicValue)
{
return
ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/**
* @title OptimistConstants
* @notice Library for storing Optimist related constants that are shared in multiple contracts.
*/
library OptimistConstants {
/**
* @notice Attestation key issued by OptimistInviter allowing the attested account to mint.
*/
bytes32 internal constant OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY =
bytes32("optimist.can-mint-from-invite");
}
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