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

[Consensus] SignerIndices Optimization (2/4) #2101

Merged
merged 32 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
acc75b3
implement IdentitiesByIndices
zhangchiqing Feb 25, 2022
e54c08e
convert signer index
zhangchiqing Feb 28, 2022
99c1858
implement encoding and decoding
zhangchiqing Mar 2, 2022
54be691
add test cases
zhangchiqing Mar 2, 2022
11947f1
remove signer indices file
zhangchiqing Mar 2, 2022
a5b6ae8
fix Packer
zhangchiqing Mar 2, 2022
d612738
add comments
zhangchiqing Mar 2, 2022
709fc96
remove IdentitiesByIndices
zhangchiqing Mar 3, 2022
e084823
update mock
zhangchiqing Mar 3, 2022
52e2bc9
fix test cases
zhangchiqing Mar 3, 2022
64f8324
add comments
zhangchiqing Mar 3, 2022
7b6a1cf
remove fields
zhangchiqing Mar 3, 2022
32d162c
fix ParentVoterIDs
zhangchiqing Mar 4, 2022
6c0586d
fix verifier
zhangchiqing Mar 4, 2022
c7be2ad
fix mock
zhangchiqing Mar 4, 2022
0dc3c71
implement staking vote progressor
zhangchiqing Mar 4, 2022
84cb177
fix tests
zhangchiqing Mar 4, 2022
715743e
move to module/packer
zhangchiqing Mar 5, 2022
3675126
fix tests
zhangchiqing Mar 5, 2022
929bcba
fix validator test
zhangchiqing Mar 7, 2022
a1aca6a
fix validator test
zhangchiqing Mar 7, 2022
f47f27c
use ParentVoterIndices
zhangchiqing Mar 8, 2022
2086754
add QuorumCertificateWithSignerIDs
zhangchiqing Mar 9, 2022
dd85d5e
update blockSummary
zhangchiqing Mar 9, 2022
cf648c5
move module
zhangchiqing Mar 10, 2022
af6f97d
add test cases
zhangchiqing Mar 10, 2022
b09691f
remove comments
zhangchiqing Mar 10, 2022
f7c8290
add check to EncodeSignerIndices
zhangchiqing Mar 10, 2022
3ab8b59
Apply suggestions from code review
zhangchiqing Mar 11, 2022
b886dea
Apply suggestions from code review
zhangchiqing Mar 17, 2022
619a82e
Apply suggestions from code review
zhangchiqing Mar 17, 2022
c94e689
[Consensus] Use SignerIndices in CollectionGuarantee (3/4) (#2140)
zhangchiqing Mar 30, 2022
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
12 changes: 8 additions & 4 deletions cmd/bootstrap/cmd/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
model "github.com/onflow/flow-go/model/bootstrap"
"github.com/onflow/flow-go/model/cluster"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/model/flow/assignment"
"github.com/onflow/flow-go/model/flow/factory"
"github.com/onflow/flow-go/model/flow/filter"
)

Expand All @@ -23,20 +25,22 @@ func constructClusterAssignment(partnerNodes, internalNodes []model.NodeInfo, se
internals = internals.DeterministicShuffle(seed)

nClusters := flagCollectionClusters
assignments := make(flow.AssignmentList, nClusters)
identifierLists := make([]flow.IdentifierList, nClusters)

// first, round-robin internal nodes into each cluster
for i, node := range internals {
assignments[i%len(assignments)] = append(assignments[i%len(assignments)], node.NodeID)
identifierLists[i%len(identifierLists)] = append(identifierLists[i%len(identifierLists)], node.NodeID)
}

// next, round-robin partner nodes into each cluster
for i, node := range partners {
assignments[i%len(assignments)] = append(assignments[i%len(assignments)], node.NodeID)
identifierLists[i%len(identifierLists)] = append(identifierLists[i%len(identifierLists)], node.NodeID)
}

assignments := assignment.FromIdentifierLists(identifierLists)

collectors := append(partners, internals...)
clusters, err := flow.NewClusterList(assignments, collectors)
clusters, err := factory.NewClusterList(assignments, collectors)
if err != nil {
log.Fatal().Err(err).Msg("could not create cluster list")
}
Expand Down
12 changes: 6 additions & 6 deletions cmd/bootstrap/cmd/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ func checkConstraints(partnerNodes, internalNodes []model.NodeInfo) {
if _, exists := internals.ByNodeID(node.NodeID); exists {
clusterInternalCount++
}
if clusterInternalCount <= clusterPartnerCount*2 {
log.Fatal().Msgf(
"will not bootstrap configuration without Byzantine majority within cluster: "+
"(partners=%d, internals=%d, min_internals=%d)",
clusterPartnerCount, clusterInternalCount, clusterPartnerCount*2+1)
}
}
if clusterInternalCount <= clusterPartnerCount*2 {
log.Fatal().Msgf(
"will not bootstrap configuration without Byzantine majority within cluster: "+
"(partners=%d, internals=%d, min_internals=%d)",
clusterPartnerCount, clusterInternalCount, clusterPartnerCount*2+1)
}
partnerCOLCount += clusterPartnerCount
internalCOLCount += clusterInternalCount
Expand Down
18 changes: 17 additions & 1 deletion cmd/bootstrap/cmd/seal.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/onflow/flow-go/model/dkg"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/model/flow/order"
"github.com/onflow/flow-go/module/signature"
)

