node/p2p: enforce freshness of heartbeats and correctness of guardianAddr
This commit is contained in:
parent
f4ec4bfde9
commit
71188d66d1
|
@ -65,6 +65,9 @@ var heartbeatMessagePrefix = []byte("heartbeat|")
|
||||||
|
|
||||||
var signedObservationRequestPrefix = []byte("signed_observation_request|")
|
var signedObservationRequestPrefix = []byte("signed_observation_request|")
|
||||||
|
|
||||||
|
// heartbeatMaxTimeDifference specifies the maximum time difference between the local clock and the timestamp in incoming heartbeat messages. Heartbeats that are this old or this much into the future will be dropped. This value should encompass clock skew and network delay.
|
||||||
|
var heartbeatMaxTimeDifference = time.Minute * 15
|
||||||
|
|
||||||
func heartbeatDigest(b []byte) common.Hash {
|
func heartbeatDigest(b []byte) common.Hash {
|
||||||
return ethcrypto.Keccak256Hash(append(heartbeatMessagePrefix, b...))
|
return ethcrypto.Keccak256Hash(append(heartbeatMessagePrefix, b...))
|
||||||
}
|
}
|
||||||
|
@ -227,54 +230,59 @@ func Run(
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
DefaultRegistry.mu.Lock()
|
|
||||||
networks := make([]*gossipv1.Heartbeat_Network, 0, len(DefaultRegistry.networkStats))
|
|
||||||
for _, v := range DefaultRegistry.networkStats {
|
|
||||||
errCtr := DefaultRegistry.GetErrorCount(vaa.ChainID(v.Id))
|
|
||||||
v.ErrorCount = errCtr
|
|
||||||
networks = append(networks, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
features := make([]string, 0)
|
// create a heartbeat
|
||||||
if gov != nil {
|
b := func() []byte {
|
||||||
features = append(features, "governor")
|
DefaultRegistry.mu.Lock()
|
||||||
}
|
defer DefaultRegistry.mu.Unlock()
|
||||||
if acct != nil {
|
networks := make([]*gossipv1.Heartbeat_Network, 0, len(DefaultRegistry.networkStats))
|
||||||
features = append(features, acct.FeatureString())
|
for _, v := range DefaultRegistry.networkStats {
|
||||||
}
|
errCtr := DefaultRegistry.GetErrorCount(vaa.ChainID(v.Id))
|
||||||
|
v.ErrorCount = errCtr
|
||||||
|
networks = append(networks, v)
|
||||||
|
}
|
||||||
|
|
||||||
heartbeat := &gossipv1.Heartbeat{
|
features := make([]string, 0)
|
||||||
NodeName: nodeName,
|
if gov != nil {
|
||||||
Counter: ctr,
|
features = append(features, "governor")
|
||||||
Timestamp: time.Now().UnixNano(),
|
}
|
||||||
Networks: networks,
|
if acct != nil {
|
||||||
Version: version.Version(),
|
features = append(features, acct.FeatureString())
|
||||||
GuardianAddr: DefaultRegistry.guardianAddress,
|
}
|
||||||
BootTimestamp: bootTime.UnixNano(),
|
|
||||||
Features: features,
|
|
||||||
}
|
|
||||||
|
|
||||||
ourAddr := ethcrypto.PubkeyToAddress(gk.PublicKey)
|
heartbeat := &gossipv1.Heartbeat{
|
||||||
if err := gst.SetHeartbeat(ourAddr, h.ID(), heartbeat); err != nil {
|
NodeName: nodeName,
|
||||||
panic(err)
|
Counter: ctr,
|
||||||
}
|
Timestamp: time.Now().UnixNano(),
|
||||||
collectNodeMetrics(ourAddr, h.ID(), heartbeat)
|
Networks: networks,
|
||||||
|
Version: version.Version(),
|
||||||
|
GuardianAddr: DefaultRegistry.guardianAddress,
|
||||||
|
BootTimestamp: bootTime.UnixNano(),
|
||||||
|
Features: features,
|
||||||
|
}
|
||||||
|
|
||||||
if gov != nil {
|
ourAddr := ethcrypto.PubkeyToAddress(gk.PublicKey)
|
||||||
gov.CollectMetrics(heartbeat, gossipSendC, gk, ourAddr)
|
if err := gst.SetHeartbeat(ourAddr, h.ID(), heartbeat); err != nil {
|
||||||
}
|
panic(err)
|
||||||
DefaultRegistry.mu.Unlock()
|
}
|
||||||
|
collectNodeMetrics(ourAddr, h.ID(), heartbeat)
|
||||||
|
|
||||||
msg := gossipv1.GossipMessage{
|
if gov != nil {
|
||||||
Message: &gossipv1.GossipMessage_SignedHeartbeat{
|
gov.CollectMetrics(heartbeat, gossipSendC, gk, ourAddr)
|
||||||
SignedHeartbeat: createSignedHeartbeat(gk, heartbeat),
|
}
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := proto.Marshal(&msg)
|
msg := gossipv1.GossipMessage{
|
||||||
if err != nil {
|
Message: &gossipv1.GossipMessage_SignedHeartbeat{
|
||||||
panic(err)
|
SignedHeartbeat: createSignedHeartbeat(gk, heartbeat),
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := proto.Marshal(&msg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}()
|
||||||
|
|
||||||
err = th.Publish(ctx, b)
|
err = th.Publish(ctx, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -515,6 +523,14 @@ func processSignedHeartbeat(from peer.ID, s *gossipv1.SignedHeartbeat, gs *node_
|
||||||
return nil, fmt.Errorf("failed to unmarshal heartbeat: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal heartbeat: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if time.Until(time.Unix(0, h.Timestamp)).Abs() > heartbeatMaxTimeDifference {
|
||||||
|
return nil, fmt.Errorf("heartbeat is too old or too far into the future")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GuardianAddr != signerAddr.String() {
|
||||||
|
return nil, fmt.Errorf("GuardianAddr in heartbeat does not match signerAddr")
|
||||||
|
}
|
||||||
|
|
||||||
// Store verified heartbeat in global guardian set state.
|
// Store verified heartbeat in global guardian set state.
|
||||||
if err := gst.SetHeartbeat(signerAddr, from, &h); err != nil {
|
if err := gst.SetHeartbeat(signerAddr, from, &h); err != nil {
|
||||||
return nil, fmt.Errorf("failed to store in guardian set state: %w", err)
|
return nil, fmt.Errorf("failed to store in guardian set state: %w", err)
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package p2p
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
node_common "github.com/certusone/wormhole/node/pkg/common"
|
||||||
|
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignedHeartbeat(t *testing.T) {
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
timestamp int64
|
||||||
|
gk *ecdsa.PrivateKey
|
||||||
|
heartbeatGuardianAddr string
|
||||||
|
expectSuccess bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// define the tests
|
||||||
|
|
||||||
|
gk, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
gAddr := ethcrypto.PubkeyToAddress(gk.PublicKey)
|
||||||
|
|
||||||
|
gk2, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
gAddr2 := ethcrypto.PubkeyToAddress(gk2.PublicKey)
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
// happy case
|
||||||
|
{
|
||||||
|
timestamp: time.Now().UnixNano(),
|
||||||
|
gk: gk,
|
||||||
|
heartbeatGuardianAddr: gAddr.String(),
|
||||||
|
expectSuccess: true,
|
||||||
|
},
|
||||||
|
// guardian signed a heartbeat for another guardian
|
||||||
|
{
|
||||||
|
timestamp: time.Now().UnixNano(),
|
||||||
|
gk: gk,
|
||||||
|
heartbeatGuardianAddr: gAddr2.String(),
|
||||||
|
expectSuccess: false,
|
||||||
|
},
|
||||||
|
// old heartbeat
|
||||||
|
{
|
||||||
|
timestamp: time.Now().Add(-time.Hour).UnixNano(),
|
||||||
|
gk: gk,
|
||||||
|
heartbeatGuardianAddr: gAddr2.String(),
|
||||||
|
expectSuccess: false,
|
||||||
|
},
|
||||||
|
// heartbeat from the distant future
|
||||||
|
{
|
||||||
|
timestamp: time.Now().Add(time.Hour).UnixNano(),
|
||||||
|
gk: gk,
|
||||||
|
heartbeatGuardianAddr: gAddr2.String(),
|
||||||
|
expectSuccess: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// run the tests
|
||||||
|
|
||||||
|
testFunc := func(t *testing.T, tc testCase) {
|
||||||
|
|
||||||
|
addr := ethcrypto.PubkeyToAddress(gk.PublicKey)
|
||||||
|
|
||||||
|
heartbeat := &gossipv1.Heartbeat{
|
||||||
|
NodeName: "someNode",
|
||||||
|
Counter: 1,
|
||||||
|
Timestamp: tc.timestamp,
|
||||||
|
Networks: []*gossipv1.Heartbeat_Network{},
|
||||||
|
Version: "0.0.1beta",
|
||||||
|
GuardianAddr: tc.heartbeatGuardianAddr,
|
||||||
|
BootTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano(),
|
||||||
|
Features: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := createSignedHeartbeat(gk, heartbeat)
|
||||||
|
gs := &node_common.GuardianSet{
|
||||||
|
Keys: []common.Address{addr},
|
||||||
|
Index: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
gst := node_common.NewGuardianSetState(nil)
|
||||||
|
|
||||||
|
heartbeatResult, err := processSignedHeartbeat("someone", s, gs, gst, false)
|
||||||
|
|
||||||
|
if tc.expectSuccess {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, heartbeat.GuardianAddr, heartbeatResult.GuardianAddr)
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
testFunc(t, tc)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue