Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree configuration #138

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9840f5f
added tree configuration and testcases
hanish520 Dec 22, 2024
0131823
chore(trees): gofmt and fixed lint issues
meling Dec 22, 2024
5fe6e96
chore(trees): removed TreeConfiguration interface
meling Dec 22, 2024
2bd3535
test(trees): make the TestCreateTree table use labeled entries
meling Dec 22, 2024
376c42c
refactor(trees): remove double storage of tree position to ID map
meling Dec 22, 2024
7266f1f
Addressed review comments and added benchmarks for ChildrenOf api
hanish520 Dec 23, 2024
30a1ae5
fix(tree): do the panicking first on bad input then compute height
meling Dec 24, 2024
1871d1d
refactor(tree): rename GetTreeHeight to TreeHeight
meling Dec 24, 2024
64b3d23
fix(tree): removed unnecessary check for -1
meling Dec 24, 2024
c29ed18
refactor(tree): rename to ChildrenOf(node) and NodeChildren()
meling Dec 24, 2024
5f92d28
refactor(tree): rename GetHeight to NodeHeight
meling Dec 24, 2024
ad4f140
refactor(tree): avoid allocation of empty slice of IDs
meling Dec 24, 2024
8001f10
refactor(tree): simplified SubTree implementation
meling Dec 24, 2024
8f780f9
doc(tree): added a comment about computing height of the tree
meling Dec 24, 2024
6e02d5f
refactor(tree): rename Node/node methods/vars to Replica/replica
meling Dec 24, 2024
8945718
chore(tree): moved private helper funcs to bottom of file
meling Dec 24, 2024
e470824
refactor(tree): allocate less in SubTree() method
meling Dec 24, 2024
6f94b07
refactor(tree): avoid allocation in ChildrenOf()
meling Dec 24, 2024
7dddede
refactor(tree): avoid math.Pow in heightOf()
meling Dec 24, 2024
8fa760c
bench(tree): use modern table-driven benchmarks
meling Dec 24, 2024
c3f8a5a
fix(tree): removed unused isWithInIndex()
meling Dec 24, 2024
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
185 changes: 185 additions & 0 deletions trees/treeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package trees
hanish520 marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"math"
"slices"

"github.com/relab/hotstuff"
)

// Tree implements a fault free tree configuration.
type Tree struct {
id hotstuff.ID
configurationSize int
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
height int
posToIDMapping []hotstuff.ID
branchFactor int
}

// CreateTree creates the tree configuration, currently only fault free tree configuration is supported.
func CreateTree(configurationSize int, myID hotstuff.ID, bf int) *Tree {
if configurationSize <= 0 {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
temp := configurationSize
temp = temp - 1 // root
height := 1
for i := 1; temp > 0; i++ {
temp = temp - int(math.Pow(float64(bf), float64(i)))
height++
}
return &Tree{
id: myID,
configurationSize: configurationSize,
height: height,
branchFactor: bf,
}
}

// InitializeWithPIDs uses the map to initialize the position of replicas.
func (t *Tree) InitializeWithPIDs(ids []hotstuff.ID) error {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
if len(ids) != t.configurationSize {
return fmt.Errorf("invalid number of replicas")
}
// check for duplicate IDs
idToIndexMap := make(map[hotstuff.ID]int)
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
for index, id := range ids {
if _, ok := idToIndexMap[id]; !ok {
idToIndexMap[id] = index
} else {
return fmt.Errorf("duplicate replica ID: %d", id)
}
}
t.posToIDMapping = ids
return nil
}

func (t *Tree) GetTreeHeight() int {
return t.height
}

func (t *Tree) getPosition() (int, error) {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
pos := slices.Index(t.posToIDMapping, t.id)
if pos == -1 {
return 0, fmt.Errorf("replica not found")
}
return pos, nil
}

func (t *Tree) getReplicaPosition(replicaId hotstuff.ID) (int, error) {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
pos := slices.Index(t.posToIDMapping, replicaId)
if pos == -1 {
return 0, fmt.Errorf("replica not found")
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
}
return pos, nil
}

// GetParent fetches the ID of the parent, if root, returns itself.
func (t *Tree) GetParent() (hotstuff.ID, bool) {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
myPos, err := t.getPosition()
if err != nil {
return hotstuff.ID(0), false
}
if myPos == 0 {
return t.id, false
}
return t.posToIDMapping[(myPos-1)/t.branchFactor], true
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
}

// GetChildren returns the children of the replicas, if any.
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
func (t *Tree) GetChildren() []hotstuff.ID {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
return t.GetChildrenOfNode(t.id)
}

func (t *Tree) isWithInIndex(position int) bool {
return position < t.configurationSize
}

// IsRoot return true if the replica is at root of the tree.
func (t *Tree) IsRoot(nodeID hotstuff.ID) bool {
pos, err := t.getReplicaPosition(nodeID)
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false
}
return pos == 0
}

