diff --git a/.github/workflows/govulncheck.yaml b/.github/workflows/govulncheck.yaml index fc8cc614d..175d3d294 100644 --- a/.github/workflows/govulncheck.yaml +++ b/.github/workflows/govulncheck.yaml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: '1.25.x' + go-version: '1.25.10' check-latest: true - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install govulncheck diff --git a/go.mod b/go.mod index e2b8fc903..cd3e2dc0f 100644 --- a/go.mod +++ b/go.mod @@ -60,8 +60,8 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 go.uber.org/mock v0.5.2 - golang.org/x/crypto v0.47.0 - golang.org/x/sync v0.19.0 + golang.org/x/crypto v0.50.0 + golang.org/x/sync v0.20.0 golang.org/x/time v0.13.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 @@ -297,13 +297,13 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index 426d2435b..c8f64a93c 100644 --- a/go.sum +++ b/go.sum @@ -883,8 +883,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= @@ -892,8 +892,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -914,8 +914,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -925,8 +925,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -959,10 +959,10 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -970,8 +970,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -981,8 +981,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -993,8 +993,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/discovery/p2p/xatu/xatu.go b/pkg/discovery/p2p/xatu/xatu.go index 67a01c959..6ebc0cfc0 100644 --- a/pkg/discovery/p2p/xatu/xatu.go +++ b/pkg/discovery/p2p/xatu/xatu.go @@ -224,16 +224,36 @@ func (c *Coordinator) startCrons(ctx context.Context) error { return } - if err = c.discV5.UpdateBootNodes([]string{res.NodeRecord}); err != nil { - c.log.WithError(err).Error("Failed to update discV5 boot nodes") + nodeRecord := res.GetNodeRecord() + + if c.config.DiscV4 && c.discV4 != nil { + if err = c.discV4.UpdateBootNodes([]string{nodeRecord}); err != nil { + c.log.WithError(err).Error("Failed to update discV4 boot nodes") + + return + } + + if err = c.discV4.Start(ctx); err != nil { + c.log.WithError(err).Error("Failed to start discV4") + + return + } return } - if err := c.discV5.Start(ctx); err != nil { - c.log.WithError(err).Error("Failed to start discV5") + if c.config.DiscV5 && c.discV5 != nil { + if err = c.discV5.UpdateBootNodes([]string{nodeRecord}); err != nil { + c.log.WithError(err).Error("Failed to update discV5 boot nodes") - return + return + } + + if err := c.discV5.Start(ctx); err != nil { + c.log.WithError(err).Error("Failed to start discV5") + + return + } } }, ctx, diff --git a/pkg/server/persistence/integration_test.go b/pkg/server/persistence/integration_test.go index 80f4eefa4..7fc9fe42f 100644 --- a/pkg/server/persistence/integration_test.go +++ b/pkg/server/persistence/integration_test.go @@ -588,6 +588,83 @@ func TestPersistenceIntegration(t *testing.T) { assert.GreaterOrEqual(t, len(available), 1) }) + t.Run("ListAvailableExecutionNodeRecordsBalancesImplementations", func(t *testing.T) { + networkID := uint64(424242) + forkIDHash := []byte{0xaa, 0xbb, 0xcc, 0xdd} + baseTime := time.Now().Add(-2 * time.Hour) + + testData := []struct { + enr string + name string + createTime time.Time + }{ + { + enr: "enr:-IS4QBalancedGeth1-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + name: "Geth/v1.0.0", + createTime: baseTime, + }, + { + enr: "enr:-IS4QBalancedGeth2-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + name: "Geth/v1.0.1", + createTime: baseTime.Add(time.Minute), + }, + { + enr: "enr:-IS4QBalancedGeth3-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + name: "Geth/v1.0.2", + createTime: baseTime.Add(2 * time.Minute), + }, + { + enr: "enr:-IS4QBalancedBesu1-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + name: "Besu/v1.0.0", + createTime: baseTime.Add(time.Hour), + }, + { + enr: "enr:-IS4QBalancedReth1-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + name: "Reth/v1.0.0", + createTime: baseTime.Add(time.Hour + time.Minute), + }, + } + + for _, td := range testData { + nodeRecord := createTestNodeRecord(td.enr) + nodeRecord.ETH2 = nil + err := tc.Client.InsertNodeRecords(ctx, []*node.Record{nodeRecord}) + require.NoError(t, err) + + execRecord := createTestExecutionRecord(td.enr) + execRecord.Name = td.name + execRecord.NetworkID = fmt.Sprint(networkID) + execRecord.ForkIDHash = forkIDHash + err = tc.Client.InsertNodeRecordExecution(ctx, execRecord) + require.NoError(t, err) + + _, err = tc.DB.ExecContext(ctx, "UPDATE node_record_execution SET create_time = $1 WHERE enr = $2", td.createTime, td.enr) + require.NoError(t, err) + } + + available, err := tc.Client.ListAvailableExecutionNodeRecords(ctx, "requesting-client", nil, []uint64{networkID}, [][]byte{forkIDHash}, nil, 3) + require.NoError(t, err) + require.Len(t, available, 3) + + availableByENR := map[string]bool{} + for _, enr := range available { + availableByENR[*enr] = true + } + + assert.True(t, availableByENR["enr:-IS4QBalancedBesu1-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"]) + assert.True(t, availableByENR["enr:-IS4QBalancedReth1-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"]) + + gethRecords := 0 + + for _, td := range testData { + if strings.HasPrefix(td.name, "Geth/") && availableByENR[td.enr] { + gethRecords++ + } + } + + assert.Equal(t, 1, gethRecords) + }) + t.Run("ListAvailableConsensusNodeRecords", func(t *testing.T) { // Create consensus node records with different characteristics testData := []struct { diff --git a/pkg/server/persistence/node_record_activity.go b/pkg/server/persistence/node_record_activity.go index ae97e0567..5fcda318e 100644 --- a/pkg/server/persistence/node_record_activity.go +++ b/pkg/server/persistence/node_record_activity.go @@ -3,6 +3,7 @@ package persistence import ( "context" "fmt" + "strings" "time" "github.com/ethpandaops/xatu/pkg/server/persistence/node" @@ -27,6 +28,15 @@ type AvailableConsensusNodeRecord struct { var availableConsensusNodeRecordStruct = sqlbuilder.NewStruct(new(AvailableConsensusNodeRecord)).For(sqlbuilder.PostgreSQL) +var executionClientImplementations = []string{ + "besu", + "erigon", + "ethrex", + "geth", + "nethermind", + "reth", +} + func (c *Client) UpsertNodeRecordActivities(ctx context.Context, activities []*node.Activity) error { // Return early if there are no activities to upsert if len(activities) == 0 { @@ -57,6 +67,84 @@ func (c *Client) UpsertNodeRecordActivities(ctx context.Context, activities []*n } func (c *Client) ListAvailableExecutionNodeRecords(ctx context.Context, clientID string, ignoredNodeRecords []string, networkIds []uint64, forkIDHashes [][]byte, capabilities []string, limit int) ([]*string, error) { + if limit <= 0 { + return []*string{}, nil + } + + candidatesByImplementation := make(map[string][]*string, len(executionClientImplementations)) + for _, implementation := range executionClientImplementations { + records, err := c.listAvailableExecutionNodeRecords(ctx, clientID, ignoredNodeRecords, networkIds, forkIDHashes, capabilities, implementation, limit) + if err != nil { + return nil, err + } + + candidatesByImplementation[implementation] = records + } + + nodeRecords := make([]*string, 0, limit) + seen := make(map[string]struct{}, limit) + + addRecord := func(record *string) bool { + if record == nil { + return len(nodeRecords) < limit + } + + if _, ok := seen[*record]; ok { + return len(nodeRecords) < limit + } + + seen[*record] = struct{}{} + nodeRecords = append(nodeRecords, record) + + return len(nodeRecords) < limit + } + + for i := 0; len(nodeRecords) < limit; i++ { + added := false + + for _, implementation := range executionClientImplementations { + candidates := candidatesByImplementation[implementation] + if i >= len(candidates) { + continue + } + + added = true + + if !addRecord(candidates[i]) { + break + } + } + + if !added { + break + } + } + + if len(nodeRecords) < limit { + fillIgnoredNodeRecords := append([]string{}, ignoredNodeRecords...) + + for _, record := range nodeRecords { + if record != nil { + fillIgnoredNodeRecords = append(fillIgnoredNodeRecords, *record) + } + } + + records, err := c.listAvailableExecutionNodeRecords(ctx, clientID, fillIgnoredNodeRecords, networkIds, forkIDHashes, capabilities, "", limit-len(nodeRecords)) + if err != nil { + return nil, err + } + + for _, record := range records { + if !addRecord(record) { + break + } + } + } + + return nodeRecords, nil +} + +func (c *Client) listAvailableExecutionNodeRecords(ctx context.Context, clientID string, ignoredNodeRecords []string, networkIds []uint64, forkIDHashes [][]byte, capabilities []string, implementation string, limit int) ([]*string, error) { inr := make([]any, 0, len(ignoredNodeRecords)) for _, enr := range ignoredNodeRecords { inr = append(inr, enr) @@ -129,6 +217,14 @@ func (c *Client) ListAvailableExecutionNodeRecords(ctx context.Context, clientID } } + if implementation != "" { + normalizedImplementation := strings.ToLower(implementation) + where = append(where, sb.Or( + sb.Equal("LOWER(nre.name)", normalizedImplementation), + sb.Like("LOWER(nre.name)", normalizedImplementation+"/%"), + )) + } + sb.Where(where...) sb.GroupBy("nre.enr") sb.OrderBy("last_connect_time ASC")