mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-25 04:47:18 +00:00
aeb6940935
* Prune synced tips * Terence's minor cleanups * Update optimistic_sync.go * go fmt Co-authored-by: terence tsao <terence@prysmaticlabs.com>
550 lines
15 KiB
Go
550 lines
15 KiB
Go
package protoarray
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
types "github.com/prysmaticlabs/eth2-types"
|
|
"github.com/prysmaticlabs/prysm/config/params"
|
|
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
|
"github.com/prysmaticlabs/prysm/testing/require"
|
|
)
|
|
|
|
// We test the algorithm to check the optimistic status of a node. The
|
|
// status for this test is the following branching diagram
|
|
//
|
|
// -- E -- F
|
|
// /
|
|
// -- C -- D
|
|
// /
|
|
// 0 -- 1 -- A -- B -- J -- K
|
|
// \ /
|
|
// -- G -- H -- I
|
|
//
|
|
// Here nodes 0, 1, A, B, C, D are fully validated and nodes
|
|
// E, F, G, H, J, K are optimistic.
|
|
// Synced Tips are nodes B, C, D
|
|
// nodes 0 and 1 are outside the Fork Choice Store.
|
|
|
|
func TestOptimistic(t *testing.T) {
|
|
root0 := bytesutil.ToBytes32([]byte("hello0"))
|
|
slot0 := types.Slot(98)
|
|
root1 := bytesutil.ToBytes32([]byte("hello1"))
|
|
slot1 := types.Slot(99)
|
|
|
|
nodeA := &Node{
|
|
slot: types.Slot(100),
|
|
root: bytesutil.ToBytes32([]byte("helloA")),
|
|
bestChild: 1,
|
|
}
|
|
nodeB := &Node{
|
|
slot: types.Slot(101),
|
|
root: bytesutil.ToBytes32([]byte("helloB")),
|
|
bestChild: 2,
|
|
parent: 0,
|
|
}
|
|
nodeC := &Node{
|
|
slot: types.Slot(102),
|
|
root: bytesutil.ToBytes32([]byte("helloC")),
|
|
bestChild: 3,
|
|
parent: 1,
|
|
}
|
|
nodeD := &Node{
|
|
slot: types.Slot(103),
|
|
root: bytesutil.ToBytes32([]byte("helloD")),
|
|
bestChild: NonExistentNode,
|
|
parent: 2,
|
|
}
|
|
nodeE := &Node{
|
|
slot: types.Slot(103),
|
|
root: bytesutil.ToBytes32([]byte("helloE")),
|
|
bestChild: 5,
|
|
parent: 2,
|
|
}
|
|
nodeF := &Node{
|
|
slot: types.Slot(104),
|
|
root: bytesutil.ToBytes32([]byte("helloF")),
|
|
bestChild: NonExistentNode,
|
|
parent: 4,
|
|
}
|
|
nodeG := &Node{
|
|
slot: types.Slot(102),
|
|
root: bytesutil.ToBytes32([]byte("helloG")),
|
|
bestChild: 7,
|
|
parent: 1,
|
|
}
|
|
nodeH := &Node{
|
|
slot: types.Slot(103),
|
|
root: bytesutil.ToBytes32([]byte("helloH")),
|
|
bestChild: 8,
|
|
parent: 6,
|
|
}
|
|
nodeI := &Node{
|
|
slot: types.Slot(104),
|
|
root: bytesutil.ToBytes32([]byte("helloI")),
|
|
bestChild: NonExistentNode,
|
|
parent: 7,
|
|
}
|
|
nodeJ := &Node{
|
|
slot: types.Slot(103),
|
|
root: bytesutil.ToBytes32([]byte("helloJ")),
|
|
bestChild: 10,
|
|
parent: 6,
|
|
}
|
|
nodeK := &Node{
|
|
slot: types.Slot(104),
|
|
root: bytesutil.ToBytes32([]byte("helloK")),
|
|
bestChild: NonExistentNode,
|
|
parent: 9,
|
|
}
|
|
nodes := []*Node{
|
|
nodeA,
|
|
nodeB,
|
|
nodeC,
|
|
nodeD,
|
|
nodeE,
|
|
nodeF,
|
|
nodeG,
|
|
nodeH,
|
|
nodeI,
|
|
nodeJ,
|
|
nodeK,
|
|
}
|
|
ni := map[[32]byte]uint64{
|
|
nodeA.root: 0,
|
|
nodeB.root: 1,
|
|
nodeC.root: 2,
|
|
nodeD.root: 3,
|
|
nodeE.root: 4,
|
|
nodeF.root: 5,
|
|
nodeG.root: 6,
|
|
nodeH.root: 7,
|
|
nodeI.root: 8,
|
|
nodeJ.root: 9,
|
|
nodeK.root: 10,
|
|
}
|
|
|
|
s := &Store{
|
|
nodes: nodes,
|
|
nodesIndices: ni,
|
|
}
|
|
|
|
tips := map[[32]byte]types.Slot{
|
|
nodeB.root: nodeB.slot,
|
|
nodeC.root: nodeC.slot,
|
|
nodeD.root: nodeD.slot,
|
|
}
|
|
st := &optimisticStore{
|
|
validatedTips: tips,
|
|
}
|
|
f := &ForkChoice{
|
|
store: s,
|
|
syncedTips: st,
|
|
}
|
|
ctx := context.Background()
|
|
// We test the implementation of boundarySyncedTips
|
|
min, max := f.boundarySyncedTips()
|
|
require.Equal(t, min, types.Slot(101), "minimum tip slot is different")
|
|
require.Equal(t, max, types.Slot(103), "maximum tip slot is different")
|
|
|
|
// We test first nodes outside the Fork Choice store
|
|
op, err := f.Optimistic(ctx, root0, slot0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, false)
|
|
|
|
op, err = f.Optimistic(ctx, root1, slot1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, false)
|
|
|
|
// We check all nodes in the Fork Choice store.
|
|
op, err = f.Optimistic(ctx, nodeA.root, nodeA.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, false)
|
|
|
|
op, err = f.Optimistic(ctx, nodeB.root, nodeB.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, false)
|
|
|
|
op, err = f.Optimistic(ctx, nodeC.root, nodeC.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, false)
|
|
|
|
op, err = f.Optimistic(ctx, nodeD.root, nodeD.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, false)
|
|
|
|
op, err = f.Optimistic(ctx, nodeE.root, nodeE.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
|
|
op, err = f.Optimistic(ctx, nodeF.root, nodeF.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
|
|
op, err = f.Optimistic(ctx, nodeG.root, nodeG.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
|
|
op, err = f.Optimistic(ctx, nodeH.root, nodeH.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
|
|
op, err = f.Optimistic(ctx, nodeI.root, nodeI.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
|
|
op, err = f.Optimistic(ctx, nodeJ.root, nodeJ.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
|
|
op, err = f.Optimistic(ctx, nodeK.root, nodeK.slot)
|
|
require.NoError(t, err)
|
|
require.Equal(t, op, true)
|
|
}
|
|
|
|
// This tests the algorithm to update syncedTips
|
|
// We start with the following diagram
|
|
//
|
|
// E -- F
|
|
// /
|
|
// C -- D
|
|
// / \
|
|
// A -- B G -- H -- I
|
|
// \ \
|
|
// J -- K -- L
|
|
//
|
|
// And every block in the Fork choice is optimistic. Synced_Tips contains a
|
|
// single block that is outside of Fork choice
|
|
//
|
|
func TestUpdateSyncTipsWithValidRoots(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := setup(1, 1)
|
|
|
|
require.NoError(t, f.ProcessBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{}, 1, 1))
|
|
tests := []struct {
|
|
root [32]byte // the root of the new VALID block
|
|
tips map[[32]byte]types.Slot // the old synced tips
|
|
newtips map[[32]byte]types.Slot // the updated synced tips
|
|
wantedErr error
|
|
}{
|
|
{
|
|
[32]byte{'i'},
|
|
map[[32]byte]types.Slot{[32]byte{'z'}: 90},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
[32]byte{'i'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
[32]byte{'i'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'e'}: 103,
|
|
},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'e'}: 104,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
[32]byte{'j'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'f'}: 105,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'f'}: 105,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
[32]byte{'j'}: 102,
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
[32]byte{'g'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'f'}: 105,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'f'}: 105,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
[32]byte{'j'}: 102,
|
|
},
|
|
errInvalidBestChildIndex,
|
|
},
|
|
{
|
|
[32]byte{'p'},
|
|
map[[32]byte]types.Slot{},
|
|
map[[32]byte]types.Slot{},
|
|
errInvalidNodeIndex,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
f.syncedTips.Lock()
|
|
f.syncedTips.validatedTips = tc.tips
|
|
f.syncedTips.Unlock()
|
|
err := f.UpdateSyncedTipsWithValidRoot(context.Background(), tc.root)
|
|
if tc.wantedErr != nil {
|
|
require.ErrorIs(t, err, tc.wantedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
f.syncedTips.RLock()
|
|
require.DeepEqual(t, f.syncedTips.validatedTips, tc.newtips)
|
|
f.syncedTips.RUnlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
// We test the algorithm to update a node from SYNCING to INVALID
|
|
// We start with the same diagram as above:
|
|
//
|
|
// E(2) -- F(1)
|
|
// /
|
|
// C(7) -- D(6)
|
|
// / \
|
|
// A(10) -- B(9) G(3) -- H(1) -- I(0)
|
|
// \ \
|
|
// J(1) -- K(1) -- L(0)
|
|
//
|
|
// And every block in the Fork choice is optimistic. Synced_Tips contains a
|
|
// single block that is outside of Fork choice. The numbers in parenthesis are
|
|
// the weights of the nodes before removal
|
|
//
|
|
func TestUpdateSyncTipsWithInvalidRoot(t *testing.T) {
|
|
tests := []struct {
|
|
root [32]byte // the root of the new INVALID block
|
|
tips map[[32]byte]types.Slot // the old synced tips
|
|
wantedParentTip bool
|
|
newBestChild uint64
|
|
newBestDescendant uint64
|
|
newParentWeight uint64
|
|
}{
|
|
{
|
|
[32]byte{'j'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 104,
|
|
},
|
|
false,
|
|
3,
|
|
4,
|
|
8,
|
|
},
|
|
{
|
|
[32]byte{'j'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
},
|
|
true,
|
|
3,
|
|
4,
|
|
8,
|
|
},
|
|
{
|
|
[32]byte{'i'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'h'}: 105,
|
|
},
|
|
true,
|
|
NonExistentNode,
|
|
NonExistentNode,
|
|
1,
|
|
},
|
|
{
|
|
[32]byte{'i'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 104,
|
|
},
|
|
false,
|
|
NonExistentNode,
|
|
NonExistentNode,
|
|
1,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
ctx := context.Background()
|
|
f := setup(1, 1)
|
|
|
|
require.NoError(t, f.ProcessBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{}, 1, 1))
|
|
weights := []uint64{10, 10, 9, 7, 1, 6, 2, 3, 1, 1, 1, 0, 0}
|
|
f.syncedTips.Lock()
|
|
f.syncedTips.validatedTips = tc.tips
|
|
f.syncedTips.Unlock()
|
|
f.store.nodesLock.Lock()
|
|
for i, node := range f.store.nodes {
|
|
node.weight = weights[i]
|
|
}
|
|
// Make j be the best child and descendant of b
|
|
nodeB := f.store.nodes[2]
|
|
nodeB.bestChild = 4
|
|
nodeB.bestDescendant = 4
|
|
idx := f.store.nodesIndices[tc.root]
|
|
node := f.store.nodes[idx]
|
|
parentIndex := node.parent
|
|
require.NotEqual(t, NonExistentNode, parentIndex)
|
|
parent := f.store.nodes[parentIndex]
|
|
f.store.nodesLock.Unlock()
|
|
err := f.UpdateSyncedTipsWithInvalidRoot(context.Background(), tc.root)
|
|
require.NoError(t, err)
|
|
f.syncedTips.RLock()
|
|
_, parentSyncedTip := f.syncedTips.validatedTips[parent.root]
|
|
f.syncedTips.RUnlock()
|
|
require.Equal(t, tc.wantedParentTip, parentSyncedTip)
|
|
require.Equal(t, tc.newBestChild, parent.bestChild)
|
|
require.Equal(t, tc.newBestDescendant, parent.bestDescendant)
|
|
require.Equal(t, tc.newParentWeight, parent.weight)
|
|
}
|
|
}
|
|
|
|
// This tests the algorithm to find the tip of a given node
|
|
// We start with the following diagram
|
|
//
|
|
// E -- F
|
|
// /
|
|
// C -- D
|
|
// / \
|
|
// A -- B G -- H -- I
|
|
// \ \
|
|
// J -- K -- L
|
|
//
|
|
//
|
|
func TestFindSyncedTip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := setup(1, 1)
|
|
|
|
require.NoError(t, f.ProcessBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{}, 1, 1))
|
|
require.NoError(t, f.ProcessBlock(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{}, 1, 1))
|
|
tests := []struct {
|
|
root [32]byte // the root of the block
|
|
tips map[[32]byte]types.Slot // the synced tips
|
|
wanted [32]byte // the root of expected tip
|
|
}{
|
|
{
|
|
[32]byte{'i'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 104,
|
|
},
|
|
[32]byte{'g'},
|
|
},
|
|
{
|
|
[32]byte{'g'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'h'}: 104,
|
|
[32]byte{'k'}: 106,
|
|
},
|
|
[32]byte{'d'},
|
|
},
|
|
{
|
|
[32]byte{'e'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'d'}: 103,
|
|
[32]byte{'g'}: 103,
|
|
},
|
|
[32]byte{'d'},
|
|
},
|
|
{
|
|
[32]byte{'j'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'f'}: 105,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
[32]byte{'b'},
|
|
},
|
|
{
|
|
[32]byte{'g'},
|
|
map[[32]byte]types.Slot{
|
|
[32]byte{'b'}: 101,
|
|
[32]byte{'f'}: 105,
|
|
[32]byte{'g'}: 104,
|
|
[32]byte{'i'}: 106,
|
|
},
|
|
[32]byte{'g'},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
f.store.nodesLock.RLock()
|
|
defer f.store.nodesLock.RUnlock()
|
|
node := f.store.nodes[f.store.nodesIndices[tc.root]]
|
|
syncedTips := &optimisticStore{
|
|
validatedTips: tc.tips,
|
|
}
|
|
syncedTips.RLock()
|
|
defer syncedTips.RUnlock()
|
|
idx, err := f.store.findSyncedTip(ctx, node, syncedTips)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.wanted, f.store.nodes[idx].root)
|
|
}
|
|
}
|