Skip to content

Commit 8cfb923

Browse files
authored
Merge pull request #8013 from onflow/tim/7846-bootstrap-clustering
Add cluster bootstrapping commands
2 parents 4ef0352 + e572aff commit 8cfb923

File tree

26 files changed

+700
-121
lines changed

26 files changed

+700
-121
lines changed

cmd/bootstrap/README.md

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,22 @@ The bootstrapping will generate the following information:
3232
- public networking key
3333
- weight
3434

35+
36+
#### Collector clusters
37+
_Each cluster_ of collector nodes needs to have its own root Block and root QC
38+
* Root clustering: assignment of collector nodes to clusters
39+
* For each cluster:
40+
* Root `cluster.Block`
41+
* Root QC: votes from collector nodes for the respective root `cluster.Block`
42+
43+
3544
#### Root Block for main consensus
3645
* Root Block
3746
* Root QC: votes from consensus nodes for the root block (required to start consensus)
3847
* Root Execution Result: execution result for the initial execution state
3948
* Root Block Seal: block seal for the initial execution result
4049

4150

42-
#### Root Blocks for Collector clusters
43-
_Each cluster_ of collector nodes needs to have its own root Block and root QC
44-
* Root `ClusterBlockProposal`
45-
* Root QC from cluster for their respective `ClusterBlockProposal`
46-
47-
4851
# Usage
4952

5053
`go run ./cmd/bootstrap` prints usage information
@@ -97,6 +100,8 @@ Each input is a config file specified as a command line parameter:
97100
* folder containing the `<NodeID>.node-info.pub.json` files for _all_ partner nodes (see `.example_files/partner-node-infos`)
98101
* `json` containing the weight value for all partner nodes (see `./example_files/partner-weights.json`).
99102
Format: ```<NodeID>: <weight value>```
103+
* random seed for the new collector node clustering and epoch RandomSource (min 32 bytes in hex encoding)
104+
Provided seeds should be derived from a verifiable random source, such as the previous epoch's RandomSource.
100105

101106
#### Example
102107
```bash
@@ -121,6 +126,19 @@ go run . keygen \
121126

122127
```
123128

129+
```bash
130+
go run . cluster-assignment \
131+
--epoch-counter 0 \
132+
--collection-clusters 1 \
133+
--clustering-random-seed 00000000000000000000000000000000000000000000000000000000deadbeef \
134+
--config ./bootstrap-example/node-config.json \
135+
-o ./bootstrap-example \
136+
--partner-dir ./example_files/partner-node-infos \
137+
--partner-weights ./example_files/partner-weights.json \
138+
--internal-priv-dir ./bootstrap-example/keys
139+
140+
```
141+
124142
```bash
125143
go run . rootblock \
126144
--root-chain bench \
@@ -131,15 +149,19 @@ go run . rootblock \
131149
--epoch-length 30000 \
132150
--epoch-staking-phase-length 20000 \
133151
--epoch-dkg-phase-length 2000 \
152+
--random-seed 00000000000000000000000000000000000000000000000000000000deadbeef \
134153
--collection-clusters 1 \
135154
--protocol-version=0 \
136155
--use-default-epoch-timing \
137-
--epoch-commit-safety-threshold=1000 \
156+
--kvstore-finalization-safety-threshold=1000 \
157+
--kvstore-epoch-extension-view-count=2000 \
138158
--config ./bootstrap-example/node-config.json \
139159
-o ./bootstrap-example \
140160
--partner-dir ./example_files/partner-node-infos \
141161
--partner-weights ./example_files/partner-weights.json \
142-
--internal-priv-dir ./bootstrap-example/keys
162+
--internal-priv-dir ./bootstrap-example/keys \
163+
--intermediary-clustering-data ./bootstrap-example/public-root-information/root-clustering.json \
164+
--cluster-votes-dir ./bootstrap-example/public-root-information/root-block-votes/
143165
```
144166

