controller_test.go 7.01 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
package syncnode

import (
	"context"
	"fmt"
	"sync"
	"testing"

	"github.com/ethereum-optimism/optimism/op-service/eth"
	"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset"
	"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
	"github.com/ethereum/go-ethereum/log"
	"github.com/stretchr/testify/require"
)

type mockChainsDB struct {
	updateLocalSafeFn func(chainID types.ChainID, ref eth.BlockRef, derived eth.BlockRef) error
}

func (m *mockChainsDB) UpdateLocalSafe(chainID types.ChainID, ref eth.BlockRef, derived eth.BlockRef) error {
	if m.updateLocalSafeFn != nil {
		return m.updateLocalSafeFn(chainID, ref, derived)
	}
	return nil
}

type mockSyncControl struct {
	TryDeriveNextFn func(ctx context.Context, ref eth.BlockRef) (eth.BlockRef, error)
}

func (m *mockSyncControl) TryDeriveNext(ctx context.Context, ref eth.BlockRef) (eth.BlockRef, error) {
	if m.TryDeriveNextFn != nil {
		return m.TryDeriveNextFn(ctx, ref)
	}
	return eth.BlockRef{}, nil
}
func sampleDepSet(t *testing.T) depset.DependencySet {
	depSet, err := depset.NewStaticConfigDependencySet(
		map[types.ChainID]*depset.StaticConfigDependency{
			types.ChainIDFromUInt64(900): {
				ChainIndex:     900,
				ActivationTime: 42,
				HistoryMinTime: 100,
			},
			types.ChainIDFromUInt64(901): {
				ChainIndex:     901,
				ActivationTime: 30,
				HistoryMinTime: 20,
			},
		})
	require.NoError(t, err)
	return depSet
}

// TestAttachNodeController tests the AttachNodeController function of the SyncNodesController.
// Only controllers for chains in the dependency set can be attached.
func TestAttachNodeController(t *testing.T) {
	logger := log.New()
	depSet := sampleDepSet(t)
	controller := NewSyncNodesController(logger, depSet, nil)

	require.Zero(t, controller.controllers.Len(), "controllers should be empty to start")

	// Attach a controller for chain 900
	ctrl := mockSyncControl{}
	err := controller.AttachNodeController(types.ChainIDFromUInt64(900), &ctrl)
	require.NoError(t, err)

	require.Equal(t, 1, controller.controllers.Len(), "controllers should have 1 entry")

	// Attach a controller for chain 901
	ctrl2 := mockSyncControl{}
	err = controller.AttachNodeController(types.ChainIDFromUInt64(901), &ctrl2)
	require.NoError(t, err)

	require.Equal(t, 2, controller.controllers.Len(), "controllers should have 2 entries")

	// Attach a controller for chain 902 (which is not in the dependency set)
	ctrl3 := mockSyncControl{}
	err = controller.AttachNodeController(types.ChainIDFromUInt64(902), &ctrl3)
	require.Error(t, err)
	require.Equal(t, 2, controller.controllers.Len(), "controllers should still have 2 entries")
}

