Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 30 additions & 16 deletions cmd/bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,22 @@ The bootstrapping will generate the following information:
- public networking key
- weight


#### Collector clusters
_Each cluster_ of collector nodes needs to have its own root Block and root QC
* Root clustering: assignment of collector nodes to clusters
* For each cluster:
* Root `cluster.Block`
* Root QC: votes from collector nodes for the respective root `cluster.Block`


#### Root Block for main consensus
* Root Block
* Root QC: votes from consensus nodes for the root block (required to start consensus)
* Root Execution Result: execution result for the initial execution state
* Root Block Seal: block seal for the initial execution result


#### Root Blocks for Collector clusters
_Each cluster_ of collector nodes needs to have its own root Block and root QC
* Root `ClusterBlockProposal`
* Root QC from cluster for their respective `ClusterBlockProposal`


# Usage

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

#### Example
```bash
Expand All @@ -121,6 +126,19 @@ go run . keygen \

```

```bash
go run . cluster-assignment \
--epoch-counter 0 \
--collection-clusters 1 \
--clustering-random-seed 00000000000000000000000000000000000000000000000000000000deadbeef \
--config ./bootstrap-example/node-config.json \
-o ./bootstrap-example \
--partner-dir ./example_files/partner-node-infos \
--partner-weights ./example_files/partner-weights.json \
--internal-priv-dir ./bootstrap-example/keys

```

```bash
go run . rootblock \
--root-chain bench \
Expand All @@ -131,15 +149,19 @@ go run . rootblock \
--epoch-length 30000 \
--epoch-staking-phase-length 20000 \
--epoch-dkg-phase-length 2000 \
--random-seed 00000000000000000000000000000000000000000000000000000000deadbeef \
--collection-clusters 1 \
--protocol-version=0 \
--use-default-epoch-timing \
--epoch-commit-safety-threshold=1000 \
--kvstore-finalization-safety-threshold=1000 \
--kvstore-epoch-extension-view-count=2000 \
--config ./bootstrap-example/node-config.json \
-o ./bootstrap-example \
--partner-dir ./example_files/partner-node-infos \
--partner-weights ./example_files/partner-weights.json \
--internal-priv-dir ./bootstrap-example/keys
--internal-priv-dir ./bootstrap-example/keys \
--intermediary-clustering-data ./bootstrap-example/public-root-information/root-clustering.json \
--cluster-votes-dir ./bootstrap-example/public-root-information/root-block-votes/
```

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

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

## Generating networking key for Observer