// GetChildrenOfNode returns the children of a specific replica.
func (t *Tree) GetChildrenOfNode(nodeID hotstuff.ID) []hotstuff.ID {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
children := make([]hotstuff.ID, 0)
nodePos, err := t.getReplicaPosition(nodeID)
if err != nil {
return children
}
for i := 1; i <= t.branchFactor; i++ {
childPos := (t.branchFactor * nodePos) + i
if t.isWithInIndex(childPos) {
children = append(children, t.posToIDMapping[childPos])
} else {
break
}
}
return children
}

// getHeight returns the height of a given replica.
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
func (t *Tree) getHeight(nodeID hotstuff.ID) int {
if t.IsRoot(nodeID) {
return t.height
}
nodePos, err := t.getReplicaPosition(nodeID)
if err != nil {
return 0
}
startLimit := 0
endLimit := 0
for i := 1; i < t.height; i++ {
startLimit = startLimit + int(math.Pow(float64(t.branchFactor), float64(i-1)))
endLimit = endLimit + int(math.Pow(float64(t.branchFactor), float64(i)))
if nodePos >= startLimit && nodePos <= endLimit {
return t.height - i
}
}
return 0
}

// GetHeight returns the height of the replica
func (t *Tree) GetHeight() int {
meling marked this conversation as resolved.
Show resolved Hide resolved
return t.getHeight(t.id)
}

// GetPeers returns the peers of given ID, if any.
func (t *Tree) GetPeers(nodeID hotstuff.ID) []hotstuff.ID {
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
peers := make([]hotstuff.ID, 0)
if t.IsRoot(nodeID) {
return peers
}
parent, ok := t.GetParent()
if !ok {
return peers
}
parentChildren := t.GetChildrenOfNode(parent)
hanish520 marked this conversation as resolved.
Show resolved Hide resolved
return parentChildren
}

// GetSubTreeNodes returns all the nodes of its subtree.
func (t *Tree) GetSubTreeNodes() []hotstuff.ID {
meling marked this conversation as resolved.
Show resolved Hide resolved
nodeID := t.id
subTreeNodes := make([]hotstuff.ID, 0)
children := t.GetChildrenOfNode(nodeID)
queue := make([]hotstuff.ID, 0)
queue = append(queue, children...)
subTreeNodes = append(subTreeNodes, children...)
if len(children) == 0 {
return subTreeNodes
meling marked this conversation as resolved.
Show resolved Hide resolved
}
for len(queue) > 0 {
meling marked this conversation as resolved.
Show resolved Hide resolved
child := queue[0]
queue = queue[1:]
children := t.GetChildrenOfNode(child)
subTreeNodes = append(subTreeNodes, children...)
queue = append(queue, children...)
}
return subTreeNodes
}
140 changes: 140 additions & 0 deletions trees/treeconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package trees

