From b289d2baf44ae52ec9ee44b83d4f9ce2c56919b4 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Mon, 1 Jan 2018 20:21:42 -0500 Subject: [PATCH] persistent node key and ID --- config/config.go | 9 ++++ node/node.go | 16 +++++-- p2p/key.go | 111 +++++++++++++++++++++++++++++++++++++++++++++ p2p/key_test.go | 49 ++++++++++++++++++++ p2p/switch.go | 33 +++++++++----- p2p/switch_test.go | 4 +- 6 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 p2p/key.go create mode 100644 p2p/key_test.go diff --git a/config/config.go b/config/config.go index 5d4a8ef65..6018dc8de 100644 --- a/config/config.go +++ b/config/config.go @@ -72,6 +72,9 @@ type BaseConfig struct { // A JSON file containing the private key to use as a validator in the consensus protocol PrivValidator string `mapstructure:"priv_validator_file"` + // A JSON file containing the private key to use for p2p authenticated encryption + NodeKey string `mapstructure:"node_key"` + // A custom human readable name for this node Moniker string `mapstructure:"moniker"` @@ -109,6 +112,7 @@ func DefaultBaseConfig() BaseConfig { return BaseConfig{ Genesis: "genesis.json", PrivValidator: "priv_validator.json", + NodeKey: "node_key.json", Moniker: defaultMoniker, ProxyApp: "tcp://127.0.0.1:46658", ABCI: "socket", @@ -141,6 +145,11 @@ func (b BaseConfig) PrivValidatorFile() string { return rootify(b.PrivValidator, b.RootDir) } +// NodeKeyFile returns the full path to the node_key.json file +func (b BaseConfig) NodeKeyFile() string { + return rootify(b.NodeKey, b.RootDir) +} + // DBDir returns the full path to the database directory func (b BaseConfig) DBDir() string { return rootify(b.DBPath, b.RootDir) diff --git a/node/node.go b/node/node.go index 7f845f902..0027c6802 100644 --- a/node/node.go +++ b/node/node.go @@ -367,12 +367,20 @@ func (n *Node) OnStart() error { n.sw.AddListener(l) // Generate node PrivKey - // TODO: Load - privKey := crypto.GenPrivKeyEd25519().Wrap() + // TODO: both the loading function and the target + // will need to be configurable + difficulty := uint8(16) // number of leading 0s in bitstring + target := p2p.MakePoWTarget(difficulty) + nodeKey, err := p2p.LoadOrGenNodeKey(n.config.NodeKeyFile(), target) + if err != nil { + return err + } + n.Logger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", n.config.NodeKeyFile()) // Start the switch - n.sw.SetNodeInfo(n.makeNodeInfo(privKey.PubKey())) - n.sw.SetNodePrivKey(privKey) + n.sw.SetNodeInfo(n.makeNodeInfo(nodeKey.PubKey())) + n.sw.SetNodeKey(nodeKey) + n.sw.SetPeerIDTarget(target) err = n.sw.Start() if err != nil { return err diff --git a/p2p/key.go b/p2p/key.go new file mode 100644 index 000000000..aa2ac7677 --- /dev/null +++ b/p2p/key.go @@ -0,0 +1,111 @@ +package p2p + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + + crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" +) + +//------------------------------------------------------------------------------ +// Persistent peer ID +// TODO: encrypt on disk + +// NodeKey is the persistent peer key. +// It contains the nodes private key for authentication. +type NodeKey struct { + PrivKey crypto.PrivKey `json:"priv_key"` // our priv key +} + +// ID returns the peer's canonical ID - the hash of its public key. +func (nodeKey *NodeKey) ID() []byte { + return nodeKey.PrivKey.PubKey().Address() +} + +// PubKey returns the peer's PubKey +func (nodeKey *NodeKey) PubKey() crypto.PubKey { + return nodeKey.PrivKey.PubKey() +} + +// LoadOrGenNodeKey attempts to load the NodeKey from the given filePath, +// and checks that the corresponding ID is less than the target. +// If the file does not exist, it generates and saves a new NodeKey +// with ID less than target. +func LoadOrGenNodeKey(filePath string, target []byte) (*NodeKey, error) { + if cmn.FileExists(filePath) { + nodeKey, err := loadNodeKey(filePath) + if err != nil { + return nil, err + } + if bytes.Compare(nodeKey.ID(), target) >= 0 { + return nil, fmt.Errorf("Loaded ID (%X) does not satisfy target (%X)", nodeKey.ID(), target) + } + return nodeKey, nil + } else { + return genNodeKey(filePath, target) + } +} + +// MakePoWTarget returns a 20 byte target byte array. +func MakePoWTarget(difficulty uint8) []byte { + zeroPrefixLen := (int(difficulty) / 8) + prefix := bytes.Repeat([]byte{0}, zeroPrefixLen) + mod := (difficulty % 8) + if mod > 0 { + nonZeroPrefix := byte(1 << (8 - mod)) + prefix = append(prefix, nonZeroPrefix) + } + return append(prefix, bytes.Repeat([]byte{255}, 20-len(prefix))...) +} + +func loadNodeKey(filePath string) (*NodeKey, error) { + jsonBytes, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + nodeKey := new(NodeKey) + err = json.Unmarshal(jsonBytes, nodeKey) + if err != nil { + return nil, fmt.Errorf("Error reading NodeKey from %v: %v\n", filePath, err) + } + return nodeKey, nil +} + +func genNodeKey(filePath string, target []byte) (*NodeKey, error) { + privKey := genPrivKeyEd25519PoW(target).Wrap() + nodeKey := &NodeKey{ + PrivKey: privKey, + } + + jsonBytes, err := json.Marshal(nodeKey) + if err != nil { + return nil, err + } + err = ioutil.WriteFile(filePath, jsonBytes, 0600) + if err != nil { + return nil, err + } + return nodeKey, nil +} + +// generate key with address satisfying the difficult target +func genPrivKeyEd25519PoW(target []byte) crypto.PrivKeyEd25519 { + secret := crypto.CRandBytes(32) + var privKey crypto.PrivKeyEd25519 + for i := 0; ; i++ { + privKey = crypto.GenPrivKeyEd25519FromSecret(secret) + if bytes.Compare(privKey.PubKey().Address(), target) < 0 { + break + } + z := new(big.Int) + z.SetBytes(secret) + z = z.Add(z, big.NewInt(1)) + secret = z.Bytes() + + } + return privKey +} diff --git a/p2p/key_test.go b/p2p/key_test.go new file mode 100644 index 000000000..ef885e55d --- /dev/null +++ b/p2p/key_test.go @@ -0,0 +1,49 @@ +package p2p + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tmlibs/common" +) + +func TestLoadOrGenNodeKey(t *testing.T) { + filePath := filepath.Join(os.TempDir(), cmn.RandStr(12)+"_peer_id.json") + + target := MakePoWTarget(2) + nodeKey, err := LoadOrGenNodeKey(filePath, target) + assert.Nil(t, err) + + nodeKey2, err := LoadOrGenNodeKey(filePath, target) + assert.Nil(t, err) + + assert.Equal(t, nodeKey, nodeKey2) +} + +func repeatBytes(val byte, n int) []byte { + return bytes.Repeat([]byte{val}, n) +} + +func TestPoWTarget(t *testing.T) { + + cases := []struct { + difficulty uint8 + target []byte + }{ + {0, bytes.Repeat([]byte{255}, 20)}, + {1, append([]byte{128}, repeatBytes(255, 19)...)}, + {8, append([]byte{0}, repeatBytes(255, 19)...)}, + {9, append([]byte{0, 128}, repeatBytes(255, 18)...)}, + {10, append([]byte{0, 64}, repeatBytes(255, 18)...)}, + {16, append([]byte{0, 0}, repeatBytes(255, 18)...)}, + {17, append([]byte{0, 0, 128}, repeatBytes(255, 17)...)}, + } + + for _, c := range cases { + assert.Equal(t, MakePoWTarget(c.difficulty), c.target) + } + +} diff --git a/p2p/switch.go b/p2p/switch.go index d4e3b3484..344c3c1e1 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -81,8 +81,9 @@ type Switch struct { reactorsByCh map[byte]Reactor peers *PeerSet dialing *cmn.CMap - nodeInfo *NodeInfo // our node info - nodePrivKey crypto.PrivKey // our node privkey + nodeInfo *NodeInfo // our node info + nodeKey *NodeKey // our node privkey + peerIDTarget []byte filterConnByAddr func(net.Addr) error filterConnByPubKey func(crypto.PubKey) error @@ -181,16 +182,22 @@ func (sw *Switch) NodeInfo() *NodeInfo { return sw.nodeInfo } -// SetNodePrivKey sets the switch's private key for authenticated encryption. +// SetNodeKey sets the switch's private key for authenticated encryption. // NOTE: Overwrites sw.nodeInfo.PubKey. // NOTE: Not goroutine safe. -func (sw *Switch) SetNodePrivKey(nodePrivKey crypto.PrivKey) { - sw.nodePrivKey = nodePrivKey +func (sw *Switch) SetNodeKey(nodeKey *NodeKey) { + sw.nodeKey = nodeKey if sw.nodeInfo != nil { - sw.nodeInfo.PubKey = nodePrivKey.PubKey() + sw.nodeInfo.PubKey = nodeKey.PubKey() } } +// SetPeerIDTarget sets the target for incoming peer ID's - +// the ID must be less than the target +func (sw *Switch) SetPeerIDTarget(target []byte) { + sw.peerIDTarget = target +} + // OnStart implements BaseService. It starts all the reactors, peers, and listeners. func (sw *Switch) OnStart() error { // Start reactors @@ -370,7 +377,7 @@ func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) (Peer, defer sw.dialing.Delete(addr.IP.String()) sw.Logger.Info("Dialing peer", "address", addr) - peer, err := newOutboundPeer(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, sw.peerConfig) + peer, err := newOutboundPeer(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, sw.peerConfig) if err != nil { sw.Logger.Error("Failed to dial peer", "address", addr, "err", err) return nil, err @@ -598,24 +605,26 @@ func StartSwitches(switches []*Switch) error { } func makeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch { - privKey := crypto.GenPrivKeyEd25519() // new switch, add reactors // TODO: let the config be passed in? + nodeKey := &NodeKey{ + PrivKey: crypto.GenPrivKeyEd25519().Wrap(), + } s := initSwitch(i, NewSwitch(cfg)) s.SetNodeInfo(&NodeInfo{ - PubKey: privKey.PubKey(), + PubKey: nodeKey.PubKey(), Moniker: cmn.Fmt("switch%d", i), Network: network, Version: version, RemoteAddr: cmn.Fmt("%v:%v", network, rand.Intn(64512)+1023), ListenAddr: cmn.Fmt("%v:%v", network, rand.Intn(64512)+1023), }) - s.SetNodePrivKey(privKey.Wrap()) + s.SetNodeKey(nodeKey) return s } func (sw *Switch) addPeerWithConnection(conn net.Conn) error { - peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, sw.peerConfig) + peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, sw.peerConfig) if err != nil { if err := conn.Close(); err != nil { sw.Logger.Error("Error closing connection", "err", err) @@ -632,7 +641,7 @@ func (sw *Switch) addPeerWithConnection(conn net.Conn) error { } func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error { - peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config) + peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, config) if err != nil { if err := conn.Close(); err != nil { sw.Logger.Error("Error closing connection", "err", err) diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 1d6d869af..6c606a67a 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -236,7 +236,7 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { rp.Start() defer rp.Stop() - peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, DefaultPeerConfig()) + peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, DefaultPeerConfig()) require.Nil(err) err = sw.addPeer(peer) require.Nil(err) @@ -263,7 +263,7 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { rp.Start() defer rp.Stop() - peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, DefaultPeerConfig()) + peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, DefaultPeerConfig()) peer.makePersistent() require.Nil(err) err = sw.addPeer(peer)