Expand Down
4 changes: 2 additions & 2 deletions cmd/bootstrap/cmd/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ func constructRootEpochEvents(
clusterQCs []*flow.QuorumCertificate,
dkgData dkg.ThresholdKeySet,
dkgIndexMap flow.DKGIndexMap,
csprg random.Rand,
rng random.Rand,
) (*flow.EpochSetup, *flow.EpochCommit, error) {
randomSource := make([]byte, flow.EpochSetupRandomSourceLength)
csprg.Read(randomSource)
rng.Read(randomSource)
epochSetup, err := flow.NewEpochSetup(
flow.UntrustedEpochSetup{
Counter: flagEpochCounter,
Expand Down
180 changes: 180 additions & 0 deletions cmd/bootstrap/cmd/clustering.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package cmd
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The bootstrap CLI readme has some general documentation about this process. It also has some example commands which can be used to test the full bootstrapping flow. Could you update these example commands and the relevant documentation in the README?


import (
"fmt"
"path/filepath"

"github.com/spf13/cobra"

"github.com/onflow/flow-go/cmd"
"github.com/onflow/flow-go/cmd/bootstrap/run"
"github.com/onflow/flow-go/cmd/util/cmd/common"
hotstuff "github.com/onflow/flow-go/consensus/hotstuff/model"
model "github.com/onflow/flow-go/model/bootstrap"
"github.com/onflow/flow-go/model/flow"
cluster2 "github.com/onflow/flow-go/state/cluster"
"github.com/onflow/flow-go/state/protocol/prg"
)

var (
flagClusteringRandomSeed []byte
)

// clusterAssignmentCmd represents the clusterAssignment command
var clusterAssignmentCmd = &cobra.Command{
Use: "cluster-assignment",
Short: "Generate cluster assignment",
Long: `Generate cluster assignment for collection nodes based on partner and internal node info and weights. Serialize into file with Epoch Counter`,
Run: clusterAssignment,
}

func init() {
rootCmd.AddCommand(clusterAssignmentCmd)
addClusterAssignmentCmdFlags()
}

func addClusterAssignmentCmdFlags() {
// required parameters for network configuration and generation of root node identities
clusterAssignmentCmd.Flags().StringVar(&flagConfig, "config", "",
"path to a JSON file containing multiple node configurations (fields Role, Address, Weight)")
clusterAssignmentCmd.Flags().StringVar(&flagInternalNodePrivInfoDir, "internal-priv-dir", "", "path to directory "+
"containing the output from the `keygen` command for internal nodes")
clusterAssignmentCmd.Flags().StringVar(&flagPartnerNodeInfoDir, "partner-dir", "", "path to directory "+
"containing one JSON file starting with node-info.pub.<NODE_ID>.json for every partner node (fields "+
" in the JSON file: Role, Address, NodeID, NetworkPubKey, StakingPubKey)")
clusterAssignmentCmd.Flags().StringVar(&flagPartnerWeights, "partner-weights", "", "path to a JSON file containing "+
"a map from partner node's NodeID to their stake")

cmd.MarkFlagRequired(clusterAssignmentCmd, "config")
cmd.MarkFlagRequired(clusterAssignmentCmd, "internal-priv-dir")
cmd.MarkFlagRequired(clusterAssignmentCmd, "partner-dir")
cmd.MarkFlagRequired(clusterAssignmentCmd, "partner-weights")

// optional parameters for cluster assignment
clusterAssignmentCmd.Flags().UintVar(&flagCollectionClusters, "collection-clusters", 2, "number of collection clusters")

// required parameters for generation of cluster root blocks
clusterAssignmentCmd.Flags().Uint64Var(&flagEpochCounter, "epoch-counter", 0, "epoch counter for the epoch beginning with the root block")
cmd.MarkFlagRequired(clusterAssignmentCmd, "epoch-counter")

clusterAssignmentCmd.Flags().BytesHexVar(&flagClusteringRandomSeed, "clustering-random-seed", nil, "random seed to generate the clustering assignment")
cmd.MarkFlagRequired(clusterAssignmentCmd, "clustering-random-seed")

}

func clusterAssignment(cmd *cobra.Command, args []string) {
// Read partner node's information and internal node's information.
// With "internal nodes" we reference nodes, whose private keys we have. In comparison,
// for "partner nodes" we generally do not have their keys. However, we allow some overlap,
// in that we tolerate a configuration where information about an "internal node" is also
// duplicated in the list of "partner nodes".
log.Info().Msg("collecting partner network and staking keys")
rawPartnerNodes, err := common.ReadFullPartnerNodeInfos(log, flagPartnerWeights, flagPartnerNodeInfoDir)
if err != nil {
log.Fatal().Err(err).Msg("failed to read full partner node infos")
}
log.Info().Msg("")

log.Info().Msg("generating internal private networking and staking keys")
internalNodes, err := common.ReadFullInternalNodeInfos(log, flagInternalNodePrivInfoDir, flagConfig)
if err != nil {
log.Fatal().Err(err).Msg("failed to read full internal node infos")
}
log.Info().Msg("")

// we now convert to the strict meaning of: "internal nodes" vs "partner nodes"
// • "internal nodes" we have they private keys for
// • "partner nodes" we don't have the keys for
// • both sets are disjoint (no common nodes)
log.Info().Msg("remove internal partner nodes")
partnerNodes := common.FilterInternalPartners(rawPartnerNodes, internalNodes)
log.Info().Msgf("removed %d internal partner nodes", len(rawPartnerNodes)-len(partnerNodes))

log.Info().Msg("checking constraints on consensus nodes")
checkConstraints(partnerNodes, internalNodes)
log.Info().Msg("")

log.Info().Msg("assembling network and staking keys")
stakingNodes, err := mergeNodeInfos(internalNodes, partnerNodes)
if err != nil {
log.Fatal().Err(err).Msgf("failed to merge node infos")
}
publicInfo, err := model.ToPublicNodeInfoList(stakingNodes)
if err != nil {
log.Fatal().Msg("failed to read public node info")
}
err = common.WriteJSON(model.PathNodeInfosPub, flagOutdir, publicInfo)
if err != nil {
log.Fatal().Err(err).Msg("failed to write json")
}
log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathNodeInfosPub)
log.Info().Msg("")

// Convert to IdentityList
partnerList := model.ToIdentityList(partnerNodes)
internalList := model.ToIdentityList(internalNodes)

clusteringPrg, err := prg.New(flagClusteringRandomSeed, prg.BootstrapClusterAssignment, nil)
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize pseudorandom generator")
}

log.Info().Msg("computing collection node clusters")
assignments, clusters, err := common.ConstructClusterAssignment(log, partnerList, internalList, int(flagCollectionClusters), clusteringPrg)
if err != nil {
log.Fatal().Err(err).Msg("unable to generate cluster assignment")
}
log.Info().Msg("")

// Output assignment with epoch counter
output := IntermediaryClusteringData{
EpochCounter: flagEpochCounter,
Assignments: assignments,
Clusters: clusters,
}
err = common.WriteJSON(model.PathClusteringData, flagOutdir, output)
if err != nil {
log.Fatal().Err(err).Msg("failed to write json")
}
log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathClusteringData)
log.Info().Msg("")

log.Info().Msg("constructing and writing cluster block votes for internal nodes")
constructClusterRootVotes(
output,
model.FilterByRole(internalNodes, flow.RoleCollection),
)
log.Info().Msg("")
}

