Skip to content

Commit 0f25336

Browse files
committed
Add transit script commands for cluster root block voting
The new commands are to be used by collector nodes. Following creation of the cluster assignment by `cmd/bootstrap/run/clustering.go`, Collector nodes retrieve the assignment, create a vote for the root block of the collection cluster they are assigned to, and upload that vote. Once enough votes have been received to construct QCs for all the cluster root blocks, bootstrapping can continue to the next step (the consensus root block).
1 parent 1e4497b commit 0f25336

File tree

4 files changed

+299
-1
lines changed

4 files changed

+299
-1
lines changed

cmd/bootstrap/transit/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
The transit script is an utility used by node operators to upload and download relevant data before and after a Flow spork.
44
It is used to download the root snapshot after a spork.
5-
Additionally, for a consensus node, it is used to upload transit keys and to submit root block votes.
5+
Additionally, for a consensus node, it is used to upload transit keys and to submit root block votes,
6+
and for a collection node, it is used to submit cluster root block votes.
67

78
## Server token
89

@@ -83,3 +84,30 @@ Running `transit push-transit-key` will perform the following actions:
8384
- `transit-key.priv.<id>`
8485
1. Upload the node's public files to the server
8586
- `transit-key.pub.<id>`
87+
88+
## Collection nodes
89+
90+
The transit script has three commands applicable to collection nodes:
91+
92+
```shell
93+
$ transit pull-clustering -t ${server-token} -d ${bootstrap-dir}
94+
$ transit generate-cluster-block-vote -t ${server-token} -d ${bootstrap-dir}
95+
$ transit push-cluster-block-vote -t ${server-token} -d ${bootstrap-dir} -v ${vote-file}
96+
```
97+
98+
### Pull Clustering Assignment
99+
100+
Running `transit pull-clustering` will perform the following actions:
101+
102+
1. Fetch the assignment of collection nodes to clusters for the upcoming spork and write it to `<bootstrap-dir>/public-root-information/root-clustering.json`
103+
104+
### Sign Cluster Root Block
105+
106+
After the root block and random beacon key have been fetched, running `transit generate-cluster-block-vote` will:
107+
108+
1. Create a signature over the cluster root block, for the cluster the node is assigned to, using the node's private staking key.
109+
2. Store the resulting vote to the file `<bootstrap-dir>/private-root-information/private-node-info_<node_id>/root-cluster-block-vote.json`
110+
111+
### Upload Vote
112+
113+
Once a vote has been generated, running `transit push-cluster-block-vote` will upload the vote file to the server.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"path/filepath"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/onflow/flow-go/cmd"
11+
cmd2 "github.com/onflow/flow-go/cmd/bootstrap/cmd"
12+
"github.com/onflow/flow-go/consensus/hotstuff/model"
13+
"github.com/onflow/flow-go/consensus/hotstuff/verification"
14+
"github.com/onflow/flow-go/model/bootstrap"
15+
"github.com/onflow/flow-go/model/flow"
16+
"github.com/onflow/flow-go/module/local"
17+
"github.com/onflow/flow-go/state/cluster"
18+
"github.com/onflow/flow-go/utils/io"
19+
)
20+
21+
var generateClusterVoteCmd = &cobra.Command{
22+
Use: "generate-cluster-block-vote",
23+
Short: "Generate cluster block vote",
24+
Run: generateClusterVote,
25+
}
26+
27+
func init() {
28+
rootCmd.AddCommand(generateClusterVoteCmd)
29+
addGenerateClusterVoteCmdFlags()
30+
}
31+
32+
func addGenerateClusterVoteCmdFlags() {
33+
generateClusterVoteCmd.Flags().StringVarP(&flagOutputDir, "outputDir", "o", "", "ouput directory for vote files; if not set defaults to bootstrap directory")
34+
}
35+
36+
func generateClusterVote(c *cobra.Command, args []string) {
37+
log.Info().Msg("generating root block vote")
38+
39+
nodeIDString, err := readNodeID()
40+
if err != nil {
41+
log.Fatal().Err(err).Msg("could not read node ID")
42+
}
43+
44+
nodeID, err := flow.HexStringToIdentifier(nodeIDString)
45+
if err != nil {
46+
log.Fatal().Err(err).Msg("could not parse node ID")
47+
}
48+
49+
nodeInfo, err := cmd.LoadPrivateNodeInfo(flagBootDir, nodeID)
50+
if err != nil {
51+
log.Fatal().Err(err).Msg("could not load private node info")
52+
}
53+
54+
stakingPrivKey := nodeInfo.StakingPrivKey.PrivateKey
55+
identity := flow.IdentitySkeleton{
56+
NodeID: nodeID,
57+
Address: nodeInfo.Address,
58+
Role: nodeInfo.Role,
59+
InitialWeight: flow.DefaultInitialWeight,
60+
StakingPubKey: stakingPrivKey.PublicKey(),
61+
NetworkPubKey: nodeInfo.NetworkPrivKey.PrivateKey.PublicKey(),
62+
}
63+
64+
me, err := local.New(identity, nodeInfo.StakingPrivKey.PrivateKey)
65+
if err != nil {
66+
log.Fatal().Err(err).Msg("creating local signer abstraction failed")
67+
}
68+
69+
path := filepath.Join(flagBootDir, bootstrap.PathClusteringData)
70+
// If output directory is specified, use it for the root-clustering.json
71+
if flagOutputDir != "" {
72+
path = filepath.Join(flagOutputDir, "root-clustering.json")
73+
}
74+
75+
data, err := io.ReadFile(path)
76+
if err != nil {
77+
log.Fatal().Err(err).Msg("could not read clustering file")
78+
}
79+
80+
var clustering cmd2.IntermediaryClusteringData
81+
err = json.Unmarshal(data, &clustering)
82+
if err != nil {
83+
log.Fatal().Err(err).Msg("could not unmarshal clustering data")
84+
}
85+
86+
var myCluster flow.IdentifierList
87+
for _, assignment := range clustering.Assignments {
88+
if assignment.Contains(me.NodeID()) {
89+
myCluster = assignment
90+
}
91+
}
92+
if myCluster == nil {
93+
log.Fatal().Msg("node not a member of any clusters")
94+
}
95+
clusterBlock, err := cluster.CanonicalRootBlock(clustering.EpochCounter, myCluster)
96+
if err != nil {
97+
log.Fatal().Err(err).Msg("could not create canonical root cluster block")
98+
}
99+
100+
// generate root block vote
101+
vote, err := verification.NewStakingSigner(me).CreateVote(model.GenesisBlockFromFlow(clusterBlock.ToHeader()))
102+
if err != nil {
103+
log.Fatal().Err(err).Msgf("could not create cluster vote for participant %v", me.NodeID())
104+
}
105+
106+
voteFile := fmt.Sprintf(bootstrap.PathNodeRootClusterBlockVote, nodeID)
107+
108+
// By default, use the bootstrap directory for storing the vote file
109+
voteFilePath := filepath.Join(flagBootDir, voteFile)
110+
111+
// If output directory is specified, use it for the vote file path
112+
if flagOutputDir != "" {
113+
voteFilePath = filepath.Join(flagOutputDir, "root-cluster-block-vote.json")
114+
}
115+
116+
if err = io.WriteJSON(voteFilePath, vote); err != nil {
117+
log.Fatal().Err(err).Msg("could not write vote to file")
118+
}
119+
120+
log.Info().Msgf("node %v successfully generated vote file for root cluster block %v", nodeID, clusterBlock.ID())
121+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"time"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/onflow/flow-go/cmd/bootstrap/gcs"
11+
"github.com/onflow/flow-go/model/bootstrap"
12+
)
13+
14+
var pullClusteringCmd = &cobra.Command{
15+
Use: "pull-clustering",
16+
Short: "Pull epoch clustering",
17+
Run: pullClustering,
18+
}
19+
20+
func init() {
21+
rootCmd.AddCommand(pullClusteringCmd)
22+
addPullClusteringFlags()
23+
}
24+
25+
func addPullClusteringFlags() {
26+
pullClusteringCmd.Flags().StringVarP(&flagToken, "token", "t", "", "token provided by the Flow team to access the Transit server")
27+
pullClusteringCmd.Flags().StringVarP(&flagBucketName, "bucket-name", "g", "flow-genesis-bootstrap", `bucket for pulling root clustering`)
28+
pullClusteringCmd.Flags().StringVarP(&flagOutputDir, "outputDir", "o", "", "output directory for clustering file; if not set defaults to bootstrap directory")
29+
_ = pullClusteringCmd.MarkFlagRequired("token")
30+
}
31+
32+
func pullClustering(c *cobra.Command, args []string) {
33+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
34+
defer cancel()
35+
36+
// create new bucket instance with Flow Bucket name
37+
bucket := gcs.NewGoogleBucket(flagBucketName)
38+
39+
// initialize a new client to GCS
40+
client, err := bucket.NewClient(ctx)
41+
if err != nil {
42+
log.Fatal().Err(err).Msgf("error trying get new google bucket client")
43+
}
44+
defer client.Close()
45+
46+
log.Info().Msg("downloading clustering assignment")
47+
48+
clusteringFile := filepath.Join(flagToken, bootstrap.PathClusteringData)
49+
fullClusteringPath := filepath.Join(flagBootDir, bootstrap.PathClusteringData)
50+
if flagOutputDir != "" {
51+
fullClusteringPath = filepath.Join(flagOutputDir, "root-clustering.json")
52+
}
53+
54+
log.Info().Str("source", clusteringFile).Str("dest", fullClusteringPath).Msgf("downloading clustering file from transit servers")
55+
err = bucket.DownloadFile(ctx, client, fullClusteringPath, clusteringFile)
56+
if err != nil {
57+
log.Fatal().Err(err).Msgf("could not download google bucket file")
58+
}
59+
60+
log.Info().Msg("successfully downloaded clustering")
61+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"path/filepath"
7+
"time"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/onflow/flow-go/cmd/bootstrap/gcs"
12+
"github.com/onflow/flow-go/model/bootstrap"
13+
"github.com/onflow/flow-go/model/flow"
14+
)
15+
16+
var pushClusterVoteCmd = &cobra.Command{
17+
Use: "push-cluster-block-vote",
18+
Short: "Push cluster block vote",
19+
Run: pushClusterVote,
20+
}
21+
22+
func init() {
23+
rootCmd.AddCommand(pushClusterVoteCmd)
24+
addPushClusterVoteCmdFlags()
25+
}
26+
27+
func addPushClusterVoteCmdFlags() {
28+
defaultVoteFilePath := fmt.Sprintf(bootstrap.PathNodeRootClusterBlockVote, "<node_id>")
29+
pushClusterVoteCmd.Flags().StringVarP(&flagToken, "token", "t", "", "token provided by the Flow team to access the Transit server")
30+
pushClusterVoteCmd.Flags().StringVarP(&flagVoteFile, "vote-file", "v", "", fmt.Sprintf("path under bootstrap directory of the vote file to upload (default: %s)", defaultVoteFilePath))
31+
pushClusterVoteCmd.Flags().StringVarP(&flagVoteFilePath, "vote-file-dir", "d", "", "directory for vote file to upload, ONLY for vote files outside the bootstrap directory")
32+
pushClusterVoteCmd.Flags().StringVarP(&flagBucketName, "bucket-name", "g", "flow-genesis-bootstrap", `bucket for pushing root cluster block vote files`)
33+
34+
_ = pushClusterVoteCmd.MarkFlagRequired("token")
35+
pushClusterVoteCmd.MarkFlagsMutuallyExclusive("vote-file", "vote-file-dir")
36+
}
37+
38+
func pushClusterVote(c *cobra.Command, args []string) {
39+
nodeIDString, err := readNodeID()
40+
if err != nil {
41+
log.Fatal().Err(err).Msg("could not read node ID")
42+
}
43+
44+
nodeID, err := flow.HexStringToIdentifier(nodeIDString)
45+
if err != nil {
46+
log.Fatal().Err(err).Msg("could not parse node ID")
47+
}
48+
49+
voteFile := flagVoteFile
50+
51+
// If --vote-file-dir is not specified, use the bootstrap directory
52+
voteFilePath := filepath.Join(flagBootDir, voteFile)
53+
54+
// if --vote-file is not specified, use default file name within bootstrap directory
55+
if voteFile == "" {
56+
voteFile = fmt.Sprintf(bootstrap.PathNodeRootClusterBlockVote, nodeID)
57+
voteFilePath = filepath.Join(flagBootDir, voteFile)
58+
}
59+
60+
// If vote-file-dir is specified, use it to construct the full path to the vote file (with default file name)
61+
if flagVoteFilePath != "" {
62+
voteFilePath = filepath.Join(flagVoteFilePath, "root-cluster-block-vote.json")
63+
}
64+
65+
destination := filepath.Join(flagToken, fmt.Sprintf(bootstrap.FilenameRootClusterBlockVote, nodeID))
66+
67+
log.Info().Msg("pushing root cluster block vote")
68+
69+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
70+
defer cancel()
71+
72+
// create new bucket instance with Flow Bucket name
73+
bucket := gcs.NewGoogleBucket(flagBucketName)
74+
75+
// initialize a new client to GCS
76+
client, err := bucket.NewClient(ctx)
77+
if err != nil {
78+
log.Fatal().Err(err).Msgf("error trying get new google bucket client")
79+
}
80+
defer client.Close()
81+
82+
err = bucket.UploadFile(ctx, client, destination, voteFilePath)
83+
if err != nil {
84+
log.Fatal().Err(err).Msg("failed to upload cluster vote file")
85+
}
86+
87+
log.Info().Msg("successfully pushed cluster vote file")
88+
}

0 commit comments

Comments
 (0)