diff --git a/.github/actions/monitor-cdk-verified-batches/action.yml b/.github/actions/monitor-cdk-verified-batches/action.yml new file mode 100644 index 0000000..83372bd --- /dev/null +++ b/.github/actions/monitor-cdk-verified-batches/action.yml @@ -0,0 +1,21 @@ +--- +name: monitor-cdk-verified-batches +description: Check that batches are being verified in a CDK environment + +inputs: + verified_batches_target: + description: The minimum number of batches to be verified + required: false + default: '30' + timeout: + description: The script timeout in seconds + required: false + default: '600' # 10 minutes + +runs: + using: "composite" + steps: + - name: Check that batches are being verified + working-directory: .github/actions/monitor-cdk-verified-batches + shell: bash + run: ./batch_verification_monitor.sh ${{ inputs.verified_batches_target }} ${{ inputs.timeout }} diff --git a/.github/actions/monitor-cdk-verified-batches/batch_verification_monitor.sh b/.github/actions/monitor-cdk-verified-batches/batch_verification_monitor.sh new file mode 100644 index 0000000..16a34a6 --- /dev/null +++ b/.github/actions/monitor-cdk-verified-batches/batch_verification_monitor.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# This script monitors the verification progress of zkEVM batches. +# Usage: ./batch_verification_monitor + +# The number of batches to be verified. +verified_batches_target="$1" + +# The script timeout (in seconds). +timeout="$2" + +start_time=$(date +%s) +end_time=$((start_time + timeout)) + +rpc_url="$(kurtosis port print cdk-v1 cdk-erigon-node-001 http-rpc)" +while true; do + verified_batches="$(cast to-dec "$(cast rpc --rpc-url "$rpc_url" zkevm_verifiedBatchNumber | sed 's/"//g')")" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verified Batches: $verified_batches" + + current_time=$(date +%s) + if (( current_time > end_time )); then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ Exiting... Timeout reached!" + exit 1 + fi + + if (( verified_batches > verified_batches_target )); then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Exiting... $verified_batches batches were verified!" + exit 0 + fi + + sleep 10 +done diff --git a/.github/workflows/regression-tests.yml b/.github/workflows/regression-tests.yml index cf3b5ca..2d2a337 100644 --- a/.github/workflows/regression-tests.yml +++ b/.github/workflows/regression-tests.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 with: repository: 0xPolygon/kurtosis-cdk - ref: feat/cdk-erigon-zkevm + ref: main path: kurtosis-cdk - name: Install Kurtosis CDK tools @@ -39,7 +39,11 @@ jobs: working-directory: ./kurtosis-cdk run: kurtosis run --enclave cdk-v1 --args-file params.yml --image-download always . + - name: Set executable permissions for the script + working-directory: ./cdk-data-availability + run: sudo chmod +x .github/actions/monitor-cdk-verified-batches/batch_verification_monitor.sh + - name: Monitor verified batches - working-directory: ./kurtosis-cdk + working-directory: ./cdk-data-availability shell: bash run: .github/actions/monitor-cdk-verified-batches/batch_verification_monitor.sh 19 600 \ No newline at end of file diff --git a/.gitignore b/.gitignore index cea8f96..2d9e949 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist test/gethData test/coverage.out +coverage.out diff --git a/client/client_test.go b/client/client_test.go index 1e5b0c1..7211484 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -318,3 +318,102 @@ func TestClient_ListOffChainData(t *testing.T) { }) } } + +func TestClient_SignSequenceBanana(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ssb types.SignedSequenceBanana + result string + signature []byte + statusCode int + err error + }{ + { + name: "successfully signed banana sequence", + ssb: types.SignedSequenceBanana{ + Sequence: types.SequenceBanana{}, + Signature: []byte("signature00"), + }, + result: fmt.Sprintf(`{"result":"%s"}`, hex.EncodeToString([]byte("signature11"))), + signature: []byte("signature11"), + }, + { + name: "error returned by rpc server", + ssb: types.SignedSequenceBanana{ + Sequence: types.SequenceBanana{}, + Signature: []byte("signature00"), + }, + result: `{"error":{"code":123,"message":"test error"}}`, + err: errors.New("123 test error"), + }, + { + name: "invalid signature returned by rpc server", + ssb: types.SignedSequenceBanana{ + Sequence: types.SequenceBanana{}, + Signature: []byte("signature00"), + }, + result: `{"result":"invalid-signature"}`, + }, + { + name: "unsuccessful status code returned by rpc server", + ssb: types.SignedSequenceBanana{ + Sequence: types.SequenceBanana{}, + Signature: []byte("signature00"), + }, + statusCode: http.StatusInternalServerError, + err: errors.New("invalid status code, expected: 200, found: 500"), + }, + } + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request rpc.Request + require.NoError(t, json.NewDecoder(r.Body).Decode(&request)) + require.Equal(t, "datacom_signSequenceBanana", request.Method) + + var params []types.SignedSequenceBanana + require.NoError(t, json.Unmarshal(request.Params, ¶ms)) + require.Equal(t, tt.ssb, params[0]) + + if tt.statusCode > 0 { + w.WriteHeader(tt.statusCode) + } + + _, err := fmt.Fprint(w, tt.result) + require.NoError(t, err) + })) + defer srv.Close() + + client := New(srv.URL) + + result, err := client.SignSequenceBanana(context.Background(), tt.ssb) + if tt.err != nil { + require.Error(t, err) + require.EqualError(t, tt.err, err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.signature, result) + } + }) + } +} + +func TestClient_Factory_New(t *testing.T) { + t.Parallel() + + url := "http://example.com" + f := NewFactory() + + c := f.New(url) + require.NotNil(t, c) + + client, ok := c.(*client) + require.True(t, ok) + require.Equal(t, url, client.url) +} diff --git a/cmd/main.go b/cmd/main.go index b18bf6c..409c2fd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -127,7 +127,7 @@ func start(cliCtx *cli.Context) error { log.Fatal(err) } - if err = detector.Start(); err != nil { + if err = detector.Start(cliCtx.Context); err != nil { log.Fatal(err) } diff --git a/config/config_test.go b/config/config_test.go index b850a24..099e113 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -15,6 +15,8 @@ import ( ) func Test_Defaults(t *testing.T) { + t.Parallel() + tcs := []struct { path string expectedValue interface{} @@ -49,6 +51,8 @@ func Test_Defaults(t *testing.T) { for _, tc := range tcs { tc := tc t.Run(tc.path, func(t *testing.T) { + t.Parallel() + actual := getValueFromStruct(tc.path, cfg) require.Equal(t, tc.expectedValue, actual) }) @@ -56,6 +60,8 @@ func Test_Defaults(t *testing.T) { } func Test_ConfigFileNotFound(t *testing.T) { + t.Parallel() + flags := flag.FlagSet{} flags.String("cfg", "/fictitious-file/foo.cfg", "") @@ -65,6 +71,8 @@ func Test_ConfigFileNotFound(t *testing.T) { } func Test_ConfigFileOverride(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() overrides := filepath.Join(tempDir, "overrides.toml") f, err := os.Create(overrides) @@ -81,6 +89,59 @@ func Test_ConfigFileOverride(t *testing.T) { require.Equal(t, "0xDEADBEEF", cfg.L1.PolygonValidiumAddress) } +func Test_NewKeyFromKeystore(t *testing.T) { + t.Parallel() + + t.Run("valid keystore file", func(t *testing.T) { + t.Parallel() + + cfg := types.KeystoreFileConfig{ + Path: "../test/config/test-member.keystore", + Password: "testonly", + } + + key, err := NewKeyFromKeystore(cfg) + require.NoError(t, err) + require.NotNil(t, key) + }) + + t.Run("no path and password", func(t *testing.T) { + t.Parallel() + + cfg := types.KeystoreFileConfig{} + + key, err := NewKeyFromKeystore(cfg) + require.NoError(t, err) + require.Nil(t, key) + }) + + t.Run("invalid keystore file", func(t *testing.T) { + t.Parallel() + + cfg := types.KeystoreFileConfig{ + Path: "non-existent.keystore", + Password: "testonly", + } + + key, err := NewKeyFromKeystore(cfg) + require.ErrorContains(t, err, "no such file or directory") + require.Nil(t, key) + }) + + t.Run("invalid password", func(t *testing.T) { + t.Parallel() + + cfg := types.KeystoreFileConfig{ + Path: "../test/config/test-member.keystore", + Password: "invalid", + } + + key, err := NewKeyFromKeystore(cfg) + require.ErrorContains(t, err, "could not decrypt key with given password") + require.Nil(t, key) + }) +} + func getValueFromStruct(path string, object interface{}) interface{} { keySlice := strings.Split(path, ".") v := reflect.ValueOf(object) diff --git a/rpc/client_test.go b/rpc/client_test.go index a08a703..c317889 100644 --- a/rpc/client_test.go +++ b/rpc/client_test.go @@ -1,7 +1,6 @@ package rpc import ( - "context" "encoding/json" "errors" "fmt" @@ -55,7 +54,7 @@ func Test_JSONRPCCallWithContext(t *testing.T) { })) defer svr.Close() - got, err := JSONRPCCallWithContext(context.Background(), svr.URL, "test") + got, err := JSONRPCCall(svr.URL, "test") if tt.err != nil { require.Error(t, err) require.EqualError(t, tt.err, err.Error()) diff --git a/rpc/server.go b/rpc/server.go index 1e206b2..99ac23d 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -245,21 +245,6 @@ func handleError(w http.ResponseWriter, err error) { } } -// RPCErrorResponse formats error to be returned through RPC -func RPCErrorResponse(code int, message string, err error) (interface{}, Error) { - return RPCErrorResponseWithData(code, message, nil, err) -} - -// RPCErrorResponseWithData formats error to be returned through RPC -func RPCErrorResponseWithData(code int, message string, data *[]byte, err error) (interface{}, Error) { - if err != nil { - log.Errorf("%v: %v", message, err.Error()) - } else { - log.Error(message) - } - return nil, NewRPCErrorWithData(code, message, data) -} - func combinedLog(r *http.Request, start time.Time, httpStatus, dataLen int) { log.Infof("%s - - %s \"%s %s %s\" %d %d \"%s\" \"%s\"", r.RemoteAddr, diff --git a/sequencer/tracker.go b/sequencer/tracker.go index 7b4b414..339e840 100644 --- a/sequencer/tracker.go +++ b/sequencer/tracker.go @@ -133,6 +133,7 @@ func (st *Tracker) trackAddrChanges(ctx context.Context) { if ctx.Err() != nil && ctx.Err() != context.DeadlineExceeded { log.Warnf("context cancelled: %v", ctx.Err()) } + return case <-st.stop: return } @@ -227,6 +228,7 @@ func (st *Tracker) trackUrlChanges(ctx context.Context) { if ctx.Err() != nil && ctx.Err() != context.DeadlineExceeded { log.Warnf("context cancelled: %v", ctx.Err()) } + return case <-st.stop: return } diff --git a/services/datacom/datacom_test.go b/services/datacom/datacom_test.go index 8261f43..a4ab3d8 100644 --- a/services/datacom/datacom_test.go +++ b/services/datacom/datacom_test.go @@ -122,19 +122,6 @@ func TestDataCom_SignSequence(t *testing.T) { }) }) - t.Run("Unauthorized sender", func(t *testing.T) { - t.Parallel() - - testFn(t, testConfig{ - sender: privateKey, - expectedError: "unauthorized", - sequence: types.Sequence{ - types.ArgBytes{0, 1}, - types.ArgBytes{2, 3}, - }, - }) - }) - t.Run("Fail to store off chain data", func(t *testing.T) { t.Parallel() @@ -182,3 +169,144 @@ func TestDataCom_SignSequence(t *testing.T) { }) }) } + +func TestDataCom_SignSequenceBanana(t *testing.T) { + t.Parallel() + + type testConfig struct { + storeOffChainDataReturns []interface{} + sender *ecdsa.PrivateKey + signer *ecdsa.PrivateKey + sequence types.SequenceBanana + expectedError string + } + + sequenceSignerKey, err := crypto.GenerateKey() + require.NoError(t, err) + + trustedSequencerKey, err := crypto.GenerateKey() + require.NoError(t, err) + + unknownKey, err := crypto.GenerateKey() + require.NoError(t, err) + + testFn := func(t *testing.T, cfg testConfig) { + t.Helper() + + var ( + signer = sequenceSignerKey + signedSequence *types.SignedSequenceBanana + err error + ) + + dbMock := mocks.NewDB(t) + + if len(cfg.storeOffChainDataReturns) > 0 { + dbMock.On("StoreOffChainData", mock.Anything, cfg.sequence.OffChainData()).Return( + cfg.storeOffChainDataReturns...).Once() + } + + ethermanMock := mocks.NewEtherman(t) + + ethermanMock.On("TrustedSequencer", mock.Anything).Return(crypto.PubkeyToAddress(trustedSequencerKey.PublicKey), nil).Once() + ethermanMock.On("TrustedSequencerURL", mock.Anything).Return("http://some-url", nil).Once() + + sqr := sequencer.NewTracker(config.L1Config{ + Timeout: cfgTypes.Duration{Duration: time.Minute}, + RetryPeriod: cfgTypes.Duration{Duration: time.Second}, + }, ethermanMock) + + sqr.Start(context.Background()) + + if cfg.sender != nil { + signature, err := cfg.sequence.Sign(cfg.sender) + require.NoError(t, err) + signedSequence = &types.SignedSequenceBanana{ + Sequence: cfg.sequence, + Signature: signature, + } + } else { + signedSequence = &types.SignedSequenceBanana{ + Sequence: cfg.sequence, + Signature: []byte{}, + } + } + + if cfg.signer != nil { + signer = cfg.signer + } + + dce := NewEndpoints(dbMock, signer, sqr) + + sig, err := dce.SignSequenceBanana(*signedSequence) + if cfg.expectedError != "" { + require.ErrorContains(t, err, cfg.expectedError) + } else { + require.NoError(t, err) + require.NotEmpty(t, sig) + } + + sqr.Stop() + + dbMock.AssertExpectations(t) + ethermanMock.AssertExpectations(t) + } + + t.Run("Failed to verify sender", func(t *testing.T) { + t.Parallel() + + testFn(t, testConfig{ + expectedError: "failed to verify sender", + sequence: types.SequenceBanana{}, + }) + }) + + t.Run("Unauthorized sender", func(t *testing.T) { + t.Parallel() + + testFn(t, testConfig{ + sender: sequenceSignerKey, + expectedError: "unauthorized", + sequence: types.SequenceBanana{}, + signer: unknownKey, + }) + }) + + t.Run("Fail to store off chain data", func(t *testing.T) { + t.Parallel() + + testFn(t, testConfig{ + sender: trustedSequencerKey, + expectedError: "failed to store offchain data", + storeOffChainDataReturns: []interface{}{errors.New("error")}, + sequence: types.SequenceBanana{}, + }) + }) + + t.Run("Fail to sign sequence", func(t *testing.T) { + t.Parallel() + + key, err := crypto.GenerateKey() + require.NoError(t, err) + + key.D = common.Big0 // alter the key so that signing does not pass + + testFn(t, testConfig{ + sender: trustedSequencerKey, + signer: key, + storeOffChainDataReturns: []interface{}{nil}, + expectedError: "failed to sign", + sequence: types.SequenceBanana{}, + }) + }) + + t.Run("Happy path - sequence signed", func(t *testing.T) { + t.Parallel() + + testFn(t, testConfig{ + sender: trustedSequencerKey, + storeOffChainDataReturns: []interface{}{nil}, + sequence: types.SequenceBanana{}, + }) + }) +} diff --git a/services/sync/sync_test.go b/services/sync/sync_test.go index 12ed3bf..0aa3ac9 100644 --- a/services/sync/sync_test.go +++ b/services/sync/sync_test.go @@ -2,12 +2,14 @@ package sync import ( "context" + "crypto/rand" "errors" "testing" "github.com/0xPolygon/cdk-data-availability/mocks" "github.com/0xPolygon/cdk-data-availability/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" ) @@ -81,7 +83,7 @@ func TestSyncEndpoints_ListOffChainData(t *testing.T) { }{ { name: "successfully got offchain data", - hashes: []types.ArgHash{}, + hashes: generateRandomHashes(t, 1), data: []types.OffChainData{{ Key: common.BytesToHash(nil), Value: types.ArgBytes("offchaindata"), @@ -99,6 +101,11 @@ func TestSyncEndpoints_ListOffChainData(t *testing.T) { dbErr: errors.New("test error"), err: errors.New("failed to list the requested data"), }, + { + name: "too many hashes requested", + hashes: generateRandomHashes(t, maxListHashes+1), + err: errors.New("too many hashes requested"), + }, } for _, tt := range tests { tt := tt @@ -113,17 +120,19 @@ func TestSyncEndpoints_ListOffChainData(t *testing.T) { keys[i] = hash.Hash() } - dbMock.On("ListOffChainData", context.Background(), keys). - Return(tt.data, tt.dbErr) + if tt.data != nil { + dbMock.On("ListOffChainData", context.Background(), keys). + Return(tt.data, tt.dbErr) - defer dbMock.AssertExpectations(t) + defer dbMock.AssertExpectations(t) + } z := &Endpoints{db: dbMock} got, err := z.ListOffChainData(tt.hashes) if tt.err != nil { require.Error(t, err) - require.EqualError(t, tt.err, err.Error()) + require.ErrorContains(t, tt.err, err.Error()) } else { require.NoError(t, err) @@ -137,3 +146,25 @@ func TestSyncEndpoints_ListOffChainData(t *testing.T) { }) } } + +func generateRandomHashes(t *testing.T, numOfHashes int) []types.ArgHash { + t.Helper() + + hashes := make([]types.ArgHash, numOfHashes) + for i := 0; i < numOfHashes; i++ { + hashes[i] = types.ArgHash(generateRandomHash(t)) + } + + return hashes +} + +func generateRandomHash(t *testing.T) common.Hash { + t.Helper() + + randomData := make([]byte, 32) + + _, err := rand.Read(randomData) + require.NoError(t, err) + + return crypto.Keccak256Hash(randomData) +} diff --git a/synchronizer/reorg.go b/synchronizer/reorg.go index 71725b1..af2d79d 100644 --- a/synchronizer/reorg.go +++ b/synchronizer/reorg.go @@ -41,10 +41,10 @@ func (rd *ReorgDetector) Subscribe() <-chan BlockReorg { } // Start starts the ReorgDetector tracking for reorg events -func (rd *ReorgDetector) Start() error { +func (rd *ReorgDetector) Start(ctx context.Context) error { log.Info("starting block reorganization detector") - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) rd.cancel = cancel blocks := make(chan *ethgo.Block)