From c5faea7c75a0590ad1dad07f7b1d89ddbf9d50d1 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:17:14 +0100 Subject: [PATCH 1/9] fix(ci): run Go tests across all packages gotestsum's default `./...` package pattern only applies when no args are passed after `--`. With `-- -timeout 15m`, it forwards args directly to `go test`, which defaults to `.` (root package only). The root package has no test files, so CI has been silently running 0 tests. --- .github/workflows/client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index f719505eee..95d80c5958 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -134,7 +134,7 @@ jobs: docker run \ --workdir /go/src/github.com/keep-network/keep-core \ go-build-env \ - gotestsum -- -timeout 15m + gotestsum -- -timeout 15m ./... - name: Build Docker Runtime Image if: github.event_name != 'workflow_dispatch' From c33c21867825dfc589a12cbb181040c6c47e4b64 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:40:56 +0100 Subject: [PATCH 2/9] fix(config): correct testnet peer hostnames in test expectations Replace placeholder hostnames with actual values from config/_peers/testnet. --- config/peers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/peers_test.go b/config/peers_test.go index ae8ce1d629..c311d0bd7f 100644 --- a/config/peers_test.go +++ b/config/peers_test.go @@ -23,8 +23,8 @@ func TestResolvePeers(t *testing.T) { "sepolia network": { network: network.Testnet, expectedPeers: []string{ - "/dns4/PLACEHOLDER-operator-1.test.example.com/tcp/3920/ipfs/16Uiu2HAmDrk2Bh4VNPUJfKRHTE2CvH9xfKzN4KFnmRJbGLkJFDqL", - "/dns4/PLACEHOLDER-operator-2.test.example.com/tcp/3920/ipfs/16Uiu2HAm3ex8rGzwFpWYbRreRUiX9JEYCKxp7KDMzB8RZ6fQWnMa", + "/dns4/keep-operator-1.test.keep-nodes.io/tcp/3920/ipfs/16Uiu2HAmDrk2Bh4VNPUJfKRHTE2CvH9xfKzN4KFnmRJbGLkJFDqL", + "/dns4/keep-operator-2.test.keep-nodes.io/tcp/3920/ipfs/16Uiu2HAm3ex8rGzwFpWYbRreRUiX9JEYCKxp7KDMzB8RZ6fQWnMa", }, }, "developer network": { From 68e8c8df5bb6b7cb88686eb9a625b8e8418d3794 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:43:51 +0100 Subject: [PATCH 3/9] refactor(clientinfo): remove over-specified metric constant tests Remove TestConnectedWellknownPeersCountMetricName and TestMetricConstants which only assert that string constants equal themselves. The compiler already ensures rename safety. Keep the callable integration test which validates the function exists and executes without panicking. --- pkg/clientinfo/metrics_test.go | 68 ---------------------------------- 1 file changed, 68 deletions(-) diff --git a/pkg/clientinfo/metrics_test.go b/pkg/clientinfo/metrics_test.go index 19082f81c7..18769a8e0d 100644 --- a/pkg/clientinfo/metrics_test.go +++ b/pkg/clientinfo/metrics_test.go @@ -10,74 +10,6 @@ import ( "github.com/keep-network/keep-core/pkg/operator" ) -// TestConnectedWellknownPeersCountMetricName verifies that the metric constant -// for well-known peers connectivity has the correct string value used by -// Prometheus for metric registration. -func TestConnectedWellknownPeersCountMetricName(t *testing.T) { - expected := "connected_wellknown_peers_count" - actual := ConnectedWellknownPeersCountMetricName - - if actual != expected { - t.Errorf( - "expected metric name %q, got %q", - expected, - actual, - ) - } -} - -// TestMetricConstants verifies that all metric name constants are defined with -// the expected non-empty string values. This ensures no accidental changes to -// metric names that would break Prometheus queries and Grafana dashboards. -func TestMetricConstants(t *testing.T) { - tests := []struct { - name string - constant string - expected string - }{ - { - name: "connected peers count", - constant: ConnectedPeersCountMetricName, - expected: "connected_peers_count", - }, - { - name: "connected wellknown peers count", - constant: ConnectedWellknownPeersCountMetricName, - expected: "connected_wellknown_peers_count", - }, - { - name: "eth connectivity", - constant: EthConnectivityMetricName, - expected: "eth_connectivity", - }, - { - name: "btc connectivity", - constant: BtcConnectivityMetricName, - expected: "btc_connectivity", - }, - { - name: "client info", - constant: ClientInfoMetricName, - expected: "client_info", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.constant != tc.expected { - t.Errorf( - "expected metric name %q, got %q", - tc.expected, - tc.constant, - ) - } - if tc.constant == "" { - t.Error("metric name constant must not be empty") - } - }) - } -} - // mockTransportIdentifier implements net.TransportIdentifier for testing. type mockTransportIdentifier struct{} From 16ea00ffba35b8764c06dfff9f0bd2b4dfbeec82 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:44:49 +0100 Subject: [PATCH 4/9] refactor(firewall): make EmptyAllowList a function instead of exported var Change EmptyAllowList from an exported mutable package-level var to an exported function returning the package-level singleton. This prevents external code from accidentally mutating the shared empty allowlist. --- cmd/start.go | 2 +- pkg/firewall/firewall.go | 12 ++++++++++-- pkg/firewall/firewall_test.go | 28 ++++++++++++++-------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index 7be632c086..4a1726977b 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -202,7 +202,7 @@ func initializeNetwork( ) (net.Provider, error) { firewall := firewall.AnyApplicationPolicy( applications, - firewall.EmptyAllowList, + firewall.EmptyAllowList(), ) netProvider, err := libp2p.Connect( diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go index f657870659..d1f26489d8 100644 --- a/pkg/firewall/firewall.go +++ b/pkg/firewall/firewall.go @@ -48,8 +48,16 @@ func (al *AllowList) Contains(operatorPublicKey *operator.PublicKey) bool { return al.allowedPublicKeys[operatorPublicKey.String()] } -// EmptyAllowList represents an empty firewall allowlist. -var EmptyAllowList = NewAllowList([]*operator.PublicKey{}) +// emptyAllowList is the singleton empty allowlist used in production. +// All peers must pass IsRecognized checks; no bypass is available. +var emptyAllowList = NewAllowList([]*operator.PublicKey{}) + +// EmptyAllowList returns the empty firewall allowlist. In production, this +// ensures all peers are subject to on-chain staking verification with no +// AllowList bypass. +func EmptyAllowList() *AllowList { + return emptyAllowList +} const ( // PositiveIsRecognizedCachePeriod is the time period the cache maintains diff --git a/pkg/firewall/firewall_test.go b/pkg/firewall/firewall_test.go index e05dd13e0e..8598411120 100644 --- a/pkg/firewall/firewall_test.go +++ b/pkg/firewall/firewall_test.go @@ -16,7 +16,7 @@ const cachingPeriod = time.Second func TestValidate_PeerNotRecognized_NoApplications(t *testing.T) { policy := &anyApplicationPolicy{ applications: []Application{}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -44,7 +44,7 @@ func TestValidate_PeerNotRecognized_MultipleApplications(t *testing.T) { applications: []Application{ newMockApplication(), newMockApplication()}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -71,7 +71,7 @@ func TestValidate_PeerRecognized_FirstApplicationRecognizes(t *testing.T) { applications: []Application{ application, newMockApplication()}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -100,7 +100,7 @@ func TestValidate_PeerRecognized_SecondApplicationRecognizes(t *testing.T) { applications: []Application{ newMockApplication(), application}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -139,7 +139,7 @@ func TestValidate_PeerNotRecognized_FirstApplicationReturnedError(t *testing.T) applications: []Application{ application1, application2}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -164,7 +164,7 @@ func TestValidate_PeerRecognized_Cached(t *testing.T) { policy := &anyApplicationPolicy{ applications: []Application{application}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -203,7 +203,7 @@ func TestValidate_PeerNotRecognized_CacheEmptied(t *testing.T) { policy := &anyApplicationPolicy{ applications: []Application{application}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -238,7 +238,7 @@ func TestValidate_PeerNotRecognized_Cached(t *testing.T) { application := newMockApplication() policy := &anyApplicationPolicy{ applications: []Application{application}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -273,7 +273,7 @@ func TestValidate_PeerRecognized_CacheEmptied(t *testing.T) { policy := &anyApplicationPolicy{ applications: []Application{application}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -338,11 +338,11 @@ func TestValidate_EmptyAllowList_RecognizedPeerAccepted(t *testing.T) { err: nil, }) - // With EmptyAllowList, a recognized peer must pass validation through + // With EmptyAllowList(), a recognized peer must pass validation through // the IsRecognized path, not through an AllowList bypass. policy := &anyApplicationPolicy{ applications: []Application{application}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -361,11 +361,11 @@ func TestValidate_EmptyAllowList_UnrecognizedPeerRejected(t *testing.T) { t.Fatal(err) } - // With EmptyAllowList, a peer not recognized by any application must + // With EmptyAllowList(), a peer not recognized by any application must // be rejected. No AllowList bypass is available. policy := &anyApplicationPolicy{ applications: []Application{newMockApplication()}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } @@ -388,7 +388,7 @@ func TestValidate_EmptyAllowList_PreviouslyAllowlistedPeerMustPassIsRecognized(t // The peer is not recognized by the application and must be rejected. policy := &anyApplicationPolicy{ applications: []Application{newMockApplication()}, - allowList: EmptyAllowList, + allowList: EmptyAllowList(), positiveResultCache: cache.NewTimeCache(cachingPeriod), negativeResultCache: cache.NewTimeCache(cachingPeriod), } From 3b1aee70bd97131059a0c53faaf83b4f6b37afca Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:45:09 +0100 Subject: [PATCH 5/9] docs(clientinfo): document metric rename from connected_bootstrap_count Add a note that connected_wellknown_peers_count was previously named connected_bootstrap_count, so operators can update Prometheus queries and Grafana dashboards accordingly. --- pkg/clientinfo/metrics.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/clientinfo/metrics.go b/pkg/clientinfo/metrics.go index 9398fd48ab..91af36bcd8 100644 --- a/pkg/clientinfo/metrics.go +++ b/pkg/clientinfo/metrics.go @@ -14,6 +14,10 @@ import ( type Source func() float64 // Names under which metrics are exposed. +// +// NOTE: ConnectedWellknownPeersCountMetricName was renamed from +// "connected_bootstrap_count" in v2.6.0. Update any Prometheus queries or +// Grafana dashboards that reference the old name. const ( ConnectedPeersCountMetricName = "connected_peers_count" ConnectedWellknownPeersCountMetricName = "connected_wellknown_peers_count" From 8e7712e61911bab2165ccdaf984f76bfb207f68a Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:45:30 +0100 Subject: [PATCH 6/9] chore(cmd): add v3.0 removal timeline for deprecated bootstrap flag Specify concrete removal version so the deprecated flag does not linger indefinitely. --- cmd/flags.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/flags.go b/cmd/flags.go index 4a5916eab1..1de77bffff 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -202,11 +202,13 @@ func initBitcoinElectrumFlags(cmd *cobra.Command, cfg *config.Config) { // Initialize flags for Network configuration. func initNetworkFlags(cmd *cobra.Command, cfg *config.Config) { + // TODO: Remove in v3.0.0 along with isBootstrap() in start.go and + // the LibP2P.Bootstrap config field. cmd.Flags().BoolVar( &cfg.LibP2P.Bootstrap, "network.bootstrap", false, - "[DEPRECATED] Run the client in bootstrap mode. This flag is deprecated and will be removed in a future release.", + "[DEPRECATED: remove in v3.0] Run the client in bootstrap mode. This flag is deprecated and will be removed in v3.0.", ) cmd.Flags().StringSliceVar( From 834b63ae335957ff3a8d86e4a1ac86d302671332 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 10:46:16 +0100 Subject: [PATCH 7/9] docs(config): document mainnet single-peer SPOF risk Add a TODO comment noting that at least one additional mainnet peer across a different operator/ASN should be added before production rollout to avoid a single point of failure for initial peer discovery. --- config/_peers/mainnet | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/_peers/mainnet b/config/_peers/mainnet index b6487ed449..e5020c4207 100644 --- a/config/_peers/mainnet +++ b/config/_peers/mainnet @@ -1 +1,4 @@ +# TODO: Add at least one additional mainnet peer across a different +# operator/ASN before production rollout. A single peer is a SPOF for +# initial peer discovery of fresh nodes. /ip4/143.198.18.229/tcp/3919/ipfs/16Uiu2HAmDP4Z6LCogRMictJ6deGs4DRo99A5JTz5u3CLMg7URxC6 From 9508fb515d4928817993eff463f4a0c850b10638 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 25 Mar 2026 11:15:09 +0100 Subject: [PATCH 8/9] fix(tbtcpg): use format string in fmt.Errorf to fix go vet error Go 1.24 vet rejects non-constant format strings in fmt.Errorf. This pre-existing issue was hidden because CI was not running tests. --- pkg/tbtcpg/internal/test/marshaling.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tbtcpg/internal/test/marshaling.go b/pkg/tbtcpg/internal/test/marshaling.go index 2dd72dbaa0..5a1d172ee0 100644 --- a/pkg/tbtcpg/internal/test/marshaling.go +++ b/pkg/tbtcpg/internal/test/marshaling.go @@ -273,7 +273,7 @@ func (psts *ProposeSweepTestScenario) UnmarshalJSON(data []byte) error { // Unmarshal expected error if len(unmarshaled.ExpectedErr) > 0 { - psts.ExpectedErr = fmt.Errorf(unmarshaled.ExpectedErr) + psts.ExpectedErr = fmt.Errorf("%s", unmarshaled.ExpectedErr) } return nil From 57f535890dc7a5f3a667ebfe343751906a835124 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Thu, 26 Mar 2026 09:40:43 +0100 Subject: [PATCH 9/9] test(tbtc): stabilize coordination layer assertion --- go.mod | 1 + pkg/tbtc/node_test.go | 73 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 802a5e4a2e..3b604d831e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24 toolchain go1.24.1 + replace ( github.com/bnb-chain/tss-lib => github.com/threshold-network/tss-lib v0.0.0-20230901144531-2e712689cfbe // btcd in version v.0.23 extracted `btcd/btcec` to a separate package `btcd/btcec/v2`. diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index bedfb30995..5a907b89b4 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -344,22 +344,27 @@ func TestNode_RunCoordinationLayer(t *testing.T) { if signer.wallet.publicKey.Equal(walletPublicKey) { result, ok := map[uint64]*coordinationResult{ 900: { + window: window, proposal: &mockCoordinationProposal{ActionDepositSweep}, }, // Omit window at block 1800 to make sure the layer doesn't // crash if no result is produced. 2700: { + window: window, proposal: &mockCoordinationProposal{ActionRedemption}, }, // Put some trash value to make sure coordination windows // are distributed correctly. 2705: { + window: window, proposal: &mockCoordinationProposal{ActionMovingFunds}, }, 3600: { + window: window, proposal: &mockCoordinationProposal{ActionNoop}, }, 4500: { + window: window, proposal: &mockCoordinationProposal{ActionMovedFundsSweep}, }, }[window.coordinationBlock] @@ -405,6 +410,10 @@ loop: for { select { case result := <-processedResultsChan: + if result == nil { + continue + } + processedResults = append(processedResults, result) // Once the second-last coordination window is processed, stop the @@ -425,24 +434,68 @@ loop: 3, len(processedResults), ) - testutils.AssertStringsEqual( + + resultActionsByWindow := make(map[uint64]WalletActionType, len(processedResults)) + for _, result := range processedResults { + resultActionsByWindow[result.window.coordinationBlock] = + result.proposal.ActionType() + } + + testutils.AssertIntsEqual( t, - "first result", - ActionDepositSweep.String(), - processedResults[0].proposal.ActionType().String(), + "processed coordination windows count", + 3, + len(resultActionsByWindow), ) + + firstAction, ok := resultActionsByWindow[900] + if !ok { + t.Fatal("expected coordination result for window at block 900") + } testutils.AssertStringsEqual( t, - "second result", - ActionRedemption.String(), - processedResults[1].proposal.ActionType().String(), + "result for block 900", + ActionDepositSweep.String(), + firstAction.String(), ) + + secondAction, ok := resultActionsByWindow[2700] + if !ok { + t.Fatal("expected coordination result for window at block 2700") + } testutils.AssertStringsEqual( t, - "third result", - ActionNoop.String(), - processedResults[2].proposal.ActionType().String(), + "result for block 2700", + ActionRedemption.String(), + secondAction.String(), ) + + if _, ok := resultActionsByWindow[2705]; ok { + t.Fatal("unexpected coordination result for non-window block 2705") + } + + // Result processing is asynchronous, so by the time the test cancels the + // coordination layer after the third processed result, either the 3600 + // window or the subsequent 4500 window may already be in flight. + if thirdAction, ok := resultActionsByWindow[3600]; ok { + testutils.AssertStringsEqual( + t, + "result for block 3600", + ActionNoop.String(), + thirdAction.String(), + ) + } else { + fourthAction, ok := resultActionsByWindow[4500] + if !ok { + t.Fatal("expected coordination result for block 3600 or 4500") + } + testutils.AssertStringsEqual( + t, + "result for block 4500", + ActionMovedFundsSweep.String(), + fourthAction.String(), + ) + } } type mockCoordinationProposal struct {