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' 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( 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/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 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": { 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/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" 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{} 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), } 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 { 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