// TestDeriveFromL1 tests the DeriveFromL1 function of the SyncNodesController for multiple chains
func TestDeriveFromL1(t *testing.T) {
	logger := log.New()
	depSet := sampleDepSet(t)

	// keep track of the updates for each chain with the mock
	updates := map[types.ChainID][]eth.BlockRef{}
	mockChainsDB := mockChainsDB{}
	updateMu := sync.Mutex{}
	mockChainsDB.updateLocalSafeFn = func(chainID types.ChainID, ref eth.BlockRef, derived eth.BlockRef) error {
		updateMu.Lock()
		defer updateMu.Unlock()
		updates[chainID] = append(updates[chainID], derived)
		return nil
	}
	controller := NewSyncNodesController(logger, depSet, &mockChainsDB)

	refA := eth.BlockRef{Number: 1}
	refB := eth.BlockRef{Number: 2}
	refC := eth.BlockRef{Number: 3}
	derived := []eth.BlockRef{refA, refB, refC}

	// Attach a controller for chain 900 with a mock controller function
	ctrl1 := mockSyncControl{}
	ctrl1i := 0
	// the controller will return the next derived block each time TryDeriveNext is called
	ctrl1.TryDeriveNextFn = func(ctx context.Context, ref eth.BlockRef) (eth.BlockRef, error) {
		defer func() { ctrl1i++ }()
		if ctrl1i >= len(derived) {
			return eth.BlockRef{}, nil
		}
		return derived[ctrl1i], nil
	}
	err := controller.AttachNodeController(types.ChainIDFromUInt64(900), &ctrl1)
	require.NoError(t, err)

	// Attach a controller for chain 900 with a mock controller function
	ctrl2 := mockSyncControl{}
	ctrl2i := 0
	// the controller will return the next derived block each time TryDeriveNext is called
	ctrl2.TryDeriveNextFn = func(ctx context.Context, ref eth.BlockRef) (eth.BlockRef, error) {
		defer func() { ctrl2i++ }()
		if ctrl2i >= len(derived) {
			return eth.BlockRef{}, nil
		}
		return derived[ctrl2i], nil
	}
	err = controller.AttachNodeController(types.ChainIDFromUInt64(901), &ctrl2)
	require.NoError(t, err)

	// Derive from L1
	err = controller.DeriveFromL1(refA)
	require.NoError(t, err)

	// Check that the derived blocks were recorded for each chain
	require.Equal(t, []eth.BlockRef{refA, refB, refC}, updates[types.ChainIDFromUInt64(900)])
	require.Equal(t, []eth.BlockRef{refA, refB, refC}, updates[types.ChainIDFromUInt64(901)])

}

// TestDeriveFromL1Error tests that if a chain fails to derive from L1, the derived blocks up to the error are still recorded
// for that chain, and all other chains that derived successfully are also recorded.
func TestDeriveFromL1Error(t *testing.T) {
	logger := log.New()
	depSet := sampleDepSet(t)

	// keep track of the updates for each chain with the mock
	updates := map[types.ChainID][]eth.BlockRef{}
	mockChainsDB := mockChainsDB{}
	updateMu := sync.Mutex{}
	mockChainsDB.updateLocalSafeFn = func(chainID types.ChainID, ref eth.BlockRef, derived eth.BlockRef) error {
		updateMu.Lock()
		defer updateMu.Unlock()
		updates[chainID] = append(updates[chainID], derived)
		return nil
	}
	controller := NewSyncNodesController(logger, depSet, &mockChainsDB)

	refA := eth.BlockRef{Number: 1}
	refB := eth.BlockRef{Number: 2}
	refC := eth.BlockRef{Number: 3}
	derived := []eth.BlockRef{refA, refB, refC}

	// Attach a controller for chain 900 with a mock controller function
	ctrl1 := mockSyncControl{}
	ctrl1i := 0
	// the controller will return the next derived block each time TryDeriveNext is called
	ctrl1.TryDeriveNextFn = func(ctx context.Context, ref eth.BlockRef) (eth.BlockRef, error) {
		defer func() { ctrl1i++ }()
		if ctrl1i >= len(derived) {
			return eth.BlockRef{}, nil
		}
		return derived[ctrl1i], nil
	}
	err := controller.AttachNodeController(types.ChainIDFromUInt64(900), &ctrl1)
	require.NoError(t, err)

	// Attach a controller for chain 900 with a mock controller function
	ctrl2 := mockSyncControl{}
	ctrl2i := 0
	// this controller will error on the last derived block
	ctrl2.TryDeriveNextFn = func(ctx context.Context, ref eth.BlockRef) (eth.BlockRef, error) {
		defer func() { ctrl2i++ }()
		if ctrl2i >= len(derived)-1 {
			return eth.BlockRef{}, fmt.Errorf("error")
		}
		return derived[ctrl2i], nil
	}
	err = controller.AttachNodeController(types.ChainIDFromUInt64(901), &ctrl2)
	require.NoError(t, err)

	// Derive from L1
	err = controller.DeriveFromL1(refA)
	require.Error(t, err)

	// Check that the derived blocks were recorded for each chain
	// and in the case of the error, the derived blocks up to the error are recorded
	require.Equal(t, []eth.BlockRef{refA, refB, refC}, updates[types.ChainIDFromUInt64(900)])
	require.Equal(t, []eth.BlockRef{refA, refB}, updates[types.ChainIDFromUInt64(901)])

}