Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5be2974
feat: add ValidatorIndex method and log our index
ilyasymbiotic Mar 18, 2026
44592e9
feat: enhance aggregation logging and error handling in aggregator
ilyasymbiotic Mar 18, 2026
b55e112
feat: add proof catch-up configuration and implement catch-up loop in…
ilyasymbiotic Mar 18, 2026
8d7a8d0
refactor: update PrintTreeValidator and related functions to use Vali…
ilyasymbiotic Mar 19, 2026
96b6b09
refactor: implement aggregation request handling and worker queue in …
ilyasymbiotic Mar 19, 2026
f0ebde8
refactor: update aggregation configuration structure and related flags
ilyasymbiotic Mar 19, 2026
f256f9e
fix: doc
ilyasymbiotic Mar 19, 2026
82c7e0a
refactor: explicitly save aggregation proof
ilyasymbiotic Mar 19, 2026
cb969a2
refactor: remove max proofs per cycle configuration and related logic
ilyasymbiotic Mar 19, 2026
9641d20
refactor: remove max proofs per cycle configuration and related logic
ilyasymbiotic Mar 19, 2026
f46d8fa
refactor: add cross-epoch aggregation support in configuration and logic
ilyasymbiotic Mar 23, 2026
671d636
feat: bumped lib and go versions, fixed linter errors
ilyasymbiotic Mar 24, 2026
a2b8e94
fix: removed unneeded log
ilyasymbiotic Mar 24, 2026
d2bb2d5
refactor: update Go version to 1.26.1 across configuration files
ilyasymbiotic Mar 24, 2026
a2a28ba
refactor: add SelfP2PID to config and handle self-sent aggregation pr…
ilyasymbiotic Mar 25, 2026
5138395
refactor: renamed variable and added comment to proof catch up logic
ilyasymbiotic Mar 25, 2026
6524f36
refactor: replace queue.Add with EnqueueRequestID for clarity
ilyasymbiotic Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@v5.5.0
with:
go-version: 1.25.3
go-version: 1.26.1

- name: Get version
id: get_version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
workflow_call:

env:
GO_VERSION: "1.25.3"
GO_VERSION: "1.26.1"
NODE_VERSION: "22"
FOUNDRY_VERSION: "v1.3.1"

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.claude/
.local/
.cache
.gocache

CLAUDE.md
go.sum
Expand Down
4 changes: 3 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version: "2"
run:
go: "1.25"
go: "1.26"
build-tags:
- integration
tests: true
Expand Down Expand Up @@ -277,6 +277,8 @@ linters:
text: 'shadow: declaration of "err" shadows declaration at line'
- path: (.+)\.go$
text: G115
- path: (.+)\.go$
text: G706
- path: cmd/utils/
text: "use of `fmt.Printf` forbidden"
paths:
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Exposes gRPC API for external clients to create signature requests, query proofs

### Prerequisites

- **Go 1.25.3 or later**
- **Go 1.26.1 or later**
- **Node.js** (for contract compilation in E2E tests)
- **Foundry/Forge** (for Solidity contracts)
- **Docker & Docker Compose** (for E2E testing)
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.25 AS builder
FROM golang:1.26 AS builder

WORKDIR /app

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ buf-lint:

.PHONY: go-lint
go-lint:
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 -v run ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 -v run ./...

.PHONY: go-lint-fix
go-lint-fix:
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 -v run ./... --fix
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 -v run ./... --fix

.PHONY: generate
generate: install-tools generate-mocks generate-api-types generate-votingpower-types generate-client-types generate-p2p-types generate-badger-types gen-abi generate-cli-docs
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Docker Hub: https://hub.docker.com/r/symbioticfi/relay

### Dependencies

- **Go 1.25.3+**
- **Go 1.26.1+**
- **Docker & Docker Compose** (for local setup and E2E tests)
- **Node.js & Foundry** (for contract compilation in E2E)

Expand Down
2 changes: 1 addition & 1 deletion api/client/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The example shows how to:

Before running the examples, ensure you have:

