Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e4d624d
refactor(clientinfo): split registerAllMetrics and trim redundant com…
piotr-roslaniec Jul 22, 2026
7ff8f47
refactor(tbtc): remove dead coordination-failed flag and redundant co…
piotr-roslaniec Jul 22, 2026
6d502e7
refactor(tbtc): introduce named types and dedupe repeated constructs
piotr-roslaniec Jul 22, 2026
4802d2b
fix(tbtcpg,protocol): preserve error causes and align naming/docs
piotr-roslaniec Jul 22, 2026
4d977d3
fix(tbtc): preserve final signing group resolution error
piotr-roslaniec Jul 22, 2026
7b5dfd8
refactor(spv): use typed metric constants and drop passthrough wrapper
piotr-roslaniec Jul 22, 2026
1b7a65c
style: normalize marshaling filename spelling
piotr-roslaniec Jul 22, 2026
8fc66fb
fix(tbtc): reattach ValidateMovingFundsSafetyMargin godoc
piotr-roslaniec Jul 22, 2026
2885b0a
docs: describe tools.go pins as build-time-only deps
piotr-roslaniec Jul 22, 2026
02169cc
style: use idiomatic zero-value and any declarations
piotr-roslaniec Jul 22, 2026
25e8d6a
test(ethereum): cover timestamp-based block search
piotr-roslaniec Jul 22, 2026
3e07e8e
refactor(libp2p): name the repeated metrics-recorder interface
piotr-roslaniec Jul 22, 2026
e87ff60
refactor(tbtcpg): extract shared capped-fee estimation
piotr-roslaniec Jul 22, 2026
4426a0c
refactor(spv): dedupe unproven-transaction search and drop dead metri…
piotr-roslaniec Jul 22, 2026
e6b688f
fix(beacon): abort protocol setup when channel filter cannot be set
piotr-roslaniec Jul 22, 2026
1e30c3b
refactor(gjkr): drop test-only receivedQualifiedSharesT field
piotr-roslaniec Jul 22, 2026
f84f483
test(tbtc): synchronize follower routine instead of sleeping
piotr-roslaniec Jul 22, 2026
978157b
fix(libp2p): keep SetMetricsRecorder param anonymous for cmd wiring
piotr-roslaniec Jul 22, 2026
f4050e3
test(spv): cover shared unproven-transaction search helper
piotr-roslaniec Jul 22, 2026
1c91e18
refactor(beacon): extract shared broadcast-channel filter helper
piotr-roslaniec Jul 22, 2026
e363e77
test(beacon): cover broadcast-channel filter abort path
piotr-roslaniec Jul 22, 2026
eb9e67a
fix(spv): clamp unproven-search start block to avoid uint64 underflow
piotr-roslaniec Jul 23, 2026
7d669ca
style(spv): drop stray blank line left by metrics-singleton removal
piotr-roslaniec Jul 23, 2026
82ed79a
test(ethereum): cover timestamp forward-compensation branch
piotr-roslaniec Jul 23, 2026
25fd084
refactor(ethereum): use errors.New for out-of-range test sentinel
piotr-roslaniec Jul 23, 2026
5465e7d
docs(gjkr): drop stale duplicated doc comment on shares-group helper
piotr-roslaniec Jul 23, 2026
b18b351
feat(maintainer): record SPV proof-submission metrics
piotr-roslaniec Jul 23, 2026
4b51d9a
test(maintainer): cover metrics gate, redemption success, assemble-fa…
piotr-roslaniec Jul 23, 2026
a01a37e
docs(maintainer): document SPV proof metrics; clarify recorder nil co…
piotr-roslaniec Jul 23, 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
43 changes: 42 additions & 1 deletion cmd/maintainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import (

"github.com/spf13/cobra"

"github.com/keep-network/keep-core/build"
"github.com/keep-network/keep-core/config"
"github.com/keep-network/keep-core/pkg/bitcoin/electrum"
"github.com/keep-network/keep-core/pkg/chain"
"github.com/keep-network/keep-core/pkg/chain/ethereum"
"github.com/keep-network/keep-core/pkg/clientinfo"
"github.com/keep-network/keep-core/pkg/maintainer"
"github.com/keep-network/keep-core/pkg/maintainer/spv"
)

// MaintainerCommand contains the definition of the maintainer command-line
Expand Down Expand Up @@ -61,7 +65,7 @@ func maintainers(cmd *cobra.Command, args []string) error {
)
}

