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

Merge branch 'develop' into clabby/migration/min-gas-fix

parents d1d02ed4 931e1de0
...@@ -1504,7 +1504,7 @@ workflows: ...@@ -1504,7 +1504,7 @@ workflows:
type: approval type: approval
filters: filters:
tags: tags:
only: /^op-[a-z0-9\-]*\/v.*/ only: /^(proxyd|op-[a-z0-9\-]*)\/v.*/
branches: branches:
ignore: /.*/ ignore: /.*/
- docker-release: - docker-release:
...@@ -1586,7 +1586,7 @@ workflows: ...@@ -1586,7 +1586,7 @@ workflows:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
- hold - hold
- docker-build: - docker-release:
name: proxyd-docker-release name: proxyd-docker-release
filters: filters:
tags: tags:
......
...@@ -13,13 +13,13 @@ ...@@ -13,13 +13,13 @@
/packages/core-utils @ethereum-optimism/legacy-reviewers /packages/core-utils @ethereum-optimism/legacy-reviewers
/packages/data-transport-layer @ethereum-optimism/legacy-reviewers /packages/data-transport-layer @ethereum-optimism/legacy-reviewers
/packages/chain-mon @smartcontracts /packages/chain-mon @smartcontracts
/packages/fault-detector @ethereum-optimism/legacy-reviewers /packages/fault-detector @ethereum-optimism/devxpod
/packages/hardhat-deploy-config @ethereum-optimism/legacy-reviewers /packages/hardhat-deploy-config @ethereum-optimism/legacy-reviewers
/packages/message-relayer @ethereum-optimism/legacy-reviewers /packages/message-relayer @ethereum-optimism/legacy-reviewers
/packages/migration-data @ethereum-optimism/legacy-reviewers /packages/migration-data @ethereum-optimism/legacy-reviewers
/packages/replica-healthcheck @ethereum-optimism/legacy-reviewers /packages/replica-healthcheck @ethereum-optimism/legacy-reviewers
/packages/sdk @ethereum-optimism/ecopod /packages/sdk @ethereum-optimism/devxpod
/packages/atst @ethereum-optimism/ecopod /packages/atst @ethereum-optimism/devxpod
# Bedrock codebases # Bedrock codebases
/bedrock-devnet @ethereum-optimism/go-reviewers /bedrock-devnet @ethereum-optimism/go-reviewers
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
# Misc # Misc
/proxyd @ethereum-optimism/infra-reviewers /proxyd @ethereum-optimism/infra-reviewers
/indexer @ethereum-optimism/infra-reviewers /indexer @ethereum-optimism/devxpod
/infra @ethereum-optimism/infra-reviewers /infra @ethereum-optimism/infra-reviewers
/specs @ethereum-optimism/contract-reviewers @ethereum-optimism/go-reviewers /specs @ethereum-optimism/contract-reviewers @ethereum-optimism/go-reviewers
/endpoint-monitor @ethereum-optimism/infra-reviewers /endpoint-monitor @ethereum-optimism/infra-reviewers
This diff is collapsed.
...@@ -13,7 +13,7 @@ const WETH9StorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"con ...@@ -13,7 +13,7 @@ const WETH9StorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"con
var WETH9StorageLayout = new(solc.StorageLayout) var WETH9StorageLayout = new(solc.StorageLayout)
var WETH9DeployedBin = "0x6080604052600436106100bc5760003560e01c8063313ce56711610074578063a9059cbb1161004e578063a9059cbb146102cb578063d0e30db0146100bc578063dd62ed3e14610311576100bc565b8063313ce5671461024b57806370a082311461027657806395d89b41146102b6576100bc565b806318160ddd116100a557806318160ddd146101aa57806323b872dd146101d15780632e1a7d4d14610221576100bc565b806306fdde03146100c6578063095ea7b314610150575b6100c4610359565b005b3480156100d257600080fd5b506100db6103a8565b6040805160208082528351818301528351919283929083019185019080838360005b838110156101155781810151838201526020016100fd565b50505050905090810190601f1680156101425780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015c57600080fd5b506101966004803603604081101561017357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610454565b604080519115158252519081900360200190f35b3480156101b657600080fd5b506101bf6104c7565b60408051918252519081900360200190f35b3480156101dd57600080fd5b50610196600480360360608110156101f457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356104cb565b34801561022d57600080fd5b506100c46004803603602081101561024457600080fd5b503561066b565b34801561025757600080fd5b50610260610700565b6040805160ff9092168252519081900360200190f35b34801561028257600080fd5b506101bf6004803603602081101561029957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610709565b3480156102c257600080fd5b506100db61071b565b3480156102d757600080fd5b50610196600480360360408110156102ee57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610793565b34801561031d57600080fd5b506101bf6004803603604081101561033457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160200135166107a7565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b6000805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b820191906000526020600020905b81548152906001019060200180831161042f57829003601f168201915b505050505081565b33600081815260046020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600360205260408120548211156104fd57600080fd5b73ffffffffffffffffffffffffffffffffffffffff84163314801590610573575073ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14155b156105ed5773ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020548211156105b557600080fd5b73ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020805483900390555b73ffffffffffffffffffffffffffffffffffffffff808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b3360009081526003602052604090205481111561068757600080fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156106c6573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b60006107a03384846104cb565b9392505050565b60046020908152600092835260408084209091529082529020548156fea265627a7a72315820fd69d075d01838a66174ca9cefc98bf8f255e049a94475b253ffc70bf383f90564736f6c63430005110032" var WETH9DeployedBin = "0x6080604052600436106100bc5760003560e01c8063313ce56711610074578063a9059cbb1161004e578063a9059cbb146102cb578063d0e30db0146100bc578063dd62ed3e14610311576100bc565b8063313ce5671461024b57806370a082311461027657806395d89b41146102b6576100bc565b806318160ddd116100a557806318160ddd146101aa57806323b872dd146101d15780632e1a7d4d14610221576100bc565b806306fdde03146100c6578063095ea7b314610150575b6100c4610359565b005b3480156100d257600080fd5b506100db6103a8565b6040805160208082528351818301528351919283929083019185019080838360005b838110156101155781810151838201526020016100fd565b50505050905090810190601f1680156101425780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015c57600080fd5b506101966004803603604081101561017357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610454565b604080519115158252519081900360200190f35b3480156101b657600080fd5b506101bf6104c7565b60408051918252519081900360200190f35b3480156101dd57600080fd5b50610196600480360360608110156101f457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356104cb565b34801561022d57600080fd5b506100c46004803603602081101561024457600080fd5b503561066b565b34801561025757600080fd5b50610260610700565b6040805160ff9092168252519081900360200190f35b34801561028257600080fd5b506101bf6004803603602081101561029957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610709565b3480156102c257600080fd5b506100db61071b565b3480156102d757600080fd5b50610196600480360360408110156102ee57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610793565b34801561031d57600080fd5b506101bf6004803603604081101561033457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160200135166107a7565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b6000805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b820191906000526020600020905b81548152906001019060200180831161042f57829003601f168201915b505050505081565b33600081815260046020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600360205260408120548211156104fd57600080fd5b73ffffffffffffffffffffffffffffffffffffffff84163314801590610573575073ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14155b156105ed5773ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020548211156105b557600080fd5b73ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020805483900390555b73ffffffffffffffffffffffffffffffffffffffff808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b3360009081526003602052604090205481111561068757600080fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156106c6573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b60006107a03384846104cb565b9392505050565b60046020908152600092835260408084209091529082529020548156fea265627a7a72315820e496abb80c5983b030f680d0bd88f66bf44e261bc3be070d612dd72f9f1f5e9a64736f6c63430005110032"
func init() { func init() {
if err := json.Unmarshal([]byte(WETH9StorageLayoutJSON), WETH9StorageLayout); err != nil { if err := json.Unmarshal([]byte(WETH9StorageLayoutJSON), WETH9StorageLayout); err != nil {
......
...@@ -103,6 +103,8 @@ type Metrics struct { ...@@ -103,6 +103,8 @@ type Metrics struct {
SequencerInconsistentL1Origin *EventMetrics SequencerInconsistentL1Origin *EventMetrics
SequencerResets *EventMetrics SequencerResets *EventMetrics
L1RequestDurationSeconds *prometheus.HistogramVec
SequencerBuildingDiffDurationSeconds prometheus.Histogram SequencerBuildingDiffDurationSeconds prometheus.Histogram
SequencerBuildingDiffTotal prometheus.Counter SequencerBuildingDiffTotal prometheus.Counter
...@@ -378,6 +380,14 @@ func NewMetrics(procName string) *Metrics { ...@@ -378,6 +380,14 @@ func NewMetrics(procName string) *Metrics {
Help: "number of unverified execution payloads buffered in quarantine", Help: "number of unverified execution payloads buffered in quarantine",
}), }),
L1RequestDurationSeconds: factory.NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Name: "l1_request_seconds",
Buckets: []float64{
.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
Help: "Histogram of L1 request time",
}, []string{"request"}),
SequencerBuildingDiffDurationSeconds: factory.NewHistogram(prometheus.HistogramOpts{ SequencerBuildingDiffDurationSeconds: factory.NewHistogram(prometheus.HistogramOpts{
Namespace: ns, Namespace: ns,
Name: "sequencer_building_diff_seconds", Name: "sequencer_building_diff_seconds",
...@@ -589,6 +599,11 @@ func (m *Metrics) RecordBandwidth(ctx context.Context, bwc *libp2pmetrics.Bandwi ...@@ -589,6 +599,11 @@ func (m *Metrics) RecordBandwidth(ctx context.Context, bwc *libp2pmetrics.Bandwi
} }
} }
// RecordL1RequestTime tracks the amount of time the derivation pipeline spent waiting for L1 data requests.
func (m *Metrics) RecordL1RequestTime(method string, duration time.Duration) {
m.L1RequestDurationSeconds.WithLabelValues(method).Observe(float64(duration) / float64(time.Second))
}
// RecordSequencerBuildingDiffTime tracks the amount of time the sequencer was allowed between // RecordSequencerBuildingDiffTime tracks the amount of time the sequencer was allowed between
// start to finish, incl. sealing, minus the block time. // start to finish, incl. sealing, minus the block time.
// Ideally this is 0, realistically the sequencer scheduler may be busy with other jobs like syncing sometimes. // Ideally this is 0, realistically the sequencer scheduler may be busy with other jobs like syncing sometimes.
......
...@@ -378,12 +378,21 @@ func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) ...@@ -378,12 +378,21 @@ func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef)
return n.rpcSync.RequestL2Range(ctx, start, end) return n.rpcSync.RequestL2Range(ctx, start, end)
} }
if n.p2pNode != nil && n.p2pNode.AltSyncEnabled() { if n.p2pNode != nil && n.p2pNode.AltSyncEnabled() {
if unixTimeStale(start.Time, 12*time.Hour) {
n.log.Debug("ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, "end", end, "start_time", start.Time)
return nil
}
return n.p2pNode.RequestL2Range(ctx, start, end) return n.p2pNode.RequestL2Range(ctx, start, end)
} }
n.log.Debug("ignoring request to sync L2 range, no sync method available", "start", start, "end", end) n.log.Debug("ignoring request to sync L2 range, no sync method available", "start", start, "end", end)
return nil return nil
} }
// unixTimeStale returns true if the unix timestamp is before the current time minus the supplied duration.
func unixTimeStale(timestamp uint64, duration time.Duration) bool {
return time.Unix(int64(timestamp), 0).Before(time.Now().Add(-1 * duration))
}
func (n *OpNode) P2P() p2p.Node { func (n *OpNode) P2P() p2p.Node {
return n.p2pNode return n.p2pNode
} }
......
package node
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestUnixTimeStale(t *testing.T) {
require.True(t, unixTimeStale(1_600_000_000, 1*time.Hour))
require.False(t, unixTimeStale(uint64(time.Now().Unix()), 1*time.Hour))
}
...@@ -30,6 +30,7 @@ type Metrics interface { ...@@ -30,6 +30,7 @@ type Metrics interface {
RecordL1ReorgDepth(d uint64) RecordL1ReorgDepth(d uint64)
EngineMetrics EngineMetrics
L1FetcherMetrics
SequencerMetrics SequencerMetrics
} }
...@@ -103,6 +104,7 @@ type AltSync interface { ...@@ -103,6 +104,7 @@ type AltSync interface {
// 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, altSync AltSync, 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 {
l1 = NewMeteredL1Fetcher(l1, metrics)
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)
......
package driver
import (
"context"
"time"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
type L1FetcherMetrics interface {
RecordL1RequestTime(method string, duration time.Duration)
}
type MeteredL1Fetcher struct {
inner derive.L1Fetcher
metrics L1FetcherMetrics
now func() time.Time
}
func NewMeteredL1Fetcher(inner derive.L1Fetcher, metrics L1FetcherMetrics) *MeteredL1Fetcher {
return &MeteredL1Fetcher{
inner: inner,
metrics: metrics,
now: time.Now,
}
}
func (m *MeteredL1Fetcher) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error) {
defer m.recordTime("L1BlockRefByLabel")()
return m.inner.L1BlockRefByLabel(ctx, label)
}
func (m *MeteredL1Fetcher) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) {
defer m.recordTime("L1BlockRefByNumber")()
return m.inner.L1BlockRefByNumber(ctx, num)
}
func (m *MeteredL1Fetcher) L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L1BlockRef, error) {
defer m.recordTime("L1BlockRefByHash")()
return m.inner.L1BlockRefByHash(ctx, hash)
}
func (m *MeteredL1Fetcher) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error) {
defer m.recordTime("InfoByHash")()
return m.inner.InfoByHash(ctx, hash)
}
func (m *MeteredL1Fetcher) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
defer m.recordTime("InfoAndTxsByHash")()
return m.inner.InfoAndTxsByHash(ctx, hash)
}
func (m *MeteredL1Fetcher) FetchReceipts(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Receipts, error) {
defer m.recordTime("FetchReceipts")()
return m.inner.FetchReceipts(ctx, blockHash)
}
var _ derive.L1Fetcher = (*MeteredL1Fetcher)(nil)
func (m *MeteredL1Fetcher) recordTime(method string) func() {
start := m.now()
return func() {
end := m.now()
m.metrics.RecordL1RequestTime(method, end.Sub(start))
}
}
package driver
import (
"context"
"errors"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestDurationRecorded(t *testing.T) {
num := uint64(1234)
hash := common.Hash{0xaa}
ref := eth.L1BlockRef{Number: num}
info := &testutils.MockBlockInfo{}
expectedErr := errors.New("test error")
tests := []struct {
method string
expect func(inner *testutils.MockL1Source)
call func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source)
}{
{
method: "L1BlockRefByLabel",
call: func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source) {
inner.ExpectL1BlockRefByLabel(eth.Finalized, ref, expectedErr)
result, err := fetcher.L1BlockRefByLabel(context.Background(), eth.Finalized)
require.Equal(t, ref, result)
require.Equal(t, expectedErr, err)
},
},
{
method: "L1BlockRefByNumber",
call: func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source) {
inner.ExpectL1BlockRefByNumber(num, ref, expectedErr)
result, err := fetcher.L1BlockRefByNumber(context.Background(), num)
require.Equal(t, ref, result)
require.Equal(t, expectedErr, err)
},
},
{
method: "L1BlockRefByHash",
call: func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source) {
inner.ExpectL1BlockRefByHash(hash, ref, expectedErr)
result, err := fetcher.L1BlockRefByHash(context.Background(), hash)
require.Equal(t, ref, result)
require.Equal(t, expectedErr, err)
},
},
{
method: "InfoByHash",
call: func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source) {
inner.ExpectInfoByHash(hash, info, expectedErr)
result, err := fetcher.InfoByHash(context.Background(), hash)
require.Equal(t, info, result)
require.Equal(t, expectedErr, err)
},
},
{
method: "InfoAndTxsByHash",
call: func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source) {
txs := types.Transactions{
&types.Transaction{},
}
inner.ExpectInfoAndTxsByHash(hash, info, txs, expectedErr)
actualInfo, actualTxs, err := fetcher.InfoAndTxsByHash(context.Background(), hash)
require.Equal(t, info, actualInfo)
require.Equal(t, txs, actualTxs)
require.Equal(t, expectedErr, err)
},
},
{
method: "FetchReceipts",
call: func(t *testing.T, fetcher *MeteredL1Fetcher, inner *testutils.MockL1Source) {
rcpts := types.Receipts{
&types.Receipt{},
}
inner.ExpectFetchReceipts(hash, info, rcpts, expectedErr)
actualInfo, actualRcpts, err := fetcher.FetchReceipts(context.Background(), hash)
require.Equal(t, info, actualInfo)
require.Equal(t, rcpts, actualRcpts)
require.Equal(t, expectedErr, err)
},
},
}
for _, test := range tests {
test := test
t.Run(test.method, func(t *testing.T) {
duration := 200 * time.Millisecond
fetcher, inner, metrics := createFetcher(duration)
defer inner.AssertExpectations(t)
defer metrics.AssertExpectations(t)
metrics.ExpectRecordRequestTime(test.method, duration)
test.call(t, fetcher, inner)
})
}
}
// createFetcher creates a MeteredL1Fetcher with a mock inner.
// The clock used to calculate the current time will advance by clockIncrement on each call, making it appear as if
// each request takes that amount of time to execute.
func createFetcher(clockIncrement time.Duration) (*MeteredL1Fetcher, *testutils.MockL1Source, *mockMetrics) {
inner := &testutils.MockL1Source{}
currTime := time.UnixMilli(1294812934000000)
clock := func() time.Time {
currTime = currTime.Add(clockIncrement)
return currTime
}
metrics := &mockMetrics{}
fetcher := MeteredL1Fetcher{
inner: inner,
metrics: metrics,
now: clock,
}
return &fetcher, inner, metrics
}
type mockMetrics struct {
mock.Mock
}
func (m *mockMetrics) RecordL1RequestTime(method string, duration time.Duration) {
m.MethodCalled("RecordL1RequestTime", method, duration)
}
func (m *mockMetrics) ExpectRecordRequestTime(method string, duration time.Duration) {
m.On("RecordL1RequestTime", method, duration).Once()
}
...@@ -6,7 +6,7 @@ DOCKER_REPO=$1 ...@@ -6,7 +6,7 @@ DOCKER_REPO=$1
GIT_TAG=$2 GIT_TAG=$2
GIT_SHA=$3 GIT_SHA=$3
IMAGE_NAME=$(echo "$GIT_TAG" | grep -Eow '^op-[a-z0-9\-]*' || true) IMAGE_NAME=$(echo "$GIT_TAG" | grep -Eow '^(proxyd|op-[a-z0-9\-]*)' || true)
if [ -z "$IMAGE_NAME" ]; then if [ -z "$IMAGE_NAME" ]; then
echo "image name could not be parsed from git tag '$GIT_TAG'" echo "image name could not be parsed from git tag '$GIT_TAG'"
exit 1 exit 1
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"@eth-optimism/contracts-bedrock/ds-test", "@eth-optimism/contracts-bedrock/ds-test",
"@eth-optimism/contracts-bedrock/forge-std", "@eth-optimism/contracts-bedrock/forge-std",
"@eth-optimism/contracts-bedrock/@rari-capital/solmate", "@eth-optimism/contracts-bedrock/@rari-capital/solmate",
"@eth-optimism/contracts-bedrock/clones-with-immutable-args",
"**/@openzeppelin/*", "**/@openzeppelin/*",
"@eth-optimism/contracts-periphery/ds-test", "@eth-optimism/contracts-periphery/ds-test",
"@eth-optimism/contracts-periphery/forge-std", "@eth-optimism/contracts-periphery/forge-std",
......
...@@ -27,6 +27,11 @@ CrossDomainOwnable_Test:test_onlyOwner_notOwner_reverts() (gas: 10597) ...@@ -27,6 +27,11 @@ CrossDomainOwnable_Test:test_onlyOwner_notOwner_reverts() (gas: 10597)
CrossDomainOwnable_Test:test_onlyOwner_succeeds() (gas: 34883) CrossDomainOwnable_Test:test_onlyOwner_succeeds() (gas: 34883)
DeployerWhitelist_Test:test_owner_succeeds() (gas: 7582) DeployerWhitelist_Test:test_owner_succeeds() (gas: 7582)
DeployerWhitelist_Test:test_storageSlots_succeeds() (gas: 33395) DeployerWhitelist_Test:test_storageSlots_succeeds() (gas: 33395)
DisputeGameFactory_Test:test_owner_succeeds() (gas: 7582)
DisputeGameFactory_Test:test_setImplementation_notOwner_reverts() (gas: 11191)
DisputeGameFactory_Test:test_setImplementation_succeeds() (gas: 32635)
DisputeGameFactory_Test:test_transferOwnership_notOwner_reverts() (gas: 10979)
DisputeGameFactory_Test:test_transferOwnership_succeeds() (gas: 13180)
FeeVault_Test:test_constructor_succeeds() (gas: 10736) FeeVault_Test:test_constructor_succeeds() (gas: 10736)
FeeVault_Test:test_minWithdrawalAmount_succeeds() (gas: 10713) FeeVault_Test:test_minWithdrawalAmount_succeeds() (gas: 10713)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352135) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352135)
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { GameType } from "../libraries/DisputeTypes.sol";
import { GameStatus } from "../libraries/DisputeTypes.sol";
import { SafeCall } from "../libraries/SafeCall.sol";
import { IDisputeGame } from "./IDisputeGame.sol";
import { IDisputeGameFactory } from "./IDisputeGameFactory.sol";
/**
* @title BondManager
* @notice The Bond Manager serves as an escrow for permissionless output proposal bonds.
*/
contract BondManager {
// The Bond Type
struct Bond {
address owner;
uint256 expiration;
bytes32 id;
uint256 amount;
}
/**
* @notice Mapping from bondId to bond.
*/
mapping(bytes32 => Bond) public bonds;
/**
* @notice BondPosted is emitted when a bond is posted.
* @param bondId is the id of the bond.
* @param owner is the address that owns the bond.
* @param expiration is the time at which the bond expires.
* @param amount is the amount of the bond.
*/
event BondPosted(bytes32 bondId, address owner, uint256 expiration, uint256 amount);
/**
* @notice BondSeized is emitted when a bond is seized.
* @param bondId is the id of the bond.
* @param owner is the address that owns the bond.
* @param seizer is the address that seized the bond.
* @param amount is the amount of the bond.
*/
event BondSeized(bytes32 bondId, address owner, address seizer, uint256 amount);
/**
* @notice BondReclaimed is emitted when a bond is reclaimed by the owner.
* @param bondId is the id of the bond.
* @param claiment is the address that reclaimed the bond.
* @param amount is the amount of the bond.
*/
event BondReclaimed(bytes32 bondId, address claiment, uint256 amount);
/**
* @notice The permissioned dispute game factory.
* @dev Used to verify the status of bonds.
*/
IDisputeGameFactory public immutable DISPUTE_GAME_FACTORY;
/**
* @notice Instantiates the bond maanger with the registered dispute game factory.
* @param _disputeGameFactory is the dispute game factory.
*/
constructor(IDisputeGameFactory _disputeGameFactory) {
DISPUTE_GAME_FACTORY = _disputeGameFactory;
}
/**
* @notice Post a bond with a given id and owner.
* @dev This function will revert if the provided bondId is already in use.
* @param _bondId is the id of the bond.
* @param _bondOwner is the address that owns the bond.
* @param _minClaimHold is the minimum amount of time the owner
* must wait before reclaiming their bond.
*/
function post(
bytes32 _bondId,
address _bondOwner,
uint256 _minClaimHold
) external payable {
require(bonds[_bondId].owner == address(0), "BondManager: BondId already posted.");
require(_bondOwner != address(0), "BondManager: Owner cannot be the zero address.");
require(msg.value > 0, "BondManager: Value must be non-zero.");
uint256 expiration = _minClaimHold + block.timestamp;
bonds[_bondId] = Bond({
owner: _bondOwner,
expiration: expiration,
id: _bondId,
amount: msg.value
});
emit BondPosted(_bondId, _bondOwner, expiration, msg.value);
}
/**
* @notice Seizes the bond with the given id.
* @dev This function will revert if there is no bond at the given id.
* @param _bondId is the id of the bond.
*/
function seize(bytes32 _bondId) external {
Bond memory b = bonds[_bondId];
require(b.owner != address(0), "BondManager: The bond does not exist.");
require(b.expiration >= block.timestamp, "BondManager: Bond expired.");
IDisputeGame caller = IDisputeGame(msg.sender);
IDisputeGame game = DISPUTE_GAME_FACTORY.games(
GameType.ATTESTATION,
caller.rootClaim(),
caller.extraData()
);
require(msg.sender == address(game), "BondManager: Unauthorized seizure.");
require(game.status() == GameStatus.CHALLENGER_WINS, "BondManager: Game incomplete.");
delete bonds[_bondId];
emit BondSeized(_bondId, b.owner, msg.sender, b.amount);
bool success = SafeCall.send(payable(msg.sender), gasleft(), b.amount);
require(success, "BondManager: Failed to send Ether.");
}
/**
* @notice Seizes the bond with the given id and distributes it to recipients.
* @dev This function will revert if there is no bond at the given id.
* @param _bondId is the id of the bond.
* @param _claimRecipients is a set of addresses to split the bond amongst.
*/
function seizeAndSplit(bytes32 _bondId, address[] calldata _claimRecipients) external {
Bond memory b = bonds[_bondId];
require(b.owner != address(0), "BondManager: The bond does not exist.");
require(b.expiration >= block.timestamp, "BondManager: Bond expired.");
IDisputeGame caller = IDisputeGame(msg.sender);
IDisputeGame game = DISPUTE_GAME_FACTORY.games(
GameType.ATTESTATION,
caller.rootClaim(),
caller.extraData()
);
require(msg.sender == address(game), "BondManager: Unauthorized seizure.");
require(game.status() == GameStatus.CHALLENGER_WINS, "BondManager: Game incomplete.");
delete bonds[_bondId];
emit BondSeized(_bondId, b.owner, msg.sender, b.amount);
uint256 len = _claimRecipients.length;
uint256 proportionalAmount = b.amount / len;
for (uint256 i = 0; i < len; i++) {
bool success = SafeCall.send(
payable(_claimRecipients[i]),
gasleft() / len,
proportionalAmount
);
require(success, "BondManager: Failed to send Ether.");
}
}
/**
* @notice Reclaims the bond of the bond owner.
* @dev This function will revert if there is no bond at the given id.
* @param _bondId is the id of the bond.
*/
function reclaim(bytes32 _bondId) external {
Bond memory b = bonds[_bondId];
require(b.owner == msg.sender, "BondManager: Unauthorized claimant.");
require(b.expiration <= block.timestamp, "BondManager: Bond isn't claimable yet.");
delete bonds[_bondId];
emit BondReclaimed(_bondId, msg.sender, b.amount);
bool success = SafeCall.send(payable(msg.sender), gasleft(), b.amount);
require(success, "BondManager: Failed to send Ether.");
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ClonesWithImmutableArgs } from "@cwia/ClonesWithImmutableArgs.sol";
import { Claim } from "../libraries/DisputeTypes.sol";
import { Hash } from "../libraries/DisputeTypes.sol";
import { GameType } from "../libraries/DisputeTypes.sol";
import { NoImplementation } from "../libraries/DisputeErrors.sol";
import { GameAlreadyExists } from "../libraries/DisputeErrors.sol";
import { IDisputeGame } from "./IDisputeGame.sol";
import { IDisputeGameFactory } from "./IDisputeGameFactory.sol";
/**
* @title DisputeGameFactory
* @notice A factory contract for creating `IDisputeGame` contracts.
*/
contract DisputeGameFactory is Ownable, IDisputeGameFactory {
/**
* @dev Allows for the creation of clone proxies with immutable arguments.
*/
using ClonesWithImmutableArgs for address;
/**
* @notice Mapping of `GameType`s to their respective `IDisputeGame` implementations.
*/
mapping(GameType => IDisputeGame) public gameImpls;
/**
* @notice Mapping of a hash of `gameType . rootClaim . extraData` to
* the deployed `IDisputeGame` clone.
* @dev Note: `.` denotes concatenation.
*/
mapping(Hash => IDisputeGame) internal disputeGames;
/**
* @notice Constructs a new DisputeGameFactory contract.
* @param _owner The owner of the contract.
*/
constructor(address _owner) Ownable() {
transferOwnership(_owner);
}
/**
* @notice Retrieves the hash of `gameType . rootClaim . extraData`
* to the deployed `DisputeGame` clone.
* @dev Note: `.` denotes concatenation.
* @param gameType The type of the DisputeGame.
* Used to decide the implementation to clone.
* @param rootClaim The root claim of the DisputeGame.
* @param extraData Any extra data that should be provided to the
* created dispute game.
* @return _proxy The clone of the `DisputeGame` created with the
* given parameters. `address(0)` if nonexistent.
*/
function games(
GameType gameType,
Claim rootClaim,
bytes calldata extraData
) external view returns (IDisputeGame _proxy) {
return disputeGames[getGameUUID(gameType, rootClaim, extraData)];
}
/**
* @notice Creates a new DisputeGame proxy contract.
* @notice If a dispute game with the given parameters already exists,
* it will be returned.
* @param gameType The type of the DisputeGame.
* Used to decide the proxy implementation.
* @param rootClaim The root claim of the DisputeGame.
* @param extraData Any extra data that should be provided
* to the created dispute game.
* @return proxy The clone of the `DisputeGame`.
*/
function create(
GameType gameType,
Claim rootClaim,
bytes calldata extraData
) external returns (IDisputeGame proxy) {
// Grab the implementation contract for the given `GameType`.
IDisputeGame impl = gameImpls[gameType];
// If there is no implementation to clone for the given `GameType`, revert.
if (address(impl) == address(0)) {
revert NoImplementation(gameType);
}
// Clone the implementation contract and initialize it with the given parameters.
bytes memory data = abi.encodePacked(rootClaim, extraData);
proxy = IDisputeGame(address(impl).clone(data));
proxy.initialize();
// Compute the unique identifier for the dispute game.
Hash uuid = getGameUUID(gameType, rootClaim, extraData);
// If a dispute game with the same UUID already exists, revert.
if (address(disputeGames[uuid]) != address(0)) {
revert GameAlreadyExists(uuid);
}
// Store the dispute game in the mapping & emit the `DisputeGameCreated` event.
disputeGames[uuid] = proxy;
emit DisputeGameCreated(address(proxy), gameType, rootClaim);
}
/**
* @notice Sets the implementation contract for a specific `GameType`.
* @param gameType The type of the DisputeGame.
* @param impl The implementation contract for the given `GameType`.
*/
function setImplementation(GameType gameType, IDisputeGame impl) external onlyOwner {
gameImpls[gameType] = impl;
}
/**
* @notice Returns a unique identifier for the given dispute game parameters.
* @dev Hashes the concatenation of `gameType . rootClaim . extraData`
* without expanding memory.
* @param gameType The type of the DisputeGame.
* @param rootClaim The root claim of the DisputeGame.
* @param extraData Any extra data that should be provided to the created dispute game.
* @return _uuid The unique identifier for the given dispute game parameters.
*/
function getGameUUID(
GameType gameType,
Claim rootClaim,
bytes memory extraData
) public pure returns (Hash _uuid) {
assembly {
// Grab the offsets of the other memory locations we will need to temporarily overwrite.
let gameTypeOffset := sub(extraData, 0x60)
let rootClaimOffset := add(gameTypeOffset, 0x20)
let pointerOffset := add(rootClaimOffset, 0x20)
// Copy the memory that we will temporarily overwrite onto the stack
// so we can restore it later
let tempA := mload(gameTypeOffset)
let tempB := mload(rootClaimOffset)
let tempC := mload(pointerOffset)
// Overwrite the memory with the data we want to hash
mstore(gameTypeOffset, gameType)
mstore(rootClaimOffset, rootClaim)
mstore(pointerOffset, 0x60)
// Compute the length of the memory to hash
// `0x60 + 0x20 + extraData.length` rounded to the *next* multiple of 32.
let hashLen := and(add(mload(extraData), 0x9F), not(0x1F))
// Hash the memory to produce the UUID digest
_uuid := keccak256(gameTypeOffset, hashLen)
// Restore the memory prior to `extraData`
mstore(gameTypeOffset, tempA)
mstore(rootClaimOffset, tempB)
mstore(pointerOffset, tempC)
}
}
}
/// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.15; pragma solidity ^0.8.15;
/** /**
...@@ -9,36 +9,36 @@ interface IBondManager { ...@@ -9,36 +9,36 @@ interface IBondManager {
/** /**
* @notice Post a bond with a given id and owner. * @notice Post a bond with a given id and owner.
* @dev This function will revert if the provided bondId is already in use. * @dev This function will revert if the provided bondId is already in use.
* @param bondId is the id of the bond. * @param _bondId is the id of the bond.
* @param owner is the address that owns the bond. * @param _bondOwner is the address that owns the bond.
* @param minClaimHold is the minimum amount of time the owner * @param _minClaimHold is the minimum amount of time the owner
* must wait before reclaiming their bond. * must wait before reclaiming their bond.
*/ */
function post( function post(
bytes32 bondId, bytes32 _bondId,
address owner, address _bondOwner,
uint256 minClaimHold uint256 _minClaimHold
) external payable; ) external payable;
/** /**
* @notice Seizes the bond with the given id. * @notice Seizes the bond with the given id.
* @dev This function will revert if there is no bond at the given id. * @dev This function will revert if there is no bond at the given id.
* @param bondId is the id of the bond. * @param _bondId is the id of the bond.
*/ */
function seize(bytes32 bondId) external; function seize(bytes32 _bondId) external;
/** /**
* @notice Seizes the bond with the given id and distributes it to recipients. * @notice Seizes the bond with the given id and distributes it to recipients.
* @dev This function will revert if there is no bond at the given id. * @dev This function will revert if there is no bond at the given id.
* @param bondId is the id of the bond. * @param _bondId is the id of the bond.
* @param recipients is a set of addresses to split the bond amongst. * @param _claimRecipients is a set of addresses to split the bond amongst.
*/ */
function seizeAndSplit(bytes32 bondId, address[] calldata recipients) external; function seizeAndSplit(bytes32 _bondId, address[] calldata _claimRecipients) external;
/** /**
* @notice Reclaims the bond of the bond owner. * @notice Reclaims the bond of the bond owner.
* @dev This function will revert if there is no bond at the given id. * @dev This function will revert if there is no bond at the given id.
* @param bondId is the id of the bond. * @param _bondId is the id of the bond.
*/ */
function reclaim(bytes32 bondId) external; function reclaim(bytes32 _bondId) external;
} }
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.15; pragma solidity ^0.8.15;
import { Claim, GameType, GameStatus, Timestamp } from "../libraries/DisputeTypes.sol"; import { Claim } from "../libraries/DisputeTypes.sol";
import { GameType } from "../libraries/DisputeTypes.sol";
import { GameStatus } from "../libraries/DisputeTypes.sol";
import { Timestamp } from "../libraries/DisputeTypes.sol";
import { IVersioned } from "./IVersioned.sol"; import { IVersioned } from "./IVersioned.sol";
import { IBondManager } from "./IBondManager.sol"; import { IBondManager } from "./IBondManager.sol";
......
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.15; pragma solidity ^0.8.15;
import { Claim, GameType } from "../libraries/DisputeTypes.sol"; import { Claim } from "../libraries/DisputeTypes.sol";
import { GameType } from "../libraries/DisputeTypes.sol";
import { IDisputeGame } from "./IDisputeGame.sol"; import { IDisputeGame } from "./IDisputeGame.sol";
import { IOwnable } from "./IOwnable.sol";
/** /**
* @title IDisputeGameFactory * @title IDisputeGameFactory
* @notice The interface for a DisputeGameFactory contract. * @notice The interface for a DisputeGameFactory contract.
*/ */
interface IDisputeGameFactory is IOwnable { interface IDisputeGameFactory {
/** /**
* @notice Emitted when a new dispute game is created * @notice Emitted when a new dispute game is created
* @param disputeProxy The address of the dispute game proxy * @param disputeProxy The address of the dispute game proxy
......
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.15; pragma solidity ^0.8.15;
import { Claim, ClaimHash, Clock, Bond, Position, Timestamp } from "../libraries/DisputeTypes.sol"; import { Clock } from "../libraries/DisputeTypes.sol";
import { Claim } from "../libraries/DisputeTypes.sol";
import { Position } from "../libraries/DisputeTypes.sol";
import { Timestamp } from "../libraries/DisputeTypes.sol";
import { ClaimHash } from "../libraries/DisputeTypes.sol";
import { BondAmount } from "../libraries/DisputeTypes.sol";
import { IDisputeGame } from "./IDisputeGame.sol"; import { IDisputeGame } from "./IDisputeGame.sol";
...@@ -60,7 +65,7 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -60,7 +65,7 @@ interface IFaultDisputeGame is IDisputeGame {
* @param claimHash The unique ClaimHash * @param claimHash The unique ClaimHash
* @return bond The Bond associated with the ClaimHash * @return bond The Bond associated with the ClaimHash
*/ */
function bonds(ClaimHash claimHash) external view returns (Bond bond); function bonds(ClaimHash claimHash) external view returns (BondAmount bond);
/** /**
* @notice Maps a unique ClaimHash its chess clock. * @notice Maps a unique ClaimHash its chess clock.
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
/**
* @title IOwnable
* @notice An interface for ownable contracts.
*/
interface IOwnable {
/**
* @notice Returns the owner of the contract
* @return _owner The address of the owner
*/
function owner() external view returns (address _owner);
/**
* @notice Transfers ownership of the contract to a new address
* @dev May only be called by the contract owner
* @param newOwner The address to transfer ownership to
*/
function transferOwnership(address newOwner) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "./DisputeTypes.sol";
////////////////////////////////////////////////////////////////
// `DisputeGameFactory` Errors //
////////////////////////////////////////////////////////////////
/**
* @notice Thrown when a dispute game is attempted to be created with an unsupported game type.
* @param gameType The unsupported game type.
*/
error NoImplementation(GameType gameType);
/**
* @notice Thrown when a dispute game that already exists is attempted to be created.
* @param uuid The UUID of the dispute game that already exists.
*/
error GameAlreadyExists(Hash uuid);
////////////////////////////////////////////////////////////////
// `DisputeGame_Fault.sol` Errors //
////////////////////////////////////////////////////////////////
/**
* @notice Thrown when a supplied bond is too low to cover the
* cost of the next possible counter claim.
*/
error BondTooLow();
/**
* @notice Thrown when a defense against the root claim is attempted.
*/
error CannotDefendRootClaim();
/**
* @notice Thrown when a claim is attempting to be made that already exists.
*/
error ClaimAlreadyExists();
////////////////////////////////////////////////////////////////
// `AttestationDisputeGame` Errors //
////////////////////////////////////////////////////////////////
/**
* @notice Thrown when an invalid signature is submitted to `challenge`.
*/
error InvalidSignature();
/**
* @notice Thrown when a signature that has already been used to support the
* `rootClaim` is submitted to `challenge`.
*/
error AlreadyChallenged();
////////////////////////////////////////////////////////////////
// `Ownable` Errors //
////////////////////////////////////////////////////////////////
/**
* @notice Thrown when a function that is protected by the `onlyOwner` modifier
* is called from an account other than the owner.
*/
error NotOwner();
...@@ -18,9 +18,9 @@ type Claim is bytes32; ...@@ -18,9 +18,9 @@ type Claim is bytes32;
type ClaimHash is bytes32; type ClaimHash is bytes32;
/** /**
* @notice A bond represents the amount of collateral that a user has locked up in a claim. * @notice A bondamount represents the amount of collateral that a user has locked up in a claim.
*/ */
type Bond is uint256; type BondAmount is uint256;
/** /**
* @notice A dedicated timestamp type. * @notice A dedicated timestamp type.
......
...@@ -6,6 +6,34 @@ pragma solidity 0.8.15; ...@@ -6,6 +6,34 @@ pragma solidity 0.8.15;
* @notice Perform low level safe calls * @notice Perform low level safe calls
*/ */
library SafeCall { library SafeCall {
/**
* @notice Performs a low level call without copying any returndata.
* @dev Passes no calldata to the call context.
*
* @param _target Address to call
* @param _gas Amount of gas to pass to the call
* @param _value Amount of value to pass to the call
*/
function send(
address _target,
uint256 _gas,
uint256 _value
) internal returns (bool) {
bool _success;
assembly {
_success := call(
_gas, // gas
_target, // recipient
_value, // ether value
0, // inloc
0, // inlen
0, // outloc
0 // outlen
)
}
return _success;
}
/** /**
* @notice Perform a low level call without copying any returndata * @notice Perform a low level call without copying any returndata
* *
......
This diff is collapsed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "../libraries/DisputeTypes.sol";
import "../libraries/DisputeErrors.sol";
import { Test } from "forge-std/Test.sol";
import { DisputeGameFactory } from "../dispute/DisputeGameFactory.sol";
import { IDisputeGame } from "../dispute/IDisputeGame.sol";
contract DisputeGameFactory_Test is Test {
DisputeGameFactory factory;
FakeClone fakeClone;
event DisputeGameCreated(
address indexed disputeProxy,
GameType indexed gameType,
Claim indexed rootClaim
);
function setUp() public {
factory = new DisputeGameFactory(address(this));
fakeClone = new FakeClone();
}
/**
* @dev Tests that the `create` function succeeds when creating a new dispute game
* with a `GameType` that has an implementation set.
*/
function testFuzz_create_succeeds(
uint8 gameType,
Claim rootClaim,
bytes calldata extraData
) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2)));
// Set all three implementations to the same `FakeClone` contract.
for (uint8 i; i < 3; i++) {
factory.setImplementation(GameType(i), IDisputeGame(address(fakeClone)));
}
vm.expectEmit(false, true, true, false);
emit DisputeGameCreated(address(0), gt, rootClaim);
IDisputeGame proxy = factory.create(gt, rootClaim, extraData);
// Ensure that the dispute game was assigned to the `disputeGames` mapping.
assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy));
}
/**
* @dev Tests that the `create` function reverts when there is no implementation
* set for the given `GameType`.
*/
function testFuzz_create_noImpl_reverts(
uint8 gameType,
Claim rootClaim,
bytes calldata extraData
) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2)));
vm.expectRevert(abi.encodeWithSelector(NoImplementation.selector, gt));
factory.create(gt, rootClaim, extraData);
}
/**
* @dev Tests that the `create` function reverts when there exists a dispute game with the same UUID.
*/
function testFuzz_create_sameUUID_reverts(
uint8 gameType,
Claim rootClaim,
bytes calldata extraData
) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2)));
// Set all three implementations to the same `FakeClone` contract.
for (uint8 i; i < 3; i++) {
factory.setImplementation(GameType(i), IDisputeGame(address(fakeClone)));
}
// Create our first dispute game - this should succeed.
vm.expectEmit(false, true, true, false);
emit DisputeGameCreated(address(0), gt, rootClaim);
IDisputeGame proxy = factory.create(gt, rootClaim, extraData);
// Ensure that the dispute game was assigned to the `disputeGames` mapping.
assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy));
// Ensure that the `create` function reverts when called with parameters that would result in the same UUID.
vm.expectRevert(
abi.encodeWithSelector(
GameAlreadyExists.selector,
factory.getGameUUID(gt, rootClaim, extraData)
)
);
factory.create(gt, rootClaim, extraData);
}
/**
* @dev Tests that the `setImplementation` function properly sets the implementation for a given `GameType`.
*/
function test_setImplementation_succeeds() public {
// There should be no implementation for the `GameType.FAULT` enum value, it has not been set.
assertEq(address(factory.gameImpls(GameType.FAULT)), address(0));
// Set the implementation for the `GameType.FAULT` enum value.
factory.setImplementation(GameType.FAULT, IDisputeGame(address(1)));
// Ensure that the implementation for the `GameType.FAULT` enum value is set.
assertEq(address(factory.gameImpls(GameType.FAULT)), address(1));
}
/**
* @dev Tests that the `setImplementation` function reverts when called by a non-owner.
*/
function test_setImplementation_notOwner_reverts() public {
// Ensure that the `setImplementation` function reverts when called by a non-owner.
vm.prank(address(0));
vm.expectRevert("Ownable: caller is not the owner");
factory.setImplementation(GameType.FAULT, IDisputeGame(address(1)));
}
/**
* @dev Tests that the `getGameUUID` function returns the correct hash when comparing
* against the keccak256 hash of the abi-encoded parameters.
*/
function testDiff_getGameUUID_succeeds(
uint8 gameType,
Claim rootClaim,
bytes calldata extraData
) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2)));
assertEq(
Hash.unwrap(factory.getGameUUID(gt, rootClaim, extraData)),
keccak256(abi.encode(gt, rootClaim, extraData))
);
}
/**
* @dev Tests that the `owner` function returns the correct address after deployment.
*/
function test_owner_succeeds() public {
assertEq(factory.owner(), address(this));
}
/**
* @dev Tests that the `transferOwnership` function succeeds when called by the owner.
*/
function test_transferOwnership_succeeds() public {
factory.transferOwnership(address(1));
assertEq(factory.owner(), address(1));
}
/**
* @dev Tests that the `transferOwnership` function reverts when called by a non-owner.
*/
function test_transferOwnership_notOwner_reverts() public {
vm.prank(address(0));
vm.expectRevert("Ownable: caller is not the owner");
factory.transferOwnership(address(1));
}
}
/**
* @dev A fake clone used for testing the `DisputeGameFactory` contract's `create` function.
*/
contract FakeClone {
function initialize() external {
// noop
}
}
...@@ -5,6 +5,45 @@ import { CommonTest } from "./CommonTest.t.sol"; ...@@ -5,6 +5,45 @@ import { CommonTest } from "./CommonTest.t.sol";
import { SafeCall } from "../libraries/SafeCall.sol"; import { SafeCall } from "../libraries/SafeCall.sol";
contract SafeCall_Test is CommonTest { contract SafeCall_Test is CommonTest {
function testFuzz_send_succeeds(
address from,
address to,
uint256 gas,
uint64 value
) external {
vm.assume(from.balance == 0);
vm.assume(to.balance == 0);
// no precompiles (mainnet)
assumeNoPrecompiles(to, 1);
// don't call the vm
vm.assume(to != address(vm));
vm.assume(from != address(vm));
// don't call the console
vm.assume(to != address(0x000000000000000000636F6e736F6c652e6c6f67));
// don't call the create2 deployer
vm.assume(to != address(0x4e59b44847b379578588920cA78FbF26c0B4956C));
// don't call the ffi interface
vm.assume(to != address(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f));
assertEq(from.balance, 0, "from balance is 0");
vm.deal(from, value);
assertEq(from.balance, value, "from balance not dealt");
uint256[2] memory balancesBefore = [from.balance, to.balance];
vm.expectCall(to, value, bytes(""));
vm.prank(from);
bool success = SafeCall.send(to, gas, value);
assertTrue(success, "send not successful");
if (from == to) {
assertEq(from.balance, balancesBefore[0], "Self-send did not change balance");
} else {
assertEq(from.balance, balancesBefore[0] - value, "from balance not drained");
assertEq(to.balance, balancesBefore[1] + value, "to balance received");
}
}
function testFuzz_call_succeeds( function testFuzz_call_succeeds(
address from, address from,
address to, address to,
......
...@@ -8,6 +8,7 @@ remappings = [ ...@@ -8,6 +8,7 @@ remappings = [
'@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/',
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/',
'@rari-capital/solmate/=node_modules/@rari-capital/solmate', '@rari-capital/solmate/=node_modules/@rari-capital/solmate',
"@cwia/=node_modules/clones-with-immutable-args/src",
'forge-std/=node_modules/forge-std/src', 'forge-std/=node_modules/forge-std/src',
'ds-test/=node_modules/ds-test/src' 'ds-test/=node_modules/ds-test/src'
] ]
......
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
"@nomiclabs/hardhat-ethers": "^2.0.0", "@nomiclabs/hardhat-ethers": "^2.0.0",
"@nomiclabs/hardhat-waffle": "^2.0.0", "@nomiclabs/hardhat-waffle": "^2.0.0",
"@rari-capital/solmate": "https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc", "@rari-capital/solmate": "https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc",
"clones-with-immutable-args": "https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args.git#105efee1b9127ed7f6fedf139e1fc796ce8791f2",
"@typechain/ethers-v5": "^10.1.0", "@typechain/ethers-v5": "^10.1.0",
"@typescript-eslint/eslint-plugin": "^5.45.1", "@typescript-eslint/eslint-plugin": "^5.45.1",
"@typescript-eslint/parser": "^5.45.1", "@typescript-eslint/parser": "^5.45.1",
......
...@@ -80,6 +80,46 @@ func TestRPCCacheImmutableRPCs(t *testing.T) { ...@@ -80,6 +80,46 @@ func TestRPCCacheImmutableRPCs(t *testing.T) {
}, },
name: "eth_getBlockByNumber earliest", name: "eth_getBlockByNumber earliest",
}, },
{
req: &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["safe", false]`),
ID: ID,
},
res: nil,
name: "eth_getBlockByNumber safe",
},
{
req: &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["finalized", false]`),
ID: ID,
},
res: nil,
name: "eth_getBlockByNumber finalized",
},
{
req: &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["pending", false]`),
ID: ID,
},
res: nil,
name: "eth_getBlockByNumber pending",
},
{
req: &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["latest", false]`),
ID: ID,
},
res: nil,
name: "eth_getBlockByNumber latest",
},
{ {
req: &RPCReq{ req: &RPCReq{
JSONRPC: "2.0", JSONRPC: "2.0",
......
...@@ -203,23 +203,22 @@ func NewConsensusPoller(bg *BackendGroup, opts ...ConsensusOpt) *ConsensusPoller ...@@ -203,23 +203,22 @@ func NewConsensusPoller(bg *BackendGroup, opts ...ConsensusOpt) *ConsensusPoller
// UpdateBackend refreshes the consensus state of a single backend // UpdateBackend refreshes the consensus state of a single backend
func (cp *ConsensusPoller) UpdateBackend(ctx context.Context, be *Backend) { func (cp *ConsensusPoller) UpdateBackend(ctx context.Context, be *Backend) {
bs := cp.backendState[be] if cp.IsBanned(be) {
if time.Now().Before(bs.bannedUntil) { log.Debug("skipping backend banned", "backend", be.Name)
log.Debug("skipping backend banned", "backend", be.Name, "bannedUntil", bs.bannedUntil)
return return
} }
// if backend it not online or not in a health state we'll only resume checkin it after ban // if backend it not online or not in a health state we'll only resume checkin it after ban
if !be.Online() || !be.IsHealthy() { if !be.Online() || !be.IsHealthy() {
log.Warn("backend banned - not online or not healthy", "backend", be.Name, "bannedUntil", bs.bannedUntil) log.Warn("backend banned - not online or not healthy", "backend", be.Name)
bs.bannedUntil = time.Now().Add(cp.banPeriod) cp.Ban(be)
} }
// if backend it not in sync we'll check again after ban // if backend it not in sync we'll check again after ban
inSync, err := cp.isInSync(ctx, be) inSync, err := cp.isInSync(ctx, be)
if err != nil || !inSync { if err != nil || !inSync {
log.Warn("backend banned - not in sync", "backend", be.Name, "bannedUntil", bs.bannedUntil) log.Warn("backend banned - not in sync", "backend", be.Name)
bs.bannedUntil = time.Now().Add(cp.banPeriod) cp.Ban(be)
} }
// if backend exhausted rate limit we'll skip it for now // if backend exhausted rate limit we'll skip it for now
...@@ -246,7 +245,11 @@ func (cp *ConsensusPoller) UpdateBackend(ctx context.Context, be *Backend) { ...@@ -246,7 +245,11 @@ func (cp *ConsensusPoller) UpdateBackend(ctx context.Context, be *Backend) {
if changed { if changed {
RecordBackendLatestBlock(be, latestBlockNumber) RecordBackendLatestBlock(be, latestBlockNumber)
log.Debug("backend state updated", "name", be.Name, "state", bs) log.Debug("backend state updated",
"name", be.Name,
"peerCount", peerCount,
"latestBlockNumber", latestBlockNumber,
"latestBlockHash", latestBlockHash)
} }
} }
...@@ -258,7 +261,7 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) { ...@@ -258,7 +261,7 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) {
currentConsensusBlockNumber := cp.GetConsensusBlockNumber() currentConsensusBlockNumber := cp.GetConsensusBlockNumber()
for _, be := range cp.backendGroup.Backends { for _, be := range cp.backendGroup.Backends {
peerCount, backendLatestBlockNumber, backendLatestBlockHash, lastUpdate := cp.getBackendState(be) peerCount, backendLatestBlockNumber, backendLatestBlockHash, lastUpdate, _ := cp.getBackendState(be)
if !be.skipPeerCountCheck && peerCount < cp.minPeerCount { if !be.skipPeerCountCheck && peerCount < cp.minPeerCount {
continue continue
...@@ -306,10 +309,10 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) { ...@@ -306,10 +309,10 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) {
- with minimum peer count - with minimum peer count
- updated recently - updated recently
*/ */
bs := cp.backendState[be] peerCount, _, _, lastUpdate, bannedUntil := cp.getBackendState(be)
notUpdated := bs.lastUpdate.Add(cp.maxUpdateThreshold).Before(time.Now()) notUpdated := lastUpdate.Add(cp.maxUpdateThreshold).Before(time.Now())
isBanned := time.Now().Before(bs.bannedUntil) isBanned := time.Now().Before(bannedUntil)
notEnoughPeers := !be.skipPeerCountCheck && bs.peerCount < cp.minPeerCount notEnoughPeers := !be.skipPeerCountCheck && peerCount < cp.minPeerCount
if !be.IsHealthy() || be.IsRateLimited() || !be.Online() || notUpdated || isBanned || notEnoughPeers { if !be.IsHealthy() || be.IsRateLimited() || !be.Online() || notUpdated || isBanned || notEnoughPeers {
filteredBackendsNames = append(filteredBackendsNames, be.Name) filteredBackendsNames = append(filteredBackendsNames, be.Name)
continue continue
...@@ -359,6 +362,22 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) { ...@@ -359,6 +362,22 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) {
log.Debug("group state", "proposedBlock", proposedBlock, "consensusBackends", strings.Join(consensusBackendsNames, ", "), "filteredBackends", strings.Join(filteredBackendsNames, ", ")) log.Debug("group state", "proposedBlock", proposedBlock, "consensusBackends", strings.Join(consensusBackendsNames, ", "), "filteredBackends", strings.Join(filteredBackendsNames, ", "))
} }
// IsBanned checks if a specific backend is banned
func (cp *ConsensusPoller) IsBanned(be *Backend) bool {
bs := cp.backendState[be]
defer bs.backendStateMux.Unlock()
bs.backendStateMux.Lock()
return time.Now().Before(bs.bannedUntil)
}
// Ban bans a specific backend
func (cp *ConsensusPoller) Ban(be *Backend) {
bs := cp.backendState[be]
defer bs.backendStateMux.Unlock()
bs.backendStateMux.Lock()
bs.bannedUntil = time.Now().Add(cp.banPeriod)
}
// Unban remove any bans from the backends // Unban remove any bans from the backends
func (cp *ConsensusPoller) Unban() { func (cp *ConsensusPoller) Unban() {
for _, be := range cp.backendGroup.Backends { for _, be := range cp.backendGroup.Backends {
...@@ -432,13 +451,14 @@ func (cp *ConsensusPoller) isInSync(ctx context.Context, be *Backend) (result bo ...@@ -432,13 +451,14 @@ func (cp *ConsensusPoller) isInSync(ctx context.Context, be *Backend) (result bo
return res, nil return res, nil
} }
func (cp *ConsensusPoller) getBackendState(be *Backend) (peerCount uint64, blockNumber hexutil.Uint64, blockHash string, lastUpdate time.Time) { func (cp *ConsensusPoller) getBackendState(be *Backend) (peerCount uint64, blockNumber hexutil.Uint64, blockHash string, lastUpdate time.Time, bannedUntil time.Time) {
bs := cp.backendState[be] bs := cp.backendState[be]
bs.backendStateMux.Lock() bs.backendStateMux.Lock()
peerCount = bs.peerCount peerCount = bs.peerCount
blockNumber = bs.latestBlockNumber blockNumber = bs.latestBlockNumber
blockHash = bs.latestBlockHash blockHash = bs.latestBlockHash
lastUpdate = bs.lastUpdate lastUpdate = bs.lastUpdate
bannedUntil = bs.bannedUntil
bs.backendStateMux.Unlock() bs.backendStateMux.Unlock()
return return
} }
......
...@@ -271,7 +271,10 @@ func (e *EthGasPriceMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRe ...@@ -271,7 +271,10 @@ func (e *EthGasPriceMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRe
} }
func isBlockDependentParam(s string) bool { func isBlockDependentParam(s string) bool {
return s == "latest" || s == "pending" return s == "latest" ||
s == "pending" ||
s == "finalized" ||
s == "safe"
} }
func decodeGetBlockByNumberParams(params json.RawMessage) (string, bool, error) { func decodeGetBlockByNumberParams(params json.RawMessage) (string, bool, error) {
...@@ -355,7 +358,11 @@ func decodeEthCallParams(req *RPCReq) (*ethCallParams, string, error) { ...@@ -355,7 +358,11 @@ func decodeEthCallParams(req *RPCReq) (*ethCallParams, string, error) {
} }
func validBlockInput(input string) bool { func validBlockInput(input string) bool {
if input == "earliest" || input == "pending" || input == "latest" { if input == "earliest" ||
input == "latest" ||
input == "pending" ||
input == "finalized" ||
input == "safe" {
return true return true
} }
_, err := decodeBlockInput(input) _, err := decodeBlockInput(input)
......
...@@ -576,7 +576,7 @@ func (s *Server) populateContext(w http.ResponseWriter, r *http.Request) context ...@@ -576,7 +576,7 @@ func (s *Server) populateContext(w http.ResponseWriter, r *http.Request) context
return nil return nil
} }
ctx = context.WithValue(r.Context(), ContextKeyAuth, s.authenticatedPaths[authorization]) // nolint:staticcheck ctx = context.WithValue(ctx, ContextKeyAuth, s.authenticatedPaths[authorization]) // nolint:staticcheck
} }
return context.WithValue( return context.WithValue(
......
# [Public] 4/26 Transaction Delays Post-Mortem
# Incident Summary
On April 26, 2023 between the hours of 1900 and 2100 UTC, OP Mainnet experienced degraded service following a ~10x increase in the rate of `eth_sendRawTransaction` requests.
While the sequencer remained online and continued to process transactions, users experienced transaction inclusion delays, rate limit errors, problems syncing nodes, and other symptoms of degraded performance.
The issue resolved itself once the rate of `eth_sendRawTransaction` requests subsided. However, we did not communicate the status of the network to our users, nor did we execute on mitigations quickly enough that could have reduced the impact of the degraded service. We recognize that this was a frustrating experience that caused significant impact to our users, particularly those participating in the DeFi ecosystem. We are sorry for this user experience, and hope that this retrospective provides insight into what happened and what we are doing to prevent similar issues from happening again.
# Leadup
OP Mainnet has not yet been upgraded to Bedrock. As a result, all OP Mainnet nodes run two components to sync the L2 chain:
- The `data-transport-layer` (or DTL), which indexes transactions from L1 or another L2 node.
- `l2geth`, which executes the transactions indexed by the DTL and maintains the L2 chain’s state.
The DTL and `l2geth` retrieve new data by polling for it. The DTL polls L1 or L2 depending on how it is configured, and `l2geth` polls the DTL. The higher the transaction throughput is, the more transactions will have to be processed between each “tick” of the polling loop. When throughput is too high, it is possible for the number of transactions between each tick to exceed what can be processed in a single tick. In this case, multiple ticks are necessary to catch up to the tip of the chain.
To protect the sequencer against traffic spikes, we route read requests - specifically `eth_getBlockRange`, which the DTL uses to sync from L2 - to a read-only replica rather than to the sequencer itself.
At 1915 UTC, sequencer throughput jumped from the usual ~8 transactions per second to a peak of 95 transactions per second over the course of 15 minutes.
# Causes
As a result of the increased throughput, our read-only replica started to fall behind. The graph below shows the delay, in seconds, between the sequencer creating new blocks and them being indexed by the read-only replica:
![outage.png](2023-04-26-transaction-delays/outage.png)
This meant that while the sequencer was processing transactions normally, users were unable to see their transactions confirmed on chain for several minutes. For DeFi apps relying on an accurate view of on-chain data, this caused transactions to be reverted and positions to be liquidated. It also made it difficult to retry transactions, since the user’s wallet nonce may have increased on the sequencer but not on the replica.
This issue was ecosystem wide. Infrastructure providers run replicas of their own, which use the same polling-based mechanism to sync data. This likely further increased the delay between when transactions were processed, and when they appeared as confirmed. This is not an error on the part of infrastructure providers, but rather a flaw in how the pre-Bedrock system is designed.
# Recovery and Lessons Learned
The issue resolved itself once the transaction volume dropped back down to normal levels. However, we did not communicate with our community for the duration of the outage. This is a significant miss, and for that we apologize. Going forward, we will do the following in an effort to avoid similar issues:
- We will add monitoring for replica lag, so that we can proactively route traffic directly to the sequencer when throughput increases beyond what the sync mechanism can handle.
- Though rate limits were not the direct cause of this incident, we will increase rate limits so that node operators can poll for new blocks more frequently.
Lastly, we will upgrade mainnet to Bedrock later this year. Bedrock fixes these issues from an architectural perspective. Specifically:
- There will be no more DTL, or polling-based sync mechanism. Blocks are either derived from L1, or gossipped over a peer-to-peer network.
- Blocks will be created every two seconds rather than on every transaction. This allows data to propagate across the network more efficiently and predictably.
- The sequencer will have a private mempool. This will allow fee-replacement transactions to work properly, and eliminate the need for aggressive rate limiting on the sequencer.
We recognize how frustrating an issue like this can be, and that is compounded when we are not proactively communicating. We’re sorry our users had this experience. We’re committed to applying these learnings moving forward and appreciate our community holding us accountable.
...@@ -7915,6 +7915,10 @@ clone@^1.0.2: ...@@ -7915,6 +7915,10 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
"clones-with-immutable-args@https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args.git#105efee1b9127ed7f6fedf139e1fc796ce8791f2":
version "2.0.0"
resolved "https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args.git#105efee1b9127ed7f6fedf139e1fc796ce8791f2"
clsx@^1.1.0: clsx@^1.1.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
......
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