Skip to content

Commit

Permalink
feat: add gnetdag package
Browse files Browse the repository at this point in the history
This adds a FixedTree type, which operates on just int values, making it
trivial to manage mapping parent-child relationships in a sorted slice.
The FixedTree nodes each have a fixed number of children.

This is intended to be used to map out parent-child relationships in a
tree for distributing block data over a public network, hence the
"g-net-dag" (Gordian, Network, Directed Acyclic Graph) name.
  • Loading branch information
mark-rushakoff committed Dec 12, 2024
1 parent 5ac09f1 commit 4eefb14
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 0 deletions.
14 changes: 14 additions & 0 deletions gnetdag/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package gnetdag (Gordian NETwork Directed Acyclic Graph)
// contains types for determining directional flow of network traffic.
//
// Types in this package are focused on int values,
// so that they remain decoupled from any concrete implementations
// of validators, network addresses, and so on.
// Callers may simply use the int values as indices into slices
// of the actual type needing the directed graph.
//
// This package currently contains the [FixedTree] type,
// which effectively maps indices in a slice such that
// every non-root node contains a fixed number of children.
// This package will be expanded with more types as deemed necessary.
package gnetdag
100 changes: 100 additions & 0 deletions gnetdag/fixedtree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package gnetdag

// FixedTree represents a tree where each non-leaf node has a fixed number of children.
// With BranchFactor=3, the entries are arranged in layers like:
//
// 0 (L0)
// 1 2 3 (L1)
// 4 5 6 7 8 9 10 11 12 (L2)
//
// The entryIdx parameter used in methods on FixedTree
// are intended to be used as indices into an existing ordered slice
// that is to be treated as a tree.
//
// Methods on FixedTree use unchecked math,
// so invalid values, such as negative entry indices or branch factors,
// or branch factor so large that bf^2 overflows an int,
// result in undefined behavior.
type FixedTree struct {
// The width of the layer at index 1.
BranchFactor int
}

// Parent returns the "parent" index of the given entry index.
// It returns -1 for entryIdx = 0.
func (t FixedTree) Parent(entryIdx int) int {
if entryIdx == 0 {
return -1
}

// Special case for first layer, to avoid some off by one math.
if entryIdx <= t.BranchFactor {
return 0
}

curLayer := t.Layer(entryIdx)

// Calculate how many entries are present before the parent layer.
parentLayer := curLayer - 1
ancestorEntries := 1
ancestorWidth := 1
for range parentLayer - 1 {
ancestorWidth *= t.BranchFactor
ancestorEntries += ancestorWidth
}

// Our current row has t.BranchFactor times more entries than the parent row,
// so map our offset in the current row, into the offset of the parent row.
parentLayerWidth := ancestorWidth * t.BranchFactor
parentOffset := (entryIdx - parentLayerWidth - ancestorEntries) / t.BranchFactor
return ancestorEntries + parentOffset
}

// FirstChild returns the entry index of the first child of the given entry index.
// Every parent contains t.BranchFactor children,
// but the FixedTree type does not track number of entries,
// so it is the caller's responsibility to confirm that there are
// at least t.BranchFactor children available.
func (t FixedTree) FirstChild(entryIdx int) int {
if entryIdx == 0 {
return 1
}

curLayerWidth := t.BranchFactor
entriesBeforeCurLayer := 1

for {
if entryIdx <= entriesBeforeCurLayer+curLayerWidth {
// Offset of the given entry index, within its respective layer.
entryLayerOffset := entryIdx - entriesBeforeCurLayer

// Then find the start of the next layer,
// and move forward t.BranchFactor times according to the current layer offset.
return entriesBeforeCurLayer + curLayerWidth + (entryLayerOffset * t.BranchFactor)
}

entriesBeforeCurLayer += curLayerWidth
curLayerWidth *= t.BranchFactor
}
}

// Layer returns the layer that would contain the given entry index.
func (t FixedTree) Layer(entryIdx int) int {
if entryIdx == 0 {
return 0
}

layer := 1
layerWidth := t.BranchFactor
entriesSoFar := 1 + t.BranchFactor

for {
if entryIdx < entriesSoFar {
return layer
}

layer++
layerWidth *= t.BranchFactor
entriesSoFar += layerWidth
}
}
65 changes: 65 additions & 0 deletions gnetdag/fixedtree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package gnetdag_test

import (
"testing"

"github.com/gordian-engine/gordian/gnetdag"
"github.com/stretchr/testify/require"
)

// Most of these tests use a branch factor of 3,
// resulting in layers like:
// 0 (L0)
// 1 2 3 (L1)
// 4 5 6 7 8 9 10 11 12 (L2)
// 13 14 15 16... (L3)

func TestFixedTree_Layer(t *testing.T) {
t.Parallel()

tree := gnetdag.FixedTree{BranchFactor: 3}
require.Equal(t, 0, tree.Layer(0))
require.Equal(t, 1, tree.Layer(1))
require.Equal(t, 2, tree.Layer(4))

tree.BranchFactor = 5
require.Equal(t, 0, tree.Layer(0))
require.Equal(t, 1, tree.Layer(4))
}

func TestFixedTree_Parent(t *testing.T) {
t.Parallel()

tree := gnetdag.FixedTree{BranchFactor: 3}
require.Equal(t, -1, tree.Parent(0))

require.Equal(t, 0, tree.Parent(1))
require.Equal(t, 0, tree.Parent(2))
require.Equal(t, 0, tree.Parent(3))

require.Equal(t, 1, tree.Parent(4))
require.Equal(t, 1, tree.Parent(5))
require.Equal(t, 1, tree.Parent(6))
require.Equal(t, 2, tree.Parent(7))
require.Equal(t, 2, tree.Parent(8))
require.Equal(t, 2, tree.Parent(9))
require.Equal(t, 3, tree.Parent(10))
require.Equal(t, 3, tree.Parent(11))
require.Equal(t, 3, tree.Parent(12))

require.Equal(t, 4, tree.Parent(13))
}

func TestFixedTree_FirstChild(t *testing.T) {
t.Parallel()

tree := gnetdag.FixedTree{BranchFactor: 3}

require.Equal(t, 1, tree.FirstChild(0))

require.Equal(t, 4, tree.FirstChild(1))
require.Equal(t, 7, tree.FirstChild(2))
require.Equal(t, 10, tree.FirstChild(3))

require.Equal(t, 13, tree.FirstChild(4))
}

0 comments on commit 4eefb14

Please sign in to comment.