// constructClusterRootVotes generates and writes vote files for internal collector nodes with private keys available.
func constructClusterRootVotes(data IntermediaryClusteringData, internalCollectors []model.NodeInfo) {
for i := range data.Clusters {
clusterRootBlock, err := cluster2.CanonicalRootBlock(data.EpochCounter, data.Assignments[i])
if err != nil {
log.Fatal().Err(err).Msg("could not construct cluster root block")
}
block := hotstuff.GenesisBlockFromFlow(clusterRootBlock.ToHeader())
// collate private NodeInfos for internal nodes in this cluster
signers := make([]model.NodeInfo, 0)
for _, nodeID := range data.Assignments[i] {
for _, node := range internalCollectors {
if node.NodeID == nodeID {
signers = append(signers, node)
}
}
}
votes, err := run.CreateClusterRootBlockVotes(signers, block)
if err != nil {
log.Fatal().Err(err).Msg("could not create cluster root block votes")
}
for _, vote := range votes {
path := filepath.Join(model.DirnameRootBlockVotes, fmt.Sprintf(model.FilenameRootClusterBlockVote, vote.SignerID))
err = common.WriteJSON(path, flagOutdir, vote)
if err != nil {
log.Fatal().Err(err).Msg("failed to write json")
}
log.Info().Msgf("wrote file %s/%s", flagOutdir, path)
}
}
}
21 changes: 3 additions & 18 deletions cmd/bootstrap/cmd/finalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ import (
)

