diff --git a/bridge/cmd/guardiand/bridge.go b/bridge/cmd/guardiand/bridge.go index 1c7039014..57d474991 100644 --- a/bridge/cmd/guardiand/bridge.go +++ b/bridge/cmd/guardiand/bridge.go @@ -9,6 +9,7 @@ import ( "syscall" eth_common "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/libp2p/go-libp2p-core/crypto" "github.com/libp2p/go-libp2p-core/peer" "github.com/spf13/cobra" @@ -37,6 +38,8 @@ var ( nodeKeyPath *string + bridgeKeyPath *string + ethRPC *string ethContract *string ethConfirmations *uint64 @@ -64,6 +67,8 @@ func init() { nodeKeyPath = BridgeCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)") + bridgeKeyPath = BridgeCmd.Flags().String("bridgeKey", "", "Path to guardian key (required)") + ethRPC = BridgeCmd.Flags().String("ethRPC", "", "Ethereum RPC URL") ethContract = BridgeCmd.Flags().String("ethContract", "", "Ethereum bridge contract address") ethConfirmations = BridgeCmd.Flags().Uint64("ethConfirmations", 15, "Ethereum confirmation count requirement") @@ -184,6 +189,9 @@ func runBridge(cmd *cobra.Command, args []string) { if *nodeKeyPath == "" && !*unsafeDevMode { // In devnet mode, keys are deterministically generated. logger.Fatal("Please specify -nodeKey") } + if *bridgeKeyPath == "" { + logger.Fatal("Please specify -bridgeKey") + } if *agentRPC == "" { logger.Fatal("Please specify -agentRPC") } @@ -216,8 +224,27 @@ func runBridge(cmd *cobra.Command, args []string) { ethContractAddr := eth_common.HexToAddress(*ethContract) + // In devnet mode, we generate a deterministic guardian key and write it to disk. + if *unsafeDevMode { + gk, err := generateDevnetGuardianKey() + if err != nil { + logger.Fatal("failed to generate devnet guardian key", zap.Error(err)) + } + + err = writeGuardianKey(gk, "auto-generated deterministic devnet key", *bridgeKeyPath) + if err != nil { + logger.Fatal("failed to write devnet guardian key", zap.Error(err)) + } + } + // Guardian key - gk := loadGuardianKey(logger) + gk, err := loadGuardianKey(*bridgeKeyPath) + if err != nil { + logger.Fatal("failed to load guardian key", zap.Error(err)) + } + + logger.Info("Loaded guardian key", zap.String( + "address", ethcrypto.PubkeyToAddress(gk.PublicKey).String())) // Node's main lifecycle context. rootCtx, rootCtxCancel = context.WithCancel(context.Background()) diff --git a/bridge/cmd/guardiand/bridgekey.go b/bridge/cmd/guardiand/bridgekey.go new file mode 100644 index 000000000..7119b5b4f --- /dev/null +++ b/bridge/cmd/guardiand/bridgekey.go @@ -0,0 +1,66 @@ +package guardiand + +import ( + "crypto/ecdsa" + "fmt" + "io/ioutil" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "google.golang.org/protobuf/encoding/prototext" + + "github.com/certusone/wormhole/bridge/pkg/devnet" + nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1" +) + +// loadGuardianKey loads a serialized guardian key from disk. +func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read guardian private key from disk: %w", err) + } + + var m nodev1.GuardianKey + err = prototext.Unmarshal(b, &m) + if err != nil { + return nil, fmt.Errorf("failed to deserialize private key from disk: %w", err) + } + + gk, err := ethcrypto.ToECDSA(m.Data) + if err != nil { + return nil, fmt.Errorf("failed to deserialize key data: %w", err) + } + + return gk, nil +} + +// writeGuardianKey serializes a guardian key and writes it to disk. +func writeGuardianKey(key *ecdsa.PrivateKey, description string, filename string) error { + m := &nodev1.GuardianKey{ + Description: description, + Data: ethcrypto.FromECDSA(key), + Pubkey: ethcrypto.PubkeyToAddress(key.PublicKey).String(), + } + + b, err := prototext.MarshalOptions{Multiline: true, EmitASCII: true}.Marshal(m) + if err != nil { + panic(err) + } + + if err := ioutil.WriteFile(filename, b, 0600); err != nil { + return err + } + + return nil +} + +// generateDevnetGuardianKey returns a deterministic testnet key. +func generateDevnetGuardianKey() (*ecdsa.PrivateKey, error) { + // Figure out our devnet index + idx, err := devnet.GetDevnetIndex() + if err != nil { + return nil, err + } + + // Generate guardian key + return devnet.DeterministicEcdsaKeyByIndex(ethcrypto.S256(), uint64(idx)), nil +} diff --git a/bridge/cmd/guardiand/nodekeys.go b/bridge/cmd/guardiand/nodekey.go similarity index 60% rename from bridge/cmd/guardiand/nodekeys.go rename to bridge/cmd/guardiand/nodekey.go index 81a23ac9a..79ac2ca8c 100644 --- a/bridge/cmd/guardiand/nodekeys.go +++ b/bridge/cmd/guardiand/nodekey.go @@ -1,40 +1,14 @@ package guardiand import ( - "crypto/ecdsa" "fmt" "io/ioutil" "os" - ethcrypto "github.com/ethereum/go-ethereum/crypto" p2pcrypto "github.com/libp2p/go-libp2p-core/crypto" "go.uber.org/zap" - - "github.com/certusone/wormhole/bridge/pkg/devnet" ) -func loadGuardianKey(logger *zap.Logger) *ecdsa.PrivateKey { - var gk *ecdsa.PrivateKey - - if *unsafeDevMode { - // Figure out our devnet index - idx, err := devnet.GetDevnetIndex() - if err != nil { - logger.Fatal("Failed to parse hostname - are we running in devnet?") - } - - // Generate guardian key - gk = devnet.DeterministicEcdsaKeyByIndex(ethcrypto.S256(), uint64(idx)) - } else { - panic("not implemented") // TODO - } - - logger.Info("Loaded guardian key", zap.String( - "address", ethcrypto.PubkeyToAddress(gk.PublicKey).String())) - - return gk -} - func getOrCreateNodeKey(logger *zap.Logger, path string) (p2pcrypto.PrivKey, error) { b, err := ioutil.ReadFile(path) if err != nil { diff --git a/devnet/bridge.yaml b/devnet/bridge.yaml index 8eeacbfea..c4a8048bc 100644 --- a/devnet/bridge.yaml +++ b/devnet/bridge.yaml @@ -76,6 +76,8 @@ spec: - --ethConfirmations - '3' - --unsafeDevMode + - --bridgeKey + - /tmp/bridge.key # - --logLevel=debug securityContext: capabilities: diff --git a/proto/node/v1/node.proto b/proto/node/v1/node.proto index f4448a150..376c786c5 100644 --- a/proto/node/v1/node.proto +++ b/proto/node/v1/node.proto @@ -6,4 +6,16 @@ option go_package = "proto/node/v1;nodev1"; import "google/api/annotations.proto"; -service Node {} +service Node { +} + +message GuardianKey { + // description is an optional, free-form description text set by the operator. + string description = 1; + + // data is the binary representation of the secp256k1 private key. + bytes data = 2; + + // pubkey is a human-readable representation of the key, included for operator convenience. + string pubkey = 3; +}