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 all 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
159 changes: 159 additions & 0 deletions internal/tree/treeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package tree

import (
"math"
"slices"

"github.com/relab/hotstuff"
)

// Tree implements a fault free tree configuration.
type Tree struct {
id hotstuff.ID
height int
posToIDMapping []hotstuff.ID
branchFactor int
}

// CreateTree creates the tree configuration.
// Currently only fault free tree configuration is supported.
func CreateTree(myID hotstuff.ID, bf int, ids []hotstuff.ID) *Tree {
if bf < 2 {
panic("Branch factor must be greater than 1")
}
if slices.Index(ids, myID) == -1 {
panic("Replica ID not found in tree configuration")
}

// compute height of the tree
temp := len(ids) - 1 // root
height := 1
for i := 1; temp > 0; i++ {
temp = temp - int(math.Pow(float64(bf), float64(i)))
height++
}
return &Tree{
id: myID,
height: height,
branchFactor: bf,
posToIDMapping: ids,
}
}

// TreeHeight returns the height of the full tree.
func (t *Tree) TreeHeight() int {
return t.height
}

// Parent returns the ID of the parent of this tree's replica and true.
// If this tree's replica is the root, the root's ID is returned
// and false to indicate it does not have a parent.
func (t *Tree) Parent() (hotstuff.ID, bool) {
myPos := t.replicaPosition(t.id)
if myPos == 0 {
return t.id, false
}
parentPos := (myPos - 1) / t.branchFactor
return t.posToIDMapping[parentPos], true
}

// IsRoot return true if the replica is at root of the tree.
func (t *Tree) IsRoot(replicaID hotstuff.ID) bool {
return t.replicaPosition(replicaID) == 0
}

// ReplicaChildren returns the children of this tree's replica, if any.
func (t *Tree) ReplicaChildren() []hotstuff.ID {
return t.ChildrenOf(t.id)
}

// ChildrenOf returns the children of a specific replica.
func (t *Tree) ChildrenOf(replicaID hotstuff.ID) []hotstuff.ID {
replicaPos := t.replicaPosition(replicaID)
if replicaPos == -1 {
return nil
}
childStart := replicaPos*t.branchFactor + 1
if childStart >= len(t.posToIDMapping) {
// no children since start is beyond slice length
return nil
}
childEnd := childStart + t.branchFactor
if childEnd > len(t.posToIDMapping) {
// clamp to the slice length
childEnd = len(t.posToIDMapping)
}
return t.posToIDMapping[childStart:childEnd]
}

// ReplicaHeight returns the height of this tree's replica.
func (t *Tree) ReplicaHeight() int {
return t.heightOf(t.id)
}

// PeersOf returns the sibling peers of given ID, if any.
func (t *Tree) PeersOf(replicaID hotstuff.ID) []hotstuff.ID {
if t.IsRoot(replicaID) {
return nil
}
parent, ok := t.Parent()
if !ok {
return nil
}
return t.ChildrenOf(parent)
}

// SubTree returns all subtree replicas of this tree's replica.
func (t *Tree) SubTree() []hotstuff.ID {
children := t.ChildrenOf(t.id)
if len(children) == 0 {
return nil
}
subTreeReplicas := make([]hotstuff.ID, len(children))
copy(subTreeReplicas, children)
for i := 0; i < len(subTreeReplicas); i++ {
node := subTreeReplicas[i]
newChildren := t.ChildrenOf(node)
subTreeReplicas = append(subTreeReplicas, newChildren...)
}
return subTreeReplicas
}