_, tbtcChain, _, _, _, err := ethereum.Connect(
_, tbtcChain, blockCounter, _, _, err := ethereum.Connect(
ctx,
clientConfig.Ethereum,
)
Expand All @@ -72,14 +76,51 @@ func maintainers(cmd *cobra.Command, args []string) error {
)
}

metricsRecorder := initializeMaintainerMetrics(ctx, blockCounter)

maintainer.Initialize(
ctx,
clientConfig.Maintainer,
btcChain,
btcDiffChain,
tbtcChain,
metricsRecorder,
)

<-ctx.Done()
return fmt.Errorf("unexpected context cancellation")
}

// initializeMaintainerMetrics sets up the client info registry and performance
// metrics for the maintainer command. It returns a metrics recorder wired to
// the SPV maintainer, or nil when the client info endpoint is not configured
// (in which case metrics recording is disabled).
func initializeMaintainerMetrics(
ctx context.Context,
blockCounter chain.BlockCounter,
) spv.MetricsRecorder {
registry, isConfigured := clientinfo.Initialize(
ctx,
clientConfig.ClientInfo.Port,
)
if !isConfigured {
logger.Infof("client info endpoint not configured")
return nil
}

perfMetrics := clientinfo.NewPerformanceMetrics(ctx, registry)

registry.RegisterMetricClientInfo(build.Version)
registry.ObserveEthConnectivity(
blockCounter,
clientConfig.ClientInfo.EthereumMetricsTick,
)
registry.RegisterEthChainInfoSource(blockCounter)

logger.Infof(
"enabled client info endpoint on port [%v]",
clientConfig.ClientInfo.Port,
)

return perfMetrics
}
32 changes: 32 additions & 0 deletions cmd/maintainer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cmd

import (
"context"
"testing"
)

// TestInitializeMaintainerMetricsDisabledWhenPortUnset verifies the metrics
// on/off gate: when the client info port is 0 (unset), the maintainer boot path
// must report the endpoint as not configured and return a genuinely nil
// recorder, leaving SPV metrics recording disabled. This is the sole production
// switch that turns the SPV proof-submission counters on or off, so inverting
// the gate would silently change maintainer behavior.
func TestInitializeMaintainerMetricsDisabledWhenPortUnset(t *testing.T) {
// clientConfig is a package-level global; restore it so the test does not
// leak state into other tests in this package.
originalPort := clientConfig.ClientInfo.Port
defer func() { clientConfig.ClientInfo.Port = originalPort }()

clientConfig.ClientInfo.Port = 0

// The block counter is only touched on the configured (enabled) path, so a
// nil value is safe for the disabled path under test.
recorder := initializeMaintainerMetrics(context.Background(), nil)

if recorder != nil {
t.Errorf(
"expected a nil recorder when the client info port is unset, got [%v]",
recorder,
)
}
}
2 changes: 2 additions & 0 deletions cmd/maintainercli.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ var submitDepositSweepProofCommand = cobra.Command{
requiredConfirmations,
btcChain,
tbtcChain,
nil,
); err != nil {
return fmt.Errorf("failed to submit deposit sweep proof [%v]", err)
}
Expand Down Expand Up @@ -444,6 +445,7 @@ var submitRedemptionProofCommand = cobra.Command{
requiredConfirmations,
btcChain,
tbtcChain,
nil,
); err != nil {
return fmt.Errorf("failed to submit redemption proof [%v]", err)
}
Expand Down
1 change: 1 addition & 0 deletions config/category.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var StartCmdCategories = []Category{
var MaintainerCategories = []Category{
Ethereum,
BitcoinElectrum,
ClientInfo,
Maintainer,
}

Expand Down
36 changes: 36 additions & 0 deletions docs/performance-metrics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,42 @@ For each action type, the following metrics are available:
*Description*: Total count of coordination duration samples
*Labels*: None

=== SPV Proof Submission Metrics (Maintainer)

These metrics are recorded by the SPV maintainer (the `maintainer` command) when
it submits Bitcoin SPV proofs to the host chain. They require the client info
endpoint to be configured.

==== `performance_deposit_sweep_proof_submissions_total`
*Type*: Counter
*Description*: Total number of deposit sweep proof submissions attempted
*Labels*: None

==== `performance_deposit_sweep_proof_submissions_success_total`
*Type*: Counter
*Description*: Total number of successful deposit sweep proof submissions
*Labels*: None

==== `performance_deposit_sweep_proof_submissions_failed_total`
*Type*: Counter
*Description*: Total number of failed deposit sweep proof submissions
*Labels*: None

==== `performance_redemption_proof_submissions_total`
*Type*: Counter
*Description*: Total number of redemption proof submissions attempted
*Labels*: None

==== `performance_redemption_proof_submissions_success_total`
*Type*: Counter
*Description*: Total number of successful redemption proof submissions
*Labels*: None

==== `performance_redemption_proof_submissions_failed_total`
*Type*: Counter
*Description*: Total number of failed redemption proof submissions
*Labels*: None

=== Network Metrics

==== `performance_incoming_message_queue_size`
Expand Down
File renamed without changes.
File renamed without changes.
8 changes: 3 additions & 5 deletions pkg/beacon/gjkr/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,9 @@ type CommitmentsVerifyingMember struct {
// Shares calculated for the current member by peer group members which passed
// the validation.
//
// receivedQualifiedSharesS are defined as `s_ji` and receivedQualifiedSharesT are
// defined as `t_ji` across the protocol specification.
// TODO remove receivedQualifiedSharesT - exists only for unit tests purpose
receivedQualifiedSharesS, receivedQualifiedSharesT map[group.MemberIndex]*big.Int
// receivedQualifiedSharesS are defined as `s_ji` across the protocol
// specification.
receivedQualifiedSharesS map[group.MemberIndex]*big.Int
// Commitments to secret shares polynomial coefficients received from
// other group members.
receivedPeerCommitments map[group.MemberIndex][]*bn256.G1
Expand Down Expand Up @@ -285,7 +284,6 @@ func (cm *CommittingMember) InitializeCommitmentsVerification() *CommitmentsVeri
return &CommitmentsVerifyingMember{
CommittingMember: cm,
receivedQualifiedSharesS: make(map[group.MemberIndex]*big.Int),
receivedQualifiedSharesT: make(map[group.MemberIndex]*big.Int),
receivedPeerCommitments: make(map[group.MemberIndex][]*bn256.G1),
}
}
Expand Down
2 changes: 0 additions & 2 deletions pkg/beacon/gjkr/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,6 @@ func (cvm *CommitmentsVerifyingMember) VerifyReceivedSharesAndCommitmentsMessage
break
}
cvm.receivedQualifiedSharesS[commitmentsMessage.senderID] = shareS
cvm.receivedQualifiedSharesT[commitmentsMessage.senderID] = shareT
break
}
}
Expand Down Expand Up @@ -766,7 +765,6 @@ func (sjm *SharesJustifyingMember) discardReceivedShares(
memberID group.MemberIndex,
) {
delete(sjm.receivedQualifiedSharesS, memberID)
delete(sjm.receivedQualifiedSharesT, memberID)
}