var (
flagConfig string
flagInternalNodePrivInfoDir string
flagPartnerNodeInfoDir string
// Deprecated: use flagPartnerWeights instead
deprecatedFlagPartnerStakes string
flagConfig string
flagInternalNodePrivInfoDir string
flagPartnerNodeInfoDir string
flagPartnerWeights string
flagDKGDataPath string
flagRootBlockPath string
Expand Down Expand Up @@ -70,8 +68,6 @@ func addFinalizeCmdFlags() {
finalizeCmd.Flags().StringVar(&flagPartnerNodeInfoDir, "partner-dir", "", "path to directory "+
"containing one JSON file starting with node-info.pub.<NODE_ID>.json for every partner node (fields "+
" in the JSON file: Role, Address, NodeID, NetworkPubKey, StakingPubKey, StakingKeyPoP)")
// Deprecated: remove this flag
finalizeCmd.Flags().StringVar(&deprecatedFlagPartnerStakes, "partner-stakes", "", "deprecated: use partner-weights instead")
finalizeCmd.Flags().StringVar(&flagPartnerWeights, "partner-weights", "", "path to a JSON file containing "+
"a map from partner node's NodeID to their weight")
finalizeCmd.Flags().StringVar(&flagDKGDataPath, "dkg-data", "", "path to a JSON file containing data as output from the random beacon key generation")
Expand Down Expand Up @@ -102,17 +98,6 @@ func addFinalizeCmdFlags() {
}

func finalize(cmd *cobra.Command, args []string) {

// maintain backward compatibility with old flag name
if deprecatedFlagPartnerStakes != "" {
log.Warn().Msg("using deprecated flag --partner-stakes (use --partner-weights instead)")
if flagPartnerWeights == "" {
flagPartnerWeights = deprecatedFlagPartnerStakes
} else {
log.Fatal().Msg("cannot use both --partner-stakes and --partner-weights flags (use only --partner-weights)")
}
}

log.Info().Msg("collecting partner network and staking keys")
partnerNodes, err := common.ReadFullPartnerNodeInfos(log, flagPartnerWeights, flagPartnerNodeInfoDir)
if err != nil {
Expand Down
9 changes: 8 additions & 1 deletion cmd/bootstrap/cmd/finalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,19 @@ func TestFinalize_HappyPath(t *testing.T) {
flagPartnerWeights = partnerWeights
flagInternalNodePrivInfoDir = internalPrivDir

flagIntermediaryClusteringDataPath = filepath.Join(bootDir, model.PathClusteringData)
flagRootClusterBlockVotesDir = filepath.Join(bootDir, model.DirnameRootBlockVotes)
flagEpochCounter = epochCounter

// clusterAssignment will generate the collector clusters
// In addition, it also generates votes from internal collector nodes
clusterAssignment(clusterAssignmentCmd, nil)

flagRootChain = chainName
flagRootParent = hex.EncodeToString(rootParent[:])
flagRootHeight = rootHeight
flagRootView = 1_000
flagRootCommit = hex.EncodeToString(rootCommit[:])
flagEpochCounter = epochCounter
flagNumViewsInEpoch = 100_000
flagNumViewsInStakingAuction = 50_000
flagNumViewsInDKGPhase = 2_000
Expand Down
16 changes: 1 addition & 15 deletions cmd/bootstrap/cmd/genconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,11 @@ var (
flagNodesConsensus int
flagNodesExecution int
flagNodesVerification int
// Deprecated: use flagWeight instead
deprecatedFlagStake uint64
flagWeight uint64
flagWeight uint64
)

// genconfigCmdRun generates the node-config.json file
func genconfigCmdRun(_ *cobra.Command, _ []string) {

// maintain backward compatibility with old flag name
if deprecatedFlagStake != 0 {
log.Warn().Msg("using deprecated flag --stake (use --weight instead)")
if flagWeight == 0 {
flagWeight = deprecatedFlagStake
} else {
log.Fatal().Msg("cannot use both --stake and --weight flags (use only --weight)")
}
}

if flagWeight != flow.DefaultInitialWeight {
log.Warn().Msgf("using non-standard initial weight %d!=%d - make sure this is desired", flagWeight, flow.DefaultInitialWeight)
}
Expand Down Expand Up @@ -85,7 +72,6 @@ func init() {
genconfigCmd.Flags().IntVar(&flagNodesExecution, "execution", 2, "number of execution nodes")
genconfigCmd.Flags().IntVar(&flagNodesVerification, "verification", 1, "number of verification nodes")
genconfigCmd.Flags().Uint64Var(&flagWeight, "weight", flow.DefaultInitialWeight, "weight for all nodes")
genconfigCmd.Flags().Uint64Var(&deprecatedFlagStake, "stake", 0, "deprecated: use --weight")
}

func createConf(r flow.Role, i int) model.NodeConfig {
Expand Down
Loading
Loading