// heightOf returns the height from the given replica's vantage point.
func (t *Tree) heightOf(replicaID hotstuff.ID) int {
if t.IsRoot(replicaID) {
return t.height
}
replicaPos := t.replicaPosition(replicaID)
if replicaPos == -1 {
return 0
}

// startLvl is the "first index" in the current level,
// levelCount is how many nodes are in this level.
//
// With branchFactor = n, the level sizes grow as:
// Level 0: 1 node (the root)
// Level 1: n nodes
// Level 2: n^2 nodes
// ...
// We start at level 1, since the root returns early.
startLvl := 1 // index of the first node at level 1
lvlCount := t.branchFactor // number of nodes at level 1

for lvl := 1; lvl < t.height; lvl++ {
endLvl := startLvl + lvlCount // one-past the last node at this level
if replicaPos >= startLvl && replicaPos < endLvl {
// replicaPos is in [startLvl, endLvl): t.height-lvl is the height.
return t.height - lvl
}
// Move to the next level:
startLvl = endLvl
lvlCount *= t.branchFactor
}
return 0
}

func (t *Tree) replicaPosition(id hotstuff.ID) int {
return slices.Index(t.posToIDMapping, id)
}
152 changes: 152 additions & 0 deletions internal/tree/treeconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package tree

import (
"fmt"
"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 {
ids := make([]hotstuff.ID, test.configurationSize)
for i := 0; i < test.configurationSize; i++ {
ids[i] = hotstuff.ID(i + 1)
}
tree := CreateTree(test.id, test.branchFactor, ids)
if tree.TreeHeight() != test.wantHeight {
t.Errorf("CreateTree(%d, %d, %d).GetTreeHeight() = %d, want %d",
test.configurationSize, test.id, test.branchFactor, tree.TreeHeight(), test.wantHeight)
}
}
}

func TestCreateTreeNegativeBF(t *testing.T) {
defer func() { _ = recover() }()
ids := []hotstuff.ID{1, 2, 3, 4, 5}
tree := CreateTree(1, -1, ids)
t.Errorf("CreateTree should panic, got %v", tree)
}

func TestCreateTreeInvalidID(t *testing.T) {
defer func() { _ = recover() }()
ids := []hotstuff.ID{1, 2, 3, 4, 5}
tree := CreateTree(10, 2, ids)
t.Errorf("CreateTree should panic, got %v", tree)
}

type treeConfigTest struct {
configurationSize int
id hotstuff.ID
branchFactor int
height int
children []hotstuff.ID
subTreeReplicas []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 {
ids := make([]hotstuff.ID, test.configurationSize)
for i := 0; i < test.configurationSize; i++ {
ids[i] = hotstuff.ID(i + 1)
}
tree := CreateTree(test.id, test.branchFactor, ids)
if tree.TreeHeight() != test.height {
t.Errorf("Expected height %d, got %d", test.height, tree.TreeHeight())
}
gotChildren := tree.ReplicaChildren()
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.ReplicaChildren())
}
subTree := tree.SubTree()
sort.Slice(subTree, func(i, j int) bool { return subTree[i] < subTree[j] })
if len(subTree) != len(test.subTreeReplicas) ||
!slices.Equal(subTree, test.subTreeReplicas) {
t.Errorf("Expected %v, got %v", test.subTreeReplicas, tree.SubTree())
}
if parent, ok := tree.Parent(); 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.ReplicaHeight() != test.replicaHeight {
t.Errorf("Expected %d, got %d", test.replicaHeight, tree.ReplicaHeight())
}
gotPeers := tree.PeersOf(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.PeersOf(test.id))
}
}
}

func BenchmarkReplicaChildren(b *testing.B) {
benchmarks := []struct {
size int
bf int
}{
{size: 10, bf: 2},
{size: 21, bf: 4},
{size: 111, bf: 10},
{size: 211, bf: 14},
{size: 421, bf: 20},
}
for _, bm := range benchmarks {
b.Run(fmt.Sprintf("size=%d/bf=%d", bm.size, bm.bf), func(b *testing.B) {
ids := make([]hotstuff.ID, bm.size)
for i := 0; i < bm.size; i++ {
ids[i] = hotstuff.ID(i + 1)
}
tree := CreateTree(1, bm.bf, ids)
// replace `for range b.N` with `for b.Loop()` when go 1.24 released (in release candidate as of writing)
// for b.Loop() {
for range b.N {
tree.ReplicaChildren()
}
})
}
}
Loading