Skip to content

Commit

Permalink
:WIP: Fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nagdahimanshu committed Feb 17, 2024
1 parent 8ac90e3 commit 02fb4be
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 88 deletions.
167 changes: 97 additions & 70 deletions cmd/faultdetector/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"math/big"
"net/http"
"strconv"
"strings"
"sync"
"testing"
Expand All @@ -19,35 +20,62 @@ import (
"github.com/LiskHQ/op-fault-detector/pkg/faultdetector"
"github.com/LiskHQ/op-fault-detector/pkg/log"
"github.com/LiskHQ/op-fault-detector/pkg/utils/notification"
slack "github.com/LiskHQ/op-fault-detector/pkg/utils/notification/channel"
"github.com/ethereum/go-ethereum/common"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
promClient "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
slackClient "github.com/slack-go/slack"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

const (
host = "0.0.0.0"
port = 8080
host = "0.0.0.0"
port = 8080
faultProofWindow = 1000
currentOutputIndex = 1
l1RpcApi = "https://rpc.notadegen.com/eth"
l2RpcApi = "https://mainnet.optimism.io/"
)

type mockOracleAccessor struct {
type mockContractOracleAccessor struct {
mock.Mock
}

func (o *mockOracleAccessor) GetNextOutputIndex() (*big.Int, error) {
type mockSlackClient struct {
mock.Mock
}

func (o *mockContractOracleAccessor) GetNextOutputIndex() (*big.Int, error) {
called := o.MethodCalled("GetNextOutputIndex")
return called.Get(0).(*big.Int), called.Error(1)
}

func (o *mockOracleAccessor) GetL2Output(index *big.Int) (chain.L2Output, error) {
func (o *mockContractOracleAccessor) GetL2Output(index *big.Int) (chain.L2Output, error) {
called := o.MethodCalled("GetL2Output", index)
return called.Get(0).(chain.L2Output), called.Error(1)
}

func (o *mockContractOracleAccessor) FinalizationPeriodSeconds() (*big.Int, error) {
called := o.MethodCalled("FinalizationPeriodSeconds")
return called.Get(0).(*big.Int), called.Error(1)
}

func (o *mockSlackClient) PostMessageContext(ctx context.Context, channelID string, options ...slackClient.MsgOption) (string, string, error) {
called := o.MethodCalled("PostMessageContext")
return called.Get(0).(string), called.Get(1).(string), called.Error(2)
}

var slackNotificationClient *mockSlackClient = new(mockSlackClient)

func randHash() (out common.Hash) {
_, _ = crand.Read(out[:])
return out
}

func prepareHTTPServer(t *testing.T, ctx context.Context, logger log.Logger, wg *sync.WaitGroup, erroChan chan error) *api.HTTPServer {
router := gin.Default()
return &api.HTTPServer{
Expand All @@ -64,35 +92,46 @@ func prepareHTTPServer(t *testing.T, ctx context.Context, logger log.Logger, wg
}
}

func randHash() (out common.Hash) {
_, _ = crand.Read(out[:])
return out
func prepareNotification(t *testing.T, ctx context.Context, logger log.Logger) *notification.Notification {
slackNotificationClient.On("PostMessageContext").Return("TestChannelID", "1234569.1000", nil)

return &notification.Notification{
Slack: &slack.Slack{
Client: slackNotificationClient,
ChannelID: "string",
Ctx: ctx,
Logger: logger,
},
}
}

func prepareFaultDetector(t *testing.T, ctx context.Context, logger log.Logger, reg *prometheus.Registry, config *config.Config, wg *sync.WaitGroup, erroChan chan error, mock bool) *faultdetector.FaultDetector {
func prepareFaultDetector(t *testing.T, ctx context.Context, logger log.Logger, wg *sync.WaitGroup, reg *prometheus.Registry, config *config.Config, erroChan chan error, mock bool) *faultdetector.FaultDetector {
var fd *faultdetector.FaultDetector
if !mock {
fd, _ = faultdetector.NewFaultDetector(ctx, logger, erroChan, wg, config.FaultDetectorConfig, reg, &notification.Notification{})
} else {
l1RpcApi, _ := chain.GetAPIClient(ctx, "https://rpc.notadegen.com/eth", logger)
l2RpcApi, _ := chain.GetAPIClient(ctx, "https://mainnet.optimism.io/", logger)
metrics := faultdetector.NewFaultDetectorMetrics(reg)

var oracle *mockOracleAccessor = new(mockOracleAccessor)
const defaultL1Timestamp uint64 = 123456
// Create chain API clients
l1RpcApi, _ := chain.GetAPIClient(ctx, l1RpcApi, logger)
l2RpcApi, _ := chain.GetAPIClient(ctx, l2RpcApi, logger)

// Mock oracle conmtract accessor
var oracle *mockContractOracleAccessor = new(mockContractOracleAccessor)
oracle.On("GetNextOutputIndex").Return(big.NewInt(2), nil)
oracle.On("FinalizationPeriodSeconds").Return(faultProofWindow, nil)
oracle.On("GetL2Output", big.NewInt(0)).Return(chain.L2Output{
OutputRoot: randHash().String(),
L1Timestamp: defaultL1Timestamp - 1,
L2BlockNumber: 115905463,
L1Timestamp: 1000000,
L2BlockNumber: 116216402, //TODO: make it dynamic
L2OutputIndex: 2,
}, nil)
oracle.On("GetL2Output", big.NewInt(1)).Return(chain.L2Output{
OutputRoot: randHash().String(),
L1Timestamp: defaultL1Timestamp + 1,
L2BlockNumber: 115905463,
L1Timestamp: 1000000,
L2BlockNumber: 116216402, //TODO: make it dynamic
L2OutputIndex: 2,
}, nil)
metrics := faultdetector.NewFaultDetectorMetrics(reg)

fd = &faultdetector.FaultDetector{
Ctx: ctx,
Expand All @@ -103,8 +142,8 @@ func prepareFaultDetector(t *testing.T, ctx context.Context, logger log.Logger,
L1RpcApi: l1RpcApi,
L2RpcApi: l2RpcApi,
OracleContractAccessor: oracle,
FaultProofWindow: 60480,
CurrentOutputIndex: 1,
FaultProofWindow: faultProofWindow,
CurrentOutputIndex: currentOutputIndex,
Diverged: false,
Ticker: time.NewTicker(2 * time.Second),
QuitTickerChan: make(chan struct{}),
Expand All @@ -116,26 +155,28 @@ func prepareFaultDetector(t *testing.T, ctx context.Context, logger log.Logger,
}

func prepareConfig(t *testing.T) *config.Config {
serverPort, _ := strconv.Atoi(fmt.Sprintf("%d", port))

return &config.Config{
System: &config.System{
LogLevel: "info",
},
Api: &config.Api{
Server: &config.Server{
Host: "0.0.0.0",
Port: 8080,
Host: host,
Port: uint(serverPort),
},
BasePath: "/api",
RegisterVersions: []string{"v1"},
},
FaultDetectorConfig: &config.FaultDetectorConfig{
L1RPCEndpoint: "https://rpc.notadegen.com/eth",
L2RPCEndpoint: "https://mainnet.optimism.io/",
L1RPCEndpoint: l1RpcApi,
L2RPCEndpoint: l2RpcApi,
Startbatchindex: -1,
L2OutputOracleContractAddress: "0x0000000000000000000000000000000000000000",
},
Notification: &config.Notification{
Enable: false,
Enable: true,
},
}
}
Expand Down Expand Up @@ -168,84 +209,70 @@ func parseMetricRes(input *strings.Reader) []map[string]map[string]interface{} {

func TestMain_E2E(t *testing.T) {
gin.SetMode(gin.TestMode)

ctx := context.Background()
wg := sync.WaitGroup{}
logger, _ := log.NewDefaultProductionLogger()
client := http.DefaultClient
erroChan := make(chan error)
registry := prometheus.NewRegistry()
testConfig := prepareConfig(&testing.T{})
testServer := prepareHTTPServer(&testing.T{}, ctx, logger, &wg, erroChan)
testFaultDetector := prepareFaultDetector(&testing.T{}, ctx, logger, registry, testConfig, &wg, erroChan, false)
testFaultDetectorMocked := prepareFaultDetector(&testing.T{}, ctx, logger, registry, testConfig, &wg, erroChan, true)

// Register handler
testServer.Router.GET("/status", v1.GetStatus)
testServer.RegisterHandler("GET", "/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry, ProcessStartTime: time.Now()}))

tests := []struct {
name string
App App
mock bool
assertion func(float64, error)
}{
{
name: "should start application with no faults detected",
App: App{
ctx: ctx,
logger: logger,
errChan: erroChan,
config: testConfig,
apiServer: testServer,
faultDetector: testFaultDetector,
notification: &notification.Notification{},
wg: &wg,
},
mock: false,
assertion: func(isStateMismatch float64, err error) {
var expected float64 = 0
assert.Equal(t, isStateMismatch, expected)
slackNotificationClient.AssertNotCalled(t, "PostMessageContext")
},
},
{
name: "should start application with faults detected",
App: App{
ctx: ctx,
logger: logger,
errChan: erroChan,
config: testConfig,
apiServer: testServer,
faultDetector: testFaultDetectorMocked,
notification: &notification.Notification{},
wg: &wg,
},
mock: true,
assertion: func(isStateMismatch float64, err error) {
var expected float64 = 1
assert.Equal(t, isStateMismatch, expected)
slackNotificationClient.AssertCalled(t, "PostMessageContext")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
wg := sync.WaitGroup{}
logger, _ := log.NewDefaultProductionLogger()
errorChan := make(chan error)
registry := prometheus.NewRegistry()
testConfig := prepareConfig(&testing.T{})
testServer := prepareHTTPServer(&testing.T{}, ctx, logger, &wg, errorChan)
testFaultDetector := prepareFaultDetector(&testing.T{}, ctx, logger, &wg, registry, testConfig, errorChan, tt.mock)
testNotificationService := prepareNotification(&testing.T{}, ctx, logger)

// Register handler
testServer.Router.GET("/status", v1.GetStatus)
testServer.RegisterHandler("GET", "/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry, ProcessStartTime: time.Now()}))

app := &App{
ctx: tt.App.ctx,
logger: tt.App.logger,
errChan: tt.App.errChan,
config: tt.App.config,
wg: tt.App.wg,
apiServer: tt.App.apiServer,
faultDetector: tt.App.faultDetector,
notification: tt.App.notification,
ctx: ctx,
logger: logger,
errChan: errorChan,
config: testConfig,
wg: &wg,
apiServer: testServer,
faultDetector: testFaultDetector,
notification: testNotificationService,
}

time.AfterFunc(5*time.Second, func() {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:%d/status", host, port), nil)
statusEndpoint := fmt.Sprintf("http://%s:%d/status", host, port)
req, err := http.NewRequest(http.MethodGet, statusEndpoint, nil)
assert.NoError(t, err)
res, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, 200, res.StatusCode)

req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:%d/metrics", host, port), nil)
metricsEndpoint := fmt.Sprintf("http://%s:%d/metrics", host, port)
req, err = http.NewRequest(http.MethodGet, metricsEndpoint, nil)
assert.NoError(t, err)
assert.Equal(t, 200, res.StatusCode)

Expand Down
8 changes: 4 additions & 4 deletions pkg/faultdetector/faultdetector.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (fd *FaultDetector) Start() {
for {
select {
case <-fd.Ticker.C:
if err := fd.checkFault(fd.OracleContractAccessor); err != nil {
if err := fd.checkFault(); err != nil {
time.Sleep(waitTimeInFailure * time.Second)
}
case <-fd.QuitTickerChan:
Expand All @@ -186,11 +186,11 @@ func (fd *FaultDetector) Stop() {
}

// checkFault continuously checks for the faults at regular interval.
func (fd *FaultDetector) checkFault(oracleContractAccessor OracleAccessor) error {
func (fd *FaultDetector) checkFault() error {
startTime := time.Now()
fd.Logger.Infof("Checking current batch with output index: %d.", fd.CurrentOutputIndex)

nextOutputIndex, err := oracleContractAccessor.GetNextOutputIndex()
nextOutputIndex, err := fd.OracleContractAccessor.GetNextOutputIndex()
if err != nil {
fd.Logger.Errorf("Failed to query next output index, error: %w.", err)
fd.Metrics.apiConnectionFailure.Inc()
Expand All @@ -204,7 +204,7 @@ func (fd *FaultDetector) checkFault(oracleContractAccessor OracleAccessor) error
return fmt.Errorf("current output index is ahead of the oracle latest batch index")
}

l2OutputData, err := oracleContractAccessor.GetL2Output(encoding.MustConvertUint64ToBigInt(fd.CurrentOutputIndex))
l2OutputData, err := fd.OracleContractAccessor.GetL2Output(encoding.MustConvertUint64ToBigInt(fd.CurrentOutputIndex))
if err != nil {
fd.Logger.Errorf("Failed to fetch output associated with index: %d, error: %w.", fd.CurrentOutputIndex, err)
fd.Metrics.apiConnectionFailure.Inc()
Expand Down
1 change: 1 addition & 0 deletions pkg/faultdetector/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ChainAPIClient interface {
type OracleAccessor interface {
GetNextOutputIndex() (*big.Int, error)
GetL2Output(index *big.Int) (chain.L2Output, error)
FinalizationPeriodSeconds() (*big.Int, error)
}

// FindFirstUnfinalizedOutputIndex finds and returns the first L2 output index that has not yet passed the fault proof window.
Expand Down
5 changes: 5 additions & 0 deletions pkg/faultdetector/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func (o *mockOracleAccessor) GetL2Output(index *big.Int) (chain.L2Output, error)
return called.Get(0).(chain.L2Output), called.Error(1)
}

func (o *mockOracleAccessor) FinalizationPeriodSeconds() (*big.Int, error) {
called := o.MethodCalled("FinalizationPeriodSeconds")
return called.Get(0).(*big.Int), called.Error(1)
}

func randHash() (out common.Hash) {
_, _ = crand.Read(out[:])
return out
Expand Down
Loading

0 comments on commit 02fb4be

Please sign in to comment.