- **[Go 1.25 or later](https://golang.org/doc/install)** installed
- **[Go 1.26 or later](https://golang.org/doc/install)** installed
- **Access to a running Symbiotic Relay Network**
- **Network connectivity** to the relay server
- **Valid key configurations** on the relay server (for signing operations)
Expand Down
48 changes: 34 additions & 14 deletions cmd/relay/root/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,18 @@ func runApp(ctx context.Context) error {
return errors.Errorf("failed to create syncer: %w", err)
}

p2pService, discoveryService, err := initP2PService(ctx, cfg, keyProvider, syncProvider, mtr)
if err != nil {
return errors.Errorf("failed to create p2p service: %w", err)
}
defer p2pService.Close()

signer, err := signerApp.NewSignerApp(signerApp.Config{
KeyProvider: keyProvider,
Repo: repo,
EntityProcessor: entityProcessor,
Metrics: mtr,
SelfP2PID: p2pService.ID(),
})
if err != nil {
return errors.Errorf("failed to create signer app: %w", err)
Expand Down Expand Up @@ -310,12 +317,6 @@ func runApp(ctx context.Context) error {
return nil
})

p2pService, discoveryService, err := initP2PService(ctx, cfg, keyProvider, syncProvider, mtr)
if err != nil {
return errors.Errorf("failed to create p2p service: %w", err)
}
defer p2pService.Close()

// Initialize tracing with instance ID from P2P service
if cfg.Tracing.Enabled {
tracer, err := tracing.New(ctx, tracing.Config{
Expand Down Expand Up @@ -452,13 +453,22 @@ func runApp(ctx context.Context) error {

var aggApp *aggregatorApp.AggregatorApp
aggApp, err = aggregatorApp.NewAggregatorApp(aggregatorApp.Config{
Repo: repo,
P2PClient: p2pService,
Aggregator: agg,
Metrics: mtr,
AggregationPolicy: aggPolicy,
KeyProvider: keyProvider,
ForceAggregator: cfg.ForceRole.Aggregator,
Repo: repo,
P2PClient: p2pService,
Aggregator: agg,
EntityProcessor: entityProcessor,
Metrics: mtr,
AggregationPolicy: aggPolicy,
KeyProvider: keyProvider,
ForceAggregator: cfg.ForceRole.Aggregator,
CrossEpochAggregation: cfg.Aggregation.CrossEpochAggregation,
ProofCatchup: aggregatorApp.ProofCatchupConfig{
Enabled: cfg.Aggregation.Catchup.Enabled,
Interval: cfg.Aggregation.Catchup.Interval,
EpochsToCheck: cfg.Aggregation.Catchup.EpochsToCheck,
EpochsOffset: cfg.Aggregation.Catchup.EpochsOffset,
MaxRequestsPerCycle: cfg.Aggregation.Catchup.MaxRequestsPerCycle,
},
})
if err != nil {
return errors.Errorf("failed to create aggregator app: %w", err)
Expand Down Expand Up @@ -547,7 +557,17 @@ func runApp(ctx context.Context) error {
})

eg.Go(func() error {
return aggApp.TryAggregateRequestsWithoutProof(ctx)
err := aggApp.HandleAggregationRequests(egCtx, cfg.Aggregation.WorkerCount)
if err != nil && !errors.Is(err, context.Canceled) {
slog.ErrorContext(ctx, "Aggregation requests handler failed", "error", err)
return errors.Errorf("failed to handle aggregation requests: %w", err)
}
slog.InfoContext(ctx, "Aggregation requests handler stopped")
return nil
})

eg.Go(func() error {
return aggApp.StartCatchupLoop(egCtx)
})

eg.Go(func() error {
Expand Down
46 changes: 44 additions & 2 deletions cmd/relay/root/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (

"github.com/go-errors/errors"
"github.com/go-playground/validator/v10"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -53,6 +52,7 @@ type config struct {
ForceRole ForceRole `mapstructure:"force-role"`
Retention RetentionConfig `mapstructure:"retention"`
Pruner PrunerConfig `mapstructure:"pruner"`
Aggregation AggregationConfig `mapstructure:"aggregation"`
Tracing TracingConfig `mapstructure:"tracing"`
Badger BadgerConfig `mapstructure:"badger"`
Bbolt BboltConfig `mapstructure:"bbolt"`
Expand Down Expand Up @@ -245,6 +245,20 @@ type PrunerConfig struct {
Interval time.Duration `mapstructure:"interval"`
}

type AggregationConfig struct {
WorkerCount int `mapstructure:"worker-count" validate:"min=1"`
CrossEpochAggregation bool `mapstructure:"cross-epoch-aggregation"`
Catchup AggregationCatchupConfig `mapstructure:"catchup"`
}

type AggregationCatchupConfig struct {
Enabled bool `mapstructure:"enabled"`
Interval time.Duration `mapstructure:"interval"`
EpochsToCheck int `mapstructure:"epochs-to-check"`
EpochsOffset int `mapstructure:"epochs-offset"`
MaxRequestsPerCycle int `mapstructure:"max-requests-per-cycle"`
}

type TracingConfig struct {
Enabled bool `mapstructure:"enabled"`
Endpoint string `mapstructure:"endpoint"`
Expand Down Expand Up @@ -351,6 +365,13 @@ func addRootFlags(cmd *cobra.Command) {
rootCmd.PersistentFlags().Uint64("retention.signature-epochs", 0, "Number of historical signature epochs to retain (0 = unlimited)")
rootCmd.PersistentFlags().Bool("pruner.enabled", false, "Enable automatic pruning of old epoch data (default: false)")
rootCmd.PersistentFlags().Duration("pruner.interval", time.Hour, "How often to run pruning (default: 1h)")
rootCmd.PersistentFlags().Int("aggregation.worker-count", 10, "Max simultaneous proof aggregations, reduce for ZK circuits with high memory and cpu usage")
rootCmd.PersistentFlags().Bool("aggregation.cross-epoch-aggregation", false, "Allow latest-epoch aggregators to aggregate proofs for older epochs when original aggregators are offline")
rootCmd.PersistentFlags().Bool("aggregation.catchup.enabled", true, "Enable periodic aggregation catch-up loop")
rootCmd.PersistentFlags().Duration("aggregation.catchup.interval", time.Minute, "How often to run aggregation catch-up")
rootCmd.PersistentFlags().Int("aggregation.catchup.epochs-to-check", 20, "Number of epochs to scan per catch-up cycle")
rootCmd.PersistentFlags().Int("aggregation.catchup.epochs-offset", 0, "Epochs back from latest to start scanning")
rootCmd.PersistentFlags().Int("aggregation.catchup.max-requests-per-cycle", 0, "Max requests to check per cycle (0 = unlimited)")
rootCmd.PersistentFlags().Bool("tracing.enabled", false, "Enable distributed tracing")
rootCmd.PersistentFlags().String("tracing.endpoint", "localhost:4317", "OTLP endpoint for tracing (e.g., Jaeger)")
rootCmd.PersistentFlags().Float64("tracing.sample-rate", 1.0, "Trace sampling rate (0.0 to 1.0)")
Expand Down Expand Up @@ -552,6 +573,27 @@ func initConfig(cmd *cobra.Command, _ []string) error {
if err := v.BindPFlag("pruner.interval", cmd.PersistentFlags().Lookup("pruner.interval")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.worker-count", cmd.PersistentFlags().Lookup("aggregation.worker-count")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.cross-epoch-aggregation", cmd.PersistentFlags().Lookup("aggregation.cross-epoch-aggregation")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.catchup.enabled", cmd.PersistentFlags().Lookup("aggregation.catchup.enabled")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.catchup.interval", cmd.PersistentFlags().Lookup("aggregation.catchup.interval")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.catchup.epochs-to-check", cmd.PersistentFlags().Lookup("aggregation.catchup.epochs-to-check")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.catchup.epochs-offset", cmd.PersistentFlags().Lookup("aggregation.catchup.epochs-offset")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("aggregation.catchup.max-requests-per-cycle", cmd.PersistentFlags().Lookup("aggregation.catchup.max-requests-per-cycle")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
if err := v.BindPFlag("tracing.enabled", cmd.PersistentFlags().Lookup("tracing.enabled")); err != nil {
return errors.Errorf("failed to bind flag: %w", err)
}
Expand Down Expand Up @@ -593,7 +635,7 @@ func initConfig(cmd *cobra.Command, _ []string) error {
}

err := v.ReadInConfig()
if err != nil && !errors.Is(err, viper.ConfigFileNotFoundError{}) && !errors.As(err, lo.ToPtr(&fs.PathError{})) {
if err != nil && !errors.Is(err, viper.ConfigFileNotFoundError{}) && !errors.As(err, new(fs.PathError)) {
return errors.Errorf("failed to read config file: %w", err)
}

Expand Down
14 changes: 12 additions & 2 deletions cmd/utils/cmd-helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func GetPassword() (string, error) {
return string(passwordBytes), nil
}

func PrintTreeValidator(leveledList pterm.LeveledList, validator symbiotic.Validator, totalVotingPower *big.Int) pterm.LeveledList {
func PrintTreeValidator(leveledList pterm.LeveledList, validator symbiotic.Validator, valset symbiotic.ValidatorSet) pterm.LeveledList {
leveledList = append(leveledList, pterm.LeveledListItem{
Level: 0,
Text: fmt.Sprintf("Validator: %s", validator.Operator.String()),
Expand All @@ -93,7 +93,7 @@ func PrintTreeValidator(leveledList pterm.LeveledList, validator symbiotic.Valid
Level: 1,
Text: fmt.Sprintf("Voting Power: %d (%0.3f%%)",
validator.VotingPower.Int,
GetPct(validator.VotingPower.Int, totalVotingPower),
GetPct(validator.VotingPower.Int, valset.GetTotalActiveVotingPower().Int),
),
})

Expand Down Expand Up @@ -140,6 +140,16 @@ func PrintTreeValidator(leveledList pterm.LeveledList, validator symbiotic.Valid
Level: 3,
Text: fmt.Sprintf("PubKey: %s", pubkeyText),
})
if key.Tag.Type().AggregationKey() {
leveledList = append(leveledList, pterm.LeveledListItem{
Level: 3,
Text: fmt.Sprintf("Aggregator: %t", valset.IsAggregator(key.Payload)),
})
leveledList = append(leveledList, pterm.LeveledListItem{
Level: 3,
Text: fmt.Sprintf("Committer: %t", valset.IsCommitter(key.Payload)),
})
}
}

return leveledList
Expand Down
4 changes: 2 additions & 2 deletions cmd/utils/network/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,13 @@ var infoCmd = &cobra.Command{
if infoFlags.ValidatorsFull {
panels = append(panels, []pterm.Panel{
{Data: pterm.DefaultBox.WithTitle("Validators").Sprint(
printValidatorsTree(&valset),
printValidatorsTree(valset),
)},
})
} else if infoFlags.Validators {
panels = append(panels, []pterm.Panel{
{Data: pterm.DefaultBox.WithTitle("Validators").Sprint(
printValidatorsTable(&valset),
printValidatorsTable(valset),
)},
})
}
Expand Down
38 changes: 17 additions & 21 deletions cmd/utils/network/printers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ type settlementReplicaData struct {
}

func printAddresses(driver symbiotic.CrossChainAddress, networkConfig *symbiotic.NetworkConfig) string {
addressesTableData := pterm.TableData{
{"Type", "Chain ID", "Address"},
{"Driver", strconv.FormatUint(driver.ChainId, 10), driver.Address.String()},
{"KeyRegistry", strconv.FormatUint(networkConfig.KeysProvider.ChainId, 10), networkConfig.KeysProvider.Address.String()},
}
addressesTableData := make(pterm.TableData, 0, 3+len(networkConfig.VotingPowerProviders)+len(networkConfig.Settlements))
addressesTableData = append(addressesTableData,
[]string{"Type", "Chain ID", "Address"},
[]string{"Driver", strconv.FormatUint(driver.ChainId, 10), driver.Address.String()},
[]string{"KeyRegistry", strconv.FormatUint(networkConfig.KeysProvider.ChainId, 10), networkConfig.KeysProvider.Address.String()},
)
for _, provider := range networkConfig.VotingPowerProviders {
addressesTableData = append(addressesTableData, []string{
"VotingPowerProvider",
Expand Down Expand Up @@ -82,28 +83,23 @@ func printNetworkInfo(epoch symbiotic.Epoch, epochStart symbiotic.Timestamp, net
return infoText
}

func printValidatorsTree(valset *symbiotic.ValidatorSet) string {
func printValidatorsTree(valset symbiotic.ValidatorSet) string {
leveledList := pterm.LeveledList{}

validators := valset.Validators

for _, validator := range validators {
leveledList = cmdhelpers.PrintTreeValidator(leveledList, validator, valset.GetTotalActiveVotingPower().Int)
for _, validator := range valset.Validators {
leveledList = cmdhelpers.PrintTreeValidator(leveledList, validator, valset)
}

// Render the tree structure using the default tree printer.
text, _ := pterm.DefaultTree.WithRoot(putils.TreeFromLeveledList(leveledList)).Srender()
return text
}

func printValidatorsTable(valset *symbiotic.ValidatorSet) string {
tableData := pterm.TableData{
{"Address", "Status", "Voting Power", "Vaults", "Keys"},
}
func printValidatorsTable(valset symbiotic.ValidatorSet) string {
tableData := make(pterm.TableData, 0, 1+len(valset.Validators))
tableData = append(tableData, []string{"Address", "Status", "Voting Power", "Vaults", "Keys"})

validators := valset.Validators

for _, validator := range validators {
for _, validator := range valset.Validators {
status := pterm.FgRed.Sprint("inactive")
if validator.IsActive {
status = pterm.FgGreen.Sprint("active")
Expand Down Expand Up @@ -144,7 +140,8 @@ func printHeaderTable(header symbiotic.ValidatorSetHeader) string {
}

func printExtraDataTable(extraData symbiotic.ExtraDataList) string {
extraDataTable := pterm.TableData{{"Key", "Value"}}
extraDataTable := make(pterm.TableData, 0, 1+len(extraData))
extraDataTable = append(extraDataTable, []string{"Key", "Value"})

for _, extraData := range extraData {
extraDataTable = append(extraDataTable, []string{
Expand Down Expand Up @@ -212,9 +209,8 @@ func printSettlementData(
networkConfig symbiotic.NetworkConfig,
settlementData []settlementReplicaData,
) string {
tableData := pterm.TableData{
{"Address", "ChainID", "Status", "Integrity", "Latest Committed Epoch", "Missed Epochs", "Header hash"},
}
tableData := make(pterm.TableData, 0, 1+len(networkConfig.Settlements))
tableData = append(tableData, []string{"Address", "ChainID", "Status", "Integrity", "Latest Committed Epoch", "Missed Epochs", "Header hash"})

for i, settlement := range networkConfig.Settlements {
hash := "N/A"
Expand Down
2 changes: 1 addition & 1 deletion cmd/utils/operator/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ var infoCmd = &cobra.Command{
}

leveledList := pterm.LeveledList{}
leveledList = cmdhelpers.PrintTreeValidator(leveledList, validator, valset.GetTotalActiveVotingPower().Int)
leveledList = cmdhelpers.PrintTreeValidator(leveledList, validator, valset)
text, _ := pterm.DefaultTree.WithRoot(putils.TreeFromLeveledList(leveledList)).Srender()
panels := pterm.Panels{{{Data: pterm.DefaultBox.WithTitle("Operator info").Sprint(text)}}}
pterm.DefaultPanel.WithPanels(panels).Render()
Expand Down
Loading
Loading