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

[BFT] Add Staking Key PoP to bootstrapping tools #2803

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
16 changes: 9 additions & 7 deletions cmd/bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
This package contains script for generating the bootstrap files needed to initialize the Flow network.
The high-level bootstrapping process is described in [Notion](https://www.notion.so/dapperlabs/Flow-Bootstrapping-ce9d227f18a8410dbce74ed7d4ddee27).

WARNING: These scripts use Go's crypto/rand package to generate seeds for private keys. Make sure you are running the bootstrap scripts on a machine that does provide proper a low-level implementation. See https://golang.org/pkg/crypto/rand/ for details.
WARNING: These scripts use Go's crypto/rand package to generate seeds for private keys, whenever seeds are not provided to the commands. Make sure you are running the bootstrap scripts on a machine that does provide a low-level cryptographically secure RNG. See https://golang.org/pkg/crypto/rand/ for details.

NOTE: Public and private keys are encoded in JSON files as base64 strings, not as hex, contrary to what might be expected.
NOTE: Public and private keys are encoded in JSON files as hex strings.

Code structure:
* `cmd/bootstrap/cmd` contains CLI logic that can exit the program and read/write files. It also uses structures and data types that are purely relevant for CLI purposes, such as encoding, decoding, etc.
Expand All @@ -18,16 +18,17 @@ Code structure:
The bootstrapping will generate the following information:

#### Per node
* Staking key (BLS key with curve BLS12-381)
* Networking key (ECDSA key)
* Random beacon key; _only_ for consensus nodes (BLS based on Joint-Feldman DKG for threshold signatures)
* Staking private key (BLS key on curve BLS12-381)
* Networking private key (ECDSA key on curve P-256)
* Random beacon private key; _only_ for consensus nodes (BLS key on curve BLS12-381, used for a BLS-based threshold signatures)

#### Node Identities
* List of all authorized Flow nodes
- node network address
- node ID
- node role
- public staking key
- proof of possession of the staking private key
- public networking key
- weight

Expand Down Expand Up @@ -61,7 +62,8 @@ Values directly specified as command line parameters:
Values can be specified as command line parameters:
- seed for generating staking key (min 48 bytes in hex encoding)
- seed for generating networking key (min 48 bytes in hex encoding)
If seeds are not provided, the CLI will try to use the system's pseudo-random number generator (PRNG), e. g. `dev/urandom`. Make sure you are running the CLI on a hardware that has a cryptographically secure PRNG, or provide seeds generated on such a system.
Provided seeds must be of high entropy, ideally generated by a crypto secure RNG.
If seeds are not provided, the CLI will try to use the system's random number generator (RNG), e. g. `dev/urandom`. Make sure you are running the CLI on a hardware that has a cryptographically secure RNG.

#### Example
```bash
Expand All @@ -76,7 +78,7 @@ go run ./cmd/bootstrap key --address "example.com:1234" --role "consensus" -o ./
file needs to be available to respective partner node at boot up (or recovery after crash)
* file `<NodeID>.node-info.pub.json`
- public information
- file needs to be delivered to Dapper Labs for Phase 2 of generating root information,
- file needs to be delivered to the Flow Foundation team for Phase 2 of generating root information,
but is not required at node start


Expand Down
55 changes: 40 additions & 15 deletions cmd/bootstrap/cmd/final_list.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"fmt"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -68,7 +69,11 @@ func finalList(cmd *cobra.Command, args []string) {
validateNodes(localNodes, registeredNodes)

// write node-config.json with the new list of nodes to be used for the `finalize` command
err := common.WriteJSON(model.PathFinallist, flagOutdir, model.ToPublicNodeInfoList(localNodes))
pubInfo, err := model.ToPublicNodeInfoList(localNodes)
if err != nil {
log.Fatal().Err(err).Msg("failed to read public info")
}
err = common.WriteJSON(model.PathFinallist, flagOutdir, pubInfo)
if err != nil {
log.Fatal().Err(err).Msg("failed to write json")
}
Expand Down Expand Up @@ -132,28 +137,44 @@ func validateNodes(localNodes []model.NodeInfo, registeredNodes []model.NodeInfo
// flow localNodes contain private key info
if matchingNode.NetworkPubKey().String() != "" {
// check networking pubkey match
matchNodeKey := matchingNode.NetworkPubKey().String()
registeredNodeKey := registeredNode.NetworkPubKey().String()
matchNodeKey := matchingNode.NetworkPubKey()
registeredNodeKey := registeredNode.NetworkPubKey()

if matchNodeKey != registeredNodeKey {
if !matchNodeKey.Equals(registeredNodeKey) {
log.Error().
Str("registered network key", registeredNodeKey).
Str("network key", matchNodeKey).
Str("registered network key", registeredNodeKey.String()).
Str("network key", matchNodeKey.String()).
Msg("networking keys do not match")
}
}

// flow localNodes contain privatekey info
if matchingNode.StakingPubKey().String() != "" {
matchNodeKey := matchingNode.StakingPubKey().String()
registeredNodeKey := registeredNode.StakingPubKey().String()
matchNodeKey := matchingNode.StakingPubKey()
registeredNodeKey := registeredNode.StakingPubKey()

if matchNodeKey != registeredNodeKey {
if !matchNodeKey.Equals(registeredNodeKey) {
log.Error().
Str("registered staking key", registeredNodeKey).
Str("staking key", matchNodeKey).
Str("registered staking key", registeredNodeKey.String()).
Str("staking key", matchNodeKey.String()).
Msg("staking keys do not match")
}

matchingPoP, err := matchingNode.StakingPoP()
if err != nil {
log.Error().Msgf("error reading matching PoP: %s", err.Error())
}
registeredPoP, err := registeredNode.StakingPoP()
if err != nil {
log.Error().Msgf("error reading registered PoP: %s", err.Error())
}

if !bytes.Equal(matchingPoP, registeredPoP) {
log.Error().
Str("registered staking PoP", fmt.Sprintf("%x", registeredPoP)).
Str("staking PoP", fmt.Sprintf("%x", matchingPoP)).
Msg("staking PoP do not match")
}
}
}
}
Expand Down Expand Up @@ -253,14 +274,17 @@ func assembleInternalNodesWithoutWeight() []model.NodeInfo {
log.Fatal().Err(err).Msg(fmt.Sprintf("invalid node ID: %s", internal.NodeID))
}

node := model.NewPrivateNodeInfo(
node, err := model.NewPrivateNodeInfo(
internal.NodeID,
internal.Role,
internal.Address,
flow.DefaultInitialWeight,
internal.NetworkPrivKey,
internal.StakingPrivKey,
internal.StakingPrivKey.PrivateKey,
)
if err != nil {
panic(err)
}

nodes = append(nodes, node)
}
Expand Down Expand Up @@ -300,9 +324,9 @@ func createPublicNodeInfo(nodes []model.NodeInfoPub) []model.NodeInfo {
if err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("invalid network public key: %s", n.NetworkPubKey))
}
err = common.ValidateStakingPubKey(n.StakingPubKey)
err = common.ValidateStakingPubKey(n.StakingPubKey, n.StakingPoP)
if err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("invalid staking public key: %s", n.StakingPubKey))
log.Fatal().Err(err).Msg(fmt.Sprintf("invalid staking public key : %s, or staking PoP: %s", n.StakingPubKey, n.StakingPoP))
}

// all nodes should have equal weight (this might change in the future)
Expand All @@ -313,6 +337,7 @@ func createPublicNodeInfo(nodes []model.NodeInfoPub) []model.NodeInfo {
flow.DefaultInitialWeight,
n.NetworkPubKey,
n.StakingPubKey,
n.StakingPoP,
)

publicInfoNodes = append(publicInfoNodes, node)
Expand Down
2 changes: 1 addition & 1 deletion cmd/bootstrap/cmd/finalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func addFinalizeCmdFlags() {
"containing the output from the `keygen` command for internal nodes")
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)")
" 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 "+
Expand Down
6 changes: 5 additions & 1 deletion cmd/bootstrap/cmd/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ func keyCmdRun(_ *cobra.Command, _ []string) {
}
log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathSecretsEncryptionKey)

err = common.WriteJSON(fmt.Sprintf(model.PathNodeInfoPub, nodeInfo.NodeID), flagOutdir, nodeInfo.Public())
public, err := nodeInfo.Public()
if err != nil {
log.Fatal().Err(err).Msg("could not access public keys")
}
err = common.WriteJSON(fmt.Sprintf(model.PathNodeInfoPub, nodeInfo.NodeID), flagOutdir, public)
if err != nil {
log.Fatal().Err(err).Msg("failed to write json")
}
Expand Down
16 changes: 12 additions & 4 deletions cmd/bootstrap/cmd/keygen.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ var keygenCmd = &cobra.Command{
}

log.Info().Msg("generating node public information")
genNodePubInfo(nodes)
err = genNodePubInfo(nodes)
if err != nil {
log.Fatal().Err(err).Msg("failed to generate nodes public info")
}
},
}

Expand Down Expand Up @@ -120,14 +123,19 @@ func isEmptyDir(path string) (bool, error) {
return false, err // Either not empty or error, suits both cases
}

func genNodePubInfo(nodes []model.NodeInfo) {
func genNodePubInfo(nodes []model.NodeInfo) error {
pubNodes := make([]model.NodeInfoPub, 0, len(nodes))
for _, node := range nodes {
pubNodes = append(pubNodes, node.Public())
pub, err := node.Public()
if err != nil {
return fmt.Errorf("failed to read public info: %w", err)
}
pubNodes = append(pubNodes, pub)
}
err := common.WriteJSON(model.PathInternalNodeInfosPub, flagOutdir, pubNodes)
if err != nil {
log.Fatal().Err(err).Msg("failed to write json")
return err
}
log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathInternalNodeInfosPub)
return nil
}
8 changes: 6 additions & 2 deletions cmd/bootstrap/cmd/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ func genNetworkAndStakingKeys() []model.NodeInfo {
return model.Sort(internalNodes, flow.Canonical[flow.Identity])
}

func assembleNodeInfo(nodeConfig model.NodeConfig, networkKey, stakingKey crypto.PrivateKey) model.NodeInfo {
func assembleNodeInfo(nodeConfig model.NodeConfig, networkKey, stakingKey crypto.PrivateKey,
) model.NodeInfo {
var err error
nodeID, found := getNameID()
if !found {
Expand All @@ -71,14 +72,17 @@ func assembleNodeInfo(nodeConfig model.NodeConfig, networkKey, stakingKey crypto
Str("stakingPubKey", stakingKey.PublicKey().String()).
Msg("encoded public staking and network keys")

nodeInfo := model.NewPrivateNodeInfo(
nodeInfo, err := model.NewPrivateNodeInfo(
nodeID,
nodeConfig.Role,
nodeConfig.Address,
nodeConfig.Weight,
networkKey,
stakingKey,
)
if err != nil {
log.Fatal().Err(err).Msg("creating node info failed")
}

return nodeInfo
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/bootstrap/cmd/partner_infos.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
networkingAddressField = "networkingAddress"
networkingKeyField = "networkingKey"
stakingKeyField = "stakingKey"
stakingKePOPyField = "stakingKeyPoP"
)

const (
Expand Down Expand Up @@ -172,13 +173,19 @@ func parseNodeInfo(info cadence.Value) (*bootstrap.NodeInfoPub, error) {
return nil, fmt.Errorf("failed to decode staking public key: %w", err)
}

stakingPOP, err := hex.DecodeString(string(fields[stakingKePOPyField].(cadence.String)))
if err != nil {
return nil, fmt.Errorf("failed to decode staking private key PoP hex (%s): %w", string(fields[stakingKePOPyField].(cadence.String)), err)
}

return &bootstrap.NodeInfoPub{
Role: flow.Role(fields[roleField].(cadence.UInt8)),
Address: string(fields[networkingAddressField].(cadence.String)),
NodeID: nodeID,
Weight: flow.DefaultInitialWeight,
NetworkPubKey: encodable.NetworkPubKey{PublicKey: networkPubKey},
StakingPubKey: encodable.StakingPubKey{PublicKey: stakingPubKey},
StakingPoP: stakingPOP,
}, nil
}

Expand Down
6 changes: 5 additions & 1 deletion cmd/bootstrap/cmd/rootblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,11 @@ func rootBlock(cmd *cobra.Command, args []string) {

log.Info().Msg("assembling network and staking keys")
stakingNodes := mergeNodeInfos(internalNodes, partnerNodes)
err = common.WriteJSON(model.PathNodeInfosPub, flagOutdir, model.ToPublicNodeInfoList(stakingNodes))
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")
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Role": "consensus",
"Address": "example.com:1234",
"NodeID": "79a7f711ba44dfdcfe774769eb97af6af732337fbcf96b788dbe6c51d29c5ec6",
"Weight": 0,
"NetworkPubKey": "0162f95166d39db8a6486625813019fcc8bb3d9439ad1e57b44f7bf01235cbcabd66411c3faa98de806e439cb4372275b76dcd3af7d384d24851cbae89f92cda",
"StakingPubKey": "84f806be7e4db914358e5b66a405244161ad5bfd87939b3a9b428a941baa6ae245d0d7a6cef684bd7168815fda5e9b6506b2cc87ec9c52576913d1990fd7c376fc2c6884247ff6a7c0c46ca143e3697422913d53c134b9534a199b7fc8f57d50",
"StakingPoP": "oEz2R3qe86/ZaRAemZfpdjcBZcOt7RHLjMhqjf7gg99XMsaLjmDma94Rr9ylciti"
}
4 changes: 3 additions & 1 deletion cmd/bootstrap/run/cluster_qc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ func createClusterParticipants(t *testing.T, n int) []model.NodeInfo {

participants := make([]model.NodeInfo, n)
for i, id := range ids {
participants[i] = model.NewPrivateNodeInfo(
var err error
participants[i], err = model.NewPrivateNodeInfo(
id.NodeID,
id.Role,
id.Address,
id.InitialWeight,
networkKeys[i],
stakingKeys[i],
)
require.NoError(t, err)
}

return participants
Expand Down
3 changes: 2 additions & 1 deletion cmd/bootstrap/run/qc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ func createSignerData(t *testing.T, n int) *ParticipantData {
participantLookup[identity.NodeID] = lookupParticipant

// add to participant list
nodeInfo := bootstrap.NewPrivateNodeInfo(
nodeInfo, err := bootstrap.NewPrivateNodeInfo(
identity.NodeID,
identity.Role,
identity.Address,
identity.InitialWeight,
networkingKeys[i],
stakingKeys[i],
)
require.NoError(t, err)
participants[i] = Participant{
NodeInfo: nodeInfo,
RandomBeaconPrivKey: randomBSKs[i],
Expand Down
10 changes: 9 additions & 1 deletion cmd/bootstrap/utils/key_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,15 @@ func GenerateStakingKey(seed []byte) (crypto.PrivateKey, error) {
}

func GenerateStakingKeys(n int, seeds [][]byte) ([]crypto.PrivateKey, error) {
return GenerateKeys(crypto.BLSBLS12381, n, seeds)
keys := make([]crypto.PrivateKey, 0, n)
for i := 0; i < n; i++ {
key, err := GenerateStakingKey(seeds[i])
if err != nil {
return nil, err
}
keys = append(keys, key)
}
return keys, nil
}

func GenerateKeys(algo crypto.SigningAlgorithm, n int, seeds [][]byte) ([]crypto.PrivateKey, error) {
Expand Down
Loading
Loading