code.vegaprotocol.io/vega@v0.79.0/core/validators/heartbeat_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package validators
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"strconv"
    22  	"testing"
    23  	"time"
    24  
    25  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    26  
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  func TestRecordHeartbeatResult(t *testing.T) {
    31  	top := getHBTestTopology(t)
    32  	tracker := top.validators["node1"].heartbeatTracker
    33  
    34  	for i := 0; i < 100; i++ {
    35  		if i%2 == 0 {
    36  			tracker.recordHeartbeatResult(true)
    37  			require.Equal(t, true, tracker.blockSigs[i%10])
    38  		} else {
    39  			tracker.recordHeartbeatResult(false)
    40  			require.Equal(t, false, tracker.blockSigs[i%10])
    41  		}
    42  		require.Equal(t, "", tracker.expectedNextHash)
    43  		require.Equal(t, time.Time{}, tracker.expectedNexthashSince)
    44  	}
    45  }
    46  
    47  func TestCheckAndExpireStaleHeartbeats(t *testing.T) {
    48  	top := getHBTestTopology(t)
    49  	top.epochSeq = 0
    50  	tt := &dummyTestTime{}
    51  	top.timeService = tt
    52  
    53  	now := time.Now()
    54  	nowPlus500 := now.Add(500 * time.Second)
    55  	tt.now = now
    56  
    57  	// no next hash - means we're not awaiting a heartbeat, nothing expired
    58  	top.checkAndExpireStaleHeartbeats()
    59  	require.Equal(t, 0, top.validators["node1"].heartbeatTracker.blockIndex)
    60  
    61  	top.validators["node1"].heartbeatTracker.expectedNextHash = "abcde"
    62  	top.validators["node1"].heartbeatTracker.expectedNexthashSince = now
    63  	top.checkAndExpireStaleHeartbeats()
    64  	require.Equal(t, 0, top.validators["node1"].heartbeatTracker.blockIndex)
    65  
    66  	// still not enough time passed
    67  	tt.now = nowPlus500
    68  	top.validators["node1"].heartbeatTracker.expectedNextHash = "abcde"
    69  	top.validators["node1"].heartbeatTracker.expectedNexthashSince = now
    70  	top.checkAndExpireStaleHeartbeats()
    71  	require.Equal(t, 0, top.validators["node1"].heartbeatTracker.blockIndex)
    72  
    73  	// enough time passed - expect invalidation
    74  	tt.now = nowPlus500.Add(1 * time.Second)
    75  	top.validators["node1"].heartbeatTracker.expectedNextHash = "abcde"
    76  	top.validators["node1"].heartbeatTracker.expectedNexthashSince = now
    77  	top.checkAndExpireStaleHeartbeats()
    78  	require.Equal(t, 1, top.validators["node1"].heartbeatTracker.blockIndex)
    79  	require.Equal(t, "", top.validators["node1"].heartbeatTracker.expectedNextHash)
    80  
    81  	// set up a new expected heartbeat and pretend the old one arrived again
    82  	cmd := &commandspb.ValidatorHeartbeat{
    83  		NodeId:  "node1",
    84  		Message: "abcde",
    85  	}
    86  	top.validators["node1"].heartbeatTracker.expectedNextHash = "abcdeagain"
    87  	top.validators["node1"].heartbeatTracker.expectedNexthashSince = now
    88  	require.Error(t, ErrHeartbeatHasExpired, top.ProcessValidatorHeartbeat(context.Background(), cmd, nil, nil))
    89  	require.Equal(t, "abcdeagain", top.validators["node1"].heartbeatTracker.expectedNextHash)
    90  }
    91  
    92  func TestProcessValidatorHeartbeat(t *testing.T) {
    93  	topology := &Topology{}
    94  	topology.validators = map[string]*valState{}
    95  	cmd := &commandspb.ValidatorHeartbeat{}
    96  	cmd.NodeId = "node1"
    97  
    98  	// invalid node ID
    99  	require.Error(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, nil, nil))
   100  
   101  	topology.validators["node1"] = &valState{
   102  		heartbeatTracker: &validatorHeartbeatTracker{},
   103  	}
   104  
   105  	for i := 0; i < 10; i++ {
   106  		topology.validators["node1"].heartbeatTracker.blockSigs[i] = true
   107  	}
   108  
   109  	// undecodable signature
   110  	cmd.VegaSignature = &commandspb.Signature{
   111  		Value: "haha",
   112  	}
   113  
   114  	require.Error(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, nil, nil))
   115  	require.Equal(t, 1, topology.validators["node1"].heartbeatTracker.blockIndex)
   116  	require.Equal(t, false, topology.validators["node1"].heartbeatTracker.blockSigs[0])
   117  
   118  	cmd.VegaSignature.Value = "abcdef"
   119  
   120  	topology.validators["node1"].data.VegaPubKey = "fooo"
   121  	require.Error(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, nil, nil))
   122  	require.Equal(t, 2, topology.validators["node1"].heartbeatTracker.blockIndex)
   123  	require.Equal(t, false, topology.validators["node1"].heartbeatTracker.blockSigs[1])
   124  
   125  	topology.validators["node1"].data.VegaPubKey = "eeee"
   126  	rejectVega := func(message, signature, pubkey []byte) error { return errors.New("unverifiable vega signature") }
   127  	rejectEth := func(message, signature []byte, hexAddress string) error {
   128  		return errors.New("unverifiable eth signature")
   129  	}
   130  	acceptVega := func(message, signature, pubkey []byte) error { return nil }
   131  	acceptEth := func(message, signature []byte, hexAddress string) error { return nil }
   132  
   133  	// unverifiable vega signature
   134  	require.Error(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, rejectVega, nil))
   135  	require.Equal(t, 3, topology.validators["node1"].heartbeatTracker.blockIndex)
   136  	require.Equal(t, false, topology.validators["node1"].heartbeatTracker.blockSigs[2])
   137  
   138  	// undecodable eth signature
   139  	cmd.EthereumSignature = &commandspb.Signature{
   140  		Value: "haha",
   141  	}
   142  
   143  	require.Error(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, acceptVega, nil))
   144  	require.Equal(t, 4, topology.validators["node1"].heartbeatTracker.blockIndex)
   145  	require.Equal(t, false, topology.validators["node1"].heartbeatTracker.blockSigs[3])
   146  
   147  	// rejected eth signature
   148  	cmd.EthereumSignature.Value = "ffff"
   149  	require.Error(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, acceptVega, rejectEth))
   150  	require.Equal(t, 5, topology.validators["node1"].heartbeatTracker.blockIndex)
   151  	require.Equal(t, false, topology.validators["node1"].heartbeatTracker.blockSigs[4])
   152  
   153  	// accepted eth signature
   154  	require.NoError(t, topology.ProcessValidatorHeartbeat(context.Background(), cmd, acceptVega, acceptEth))
   155  	require.Equal(t, 6, topology.validators["node1"].heartbeatTracker.blockIndex)
   156  	require.Equal(t, true, topology.validators["node1"].heartbeatTracker.blockSigs[5])
   157  }
   158  
   159  func TestGetNodeRequiringHB(t *testing.T) {
   160  	top := getHBTestTopology(t)
   161  	now := time.Now()
   162  	top.epochSeq = 1
   163  
   164  	tt := &dummyTestTime{}
   165  	top.timeService = tt
   166  	// initialise all to be not require resend for now
   167  	for _, vs := range top.validators {
   168  		vs.heartbeatTracker.expectedNexthashSince = now.Add(500 * time.Second)
   169  	}
   170  
   171  	top.validators["node1"].heartbeatTracker.expectedNexthashSince = now
   172  	top.validators["node2"].heartbeatTracker.expectedNexthashSince = now.Add(-200 * time.Second)
   173  	top.validators["node3"].data.FromEpoch = 2
   174  	top.validators["node3"].heartbeatTracker.expectedNexthashSince = now.Add(-300 * time.Second)
   175  
   176  	tt.now = now
   177  	res := top.getNodesRequiringHB()
   178  	require.Equal(t, 0, len(res))
   179  
   180  	// move time by 801 seconds
   181  	tt.now = now.Add(801 * time.Second)
   182  	res = top.getNodesRequiringHB()
   183  	require.Equal(t, 1, len(res))
   184  	require.Equal(t, "node2", res[0])
   185  
   186  	top.epochSeq = 2
   187  	res = top.getNodesRequiringHB()
   188  	require.Equal(t, 2, len(res))
   189  	require.Equal(t, "node2", res[0])
   190  	require.Equal(t, "node3", res[1])
   191  
   192  	// move time by 200 seconds
   193  	tt.now = now.Add(1001 * time.Second)
   194  	res = top.getNodesRequiringHB()
   195  	require.Equal(t, 3, len(res))
   196  	require.Equal(t, "node1", res[0])
   197  	require.Equal(t, "node2", res[1])
   198  	require.Equal(t, "node3", res[2])
   199  }
   200  
   201  func getHBTestTopology(t *testing.T) *Topology {
   202  	t.Helper()
   203  
   204  	topology := &Topology{}
   205  	topology.validators = map[string]*valState{}
   206  	topology.OnEpochLengthUpdate(context.Background(), 100000*time.Second)
   207  	for i := 0; i < 13; i++ {
   208  		index := strconv.Itoa(i)
   209  		topology.validators["node"+index] = &valState{
   210  			data: ValidatorData{
   211  				ID:        "node" + index,
   212  				FromEpoch: 1,
   213  			},
   214  			heartbeatTracker: &validatorHeartbeatTracker{},
   215  		}
   216  	}
   217  	return topology
   218  }
   219  
   220  type dummyTestTime struct {
   221  	now time.Time
   222  }
   223  
   224  func (tt *dummyTestTime) GetTimeNow() time.Time {
   225  	return tt.now
   226  }