145167
```bash
@@ -187,14 +209,6 @@ go run . finalize \
187209
* file `dkg-data.pub.json`
188210
- REQUIRED at NODE START by all nodes
189211

190-
* file `<ClusterID>.root-cluster-block.json`
191-
- root `ClusterBlockProposal` for collector cluster with ID `<ClusterID>`
192-
- REQUIRED at NODE START by all collectors of the respective cluster
193-
- file can be made accessible to all nodes at boot up (or recovery after crash)
194-
* file `<ClusterID>.root-cluster-qc.json`
195-
- root Quorum Certificate for `ClusterBlockProposal` for collector cluster with ID `<ClusterID>`
196-
- REQUIRED at NODE START by all collectors of the respective cluster
197-
- file can be made accessible to all nodes at boot up (or recovery after crash)
198212

199213
## Generating networking key for Observer
200214

cmd/bootstrap/cmd/block.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ func constructRootEpochEvents(
5858
clusterQCs []*flow.QuorumCertificate,
5959
dkgData dkg.ThresholdKeySet,
6060
dkgIndexMap flow.DKGIndexMap,
61-
csprg random.Rand,
61+
rng random.Rand,
6262
) (*flow.EpochSetup, *flow.EpochCommit, error) {
6363
randomSource := make([]byte, flow.EpochSetupRandomSourceLength)
64-
csprg.Read(randomSource)
64+
rng.Read(randomSource)
6565
epochSetup, err := flow.NewEpochSetup(
6666
flow.UntrustedEpochSetup{
6767
Counter: flagEpochCounter,

cmd/bootstrap/cmd/clustering.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/onflow/flow-go/cmd"
10+
"github.com/onflow/flow-go/cmd/bootstrap/run"
11+
"github.com/onflow/flow-go/cmd/util/cmd/common"
12+
hotstuff "github.com/onflow/flow-go/consensus/hotstuff/model"
13+
model "github.com/onflow/flow-go/model/bootstrap"
14+
"github.com/onflow/flow-go/model/flow"
15+
cluster2 "github.com/onflow/flow-go/state/cluster"
16+
"github.com/onflow/flow-go/state/protocol/prg"
17+
)
18+
19+
var (
20+
flagClusteringRandomSeed []byte
21+
)
22+
23+
// clusterAssignmentCmd represents the clusterAssignment command
24+
var clusterAssignmentCmd = &cobra.Command{
25+
Use: "cluster-assignment",
26+
Short: "Generate cluster assignment",
27+
Long: `Generate cluster assignment for collection nodes based on partner and internal node info and weights. Serialize into file with Epoch Counter`,
28+
Run: clusterAssignment,
29+
}
30+
31+
func init() {
32+
rootCmd.AddCommand(clusterAssignmentCmd)
33+
addClusterAssignmentCmdFlags()
34+
}
35+
36+
func addClusterAssignmentCmdFlags() {
37+
// required parameters for network configuration and generation of root node identities
38+
clusterAssignmentCmd.Flags().StringVar(&flagConfig, "config", "",
39+
"path to a JSON file containing multiple node configurations (fields Role, Address, Weight)")
40+
clusterAssignmentCmd.Flags().StringVar(&flagInternalNodePrivInfoDir, "internal-priv-dir", "", "path to directory "+
41+
"containing the output from the `keygen` command for internal nodes")
42+
clusterAssignmentCmd.Flags().StringVar(&flagPartnerNodeInfoDir, "partner-dir", "", "path to directory "+
43+
"containing one JSON file starting with node-info.pub.<NODE_ID>.json for every partner node (fields "+
44+
" in the JSON file: Role, Address, NodeID, NetworkPubKey, StakingPubKey)")
45+
clusterAssignmentCmd.Flags().StringVar(&flagPartnerWeights, "partner-weights", "", "path to a JSON file containing "+
46+
"a map from partner node's NodeID to their stake")
47+
48+
cmd.MarkFlagRequired(clusterAssignmentCmd, "config")
49+
cmd.MarkFlagRequired(clusterAssignmentCmd, "internal-priv-dir")
50+
cmd.MarkFlagRequired(clusterAssignmentCmd, "partner-dir")
51+
cmd.MarkFlagRequired(clusterAssignmentCmd, "partner-weights")
52+
53+
// optional parameters for cluster assignment
54+
clusterAssignmentCmd.Flags().UintVar(&flagCollectionClusters, "collection-clusters", 2, "number of collection clusters")
55+
56+
// required parameters for generation of cluster root blocks
57+
clusterAssignmentCmd.Flags().Uint64Var(&flagEpochCounter, "epoch-counter", 0, "epoch counter for the epoch beginning with the root block")
58+
cmd.MarkFlagRequired(clusterAssignmentCmd, "epoch-counter")
59+
60+
clusterAssignmentCmd.Flags().BytesHexVar(&flagClusteringRandomSeed, "clustering-random-seed", nil, "random seed to generate the clustering assignment")
61+
cmd.MarkFlagRequired(clusterAssignmentCmd, "clustering-random-seed")
62+
63+
}
64+
65+
func clusterAssignment(cmd *cobra.Command, args []string) {
66+
// Read partner node's information and internal node's information.
67+
// With "internal nodes" we reference nodes, whose private keys we have. In comparison,
68+
// for "partner nodes" we generally do not have their keys. However, we allow some overlap,
69+
// in that we tolerate a configuration where information about an "internal node" is also
70+
// duplicated in the list of "partner nodes".
71+
log.Info().Msg("collecting partner network and staking keys")
72+
rawPartnerNodes, err := common.ReadFullPartnerNodeInfos(log, flagPartnerWeights, flagPartnerNodeInfoDir)
73+
if err != nil {
74+
log.Fatal().Err(err).Msg("failed to read full partner node infos")
75+
}
76+
log.Info().Msg("")
77+
78+
log.Info().Msg("generating internal private networking and staking keys")
79+
internalNodes, err := common.ReadFullInternalNodeInfos(log, flagInternalNodePrivInfoDir, flagConfig)
80+
if err != nil {
81+
log.Fatal().Err(err).Msg("failed to read full internal node infos")
82+
}
83+
log.Info().Msg("")
84+
85+
// we now convert to the strict meaning of: "internal nodes" vs "partner nodes"
86+
// • "internal nodes" we have they private keys for
87+
// • "partner nodes" we don't have the keys for
88+
// • both sets are disjoint (no common nodes)
89+
log.Info().Msg("remove internal partner nodes")
90+
partnerNodes := common.FilterInternalPartners(rawPartnerNodes, internalNodes)
91+
log.Info().Msgf("removed %d internal partner nodes", len(rawPartnerNodes)-len(partnerNodes))
92+
93+
log.Info().Msg("checking constraints on consensus nodes")
94+
checkConstraints(partnerNodes, internalNodes)
95+
log.Info().Msg("")
96+
97+
log.Info().Msg("assembling network and staking keys")
98+
stakingNodes, err := mergeNodeInfos(internalNodes, partnerNodes)
99+
if err != nil {
100+
log.Fatal().Err(err).Msgf("failed to merge node infos")
101+
}
102+
publicInfo, err := model.ToPublicNodeInfoList(stakingNodes)
103+
if err != nil {
104+
log.Fatal().Msg("failed to read public node info")
105+
}
106+
err = common.WriteJSON(model.PathNodeInfosPub, flagOutdir, publicInfo)
107+
if err != nil {
108+
log.Fatal().Err(err).Msg("failed to write json")
109+
}
110+
log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathNodeInfosPub)
111+
log.Info().Msg("")
112+
113+
// Convert to IdentityList
114+
partnerList := model.ToIdentityList(partnerNodes)
115+
internalList := model.ToIdentityList(internalNodes)
116+
117+
clusteringPrg, err := prg.New(flagClusteringRandomSeed, prg.BootstrapClusterAssignment, nil)
118+
if err != nil {
119+
log.Fatal().Err(err).Msg("failed to initialize pseudorandom generator")
120+
}
121+
122+
log.Info().Msg("computing collection node clusters")
123+
assignments, clusters, err := common.ConstructClusterAssignment(log, partnerList, internalList, int(flagCollectionClusters), clusteringPrg)
124+
if err != nil {
125+
log.Fatal().Err(err).Msg("unable to generate cluster assignment")
126+
}
127+
log.Info().Msg("")
128+
129+
// Output assignment with epoch counter
130+
output := IntermediaryClusteringData{
131+
EpochCounter: flagEpochCounter,
132+
Assignments: assignments,
133+
Clusters: clusters,
134+
}
135+
err = common.WriteJSON(model.PathClusteringData, flagOutdir, output)
136+
if err != nil {
137+
log.Fatal().Err(err).Msg("failed to write json")
138+
}
139+
log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathClusteringData)
140+
log.Info().Msg("")
141+
142+
log.Info().Msg("constructing and writing cluster block votes for internal nodes")
143+
constructClusterRootVotes(
144+
output,
145+
model.FilterByRole(internalNodes, flow.RoleCollection),
146+
)
147+
log.Info().Msg("")
148+
}
149+
150+
// constructClusterRootVotes generates and writes vote files for internal collector nodes with private keys available.
151+
func constructClusterRootVotes(data IntermediaryClusteringData, internalCollectors []model.NodeInfo) {
152+
for i := range data.Clusters {
153+
clusterRootBlock, err := cluster2.CanonicalRootBlock(data.EpochCounter, data.Assignments[i])
154+
if err != nil {
155+
log.Fatal().Err(err).Msg("could not construct cluster root block")
156+
}
157+
block := hotstuff.GenesisBlockFromFlow(clusterRootBlock.ToHeader())
158+
// collate private NodeInfos for internal nodes in this cluster
159+
signers := make([]model.NodeInfo, 0)
160+
for _, nodeID := range data.Assignments[i] {
161+
for _, node := range internalCollectors {
162+
if node.NodeID == nodeID {
163+
signers = append(signers, node)
164+
}
165+
}
166+
}
167+
votes, err := run.CreateClusterRootBlockVotes(signers, block)
168+
if err != nil {
169+
log.Fatal().Err(err).Msg("could not create cluster root block votes")
170+
}
171+
for _, vote := range votes {
172+
path := filepath.Join(model.DirnameRootBlockVotes, fmt.Sprintf(model.FilenameRootClusterBlockVote, vote.SignerID))
173+
err = common.WriteJSON(path, flagOutdir, vote)
174+
if err != nil {
175+
log.Fatal().Err(err).Msg("failed to write json")
176+
}
177+
log.Info().Msgf("wrote file %s/%s", flagOutdir, path)
178+
}
179+
}
180+
}

cmd/bootstrap/cmd/finalize.go

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ import (
3131
)
3232

3333
var (
34-
flagConfig string
35-
flagInternalNodePrivInfoDir string
36-
flagPartnerNodeInfoDir string
37-
// Deprecated: use flagPartnerWeights instead
38-
deprecatedFlagPartnerStakes string
34+
flagConfig string
35+
flagInternalNodePrivInfoDir string
36+
flagPartnerNodeInfoDir string
3937
flagPartnerWeights string
4038
flagDKGDataPath string
4139
flagRootBlockPath string
@@ -70,8 +68,6 @@ func addFinalizeCmdFlags() {
7068
finalizeCmd.Flags().StringVar(&flagPartnerNodeInfoDir, "partner-dir", "", "path to directory "+
7169
"containing one JSON file starting with node-info.pub.<NODE_ID>.json for every partner node (fields "+
7270
" in the JSON file: Role, Address, NodeID, NetworkPubKey, StakingPubKey, StakingKeyPoP)")
73-
// Deprecated: remove this flag
74-
finalizeCmd.Flags().StringVar(&deprecatedFlagPartnerStakes, "partner-stakes", "", "deprecated: use partner-weights instead")
7571
finalizeCmd.Flags().StringVar(&flagPartnerWeights, "partner-weights", "", "path to a JSON file containing "+
7672
"a map from partner node's NodeID to their weight")
7773
finalizeCmd.Flags().StringVar(&flagDKGDataPath, "dkg-data", "", "path to a JSON file containing data as output from the random beacon key generation")
@@ -102,17 +98,6 @@ func addFinalizeCmdFlags() {
10298
}
10399

104100
func finalize(cmd *cobra.Command, args []string) {
105-
106-
// maintain backward compatibility with old flag name
107-
if deprecatedFlagPartnerStakes != "" {
108-
log.Warn().Msg("using deprecated flag --partner-stakes (use --partner-weights instead)")
109-
if flagPartnerWeights == "" {
110-
flagPartnerWeights = deprecatedFlagPartnerStakes
111-
} else {
112-
log.Fatal().Msg("cannot use both --partner-stakes and --partner-weights flags (use only --partner-weights)")
113-
}
114-
}
115-
116101
log.Info().Msg("collecting partner network and staking keys")
117102
partnerNodes, err := common.ReadFullPartnerNodeInfos(log, flagPartnerWeights, flagPartnerNodeInfoDir)
118103
if err != nil {

cmd/bootstrap/cmd/finalize_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,19 @@ func TestFinalize_HappyPath(t *testing.T) {
6868
flagPartnerWeights = partnerWeights
6969
flagInternalNodePrivInfoDir = internalPrivDir
7070

71+
flagIntermediaryClusteringDataPath = filepath.Join(bootDir, model.PathClusteringData)
72+
flagRootClusterBlockVotesDir = filepath.Join(bootDir, model.DirnameRootBlockVotes)
73+
flagEpochCounter = epochCounter
74+
75+
// clusterAssignment will generate the collector clusters
76+
// In addition, it also generates votes from internal collector nodes
77+
clusterAssignment(clusterAssignmentCmd, nil)
78+
7179
flagRootChain = chainName
7280
flagRootParent = hex.EncodeToString(rootParent[:])
7381
flagRootHeight = rootHeight
7482
flagRootView = 1_000
7583
flagRootCommit = hex.EncodeToString(rootCommit[:])
76-
flagEpochCounter = epochCounter
7784
flagNumViewsInEpoch = 100_000
7885
flagNumViewsInStakingAuction = 50_000
7986
flagNumViewsInDKGPhase = 2_000

cmd/bootstrap/cmd/genconfig.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,11 @@ var (
1717
flagNodesConsensus int
1818
flagNodesExecution int
1919
flagNodesVerification int
20-
// Deprecated: use flagWeight instead
21-
deprecatedFlagStake uint64
22-
flagWeight uint64
20+
flagWeight uint64
2321
)
2422

2523
// genconfigCmdRun generates the node-config.json file
2624
func genconfigCmdRun(_ *cobra.Command, _ []string) {
27-
28-
// maintain backward compatibility with old flag name
29-
if deprecatedFlagStake != 0 {
30-
log.Warn().Msg("using deprecated flag --stake (use --weight instead)")
31-
if flagWeight == 0 {
32-
flagWeight = deprecatedFlagStake
33-
} else {
34-
log.Fatal().Msg("cannot use both --stake and --weight flags (use only --weight)")
35-
}
36-
}
37-
3825
if flagWeight != flow.DefaultInitialWeight {
3926
log.Warn().Msgf("using non-standard initial weight %d!=%d - make sure this is desired", flagWeight, flow.DefaultInitialWeight)
4027
}
@@ -85,7 +72,6 @@ func init() {
8572
genconfigCmd.Flags().IntVar(&flagNodesExecution, "execution", 2, "number of execution nodes")
8673
genconfigCmd.Flags().IntVar(&flagNodesVerification, "verification", 1, "number of verification nodes")
8774
genconfigCmd.Flags().Uint64Var(&flagWeight, "weight", flow.DefaultInitialWeight, "weight for all nodes")
88-
genconfigCmd.Flags().Uint64Var(&deprecatedFlagStake, "stake", 0, "deprecated: use --weight")
8975
}
9076

9177
func createConf(r flow.Role, i int) model.NodeConfig {

0 commit comments

Comments
 (0)