From 5cc6de9e67482cc4ec607c88db10e1de8be43e52 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Wed, 22 Jan 2020 12:19:52 -0800 Subject: [PATCH] Part 3 of proto array fork choice - compute delta helper (#4617) * Docs * Interface definitions * Fmt and gazelle * Rename interface to interfaces * Define all the type for protoarray * Gaz * Add error types * Add compute delta helper * Compute delta tests * Gaz * Fix formatting and comments * Apply suggestions from code review Co-authored-by: Ivan Martinez --- .../forkchoice/protoarray/BUILD.bazel | 18 +- beacon-chain/forkchoice/protoarray/errors.go | 12 + beacon-chain/forkchoice/protoarray/helpers.go | 72 ++++ .../forkchoice/protoarray/helpers_test.go | 325 ++++++++++++++++++ 4 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 beacon-chain/forkchoice/protoarray/errors.go create mode 100644 beacon-chain/forkchoice/protoarray/helpers.go create mode 100644 beacon-chain/forkchoice/protoarray/helpers_test.go diff --git a/beacon-chain/forkchoice/protoarray/BUILD.bazel b/beacon-chain/forkchoice/protoarray/BUILD.bazel index 53c4865f43..d49ec40143 100644 --- a/beacon-chain/forkchoice/protoarray/BUILD.bazel +++ b/beacon-chain/forkchoice/protoarray/BUILD.bazel @@ -1,11 +1,27 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ "doc.go", + "errors.go", + "helpers.go", "types.go", ], importpath = "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/protoarray", visibility = ["//beacon-chain:__subpackages__"], + deps = [ + "//shared/params:go_default_library", + "@io_opencensus_go//trace:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["helpers_test.go"], + embed = [":go_default_library"], + deps = [ + "//shared/hashutil:go_default_library", + "//shared/params:go_default_library", + ], ) diff --git a/beacon-chain/forkchoice/protoarray/errors.go b/beacon-chain/forkchoice/protoarray/errors.go new file mode 100644 index 0000000000..7d4ed5a7b8 --- /dev/null +++ b/beacon-chain/forkchoice/protoarray/errors.go @@ -0,0 +1,12 @@ +package protoarray + +import "errors" + +var errUnknownFinalizedRoot = errors.New("unknown finalized root") +var errUnknownJustifiedRoot = errors.New("unknown justified root") +var errInvalidNodeIndex = errors.New("node index is invalid") +var errInvalidJustifiedIndex = errors.New("justified index is invalid") +var errInvalidBestDescendantIndex = errors.New("best descendant index is invalid") +var errInvalidParentDelta = errors.New("parent delta is invalid") +var errInvalidNodeDelta = errors.New("node delta is invalid") +var errInvalidDeltaLength = errors.New("delta length is invalid") diff --git a/beacon-chain/forkchoice/protoarray/helpers.go b/beacon-chain/forkchoice/protoarray/helpers.go new file mode 100644 index 0000000000..7670009064 --- /dev/null +++ b/beacon-chain/forkchoice/protoarray/helpers.go @@ -0,0 +1,72 @@ +package protoarray + +import ( + "context" + + "github.com/prysmaticlabs/prysm/shared/params" + "go.opencensus.io/trace" +) + +// This computes validator balance delta from validator votes. +// It returns a list of deltas that represents the difference between old balances and new balances. +func computeDeltas( + ctx context.Context, + blockIndices map[[32]byte]uint64, + votes []Vote, + oldBalances []uint64, + newBalances []uint64, +) ([]int, []Vote, error) { + ctx, span := trace.StartSpan(ctx, "protoArrayForkChoice.computeDeltas") + defer span.End() + + deltas := make([]int, len(blockIndices)) + + for validatorIndex, vote := range votes { + oldBalance := uint64(0) + newBalance := uint64(0) + + // Skip if validator has never voted for current root and next root (ie. if the + // votes are zero hash aka genesis block), there's nothing to compute. + if vote.currentRoot == params.BeaconConfig().ZeroHash && vote.nextRoot == params.BeaconConfig().ZeroHash { + continue + } + + // If the validator index did not exist in `oldBalance` or `newBalance` list above, the balance is just 0. + if validatorIndex < len(oldBalances) { + oldBalance = oldBalances[validatorIndex] + } + if validatorIndex < len(newBalances) { + newBalance = newBalances[validatorIndex] + } + + // Perform delta only if the validator's balance or vote has changed. + if vote.currentRoot != vote.nextRoot || oldBalance != newBalance { + // Ignore the vote if it's not known in `blockIndices`, + // that means we have not seen the block before. + nextDeltaIndex, ok := blockIndices[vote.nextRoot] + if ok { + // Extra protection against out of bound, the `nextDeltaIndex` which defines + // the block location in the dag can not exceed the total `delta` length. + if int(nextDeltaIndex) >= len(deltas) { + return nil, nil, errInvalidNodeDelta + } + deltas[nextDeltaIndex] += int(newBalance) + } + + currentDeltaIndex, ok := blockIndices[vote.currentRoot] + if ok { + // Extra protection against out of bound (same as above). + if int(currentDeltaIndex) >= len(deltas) { + return nil, nil, errInvalidNodeDelta + } + deltas[currentDeltaIndex] -= int(oldBalance) + } + } + + // Rotate the validator vote. + vote.currentRoot = vote.nextRoot + votes[validatorIndex] = vote + } + + return deltas, votes, nil +} diff --git a/beacon-chain/forkchoice/protoarray/helpers_test.go b/beacon-chain/forkchoice/protoarray/helpers_test.go new file mode 100644 index 0000000000..73bb4f96be --- /dev/null +++ b/beacon-chain/forkchoice/protoarray/helpers_test.go @@ -0,0 +1,325 @@ +package protoarray + +import ( + "context" + "encoding/binary" + "testing" + + "github.com/prysmaticlabs/prysm/shared/hashutil" + "github.com/prysmaticlabs/prysm/shared/params" +) + +func TestComputeDelta_ZeroHash(t *testing.T) { + validatorCount := uint64(16) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := make([]uint64, 0) + newBalances := make([]uint64, 0) + + for i := uint64(0); i < validatorCount; i++ { + indices[indexToHash(i)] = i + votes = append(votes, Vote{params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 0}) + oldBalances = append(oldBalances, 0) + newBalances = append(newBalances, 0) + } + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + if len(delta) != int(validatorCount) { + t.Error("Incorrect length") + } + for _, d := range delta { + if d != 0 { + t.Error("Delta should be zero") + } + } + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_AllVoteTheSame(t *testing.T) { + validatorCount := uint64(16) + balance := uint64(32) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := make([]uint64, 0) + newBalances := make([]uint64, 0) + + for i := uint64(0); i < validatorCount; i++ { + indices[indexToHash(i)] = i + votes = append(votes, Vote{params.BeaconConfig().ZeroHash, indexToHash(0), 0}) + oldBalances = append(oldBalances, balance) + newBalances = append(newBalances, balance) + } + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + if len(delta) != int(validatorCount) { + t.Error("Incorrect length") + } + + for i, d := range delta { + if i == 0 { + if uint64(d) != balance*validatorCount { + t.Error("Did not get correct balance") + } + } else { + if d != 0 { + t.Error("Delta should be zero") + } + } + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_DifferentVotes(t *testing.T) { + validatorCount := uint64(16) + balance := uint64(32) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := make([]uint64, 0) + newBalances := make([]uint64, 0) + + for i := uint64(0); i < validatorCount; i++ { + indices[indexToHash(i)] = i + votes = append(votes, Vote{params.BeaconConfig().ZeroHash, indexToHash(i), 0}) + oldBalances = append(oldBalances, balance) + newBalances = append(newBalances, balance) + } + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + if len(delta) != int(validatorCount) { + t.Error("Incorrect length") + } + + for _, d := range delta { + if uint64(d) != balance { + t.Error("Did not get correct delta") + } + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_MovingVotes(t *testing.T) { + validatorCount := uint64(16) + balance := uint64(32) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := make([]uint64, 0) + newBalances := make([]uint64, 0) + + lastIndex := uint64(len(indices) - 1) + for i := uint64(0); i < validatorCount; i++ { + indices[indexToHash(i)] = i + votes = append(votes, Vote{indexToHash(0), indexToHash(lastIndex), 0}) + oldBalances = append(oldBalances, balance) + newBalances = append(newBalances, balance) + } + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + if len(delta) != int(validatorCount) { + t.Error("Incorrect length") + } + + for i, d := range delta { + if i == 0 { + if d != -int(balance*validatorCount) { + t.Error("First root should have negative delta") + } + } else if i == int(lastIndex) { + if d != int(balance*validatorCount) { + t.Error("Last root should have positive delta") + } + } else { + if d != 0 { + t.Error("Delta should be zero") + } + } + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_MoveOutOfTree(t *testing.T) { + balance := uint64(32) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := []uint64{balance, balance} + newBalances := []uint64{balance, balance} + + indices[indexToHash(1)] = 0 + + votes = append(votes, Vote{indexToHash(1), params.BeaconConfig().ZeroHash, 0}) + votes = append(votes, Vote{indexToHash(1), [32]byte{'A'}, 0}) + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + if len(delta) != 1 { + t.Error("Incorrect length") + } + + if delta[0] != 0-2*int(balance) { + t.Error("Incorrect delta") + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_ChangingBalances(t *testing.T) { + oldBalance := uint64(32) + newBalance := oldBalance * 2 + validatorCount := uint64(16) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := make([]uint64, 0) + newBalances := make([]uint64, 0) + + indices[indexToHash(1)] = 0 + + for i := uint64(0); i < validatorCount; i++ { + indices[indexToHash(i)] = i + votes = append(votes, Vote{indexToHash(0), indexToHash(1), 0}) + oldBalances = append(oldBalances, oldBalance) + newBalances = append(newBalances, newBalance) + } + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + if len(delta) != 16 { + t.Error("Incorrect length") + } + for i, d := range delta { + if i == 0 { + if d != -int(oldBalance*validatorCount) { + t.Error("First root should have negative delta") + } + } else if i == 1 { + if d != int(newBalance*validatorCount) { + t.Error("Last root should have positive delta") + } + } else { + if d != 0 { + t.Error("Delta should be zero") + } + } + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_ValidatorAppear(t *testing.T) { + balance := uint64(32) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := []uint64{balance} + newBalances := []uint64{balance, balance} + + indices[indexToHash(1)] = 0 + indices[indexToHash(2)] = 1 + + votes = append(votes, Vote{indexToHash(1), indexToHash(2), 0}) + votes = append(votes, Vote{indexToHash(1), indexToHash(2), 0}) + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + + if len(delta) != 2 { + t.Error("Incorrect length") + } + + if delta[0] != 0-int(balance) { + t.Error("Incorrect delta") + } + if delta[1] != 2*int(balance) { + t.Error("Incorrect delta") + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func TestComputeDelta_ValidatorDisappears(t *testing.T) { + balance := uint64(32) + indices := make(map[[32]byte]uint64) + votes := make([]Vote, 0) + oldBalances := []uint64{balance, balance} + newBalances := []uint64{balance} + + indices[indexToHash(1)] = 0 + indices[indexToHash(2)] = 1 + + votes = append(votes, Vote{indexToHash(1), indexToHash(2), 0}) + votes = append(votes, Vote{indexToHash(1), indexToHash(2), 0}) + + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + if err != nil { + t.Fatal(err) + } + + if len(delta) != 2 { + t.Error("Incorrect length") + } + + if delta[0] != 0-2*int(balance) { + t.Error("Incorrect delta") + } + if delta[1] != int(balance) { + t.Error("Incorrect delta") + } + + for _, vote := range votes { + if vote.currentRoot != vote.nextRoot { + t.Errorf("The vote should have changed") + } + } +} + +func indexToHash(i uint64) [32]byte { + var b [8]byte + binary.LittleEndian.PutUint64(b[:], i) + return hashutil.Hash(b[:]) +}