diff --git a/consensus/random_polling.go b/consensus/random_polling.go new file mode 100644 index 00000000000..d52de8da519 --- /dev/null +++ b/consensus/random_polling.go @@ -0,0 +1,68 @@ +package bft + +import ( + "crypto/rand" + "math/big" + "sync" + "time" +) + +// Node represents a Fabric orderer node in the BiniBFT network. +type Node struct { + ID string + IsActive bool + LastPing time.Time +} + +// BiniBFTRandomPolling manages the Random Polling consensus. +type BiniBFTRandomPolling struct { + nodes []*Node + currentLeader *Node + mu sync.Mutex +} + +// NewBiniBFTRandomPolling initializes the Random Polling consensus. +func NewBiniBFTRandomPolling(nodes []*Node) *BiniBFTRandomPolling { + return &BiniBFTRandomPolling{ + nodes: nodes, + } +} + +// SelectLeader randomly selects an active node as leader. +func (b *BiniBFTRandomPolling) SelectLeader() (*Node, error) { + b.mu.Lock() + defer b.mu.Unlock() + + // Filter active nodes (pinged within last 10 seconds). + activeNodes := []*Node{} + for _, node := range b.nodes { + if node.IsActive && time.Since(node.LastPing) < 10*time.Second { + activeNodes = append(activeNodes, node) + } + } + + if len(activeNodes) == 0 { + return nil, errors.New("no active nodes available") + } + + // Randomly select a leader. + index, err := rand.Int(rand.Reader, big.NewInt(int64(len(activeNodes)))) + if err != nil { + return nil, err + } + b.currentLeader = activeNodes[index] + return b.currentLeader, nil +} + +// Heartbeat updates node activity status. +func (b *BiniBFTRandomPolling) Heartbeat(nodeID string) { + b.mu.Lock() + defer b.mu.Unlock() + for _, node := range b.nodes { + if node.ID == nodeID { + node.LastPing = time.Now() + node.IsActive = true + break + } + } +} diff --git a/consensus/time_weighted_test.go b/consensus/time_weighted_test.go new file mode 100644 index 00000000000..9a6c6fd9f2e --- /dev/null +++ b/consensus/time_weighted_test.go @@ -0,0 +1,46 @@ +package bft + +import ( + "testing" + "time" +) + +func TestTimeWeightedLeaderSelection(t *testing.T) { + nodes := []*Node{ + {ID: "node1", IsActive: true, LastPing: time.Now().Add(-5 * time.Second)}, + {ID: "node2", IsActive: true, LastPing: time.Now().Add(-2 * time.Second)}, + {ID: "node3", IsActive: false, LastPing: time.Now().Add(-20 * time.Second)}, + } + + bft := &BiniBFTTimeWeighted{ + nodes: nodes, + weights: map[string]float64{ + "node1": 0.6, // Older ping, lower weight + "node2": 0.9, // Recent ping, higher weight + "node3": 0.0, // Inactive, no weight + }, + } + + leader, err := bft.SelectLeader() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if leader == nil || leader.ID != "node2" { + t.Errorf("expected leader node2, got %v", leader) + } +} + +func TestTimeWeightedNoActiveNodes(t *testing.T) { + nodes := []*Node{ + {ID: "node1", IsActive: false, LastPing: time.Now().Add(-30 * time.Second)}, + } + bft := &BiniBFTTimeWeighted{ + nodes: nodes, + weights: map[string]float64{"node1": 0.0}, + } + + _, err := bft.SelectLeader() + if err == nil { + t.Fatal("expected error for no active nodes, got nil") + } +}