Skip to content

Commit 3befaaf

Browse files
committed
Integrator improvements for v1.1:
- Add Hooks struct for consensus event callbacks (OnPropose, OnVote, OnQCFormed, OnCommit, OnViewChange, OnTimeout) - Add ConsensusState interface for read-only monitoring - Add NewHotStuff2WithHooks constructor - Add MessageCodec helper - Standardise errors in classes: ErrConfig, ErrInvalidMessage, ErrByzantine, ErrInternal - Update integration guide with observability and error handling docs
1 parent 5ae6635 commit 3befaaf

10 files changed

Lines changed: 493 additions & 32 deletions

File tree

config.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,42 +72,42 @@ func NewConfig[H Hash](opts ...ConfigOption[H]) (*Config[H], error) {
7272
// validate checks that all required configuration fields are set.
7373
func (c *Config[H]) validate() error {
7474
if c.Validators == nil {
75-
return fmt.Errorf("validators is required")
75+
return wrapConfig("validators is required")
7676
}
7777

7878
if c.PrivateKey == nil {
79-
return fmt.Errorf("private key is required")
79+
return wrapConfig("private key is required")
8080
}
8181

8282
if c.Storage == nil {
83-
return fmt.Errorf("storage is required")
83+
return wrapConfig("storage is required")
8484
}
8585

8686
if c.Network == nil {
87-
return fmt.Errorf("network is required")
87+
return wrapConfig("network is required")
8888
}
8989

9090
if c.Executor == nil {
91-
return fmt.Errorf("executor is required")
91+
return wrapConfig("executor is required")
9292
}
9393

9494
if c.Timer == nil {
95-
return fmt.Errorf("timer is required")
95+
return wrapConfig("timer is required")
9696
}
9797

9898
if !c.Validators.Contains(c.MyIndex) {
99-
return fmt.Errorf("my index %d is not in validator set", c.MyIndex)
99+
return wrapConfigf("validator index %d not in validator set", c.MyIndex)
100100
}
101101

102102
if c.CryptoScheme != CryptoSchemeEd25519 && c.CryptoScheme != CryptoSchemeBLS {
103-
return fmt.Errorf("unsupported crypto scheme: %s", c.CryptoScheme)
103+
return wrapConfigf("unsupported crypto scheme: %s", c.CryptoScheme)
104104
}
105105

106106
// Validate Byzantine fault tolerance: n >= 3f + 1
107107
n := c.Validators.Count()
108108
f := c.Validators.F()
109109
if n < 3*f+1 {
110-
return fmt.Errorf("insufficient validators for Byzantine fault tolerance: n=%d, f=%d (need n >= 3f+1)", n, f)
110+
return wrapConfigf("insufficient validators: n=%d, f=%d (need n >= 3f+1)", n, f)
111111
}
112112

113113
return nil

config_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package hotstuff2
22

33
import (
4+
"errors"
5+
"strings"
46
"testing"
57

68
"github.com/edgedlt/hotstuff2/internal/crypto"
@@ -110,8 +112,14 @@ func TestConfigValidationMissingFields(t *testing.T) {
110112
t.Fatal("Expected error, got nil")
111113
}
112114

113-
if err.Error() != "invalid config: "+tt.wantErr {
114-
t.Errorf("Expected error '%s', got '%s'", tt.wantErr, err.Error())
115+
// Check that it's a config error
116+
if !errors.Is(err, ErrConfig) {
117+
t.Errorf("Expected ErrConfig, got: %v", err)
118+
}
119+
120+
// Check that error message contains the expected text
121+
if !strings.Contains(err.Error(), tt.wantErr) {
122+
t.Errorf("Expected error to contain '%s', got '%s'", tt.wantErr, err.Error())
115123
}
116124
})
117125
}
@@ -135,8 +143,8 @@ func TestConfigInvalidIndex(t *testing.T) {
135143
t.Fatal("Expected error for invalid index")
136144
}
137145

138-
if err.Error() != "invalid config: my index 5 is not in validator set" {
139-
t.Errorf("Unexpected error: %v", err)
146+
if !errors.Is(err, ErrConfig) {
147+
t.Errorf("Expected ErrConfig, got: %v", err)
140148
}
141149
}
142150

context.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,54 @@ func (c *Context[H]) highQCView() uint32 {
565565
}
566566
return c.highQC.View()
567567
}
568+
569+
// contextState wraps Context to implement ConsensusState interface.
570+
// This provides a read-only view of consensus state for external monitoring.
571+
type contextState[H Hash] struct {
572+
ctx *Context[H]
573+
}
574+
575+
// View returns the current view number.
576+
func (s *contextState[H]) View() uint32 {
577+
return s.ctx.View()
578+
}
579+
580+
// Height returns the height of the last committed block.
581+
func (s *contextState[H]) Height() uint32 {
582+
s.ctx.mu.RLock()
583+
defer s.ctx.mu.RUnlock()
584+
585+
maxHeight := uint32(0)
586+
for height := range s.ctx.committed {
587+
if height > maxHeight {
588+
maxHeight = height
589+
}
590+
}
591+
return maxHeight
592+
}
593+
594+
// LockedQCView returns the view of the locked QC, or 0 if none.
595+
func (s *contextState[H]) LockedQCView() uint32 {
596+
s.ctx.mu.RLock()
597+
defer s.ctx.mu.RUnlock()
598+
return s.ctx.lockedQCView()
599+
}
600+
601+
// HighQCView returns the view of the highest QC seen, or 0 if none.
602+
func (s *contextState[H]) HighQCView() uint32 {
603+
s.ctx.mu.RLock()
604+
defer s.ctx.mu.RUnlock()
605+
return s.ctx.highQCView()
606+
}
607+
608+
// CommittedCount returns the number of committed blocks.
609+
func (s *contextState[H]) CommittedCount() int {
610+
s.ctx.mu.RLock()
611+
defer s.ctx.mu.RUnlock()
612+
return len(s.ctx.committed)
613+
}
614+
615+
// State returns a read-only ConsensusState for the context.
616+
func (c *Context[H]) State() ConsensusState {
617+
return &contextState[H]{ctx: c}
618+
}

0 commit comments

Comments
 (0)