func constructRootResultAndSeal(
Expand Down Expand Up @@ -43,9 +44,24 @@ func constructRootResultAndSeal(
RandomSource: getRandomSource(flagBootstrapRandomSeed),
}

qcsWithSignerIDs := make([]*flow.QuorumCertificateWithSignerIDs, 0, len(clusterQCs))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why adding QuorumCertificateWithSignerIDs type?

The smart contract emits a EpochCommit Event, in which the ClusterQC contains VoterIDs, and we need to convert the cadence value into golang struct to be used in protocol. It's better to keep the data type of ClusterQC similar to the same struct in smart contract, therefore, I had to revert the change to ClusterQC in order to keep using VoterIDs field,, which makes the conversion side-effect free.

However the QC now uses SignerIndices, which means flow.ClusterQCVoteDatasFromQCs can't convert directly QC into ClusterQC.

I could change flow.ClusterQCVoteDatasFromQCs to let it take (qc, signerIDs) , but it looks weird that the two inputs seems have no relation, but in fact the signerIDs must be converted from qc.SignerIndices.

In order to make it a bit type-safe, I decided to define the QuorumCertificateWithSignerIDs. Now the caller of ClusterQCVoteDatasFromQCs is responsible to convert QC into QuorumCertificateWithSignerIDs, such that clusterQC can be converted from it.

The drawback of this approach is that now we have two QC definition QuorumCertificate and QuorumCertificateWithSignerIDs. Also weird.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
I like this option, because we anyway package the bootstrapping information on its own. By allowing the bootstrapping information to be in its own custom format, we gain flexibility.

for i, clusterQC := range clusterQCs {
members := assignments[i]
signerIDs, err := signature.DecodeSignerIndicesToIdentifiers(members, clusterQC.SignerIndices)
if err != nil {
log.Fatal().Err(err).Msgf("could not decode signer IDs from clusterQC at index %v", i)
}
qcsWithSignerIDs = append(qcsWithSignerIDs, &flow.QuorumCertificateWithSignerIDs{
View: clusterQC.View,
BlockID: clusterQC.BlockID,
SignerIDs: signerIDs,
SigData: clusterQC.SigData,
})
}

epochCommit := &flow.EpochCommit{
Counter: flagEpochCounter,
ClusterQCs: flow.ClusterQCVoteDatasFromQCs(clusterQCs),
ClusterQCs: flow.ClusterQCVoteDatasFromQCs(qcsWithSignerIDs),
DKGGroupKey: dkgData.PubGroupKey,
DKGParticipantKeys: dkgData.PubKeyShares,
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/bootstrap/run/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func GenerateRootBlock(chainID flow.ChainID, parentID flow.Identifier, height ui
PayloadHash: payload.Hash(),
Timestamp: timestamp,
View: 0,
ParentVoterIDs: nil,
ParentVoterIndices: nil,
ParentVoterSigData: nil,
ProposerID: flow.ZeroID,
ProposerSigData: nil,
Expand Down
4 changes: 2 additions & 2 deletions cmd/util/cmd/epochs/cmd/reset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestReset_LocalSnapshot(t *testing.T) {
unittest.RunWithTempDir(t, func(bootDir string) {

// create a root snapshot
rootSnapshot := unittest.RootSnapshotFixture(unittest.IdentityListFixture(10))
rootSnapshot := unittest.RootSnapshotFixture(unittest.IdentityListFixture(10, unittest.WithAllRoles()))

// write snapshot to correct path in bootDir
err := writeRootSnapshot(bootDir, rootSnapshot)
Expand Down Expand Up @@ -62,7 +62,7 @@ func TestReset_LocalSnapshot(t *testing.T) {
unittest.RunWithTempDir(t, func(bootDir string) {

// create a root snapshot
rootSnapshot := unittest.RootSnapshotFixture(unittest.IdentityListFixture(10))
rootSnapshot := unittest.RootSnapshotFixture(unittest.IdentityListFixture(10, unittest.WithAllRoles()))

// write snapshot to correct path in bootDir
err := writeRootSnapshot(bootDir, rootSnapshot)
Expand Down
38 changes: 17 additions & 21 deletions cmd/util/cmd/exec-data-json-export/block_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import (
)

type blockSummary struct {
BlockHeight uint64 `json:"block_height"`
BlockID string `json:"block_id"`
ParentBlockID string `json:"parent_block_id"`
ParentVoterIDs []string `json:"parent_voter_ids"`
// ParentVoterSigData []string `json:"parent_voter_sig"`
ProposerID string `json:"proposer_id"`
BlockHeight uint64 `json:"block_height"`
BlockID string `json:"block_id"`
ParentBlockID string `json:"parent_block_id"`
ParentVoterIndices string `json:"parent_voter_indices"`
ParentVoterSigData string `json:"parent_voter_sig"`
ProposerID string `json:"proposer_id"`
// ProposerSigData string `json:"proposer_sig"`
Timestamp time.Time `json:"timestamp"`
CollectionIDs []string `json:"collection_ids"`
Expand Down Expand Up @@ -87,22 +87,18 @@ func ExportBlocks(blockID flow.Identifier, dbPath string, outputPath string) (fl
sealsStates = append(sealsStates, hex.EncodeToString(s.FinalState[:]))
}

pvIDs := make([]string, 0)
for _, i := range header.ParentVoterIDs {
pvIDs = append(pvIDs, hex.EncodeToString(i[:]))
}

b := blockSummary{
BlockID: hex.EncodeToString(activeBlockID[:]),
BlockHeight: header.Height,
ParentBlockID: hex.EncodeToString(header.ParentID[:]),
ParentVoterIDs: pvIDs,
ProposerID: hex.EncodeToString(header.ProposerID[:]),
Timestamp: header.Timestamp,
CollectionIDs: cols,
SealedBlocks: seals,
SealedResults: sealsResults,
SealedFinalStates: sealsStates,
BlockID: hex.EncodeToString(activeBlockID[:]),
BlockHeight: header.Height,
ParentBlockID: hex.EncodeToString(header.ParentID[:]),
ParentVoterIndices: hex.EncodeToString(header.ParentVoterIndices),
ParentVoterSigData: hex.EncodeToString(header.ParentVoterSigData),
ProposerID: hex.EncodeToString(header.ProposerID[:]),
Timestamp: header.Timestamp,
CollectionIDs: cols,
SealedBlocks: seals,
SealedResults: sealsResults,
SealedFinalStates: sealsStates,
}

jsonData, err := json.Marshal(b)
Expand Down
20 changes: 13 additions & 7 deletions consensus/follower_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"testing"
"time"

"github.com/onflow/flow-go/module/signature"

"github.com/rs/zerolog"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -68,9 +70,9 @@ func (s *HotStuffFollowerSuite) SetupTest() {

// mock consensus committee
s.committee = &mockhotstuff.Committee{}
s.committee.On("Identities", mock.Anything, mock.Anything).Return(
func(blockID flow.Identifier, selector flow.IdentityFilter) flow.IdentityList {
return identities.Filter(selector)
s.committee.On("Identities", mock.Anything).Return(
func(blockID flow.Identifier) flow.IdentityList {
return identities
},
nil,
)
Expand Down Expand Up @@ -105,10 +107,13 @@ func (s *HotStuffFollowerSuite) SetupTest() {
Height: 21053,
View: 52078,
}

signerIndices, err := signature.EncodeSignersToIndices(identities.NodeIDs(), identities.NodeIDs()[:3])
require.NoError(s.T(), err)
s.rootQC = &flow.QuorumCertificate{
View: s.rootHeader.View,
BlockID: s.rootHeader.ID(),
SignerIDs: identities.NodeIDs()[:3],
View: s.rootHeader.View,
BlockID: s.rootHeader.ID(),
SignerIndices: signerIndices,
}

// we start with the latest finalized block being the root block
Expand Down Expand Up @@ -335,6 +340,7 @@ func (mc *MockConsensus) extendBlock(blockView uint64, parent *flow.Header) *flo
nextBlock := unittest.BlockHeaderWithParentFixture(parent)
nextBlock.View = blockView
nextBlock.ProposerID = mc.identities[int(blockView)%len(mc.identities)].NodeID
nextBlock.ParentVoterIDs = mc.identities.NodeIDs()
signerIndices, _ := signature.EncodeSignersToIndices(mc.identities.NodeIDs(), mc.identities.NodeIDs())
nextBlock.ParentVoterIndices = signerIndices
return &nextBlock
}
7 changes: 2 additions & 5 deletions consensus/hotstuff/committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,12 @@ import (
// Given a collector block, some logic is required to find the main consensus block
// for determining the valid collector-HotStuff participants.
type Committee interface {

// Identities returns a IdentityList with legitimate HotStuff participants for the specified block.
// The list of participants is filtered by the provided selector. The returned list of HotStuff participants
// The returned list of HotStuff participants
// * contains nodes that are allowed to sign the specified block (legitimate participants with NON-ZERO STAKE)
// * is ordered in the canonical order
// * contains no duplicates.
// The list of all legitimate HotStuff participants for the specified block can be obtained by using `filter.Any`
// TODO: selector can be removed
Identities(blockID flow.Identifier, selector flow.IdentityFilter) (flow.IdentityList, error)
Identities(blockID flow.Identifier) (flow.IdentityList, error)

// Identity returns the full Identity for specified HotStuff participant.
// The node must be a legitimate HotStuff participant with NON-ZERO STAKE at the specified block.
Expand Down
16 changes: 9 additions & 7 deletions consensus/hotstuff/committees/cluster_committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ func NewClusterCommittee(
return com, nil
}

func (c *Cluster) Identities(blockID flow.Identifier, selector flow.IdentityFilter) (flow.IdentityList, error) {

// Identities returns the identities of all cluster members that are authorized to
// participate at the given block. The order of the identities is the canonical order.
func (c *Cluster) Identities(blockID flow.Identifier) (flow.IdentityList, error) {
// blockID is a collection block not a block produced by consensus,
// to query the identities from protocol state, we need to use the reference block id from the payload
//
// first retrieve the cluster block payload
payload, err := c.payloads.ByBlockID(blockID)
if err != nil {
Expand All @@ -74,14 +78,12 @@ func (c *Cluster) Identities(blockID flow.Identifier, selector flow.IdentityFilt

// use the initial cluster members for root block
if isRootBlock {
return c.initialClusterMembers.Filter(selector), nil
return c.initialClusterMembers, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does initialClusterMembers exclude the ejected nodes?
The list returned from the non-root block snapshot below excludes the ejected nodes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queries for the cluster's root block return the initial cluster members, without excluding any nodes ejected in the future. The reason is that the root block is defined at the same time as the initial cluster members are defined -- any ejections after that point shouldn't apply. Any ejections before that point should be reflected in the initial cluster members.

}

// otherwise use the snapshot given by the reference block
identities, err := c.state.AtBlockID(payload.ReferenceBlockID).Identities(filter.And(
selector,
c.clusterMemberFilter,
))
identities, err := c.state.AtBlockID(payload.ReferenceBlockID).Identities(c.clusterMemberFilter) // remove ejected nodes

return identities, err
}

Expand Down
2 changes: 2 additions & 0 deletions consensus/hotstuff/committees/consensus_committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ func NewConsensusCommittee(state protocol.State, me flow.Identifier) (*Consensus
return com, nil
}

// Identities returns the identities of all authorized consensus participants at the given block.
// The order of the identities is the canonical order.
func (c *Consensus) Identities(blockID flow.Identifier) (flow.IdentityList, error) {
il, err := c.state.AtBlockID(blockID).Identities(filter.IsVotingConsensusCommitteeMember)
return il, err
Expand Down
76 changes: 36 additions & 40 deletions consensus/hotstuff/committees/leader/leader_selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,59 +304,55 @@ func TestZeroStakedNodeWillNotBeSelected(t *testing.T) {
rng, err := random.NewChacha20PRG(someSeed, []byte("leader_selec"))
require.NoError(t, err)

for i := 0; i < 100; i++ {
// create 1002 nodes with all 0 stake
identities := unittest.IdentityListFixture(1002, unittest.WithStake(0))

// create 2 nodes with 1 stake, and place them in between
// index 233-777
n := rng.UintN(777-233) + 233
m := rng.UintN(777-233) + 233
identities[n].Stake = 1
identities[m].Stake = 1

// the following code check the zero staker should not be selected
stakeful := identities.Filter(filter.HasStake(true))

count := 1000
selectionFromAll, err := ComputeLeaderSelectionFromSeed(0, someSeed, count, identities)
require.NoError(t, err)
// create 1002 nodes with all 0 stake
identities := unittest.IdentityListFixture(1002, unittest.WithStake(0))

selectionFromStakeful, err := ComputeLeaderSelectionFromSeed(0, someSeed, count, stakeful)
require.NoError(t, err)
// create 2 nodes with 1 stake, and place them in between
// index 233-777
n := rng.UintN(777-233) + 233
m := rng.UintN(777-233) + 233
identities[n].Stake = 1
identities[m].Stake = 1

for i := 0; i < count; i++ {
nodeIDFromAll, err := selectionFromAll.LeaderForView(uint64(i))
require.NoError(t, err)
// the following code check the zero staker should not be selected
stakeful := identities.Filter(filter.HasStake(true))

nodeIDFromStakeful, err := selectionFromStakeful.LeaderForView(uint64(i))
require.NoError(t, err)
count := 1000
selectionFromAll, err := ComputeLeaderSelectionFromSeed(0, someSeed, count, identities)
require.NoError(t, err)

// the selection should be the same
require.Equal(t, nodeIDFromStakeful, nodeIDFromAll)
}
selectionFromStakeful, err := ComputeLeaderSelectionFromSeed(0, someSeed, count, stakeful)
require.NoError(t, err)

for i := 0; i < count; i++ {
nodeIDFromAll, err := selectionFromAll.LeaderForView(uint64(i))
require.NoError(t, err)

nodeIDFromStakeful, err := selectionFromStakeful.LeaderForView(uint64(i))
require.NoError(t, err)

// the selection should be the same
require.Equal(t, nodeIDFromStakeful, nodeIDFromAll)
}

t.Run("if there is only 1 node has stake, then it will be always be the leader and the only leader", func(t *testing.T) {
rng, err := random.NewChacha20PRG(someSeed, []byte("leader_selec"))
require.NoError(t, err)

for i := 0; i < 100; i++ {
identities := unittest.IdentityListFixture(1000, unittest.WithStake(0))
identities := unittest.IdentityListFixture(1000, unittest.WithStake(0))

n := rng.UintN(1000)
stake := n + 1
identities[n].Stake = stake
onlyStaked := identities[n]
n := rng.UintN(1000)
stake := n + 1
identities[n].Stake = stake
onlyStaked := identities[n]

selections, err := ComputeLeaderSelectionFromSeed(0, someSeed, 1000, identities)
require.NoError(t, err)
selections, err := ComputeLeaderSelectionFromSeed(0, someSeed, 1000, identities)
require.NoError(t, err)

for i := 0; i < 1000; i++ {
nodeID, err := selections.LeaderForView(uint64(i))
require.NoError(t, err)
require.Equal(t, onlyStaked.NodeID, nodeID)
}
for i := 0; i < 1000; i++ {
nodeID, err := selections.LeaderForView(uint64(i))
require.NoError(t, err)
require.Equal(t, onlyStaked.NodeID, nodeID)
}
})
})
Expand Down
4 changes: 2 additions & 2 deletions consensus/hotstuff/committees/metrics_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ func NewMetricsWrapper(committee hotstuff.Committee, metrics module.HotstuffMetr
}
}

func (w CommitteeMetricsWrapper) Identities(blockID flow.Identifier, selector flow.IdentityFilter) (flow.IdentityList, error) {
func (w CommitteeMetricsWrapper) Identities(blockID flow.Identifier) (flow.IdentityList, error) {
processStart := time.Now()
identities, err := w.committee.Identities(blockID, selector)
identities, err := w.committee.Identities(blockID)
w.metrics.CommitteeProcessingDuration(time.Since(processStart))
return identities, err
}
Expand Down
4 changes: 2 additions & 2 deletions consensus/hotstuff/committees/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ type Static struct {
dkg staticDKG
}

func (s Static) Identities(_ flow.Identifier, selector flow.IdentityFilter) (flow.IdentityList, error) {
return s.participants.Filter(selector), nil
func (s Static) Identities(_ flow.Identifier) (flow.IdentityList, error) {
return s.participants, nil
}

func (s Static) Identity(_ flow.Identifier, participantID flow.Identifier) (*flow.Identity, error) {
Expand Down
Loading