Skip to content

Commit 9f5b407

Browse files
Improve error messages & add config verification (#124)
* Implement configuration verification service and update test cases * Improve Lumera client connection error message * Fix supernode port alignment error message * Fix summary method to trim trailing newline from verification results
1 parent c29b4f3 commit 9f5b407

File tree

7 files changed

+454
-4
lines changed

7 files changed

+454
-4
lines changed

supernode/cmd/start.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
cascadeService "github.com/LumeraProtocol/supernode/supernode/services/cascade"
2323
"github.com/LumeraProtocol/supernode/supernode/services/common"
2424
supernodeService "github.com/LumeraProtocol/supernode/supernode/services/common/supernode"
25+
"github.com/LumeraProtocol/supernode/supernode/services/verifier"
2526

2627
cKeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
2728
"github.com/spf13/cobra"
@@ -53,9 +54,27 @@ The supernode will connect to the Lumera network and begin participating in the
5354
// Initialize Lumera client
5455
lumeraClient, err := initLumeraClient(ctx, appConfig, kr)
5556
if err != nil {
56-
logtrace.Fatal(ctx, "Failed to initialize Lumera client", logtrace.Fields{"error": err.Error()})
57+
logtrace.Fatal(ctx, "Failed to connect Lumera, please check your configuration", logtrace.Fields{"error": err.Error()})
5758
}
5859

60+
// Verify config matches chain registration before starting services
61+
logtrace.Info(ctx, "Verifying configuration against chain registration", logtrace.Fields{})
62+
configVerifier := verifier.NewConfigVerifier(appConfig, lumeraClient, kr)
63+
verificationResult, err := configVerifier.VerifyConfig(ctx)
64+
if err != nil {
65+
logtrace.Fatal(ctx, "Config verification failed", logtrace.Fields{"error": err.Error()})
66+
}
67+
68+
if !verificationResult.IsValid() {
69+
logtrace.Fatal(ctx, "Config verification failed", logtrace.Fields{"summary": verificationResult.Summary()})
70+
}
71+
72+
if verificationResult.HasWarnings() {
73+
logtrace.Warn(ctx, "Config verification warnings", logtrace.Fields{"summary": verificationResult.Summary()})
74+
}
75+
76+
logtrace.Info(ctx, "Configuration verification successful", logtrace.Fields{})
77+
5978
// Initialize RaptorQ store for Cascade processing
6079
rqStore, err := initRQStore(ctx, appConfig)
6180
if err != nil {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package verifier
2+
3+
import (
4+
"context"
5+
"strings"
6+
)
7+
8+
// ConfigVerifierService defines the interface for config verification service
9+
type ConfigVerifierService interface {
10+
// VerifyConfig performs comprehensive config validation against chain
11+
VerifyConfig(ctx context.Context) (*VerificationResult, error)
12+
}
13+
14+
// VerificationResult contains the results of config verification
15+
type VerificationResult struct {
16+
Valid bool `json:"valid"`
17+
Errors []ConfigError `json:"errors,omitempty"`
18+
Warnings []ConfigError `json:"warnings,omitempty"`
19+
}
20+
21+
// ConfigError represents a configuration validation error or warning
22+
type ConfigError struct {
23+
Field string `json:"field"`
24+
Expected string `json:"expected,omitempty"`
25+
Actual string `json:"actual,omitempty"`
26+
Message string `json:"message"`
27+
}
28+
29+
// IsValid returns true if all verifications passed
30+
func (vr *VerificationResult) IsValid() bool {
31+
return vr.Valid && len(vr.Errors) == 0
32+
}
33+
34+
// HasWarnings returns true if there are any warnings
35+
func (vr *VerificationResult) HasWarnings() bool {
36+
return len(vr.Warnings) > 0
37+
}
38+
39+
// Summary returns a human-readable summary of verification results
40+
func (vr *VerificationResult) Summary() string {
41+
if vr.IsValid() && !vr.HasWarnings() {
42+
return "✓ Config verification successful"
43+
}
44+
45+
var summary string
46+
for _, err := range vr.Errors {
47+
summary += "✗ " + err.Message + "\n"
48+
}
49+
50+
for _, warn := range vr.Warnings {
51+
summary += "⚠ " + warn.Message + "\n"
52+
}
53+
54+
return strings.TrimSuffix(summary, "\n")
55+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package verifier
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/LumeraProtocol/supernode/pkg/lumera"
9+
"github.com/LumeraProtocol/supernode/pkg/logtrace"
10+
"github.com/LumeraProtocol/supernode/supernode/config"
11+
"github.com/cosmos/cosmos-sdk/crypto/keyring"
12+
sdk "github.com/cosmos/cosmos-sdk/types"
13+
sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types"
14+
)
15+
16+
// ConfigVerifier implements ConfigVerifierService
17+
type ConfigVerifier struct {
18+
config *config.Config
19+
lumeraClient lumera.Client
20+
keyring keyring.Keyring
21+
}
22+
23+
// NewConfigVerifier creates a new config verifier service
24+
func NewConfigVerifier(cfg *config.Config, client lumera.Client, kr keyring.Keyring) ConfigVerifierService {
25+
return &ConfigVerifier{
26+
config: cfg,
27+
lumeraClient: client,
28+
keyring: kr,
29+
}
30+
}
31+
32+
// VerifyConfig performs comprehensive config validation against chain
33+
func (cv *ConfigVerifier) VerifyConfig(ctx context.Context) (*VerificationResult, error) {
34+
result := &VerificationResult{
35+
Valid: true,
36+
Errors: []ConfigError{},
37+
Warnings: []ConfigError{},
38+
}
39+
40+
logtrace.Debug(ctx, "Starting config verification", logtrace.Fields{
41+
"identity": cv.config.SupernodeConfig.Identity,
42+
"key_name": cv.config.SupernodeConfig.KeyName,
43+
"p2p_port": cv.config.P2PConfig.Port,
44+
})
45+
46+
// Check 1: Verify keyring contains the key
47+
if err := cv.checkKeyExists(result); err != nil {
48+
return result, err
49+
}
50+
51+
// Check 2: Verify key resolves to correct identity
52+
if err := cv.checkIdentityMatches(result); err != nil {
53+
return result, err
54+
}
55+
56+
// If keyring checks failed, don't proceed with chain queries
57+
if !result.IsValid() {
58+
return result, nil
59+
}
60+
61+
// Check 3: Query chain for supernode registration
62+
supernode, err := cv.checkSupernodeExists(ctx, result)
63+
if err != nil {
64+
return result, err
65+
}
66+
67+
// If supernode doesn't exist, don't proceed with field comparisons
68+
if supernode == nil {
69+
return result, nil
70+
}
71+
72+
// Check 4: Verify P2P port matches
73+
cv.checkP2PPortMatches(result, supernode)
74+
75+
// Check 5: Verify supernode state is active
76+
cv.checkSupernodeState(result, supernode)
77+
78+
// Check 6: Check supernode port alignment with on-chain registration
79+
cv.checkSupernodePortAlignment(result, supernode)
80+
81+
// Check 7: Check host alignment with on-chain registration (warning only - may differ due to load balancer)
82+
cv.checkHostAlignment(result, supernode)
83+
84+
logtrace.Info(ctx, "Config verification completed", logtrace.Fields{
85+
"valid": result.IsValid(),
86+
"errors": len(result.Errors),
87+
"warnings": len(result.Warnings),
88+
})
89+
90+
return result, nil
91+
}
92+
93+
// checkKeyExists verifies the configured key exists in keyring
94+
func (cv *ConfigVerifier) checkKeyExists(result *VerificationResult) error {
95+
_, err := cv.keyring.Key(cv.config.SupernodeConfig.KeyName)
96+
if err != nil {
97+
result.Valid = false
98+
result.Errors = append(result.Errors, ConfigError{
99+
Field: "key_name",
100+
Actual: cv.config.SupernodeConfig.KeyName,
101+
Message: fmt.Sprintf("Key '%s' not found in keyring", cv.config.SupernodeConfig.KeyName),
102+
})
103+
}
104+
return nil
105+
}
106+
107+
// checkIdentityMatches verifies key resolves to configured identity
108+
func (cv *ConfigVerifier) checkIdentityMatches(result *VerificationResult) error {
109+
keyInfo, err := cv.keyring.Key(cv.config.SupernodeConfig.KeyName)
110+
if err != nil {
111+
// Already handled in checkKeyExists
112+
return nil
113+
}
114+
115+
pubKey, err := keyInfo.GetPubKey()
116+
if err != nil {
117+
return fmt.Errorf("failed to get public key for key '%s': %w", cv.config.SupernodeConfig.KeyName, err)
118+
}
119+
120+
addr := sdk.AccAddress(pubKey.Address())
121+
if addr.String() != cv.config.SupernodeConfig.Identity {
122+
result.Valid = false
123+
result.Errors = append(result.Errors, ConfigError{
124+
Field: "identity",
125+
Expected: addr.String(),
126+
Actual: cv.config.SupernodeConfig.Identity,
127+
Message: fmt.Sprintf("Key '%s' resolves to %s but config identity is %s", cv.config.SupernodeConfig.KeyName, addr.String(), cv.config.SupernodeConfig.Identity),
128+
})
129+
}
130+
return nil
131+
}
132+
133+
// checkSupernodeExists queries chain for supernode registration
134+
func (cv *ConfigVerifier) checkSupernodeExists(ctx context.Context, result *VerificationResult) (*sntypes.SuperNode, error) {
135+
sn, err := cv.lumeraClient.SuperNode().GetSupernodeBySupernodeAddress(ctx, cv.config.SupernodeConfig.Identity)
136+
if err != nil {
137+
result.Valid = false
138+
result.Errors = append(result.Errors, ConfigError{
139+
Field: "registration",
140+
Actual: "not_registered",
141+
Message: fmt.Sprintf("Supernode not registered on chain for address %s", cv.config.SupernodeConfig.Identity),
142+
})
143+
return nil, nil
144+
}
145+
return sn, nil
146+
}
147+
148+
// checkP2PPortMatches compares config P2P port with chain
149+
func (cv *ConfigVerifier) checkP2PPortMatches(result *VerificationResult, supernode *sntypes.SuperNode) {
150+
configPort := fmt.Sprintf("%d", cv.config.P2PConfig.Port)
151+
chainPort := supernode.P2PPort
152+
153+
if chainPort != "" && chainPort != configPort {
154+
result.Valid = false
155+
result.Errors = append(result.Errors, ConfigError{
156+
Field: "p2p_port",
157+
Expected: chainPort,
158+
Actual: configPort,
159+
Message: fmt.Sprintf("P2P port mismatch: config=%s, chain=%s", configPort, chainPort),
160+
})
161+
}
162+
}
163+
164+
// checkSupernodeState verifies supernode is in active state
165+
func (cv *ConfigVerifier) checkSupernodeState(result *VerificationResult, supernode *sntypes.SuperNode) {
166+
if len(supernode.States) > 0 {
167+
lastState := supernode.States[len(supernode.States)-1]
168+
if lastState.State.String() != "SUPERNODE_STATE_ACTIVE" {
169+
result.Valid = false
170+
result.Errors = append(result.Errors, ConfigError{
171+
Field: "state",
172+
Expected: "SUPERNODE_STATE_ACTIVE",
173+
Actual: lastState.State.String(),
174+
Message: fmt.Sprintf("Supernode state is %s (expected ACTIVE)", lastState.State.String()),
175+
})
176+
}
177+
}
178+
}
179+
180+
// checkSupernodePortAlignment compares supernode port with on-chain registered port (error if mismatch)
181+
func (cv *ConfigVerifier) checkSupernodePortAlignment(result *VerificationResult, supernode *sntypes.SuperNode) {
182+
if len(supernode.PrevIpAddresses) > 0 {
183+
chainAddress := supernode.PrevIpAddresses[len(supernode.PrevIpAddresses)-1].Address
184+
185+
// Extract port from chain address
186+
var chainPort string
187+
if idx := strings.LastIndex(chainAddress, ":"); idx != -1 {
188+
chainPort = chainAddress[idx+1:]
189+
}
190+
191+
configPort := fmt.Sprintf("%d", cv.config.SupernodeConfig.Port)
192+
if chainPort != "" && chainPort != configPort {
193+
result.Valid = false
194+
result.Errors = append(result.Errors, ConfigError{
195+
Field: "supernode_port",
196+
Expected: chainPort,
197+
Actual: configPort,
198+
Message: fmt.Sprintf("Supernode port mismatch: config=%s, chain=%s", configPort, chainPort),
199+
})
200+
}
201+
}
202+
}
203+
204+
// checkHostAlignment compares host with on-chain registered host (warning only - may differ due to load balancer)
205+
func (cv *ConfigVerifier) checkHostAlignment(result *VerificationResult, supernode *sntypes.SuperNode) {
206+
if len(supernode.PrevIpAddresses) > 0 {
207+
chainAddress := supernode.PrevIpAddresses[len(supernode.PrevIpAddresses)-1].Address
208+
209+
// Extract host from chain address
210+
chainHost := chainAddress
211+
if idx := strings.LastIndex(chainAddress, ":"); idx != -1 {
212+
chainHost = chainAddress[:idx]
213+
}
214+
215+
if chainHost != cv.config.SupernodeConfig.Host {
216+
result.Warnings = append(result.Warnings, ConfigError{
217+
Field: "host",
218+
Expected: cv.config.SupernodeConfig.Host,
219+
Actual: chainHost,
220+
Message: fmt.Sprintf("Host mismatch: config=%s, chain=%s", cv.config.SupernodeConfig.Host, chainHost),
221+
})
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)