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 {