diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4a6f9c2c..b6f08cc7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -5,12 +5,12 @@ on: push: branches: - main - - 'release-*' + - "release-*" pull_request: types: [opened, synchronize, reopened] branches: - main - - 'release-*' + - "release-*" workflow_call: env: @@ -23,12 +23,11 @@ concurrency: cancel-in-progress: true jobs: - code-quality: name: "Code Quality & Linting" runs-on: ubuntu-24.04 timeout-minutes: 10 - + steps: - name: "Checkout Repository" uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -39,7 +38,7 @@ jobs: run: | git config --global url.https://x-oauth-basic:${{ secrets.GITHUB_TOKEN }}@github.com/.insteadOf https://github.com/ echo "GOPRIVATE=github.com/symbioticfi/relay" >> $GITHUB_ENV - + - name: "Setup Go Environment" uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@v6.4.0 with: @@ -71,7 +70,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 15 needs: [code-quality] - + steps: - name: "Checkout Repository" uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -82,7 +81,7 @@ jobs: run: | git config --global url.https://x-oauth-basic:${{ secrets.GITHUB_TOKEN }}@github.com/.insteadOf https://github.com/ echo "GOPRIVATE=github.com/symbioticfi/relay" >> $GITHUB_ENV - + - name: "Setup Go Environment" uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@v6.4.0 with: @@ -121,13 +120,12 @@ jobs: files: ./coverage.out fail_ci_if_error: false - e2e-tests: name: "E2E Tests - ${{ matrix.config.name }}" runs-on: ${{ matrix.config.verification_type == 0 && 'relay-16' || 'ubuntu-24.04' }} - timeout-minutes: 35 + timeout-minutes: 40 needs: [unit-tests] - + strategy: matrix: config: @@ -153,7 +151,7 @@ jobs: aggregators: 3 verification_type: 0 epoch_time: 120 - + env: # E2E Test Configuration OPERATORS: ${{ matrix.config.operators }} @@ -222,10 +220,10 @@ jobs: cd e2e/temp-network echo "Starting Docker Compose network..." docker compose up -d - + echo "Waiting for services to initialize..." sleep 30 - + echo "✅ Network startup completed" - name: "Execute E2E Tests" @@ -233,23 +231,22 @@ jobs: echo "Running End-to-End tests..." make e2e-test - - name: "Collect Diagnostic Information" if: failure() run: | cd e2e echo "❌ Test failure detected - collecting diagnostic information..." - + # Create organized logs directory structure mkdir -p logs/{containers,system,summary} - + echo "Gathering container information..." # Get active containers (excluding anvil containers which are noisy) CONTAINER_IDS=$(docker ps -a --format "table {{.ID}}\t{{.Names}}" | grep -v -E "(anvil|buildx_buildkit|CONTAINER)" | awk '{print $1}' | tr '\n' ' ') - + if [ -n "$CONTAINER_IDS" ]; then echo "Found active containers: $(echo $CONTAINER_IDS | wc -w)" - + # Container status overview { echo "=== Container Status Overview ===" @@ -259,12 +256,12 @@ jobs: echo "" docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -v -E "(anvil|buildx_buildkit)" } > logs/containers/status-overview.log - + # Individual container logs for container_id in $CONTAINER_IDS; do if [ -n "$container_id" ]; then CONTAINER_NAME=$(docker inspect --format='{{.Name}}' $container_id 2>/dev/null | sed 's/^\/*//' || echo "container-$container_id") - + echo "Collecting logs for: $CONTAINER_NAME" { echo "=== Container: $CONTAINER_NAME ($container_id) ===" @@ -278,7 +275,7 @@ jobs: else echo "⚠️ No active containers found (excluding anvil)" > logs/containers/status-overview.log fi - + echo "Gathering system information..." { echo "=== System Diagnostics ===" @@ -296,7 +293,7 @@ jobs: echo "=== Docker Images ===" docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" } > logs/system/diagnostics.log - + echo "Creating test summary..." { echo "=== E2E Test Failure Summary ===" @@ -316,6 +313,14 @@ jobs: echo " - Block Time: ${{ env.BLOCK_TIME }}s" echo " - Finality Blocks: ${{ env.FINALITY_BLOCKS }}" echo " - Storage Type: ${{ env.STORAGE_TYPE || 'default (bbolt)' }}" + if [ -n "${{ env.E2E_PRUNING_TEST }}" ]; then + echo " - Retention Valset Epochs: ${{ env.RETENTION_VALSET_EPOCHS }}" + echo " - Retention Signature Epochs: ${{ env.RETENTION_SIGNATURE_EPOCHS }}" + echo " - Retention Proof Epochs: ${{ env.RETENTION_PROOF_EPOCHS }}" + echo " - Pruner Interval: ${{ env.PRUNER_INTERVAL }}" + echo " - Sync Epochs: ${{ env.SYNC_EPOCHS }}" + echo " - Pruning Test Enabled: ${{ env.E2E_PRUNING_TEST }}" + fi echo "" echo "=== Available Log Files ===" echo "containers/status-overview.log - Container status summary" @@ -329,7 +334,7 @@ jobs: echo "3. Verify test configuration parameters" echo "4. Consider adjusting timeout values if needed" } > logs/summary/failure-report.log - + echo "✅ Diagnostic collection completed" echo "Log file summary:" find logs -type f -name "*.log" -exec echo " - {}" \; | sort diff --git a/Makefile b/Makefile index 67067fc5..6ba6804c 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,10 @@ unit-test: e2e-test: cd e2e/tests && go test -v -timeout 40m +.PHONY: e2e-pruning-test +e2e-pruning-test: + cd e2e/tests && go test -v -timeout 40m -run 'TestPruning' + .PHONY: gen-abi gen-abi: go run github.com/ethereum/go-ethereum/cmd/abigen@latest \ diff --git a/e2e/scripts/generate_network.sh b/e2e/scripts/generate_network.sh index 128018a9..5a7d1ec3 100755 --- a/e2e/scripts/generate_network.sh +++ b/e2e/scripts/generate_network.sh @@ -177,6 +177,8 @@ generate_docker_compose() { local anvil_settlement_port=8546 local relay_start_port=8081 local sum_start_port=9091 + local host_uid=${HOST_UID:-$(id -u)} + local host_gid=${HOST_GID:-$(id -g)} # Calculate timestamp as current unix timestamp + 5 seconds local timestamp=$(($(date +%s) + 5)) @@ -313,10 +315,11 @@ EOF relay-sidecar-$i: image: relay_sidecar:dev container_name: symbiotic-relay-$i + user: "$host_uid:$host_gid" command: - sh - -c - - "chmod 777 /app/$storage_dir /deploy-data 2>/dev/null || true && /workspace/scripts/sidecar-start.sh symb/0/15/0x$SYMB_PRIVATE_KEY_HEX,symb/0/11/0x$SYMB_SECONDARY_PRIVATE_KEY_HEX,symb/2/1/0x$SYMB_BLS12381_PRIVATE_KEY_HEX,symb/1/0/0x$SYMB_PRIVATE_KEY_HEX,evm/1/31337/0x$SYMB_PRIVATE_KEY_HEX,evm/1/31338/0x$SYMB_PRIVATE_KEY_HEX,p2p/1/1/$SYMB_PRIVATE_KEY_HEX /app/$storage_dir $circuits_param" + - "/workspace/scripts/sidecar-start.sh symb/0/15/0x$SYMB_PRIVATE_KEY_HEX,symb/0/11/0x$SYMB_SECONDARY_PRIVATE_KEY_HEX,symb/2/1/0x$SYMB_BLS12381_PRIVATE_KEY_HEX,symb/1/0/0x$SYMB_PRIVATE_KEY_HEX,evm/1/31337/0x$SYMB_PRIVATE_KEY_HEX,evm/1/31338/0x$SYMB_PRIVATE_KEY_HEX,p2p/1/1/$SYMB_PRIVATE_KEY_HEX /app/$storage_dir $circuits_param" ports: - "$port:8080" volumes: @@ -372,10 +375,11 @@ EOF relay-sidecar-extra: image: relay_sidecar:dev container_name: symbiotic-relay-extra + user: "$host_uid:$host_gid" command: - sh - -c - - "chmod 777 /app/$extra_storage_dir /deploy-data 2>/dev/null || true && /workspace/scripts/sidecar-start.sh symb/0/15/0x$extra_key_hex,symb/0/11/0x$extra_secondary_key_hex,symb/2/1/0x$extra_bls12381_key_hex,symb/1/0/0x$extra_key_hex,evm/1/31337/0x$extra_key_hex,evm/1/31338/0x$extra_key_hex,p2p/1/1/$extra_key_hex /app/$extra_storage_dir $circuits_param" + - "/workspace/scripts/sidecar-start.sh symb/0/15/0x$extra_key_hex,symb/0/11/0x$extra_secondary_key_hex,symb/2/1/0x$extra_bls12381_key_hex,symb/1/0/0x$extra_key_hex,evm/1/31337/0x$extra_key_hex,evm/1/31338/0x$extra_key_hex,p2p/1/1/$extra_key_hex /app/$extra_storage_dir $circuits_param" ports: - "$extra_port:8080" volumes: @@ -442,4 +446,4 @@ main() { generate_docker_compose "$operators" "$commiters" "$aggregators" "$verification_type" "$epoch_size" "$block_time" "$finality_blocks" "$committer_slot_duration" } -main "$@" \ No newline at end of file +main "$@" diff --git a/e2e/scripts/sidecar-start.sh b/e2e/scripts/sidecar-start.sh index ae50b9a4..46dc5837 100755 --- a/e2e/scripts/sidecar-start.sh +++ b/e2e/scripts/sidecar-start.sh @@ -39,19 +39,19 @@ evm: # Retention config retention: - valset-epochs: 1000 - signature-epochs: 1000 - proof-epochs: 1000 + valset-epochs: 5 + signature-epochs: 5 + proof-epochs: 5 sync: enabled: true period: 5s timeout: 1m - epochs: 1000 + epochs: 5 pruner: enabled: true - interval: 1m + interval: 10s tracing: enabled: false diff --git a/e2e/setup.sh b/e2e/setup.sh index 7e9cdfc5..990999a2 100755 --- a/e2e/setup.sh +++ b/e2e/setup.sh @@ -102,4 +102,4 @@ fi -echo "Setup complete! Network configuration generated in temp-network/ directory." \ No newline at end of file +echo "Setup complete! Network configuration generated in temp-network/ directory." diff --git a/e2e/tests/pruning_test.go b/e2e/tests/pruning_test.go new file mode 100644 index 00000000..1de83b57 --- /dev/null +++ b/e2e/tests/pruning_test.go @@ -0,0 +1,412 @@ +package tests + +import ( + "bytes" + "encoding/hex" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/dgraph-io/badger/v4" + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "github.com/samber/lo" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" + + apiv1 "github.com/symbioticfi/relay/api/client/v1" + symbiotic "github.com/symbioticfi/relay/symbiotic/entity" +) + +type pruningEntityType string + +type remainingEntity struct { + Type pruningEntityType + Location string + Key string +} + +type pruningScope struct { + epochBytes []byte + requestIDBytes [][]byte + requestIDHexes [][]byte +} + +func TestPruningE2E_RemovesAllNonExcludedEntities(t *testing.T) { + t.Log("Starting pruning e2e test...") + + ctx := t.Context() + envInfo := loadEnvInfo(t) + sidecarConfig := loadSidecarConfig(t) + client := getGRPCClient(t, 0) + scanSidecarIndex := 0 + if strings.EqualFold(os.Getenv("STORAGE_TYPE"), "badger") { + scanSidecarIndex = int(envInfo.Operators) + } + + t.Log("Step 1: Finding a committed epoch for pruning...") + targetEpoch := getCommittedEpochForPruning(t, client) + t.Logf("Using committed epoch %d", targetEpoch) + + t.Log("Step 2: Creating a real signing request for the target epoch...") + requestID := createSignatureRequestForEpoch(t, targetEpoch, envInfo) + t.Logf("Created signing request %s", requestID) + + t.Log("Step 3: Waiting for signatures and aggregation proof...") + waitForRequestSignatures(t, client, requestID, len(envInfo.GetSidecarConfigs())) + waitForRequestProof(t, client, requestID) + + maxRetention := max( + uint64(sidecarConfig.Retention.ValsetEpochs), + uint64(sidecarConfig.Retention.SignatureEpochs), + uint64(sidecarConfig.Retention.ProofEpochs), + ) + require.Positive(t, maxRetention, "pruning test requires positive retention") + + t.Log("Step 4: Waiting until the target epoch is eligible for pruning...") + pruneReadyEpoch := targetEpoch + symbiotic.Epoch(maxRetention) + require.NoError(t, waitForErrorIsNil(ctx, 3*waitEpochTimeout(), func() error { + resp, err := client.GetCurrentEpoch(ctx, &apiv1.GetCurrentEpochRequest{}) + if err != nil { + return err + } + if symbiotic.Epoch(resp.GetEpoch()) < pruneReadyEpoch { + return errors.Errorf("current epoch %d is below target epoch %d", resp.GetEpoch(), pruneReadyEpoch) + } + return nil + })) + t.Logf("Epoch %d is now eligible for pruning", targetEpoch) + + scope := buildPruningScope(targetEpoch, common.HexToHash(requestID)) + + t.Log("Step 5: Capturing actual epoch-scoped keys present in storage before pruning...") + prePruneEntities := scanSidecarStorageOffline(t, scanSidecarIndex, envInfo, scope) + require.NotEmptyf(t, prePruneEntities, "expected to find epoch-scoped keys before pruning, got none for scope:\n%s", formatRemainingEntities(prePruneEntities)) + + t.Log("Step 6: Waiting for pruning and verifying that no non-excluded entities remain...") + time.Sleep(offlineScanDelay(t)) + remaining := scanSidecarStorageOffline(t, scanSidecarIndex, envInfo, scope) + require.Emptyf(t, remaining, "found unpruned entities:\n%s", formatRemainingEntities(remaining)) + + t.Log("Pruning e2e test completed successfully") +} + +func TestMatchesPrunedEntity(t *testing.T) { + t.Parallel() + + requestID := common.HexToHash("0x4b939f34668a2b051228cf038dd1654aa57dbac80053cd68fee2e9d68eb9c5a6") + scope := buildPruningScope(symbiotic.Epoch(15), requestID) + + t.Run("matches epoch in key", func(t *testing.T) { + key := append([]byte("validator:"), append(symbiotic.Epoch(15).Bytes(), []byte(":0xoperator")...)...) + require.True(t, matchesPrunedEntity(key, []byte("ignored"), scope)) + }) + + t.Run("matches request id in key", func(t *testing.T) { + key := append([]byte("signature:"), requestID.Bytes()...) + require.True(t, matchesPrunedEntity(key, []byte("ignored"), scope)) + }) + + t.Run("does not match unrelated value bytes", func(t *testing.T) { + key := append([]byte("validator:"), append(symbiotic.Epoch(16).Bytes(), []byte(":0xoperator")...)...) + value := append([]byte("payload:"), symbiotic.Epoch(15).Bytes()...) + require.False(t, matchesPrunedEntity(key, value, scope)) + }) +} + +func stopSidecarForStorageScan(t *testing.T, sidecarIndex int, envInfo EnvInfo) error { + t.Helper() + + serviceName := storageScanSidecarName(sidecarIndex, envInfo) + + stopCmd := exec.CommandContext(t.Context(), "docker", "compose", "stop", "-t", "1", serviceName) + stopCmd.Dir = filepath.Join("..", "temp-network") + output, err := stopCmd.CombinedOutput() + if err != nil { + return errors.Errorf("failed to stop %s: %w: %s", serviceName, err, strings.TrimSpace(string(output))) + } + + if err := waitForErrorIsNil(t.Context(), 30*time.Second, func() error { + cmd := exec.CommandContext(t.Context(), "docker", "compose", "ps", "--status", "running", "--services", serviceName) + cmd.Dir = filepath.Join("..", "temp-network") + psOutput, err := cmd.CombinedOutput() + if err != nil { + return errors.Errorf("failed to inspect %s state: %w: %s", serviceName, err, strings.TrimSpace(string(psOutput))) + } + if strings.TrimSpace(string(psOutput)) != "" { + return errors.Errorf("%s is still running", serviceName) + } + return nil + }); err != nil { + return err + } + + return nil +} + +func startSidecarAfterStorageScan(t *testing.T, sidecarIndex int, envInfo EnvInfo) error { + t.Helper() + + serviceName := storageScanSidecarName(sidecarIndex, envInfo) + if err := startContainer(t.Context(), serviceName); err != nil { + return errors.Errorf("failed to start %s: %w", serviceName, err) + } + return nil +} + +func scanSidecarStorageOffline(t *testing.T, sidecarIndex int, envInfo EnvInfo, scope pruningScope) []remainingEntity { + t.Helper() + + require.NoError(t, stopSidecarForStorageScan(t, sidecarIndex, envInfo)) + defer func() { + require.NoError(t, startSidecarAfterStorageScan(t, sidecarIndex, envInfo)) + }() + + remaining, err := scanSidecarStorage(sidecarIndex, scope) + require.NoError(t, err) + return remaining +} + +func storageScanSidecarName(sidecarIndex int, envInfo EnvInfo) string { + if sidecarIndex >= int(envInfo.Operators) { + return "relay-sidecar-extra" + } + return fmt.Sprintf("relay-sidecar-%d", sidecarIndex+1) +} + +func getCommittedEpochForPruning(t *testing.T, client *apiv1.SymbioticClient) symbiotic.Epoch { + t.Helper() + + resp, err := client.GetLastAllCommitted(t.Context(), &apiv1.GetLastAllCommittedRequest{}) + require.NoError(t, err) + require.NotEmpty(t, resp.GetEpochInfos(), "expected at least one committed epoch") + + return symbiotic.Epoch(lo.Min(lo.Map(lo.Values(resp.GetEpochInfos()), func(info *apiv1.ChainEpochInfo, _ int) uint64 { + return info.GetLastCommittedEpoch() + }))) +} + +func createSignatureRequestForEpoch(t *testing.T, epoch symbiotic.Epoch, envInfo EnvInfo) string { + t.Helper() + + msg := fmt.Sprintf("pruning-e2e-%d", time.Now().UnixNano()) + var requestID string + + for i := range envInfo.GetSidecarConfigs() { + client := getGRPCClient(t, i) + var ( + resp *apiv1.SignMessageResponse + err error + ) + + for attempts := 1; attempts <= 3; attempts++ { + resp, err = client.SignMessage(t.Context(), &apiv1.SignMessageRequest{ + KeyTag: 15, + Message: []byte(msg), + RequiredEpoch: (*uint64)(&epoch), + }) + if err == nil { + break + } + } + require.NoErrorf(t, err, "failed to sign message on sidecar %d", i) + require.NotEmpty(t, resp.GetRequestId()) + + if requestID == "" { + requestID = resp.GetRequestId() + continue + } + require.Equalf(t, requestID, resp.GetRequestId(), "request id mismatch on sidecar %d", i) + } + + return requestID +} + +func waitForRequestSignatures(t *testing.T, client *apiv1.SymbioticClient, requestID string, signerCount int) { + t.Helper() + + require.NoError(t, waitForErrorIsNil(t.Context(), waitEpochTimeout(), func() error { + resp, err := client.GetSignatures(t.Context(), &apiv1.GetSignaturesRequest{RequestId: requestID}) + if err != nil { + return err + } + if len(resp.GetSignatures()) == 0 { + return errors.Errorf("no signatures available for request %s yet", requestID) + } + threshold := signerCount*2/3 + 1 + if len(resp.GetSignatures()) < threshold { + return errors.Errorf("received %d signatures, need at least %d", len(resp.GetSignatures()), threshold) + } + return nil + })) +} + +func waitForRequestProof(t *testing.T, client *apiv1.SymbioticClient, requestID string) { + t.Helper() + + require.NoError(t, waitForErrorIsNil(t.Context(), 2*waitEpochTimeout(), func() error { + resp, err := client.GetAggregationProof(t.Context(), &apiv1.GetAggregationProofRequest{ + RequestId: requestID, + }) + if err != nil { + return err + } + if resp.GetAggregationProof() == nil || len(resp.GetAggregationProof().GetProof()) == 0 { + return errors.Errorf("aggregation proof for request %s is empty", requestID) + } + return nil + })) +} + +func offlineScanDelay(t *testing.T) time.Duration { + t.Helper() + + interval, err := time.ParseDuration(loadSidecarConfig(t).Pruner.Interval) + require.NoError(t, err) + + return 4 * interval +} + +func scanSidecarStorage( + sidecarIndex int, + scope pruningScope, +) ([]remainingEntity, error) { + if strings.EqualFold(os.Getenv("STORAGE_TYPE"), "badger") { + return scanBadgerStorage(sidecarIndex, scope) + } + return scanBboltStorage(sidecarIndex, scope) +} + +func scanBboltStorage( + sidecarIndex int, + scope pruningScope, +) ([]remainingEntity, error) { + dbPath := filepath.Join("..", "temp-network", sidecarStorageDir(sidecarIndex), "relay.db") + db, err := bolt.Open(dbPath, 0o600, &bolt.Options{ReadOnly: true, Timeout: time.Second}) + if err != nil { + return nil, err + } + defer db.Close() + + var remaining []remainingEntity + + err = db.View(func(tx *bolt.Tx) error { + return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error { + entityType := pruningEntityType(name) + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + if !matchesPrunedEntity(k, v, scope) { + continue + } + remaining = append(remaining, remainingEntity{ + Type: entityType, + Location: string(name), + Key: hex.EncodeToString(k), + }) + } + return nil + }) + }) + return remaining, err +} + +func scanBadgerStorage( + sidecarIndex int, + scope pruningScope, +) ([]remainingEntity, error) { + dir := filepath.Join("..", "temp-network", sidecarStorageDir(sidecarIndex)) + opts := badger.DefaultOptions(dir). + WithReadOnly(true). + WithBypassLockGuard(true). + WithLogger(nil) + + db, err := badger.Open(opts) + if err != nil { + return nil, err + } + defer db.Close() + + var remaining []remainingEntity + + err = db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + key := item.KeyCopy(nil) + value, err := item.ValueCopy(nil) + if err != nil { + return err + } + if !matchesPrunedEntity(key, value, scope) { + continue + } + entityType := badgerEntityType(key) + remaining = append(remaining, remainingEntity{ + Type: entityType, + Location: "badger", + Key: hex.EncodeToString(key), + }) + } + return nil + }) + + return remaining, err +} + +func badgerEntityType(key []byte) pruningEntityType { + if bytes.HasPrefix(key, []byte("request_id_epoch")) { + return "request_id_epoch" + } + if idx := bytes.IndexByte(key, ':'); idx > 0 { + return pruningEntityType(key[:idx]) + } + return pruningEntityType(key) +} + +func buildPruningScope(targetEpoch symbiotic.Epoch, seedRequestID common.Hash) pruningScope { + scope := pruningScope{ + epochBytes: targetEpoch.Bytes(), + requestIDBytes: nil, + requestIDHexes: nil, + } + if seedRequestID != (common.Hash{}) { + scope.requestIDBytes = [][]byte{seedRequestID.Bytes()} + scope.requestIDHexes = [][]byte{[]byte(seedRequestID.Hex())} + } + return scope +} + +func matchesPrunedEntity(key, _ []byte, scope pruningScope) bool { + for _, requestIDBytes := range scope.requestIDBytes { + if bytes.Contains(key, requestIDBytes) { + return true + } + } + for _, requestIDHex := range scope.requestIDHexes { + if bytes.Contains(key, requestIDHex) { + return true + } + } + return bytes.Contains(key, scope.epochBytes) +} + +func sidecarStorageDir(sidecarIndex int) string { + return fmt.Sprintf("data-%02d", sidecarIndex+1) +} + +func formatRemainingEntities(entities []remainingEntity) string { + if len(entities) == 0 { + return "(none)" + } + lines := make([]string, 0, len(entities)) + for _, entity := range entities { + lines = append(lines, fmt.Sprintf("- %s [%s] %s", entity.Type, entity.Location, entity.Key)) + } + return strings.Join(lines, "\n") +} diff --git a/e2e/tests/sidecar.yaml b/e2e/tests/sidecar.yaml index b97783ad..a03850d6 100644 --- a/e2e/tests/sidecar.yaml +++ b/e2e/tests/sidecar.yaml @@ -38,15 +38,15 @@ sync: enabled: true period: 5s timeout: 1m - epochs: 1000 + epochs: 5 # Retention config retention: - valset-epochs: 1000 - signature-epochs: 1000 - proof-epochs: 1000 + valset-epochs: 5 + signature-epochs: 5 + proof-epochs: 5 pruner: enabled: true - interval: 1m \ No newline at end of file + interval: 10s diff --git a/e2e/tests/sync_test.go b/e2e/tests/sync_test.go index e7c4f33a..201ff855 100644 --- a/e2e/tests/sync_test.go +++ b/e2e/tests/sync_test.go @@ -3,8 +3,6 @@ package tests import ( "context" "net/http" - "os" - "strconv" "testing" "time" @@ -349,12 +347,6 @@ func waitForEpoch(ctx context.Context, client *evm.Client, targetEpoch symbiotic } func waitEpochTimeout() time.Duration { - epochSeconds := 60 - if raw := os.Getenv("EPOCH_TIME"); raw != "" { - if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 { - epochSeconds = parsed - } - } - + epochSeconds := readPositiveIntEnv("EPOCH_TIME", 60) return 2 * time.Duration(epochSeconds) * time.Second } diff --git a/e2e/tests/types_test.go b/e2e/tests/types_test.go index 536e51bf..b1c95b7e 100644 --- a/e2e/tests/types_test.go +++ b/e2e/tests/types_test.go @@ -21,6 +21,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" + "gopkg.in/yaml.v3" apiv1 "github.com/symbioticfi/relay/api/client/v1" votingpowerv1 "github.com/symbioticfi/relay/internal/gen/votingpower/v1" @@ -105,6 +106,26 @@ type RelaySidecarConfig struct { RequiredSymKey crypto.PrivateKey } +type relaySidecarSyncConfig struct { + Epochs int `yaml:"epochs"` +} + +type relaySidecarRetentionConfig struct { + ValsetEpochs int `yaml:"valset-epochs"` + SignatureEpochs int `yaml:"signature-epochs"` + ProofEpochs int `yaml:"proof-epochs"` +} + +type relaySidecarPrunerConfig struct { + Interval string `yaml:"interval"` +} + +type relaySidecarFileConfig struct { + Sync relaySidecarSyncConfig `yaml:"sync"` + Retention relaySidecarRetentionConfig `yaml:"retention"` + Pruner relaySidecarPrunerConfig `yaml:"pruner"` +} + func (i EnvInfo) GetSidecarConfigs() []RelaySidecarConfig { const basePrivateKey = 1000000000000000000 @@ -177,6 +198,38 @@ type tomlNetworkData struct { ValSetDriver uint64 `toml:"valSetDriver"` } +func loadEnvInfo(t *testing.T) EnvInfo { + t.Helper() + + envInfo := EnvInfo{} + err := envconfig.Process("", &envInfo) + require.NoError(t, err, "Failed to process environment variables") + + return envInfo +} + +func loadSidecarConfig(t *testing.T) relaySidecarFileConfig { + t.Helper() + + data, err := os.ReadFile("sidecar.yaml") + require.NoError(t, err, "Failed to read sidecar config file") + + var cfg relaySidecarFileConfig + err = yaml.Unmarshal(data, &cfg) + require.NoError(t, err, "Failed to parse sidecar config YAML") + + return cfg +} + +func readPositiveIntEnv(name string, fallback int) int { + if raw := os.Getenv(name); raw != "" { + if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 { + return parsed + } + } + return fallback +} + func loadDeploymentData(t *testing.T) RelayContractsData { t.Helper() @@ -252,9 +305,7 @@ func loadDeploymentData(t *testing.T) RelayContractsData { }, } - relayContracts.Env = EnvInfo{} - err = envconfig.Process("", &relayContracts.Env) - require.NoError(t, err, "Failed to process environment variables") + relayContracts.Env = loadEnvInfo(t) return relayContracts }