import (
"slices"
"sort"
"testing"

"github.com/relab/hotstuff"
)

func TestCreateTree(t *testing.T) {
tests := []struct {
configurationSize int
id hotstuff.ID
branchFactor int
wantHeight int
}{
{configurationSize: 10, id: 1, branchFactor: 2, wantHeight: 4},
{configurationSize: 21, id: 1, branchFactor: 4, wantHeight: 3},
{configurationSize: 21, id: 1, branchFactor: 3, wantHeight: 4},
{configurationSize: 111, id: 1, branchFactor: 10, wantHeight: 3},
{configurationSize: 111, id: 1, branchFactor: 3, wantHeight: 5},
}
for _, test := range tests {
tree := CreateTree(test.configurationSize, test.id, test.branchFactor)
if tree.GetTreeHeight() != test.wantHeight {
t.Errorf("CreateTree(%d, %d, %d).GetTreeHeight() = %d, want %d",
test.configurationSize, test.id, test.branchFactor, tree.GetTreeHeight(), test.wantHeight)
}
}
}

func TestTreeWithNegativeCases(t *testing.T) {
tree := CreateTree(10, 1, 2)
if tree.GetTreeHeight() != 4 {
t.Errorf("Expected height 4, got %d", tree.GetTreeHeight())
}
if len(tree.GetChildren()) != 0 {
t.Errorf("Expected nil, got %v", tree.GetChildren())
}
if len(tree.GetSubTreeNodes()) != 0 {
t.Errorf("Expected nil, got %v", tree.GetSubTreeNodes())
}
if _, ok := tree.GetParent(); ok {
t.Errorf("Expected false, got true")
}
tree = CreateTree(-1, 1, 2)
if tree != nil {
t.Errorf("Expected nil, got %v", tree)
}
ids := []hotstuff.ID{1, 2, 3, 3, 4, 5, 6, 7, 8, 9}
tree = CreateTree(10, 1, 2)
err := tree.InitializeWithPIDs(ids)
if err == nil {
t.Errorf("Expected error, got nil")
}
ids = []hotstuff.ID{1, 2, 3, 4, 5, 6, 7, 8, 9}
err = tree.InitializeWithPIDs(ids)
if err == nil {
t.Errorf("Expected error, got nil")
}
}

type treeConfigTest struct {
configurationSize int
id hotstuff.ID
branchFactor int
height int
children []hotstuff.ID
subTreeNodes []hotstuff.ID
parent hotstuff.ID
isRoot bool
replicaHeight int
peers []hotstuff.ID
}