// Inspects evidence log looking for ephemeral public key message sent in phase
Expand Down
34 changes: 21 additions & 13 deletions pkg/beacon/gjkr/protocol_accusations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestResolveSecretSharesAccusations(t *testing.T) {
}
for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
members, err := initializeSharesJustifyingMemberGroup(
members, receivedSharesT, err := initializeSharesJustifyingMemberGroup(
dishonestThreshold,
groupSize,
)
Expand All @@ -145,7 +145,7 @@ func TestResolveSecretSharesAccusations(t *testing.T) {

accuser := findSharesJustifyingMemberByID(members, test.accuserID)
modifiedShareS := accuser.receivedQualifiedSharesS[test.accusedID]
modifiedShareT := accuser.receivedQualifiedSharesT[test.accusedID]
modifiedShareT := receivedSharesT[test.accuserID][test.accusedID]

if test.modifyShareS != nil {
modifiedShareS = test.modifyShareS(modifiedShareS)
Expand Down Expand Up @@ -422,7 +422,7 @@ func TestResolveSecretSharesAccusationsIncorrectAccussedMemberId(t *testing.T) {

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
members, err := initializeSharesJustifyingMemberGroup(
members, _, err := initializeSharesJustifyingMemberGroup(
dishonestThreshold,
groupSize,
)
Expand Down Expand Up @@ -540,19 +540,20 @@ func findCoefficientsJustifyingMemberByID(
return nil
}

// InitializeSharesJustifyingMemberGroup generates a group of members and simulates
// shares calculation and commitments sharing betwen members (Phases 3 and 4).
// It generates coefficients for each group member, calculates commitments and
// shares for each peer member individually. At the end it stores values for each
// member just like they would be received from peers.
// initializeSharesJustifyingMemberGroup initializes a group of shares
// justifying members with simulated received shares and commitments. It also
// returns the received `t_ji` shares keyed by receiver then sender member
// index; these are not stored on the member (only `s_ji` is production state)
// but some accusation tests need them to reconstruct the peer shares message.
func initializeSharesJustifyingMemberGroup(dishonestThreshold, groupSize int) (
[]*SharesJustifyingMember,
map[group.MemberIndex]map[group.MemberIndex]*big.Int,
error,
) {
commitmentsVerifyingMembers, err :=
initializeCommitmentsVerifiyingMembersGroup(dishonestThreshold, groupSize)
if err != nil {
return nil, fmt.Errorf("group initialization failed [%s]", err)
return nil, nil, fmt.Errorf("group initialization failed [%s]", err)
}

var sharesJustifyingMembers []*SharesJustifyingMember
Expand All @@ -567,14 +568,18 @@ func initializeSharesJustifyingMemberGroup(dishonestThreshold, groupSize int) (
groupCoefficientsB := make(map[group.MemberIndex][]*big.Int, groupSize)
groupCommitments := make(map[group.MemberIndex][]*bn256.G1, groupSize)

// receivedSharesT keeps the `t_ji` shares received by each member from its
// peers, keyed by receiver then sender member index.
receivedSharesT := make(map[group.MemberIndex]map[group.MemberIndex]*big.Int)

for _, m := range sharesJustifyingMembers {
memberCoefficientsA, err := generatePolynomial(dishonestThreshold)
if err != nil {
return nil, fmt.Errorf("polynomial generation failed [%s]", err)
return nil, nil, fmt.Errorf("polynomial generation failed [%s]", err)
}
memberCoefficientsB, err := generatePolynomial(dishonestThreshold)
if err != nil {
return nil, fmt.Errorf("polynomial generation failed [%s]", err)
return nil, nil, fmt.Errorf("polynomial generation failed [%s]", err)
}

// polynomial is of degree dishonestThreshold so it has
Expand All @@ -598,13 +603,16 @@ func initializeSharesJustifyingMemberGroup(dishonestThreshold, groupSize int) (
for _, p := range sharesJustifyingMembers {
if m.ID != p.ID {
p.receivedQualifiedSharesS[m.ID] = m.evaluateMemberShare(p.ID, groupCoefficientsA[m.ID])
p.receivedQualifiedSharesT[m.ID] = m.evaluateMemberShare(p.ID, groupCoefficientsB[m.ID])
if receivedSharesT[p.ID] == nil {
receivedSharesT[p.ID] = make(map[group.MemberIndex]*big.Int)
}
receivedSharesT[p.ID][m.ID] = m.evaluateMemberShare(p.ID, groupCoefficientsB[m.ID])
p.receivedPeerCommitments[m.ID] = groupCommitments[m.ID]
}
}
}

return sharesJustifyingMembers, nil
return sharesJustifyingMembers, receivedSharesT, nil
}

// initializePointsJustifyingMemberGroup generates a group of members and
Expand Down
6 changes: 0 additions & 6 deletions pkg/beacon/gjkr/protocol_commitments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,6 @@ func assertValidSharesAndCommitments(
len(verifyingMember.receivedQualifiedSharesS),
)
}
if len(verifyingMember.receivedQualifiedSharesT) != expectedReceivedSharesLength {
t.Errorf("\nexpected: %v received shares T\nactual: %v\n",
expectedReceivedSharesLength,
len(verifyingMember.receivedQualifiedSharesT),
)
}
if len(verifyingMember.receivedPeerCommitments) != groupSize-1 {
t.Errorf("\nexpected: %v received commitments\nactual: %v\n",
expectedReceivedSharesLength,
Expand Down
2 changes: 1 addition & 1 deletion pkg/beacon/gjkr/protocol_sharing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func initializeQualifiedMembersGroup(dishonestThreshold, groupSize int) (
[]*QualifiedMember,
error,
) {
sharesJustifyingMembers, err := initializeSharesJustifyingMemberGroup(
sharesJustifyingMembers, _, err := initializeSharesJustifyingMemberGroup(
dishonestThreshold,
groupSize,
)
Expand Down
6 changes: 0 additions & 6 deletions pkg/beacon/gjkr/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,6 @@ func TestRoundTrip(t *testing.T) {
len(member.receivedQualifiedSharesS),
)
}
if len(member.receivedQualifiedSharesT) != groupSize-1 {
t.Fatalf("\nexpected: %d received shares T\nactual: %d\n",
groupSize-1,
len(member.receivedQualifiedSharesT),
)
}
member.CombineMemberShares()
}

Expand Down
Loading
Loading