func TestTreeAPIWithInitializeWithPIDs(t *testing.T) {
treeConfigTestData := []treeConfigTest{
{10, 1, 2, 4, []hotstuff.ID{2, 3}, []hotstuff.ID{2, 3, 4, 5, 6, 7, 8, 9, 10}, 1, true, 4, []hotstuff.ID{}},
{10, 5, 2, 4, []hotstuff.ID{10}, []hotstuff.ID{10}, 2, false, 2, []hotstuff.ID{4, 5}},
{10, 2, 2, 4, []hotstuff.ID{4, 5}, []hotstuff.ID{4, 5, 8, 9, 10}, 1, false, 3, []hotstuff.ID{2, 3}},
{10, 3, 2, 4, []hotstuff.ID{6, 7}, []hotstuff.ID{6, 7}, 1, false, 3, []hotstuff.ID{2, 3}},
{10, 4, 2, 4, []hotstuff.ID{8, 9}, []hotstuff.ID{8, 9}, 2, false, 2, []hotstuff.ID{4, 5}},
{21, 1, 4, 3, []hotstuff.ID{2, 3, 4, 5}, []hotstuff.ID{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
}, 1, true, 3, []hotstuff.ID{}},
{21, 1, 3, 4, []hotstuff.ID{2, 3, 4}, []hotstuff.ID{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
}, 1, true, 4, []hotstuff.ID{}},
{21, 2, 4, 3, []hotstuff.ID{6, 7, 8, 9}, []hotstuff.ID{6, 7, 8, 9}, 1, false, 2, []hotstuff.ID{2, 3, 4, 5}},
{21, 2, 3, 4, []hotstuff.ID{5, 6, 7}, []hotstuff.ID{5, 6, 7, 14, 15, 16, 17, 18, 19, 20, 21}, 1, false, 3, []hotstuff.ID{2, 3, 4}},
{21, 9, 3, 4, []hotstuff.ID{}, []hotstuff.ID{}, 3, false, 2, []hotstuff.ID{8, 9, 10}},
{21, 7, 3, 4, []hotstuff.ID{20, 21}, []hotstuff.ID{20, 21}, 2, false, 2, []hotstuff.ID{5, 6, 7}},
{21, 3, 4, 3, []hotstuff.ID{10, 11, 12, 13}, []hotstuff.ID{10, 11, 12, 13}, 1, false, 2, []hotstuff.ID{2, 3, 4, 5}},
{21, 10, 4, 3, []hotstuff.ID{}, []hotstuff.ID{}, 3, false, 1, []hotstuff.ID{10, 11, 12, 13}},
{21, 15, 4, 3, []hotstuff.ID{}, []hotstuff.ID{}, 4, false, 1, []hotstuff.ID{14, 15, 16, 17}},
{21, 20, 4, 3, []hotstuff.ID{}, []hotstuff.ID{}, 5, false, 1, []hotstuff.ID{18, 19, 20, 21}},
{21, 5, 4, 3, []hotstuff.ID{18, 19, 20, 21}, []hotstuff.ID{18, 19, 20, 21}, 1, false, 2, []hotstuff.ID{2, 3, 4, 5}},
}
for _, test := range treeConfigTestData {
tree := CreateTree(test.configurationSize, test.id, test.branchFactor)
ids := make([]hotstuff.ID, test.configurationSize)
for i := 0; i < test.configurationSize; i++ {
ids[i] = hotstuff.ID(i + 1)
}
if err := tree.InitializeWithPIDs(ids); err != nil {
t.Errorf("Expected nil, got %v", err)
}
if tree.GetTreeHeight() != test.height {
t.Errorf("Expected height %d, got %d", test.height, tree.GetTreeHeight())
}
gotChildren := tree.GetChildren()
sort.Slice(gotChildren, func(i, j int) bool { return gotChildren[i] < gotChildren[j] })
if len(gotChildren) != len(test.children) || !slices.Equal(gotChildren, test.children) {
t.Errorf("Expected %v, got %v", test.children, tree.GetChildren())
}
subTree := tree.GetSubTreeNodes()
sort.Slice(subTree, func(i, j int) bool { return subTree[i] < subTree[j] })
if len(subTree) != len(test.subTreeNodes) ||
!slices.Equal(subTree, test.subTreeNodes) {
t.Errorf("Expected %v, got %v", test.subTreeNodes, tree.GetSubTreeNodes())
}
if parent, ok := tree.GetParent(); ok {
if parent != test.parent {
t.Errorf("Expected %d, got %d", test.parent, parent)
}
}
if tree.IsRoot(test.id) != test.isRoot {
t.Errorf("Expected %t, got %t", test.isRoot, tree.IsRoot(test.id))
}
if tree.GetHeight() != test.replicaHeight {
t.Errorf("Expected %d, got %d", test.replicaHeight, tree.GetHeight())
}
gotPeers := tree.GetPeers(test.id)
sort.Slice(gotPeers, func(i, j int) bool { return gotPeers[i] < gotPeers[j] })
if len(gotPeers) != len(test.peers) || !slices.Equal(gotPeers, test.peers) {
t.Errorf("Expected %v, got %v", test.peers, tree.GetPeers(test.id))
}
}
}
Loading