diff --git a/go.mod b/go.mod index 8dbbb46d25141f6bda1e7c9181a6c7bebb706de4..d2f4b1558b4aa545b6fbbabb9a3199cab8cbc302 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gobuffalo/packr/v2 v2.7.1 github.com/golang/protobuf v1.3.5 github.com/gorilla/websocket v1.4.1 - github.com/iotaledger/hive.go v0.0.0-20200508125657-76ee9eb66cf8 + github.com/iotaledger/hive.go v0.0.0-20200513180357-f0ac8c45b754 github.com/iotaledger/iota.go v1.0.0-beta.14 github.com/labstack/echo v3.3.10+incompatible github.com/labstack/gommon v0.3.0 diff --git a/go.sum b/go.sum index c39e6d8c0a5f34536257b79ccff6d35658cc0f30..c57dca7cfee03667adf45960ce5a378182dfb07a 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/iotaledger/hive.go v0.0.0-20200508125657-76ee9eb66cf8 h1:b2CAofhKmJDLkPC9ul88KXa3BU5zRHTZp3TOPbIzSsg= github.com/iotaledger/hive.go v0.0.0-20200508125657-76ee9eb66cf8/go.mod h1:HgYsLMzyQV+eaiUrxa1c7qvH9Jwi2ncycqtlw+Lczhs= +github.com/iotaledger/hive.go v0.0.0-20200513180357-f0ac8c45b754 h1:UCyAisLvAuKIWf2bMz+iYYgjGdHS7H4W2wMTpWg9yl8= +github.com/iotaledger/hive.go v0.0.0-20200513180357-f0ac8c45b754/go.mod h1:HgYsLMzyQV+eaiUrxa1c7qvH9Jwi2ncycqtlw+Lczhs= github.com/iotaledger/iota.go v1.0.0-beta.9/go.mod h1:F6WBmYd98mVjAmmPVYhnxg8NNIWCjjH8VWT9qvv3Rc8= github.com/iotaledger/iota.go v1.0.0-beta.14 h1:Oeb28MfBuJEeXcGrLhTCJFtbsnc8y1u7xidsAmiOD5A= github.com/iotaledger/iota.go v1.0.0-beta.14/go.mod h1:F6WBmYd98mVjAmmPVYhnxg8NNIWCjjH8VWT9qvv3Rc8= diff --git a/plugins/analysis/client/plugin.go b/plugins/analysis/client/plugin.go index e434a4627b61ba85fc847a92fca10242f00dfe65..678cb063c7fd9a0263df581d928a4ccabe6e64de 100644 --- a/plugins/analysis/client/plugin.go +++ b/plugins/analysis/client/plugin.go @@ -7,12 +7,15 @@ import ( "sync" "time" + "github.com/iotaledger/goshimmer/dapps/fpctest" "github.com/iotaledger/goshimmer/packages/shutdown" + "github.com/iotaledger/goshimmer/packages/vote" "github.com/iotaledger/goshimmer/plugins/analysis/packet" "github.com/iotaledger/goshimmer/plugins/autopeering" "github.com/iotaledger/goshimmer/plugins/autopeering/local" "github.com/iotaledger/goshimmer/plugins/config" "github.com/iotaledger/hive.go/daemon" + "github.com/iotaledger/hive.go/events" "github.com/iotaledger/hive.go/logger" "github.com/iotaledger/hive.go/network" "github.com/iotaledger/hive.go/node" @@ -42,6 +45,10 @@ var ( func run(_ *node.Plugin) { log = logger.NewLogger(PluginName) if err := daemon.BackgroundWorker(PluginName, func(shutdownSignal <-chan struct{}) { + + fpctest.Voter().Events().RoundExecuted.Attach(events.NewClosure(onRoundExecuted)) + defer fpctest.Voter().Events().RoundExecuted.Detach(events.NewClosure(onRoundExecuted)) + ticker := time.NewTicker(reportIntervalSec * time.Second) defer ticker.Stop() for { @@ -134,3 +141,38 @@ func reportHeartbeat(dispatchers *EventDispatchers) { hb := &packet.Heartbeat{OwnID: nodeID, OutboundIDs: outboundIDs, InboundIDs: inboundIDs} dispatchers.Heartbeat(hb) } + +func onRoundExecuted(roundStats *vote.RoundStats) { + // get own ID + var nodeID []byte + if local.GetInstance() != nil { + // doesn't copy the ID, take care not to modify underlying bytearray! + nodeID = local.GetInstance().ID().Bytes() + } + + hb := &packet.FPCHeartbeat{ + OwnID: nodeID, + RoundStats: *roundStats, + } + + data, err := packet.NewFPCHeartbeatMessage(hb) + if err != nil { + log.Info(err, " - FPC heartbeat message skipped") + return + } + + conn, err := net.Dial("tcp", config.Node.GetString(CfgServerAddress)) + if err != nil { + log.Debugf("Could not connect to reporting server: %s", err.Error()) + return + } + + managedConn := network.NewManagedConnection(conn) + + connLock.Lock() + defer connLock.Unlock() + if _, err = managedConn.Write(data); err != nil { + log.Debugw("Error while writing to connection", "Description", err) + return + } +} diff --git a/plugins/analysis/packet/fpc_heartbeat.go b/plugins/analysis/packet/fpc_heartbeat.go new file mode 100644 index 0000000000000000000000000000000000000000..7801e05e723fd9f3e6b7ae98a285213f1ee2d9ce --- /dev/null +++ b/plugins/analysis/packet/fpc_heartbeat.go @@ -0,0 +1,91 @@ +package packet + +import ( + "bytes" + "encoding/binary" + "encoding/gob" + "errors" + + "github.com/iotaledger/goshimmer/packages/vote" + "github.com/iotaledger/hive.go/protocol/message" + "github.com/iotaledger/hive.go/protocol/tlv" +) + +var ( + // ErrInvalidFPCHeartbeat is returned for invalid FPC heartbeats. + ErrInvalidFPCHeartbeat = errors.New("invalid FPC heartbeat") +) + +var ( + // HeartbeatMessageDefinition defines a heartbeat message's format. + FPCHeartbeatMessageDefinition = &message.Definition{ + ID: MessageTypeFPCHeartbeat, + MaxBytesLength: 65535, + VariableLength: true, + } +) + +// Heartbeat represents a heartbeat packet. +type FPCHeartbeat struct { + // The ID of the node who sent the heartbeat. + // Must be contained when a heartbeat is serialized. + OwnID []byte + // RoundStats contains stats about an FPC round. + RoundStats vote.RoundStats + // Finalized contains the finalized conflicts within the last FPC round. + Finalized map[string]vote.Opinion +} + +// ParseFPCHeartbeat parses a slice of bytes (serialized packet) into a FPC heartbeat. +func ParseFPCHeartbeat(data []byte) (*FPCHeartbeat, error) { + hb := &FPCHeartbeat{} + + buf := new(bytes.Buffer) + _, err := buf.Write(data) + if err != nil { + return nil, err + } + + decoder := gob.NewDecoder(buf) + err = decoder.Decode(hb) + if err != nil { + return nil, err + } + + return hb, nil +} + +func (hb FPCHeartbeat) Bytes() ([]byte, error) { + buf := new(bytes.Buffer) + encoder := gob.NewEncoder(buf) + err := encoder.Encode(hb) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// NewHeartbeatMessage serializes the given heartbeat into a byte slice and adds a tlv header to the packet. +// message = tlv header + serialized packet +func NewFPCHeartbeatMessage(hb *FPCHeartbeat) ([]byte, error) { + packet, err := hb.Bytes() + if err != nil { + return nil, err + } + + // calculate total needed bytes based on packet + packetSize := len(packet) + + // create a buffer for tlv header plus the packet + buf := bytes.NewBuffer(make([]byte, 0, tlv.HeaderMessageDefinition.MaxBytesLength+uint16(packetSize))) + // write tlv header into buffer + if err := tlv.WriteHeader(buf, MessageTypeFPCHeartbeat, uint16(packetSize)); err != nil { + return nil, err + } + // write serialized packet bytes into the buffer + if err := binary.Write(buf, binary.BigEndian, packet); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/plugins/analysis/packet/fpc_heartbeat_test.go b/plugins/analysis/packet/fpc_heartbeat_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6ce656b61d42176d030f303445d03d6ff62ad338 --- /dev/null +++ b/plugins/analysis/packet/fpc_heartbeat_test.go @@ -0,0 +1,59 @@ +package packet + +import ( + "crypto/sha256" + "testing" + "time" + + "github.com/iotaledger/goshimmer/packages/vote" + "github.com/iotaledger/hive.go/protocol/message" + "github.com/iotaledger/hive.go/protocol/tlv" + "github.com/stretchr/testify/require" +) + +var ownID = sha256.Sum256([]byte{'A'}) + +func dummyFPCHeartbeat() *FPCHeartbeat { + return &FPCHeartbeat{ + OwnID: ownID[:], + RoundStats: vote.RoundStats{ + Duration: time.Second, + RandUsed: 0.5, + ActiveVoteContexts: map[string]*vote.Context{ + "one": { + ID: "one", + Liked: 1., + Rounds: 3, + Opinions: []vote.Opinion{vote.Dislike, vote.Like, vote.Dislike}, + }}, + QueriedOpinions: []vote.QueriedOpinions{{ + OpinionGiverID: "nodeA", + Opinions: map[string]vote.Opinion{"one": vote.Like, "two": vote.Dislike}, + TimesCounted: 2, + }}, + }, + Finalized: map[string]vote.Opinion{"one": vote.Like, "two": vote.Dislike}, + } +} + +func TestFPCHeartbeat(t *testing.T) { + hb := dummyFPCHeartbeat() + + packet, err := hb.Bytes() + require.NoError(t, err) + + hbParsed, err := ParseFPCHeartbeat(packet) + require.NoError(t, err) + + require.Equal(t, hb, hbParsed) + + tlvHeaderLength := int(tlv.HeaderMessageDefinition.MaxBytesLength) + msg, err := NewFPCHeartbeatMessage(hb) + require.NoError(t, err) + + require.Equal(t, MessageTypeFPCHeartbeat, message.Type(msg[0])) + + hbParsed, err = ParseFPCHeartbeat(msg[tlvHeaderLength:]) + require.NoError(t, err) + require.Equal(t, hb, hbParsed) +} diff --git a/plugins/analysis/packet/heartbeat.go b/plugins/analysis/packet/heartbeat.go index 65b44f2f30ba4fdc350a0a12ef71a22360b942c1..41cfd39bdd3b87197a2f7b514ed327ec0a51fce0 100644 --- a/plugins/analysis/packet/heartbeat.go +++ b/plugins/analysis/packet/heartbeat.go @@ -29,16 +29,12 @@ const ( var ( // HeartbeatPacketMinSize is the minimum byte size of a heartbeat packet. - HeartbeatPacketMinSize = HeartbeatPacketPeerIDSize + HeartbeatPacketOutboundIDCountSize + HeartbeatPacketMinSize = HeartbeatPacketPeerIDSize + HeartbeatPacketOutboundIDCountSize // HeartbeatPacketMaxSize is the maximum size a heartbeat packet can have. HeartbeatPacketMaxSize = HeartbeatPacketPeerIDSize + HeartbeatPacketOutboundIDCountSize + HeartbeatMaxOutboundPeersCount*sha256.Size + HeartbeatMaxInboundPeersCount*sha256.Size ) -const ( - MessageTypeHeartbeat message.Type = 1 -) - var ( // HeartbeatMessageDefinition defines a heartbeat message's format. HeartbeatMessageDefinition = &message.Definition{ @@ -48,7 +44,6 @@ var ( } ) - // Heartbeat represents a heartbeat packet. type Heartbeat struct { // The ID of the node who sent the heartbeat. diff --git a/plugins/analysis/packet/packet.go b/plugins/analysis/packet/packet.go index 0475b6205b2a0a736c1a8ea6eefb348c73d76275..e9af131ac8ba2760607ec68d10f50e49fa0320db 100644 --- a/plugins/analysis/packet/packet.go +++ b/plugins/analysis/packet/packet.go @@ -16,14 +16,22 @@ var ( var AnalysisMsgRegistry *message.Registry func init() { - AnalysisMsgRegistry = message.NewRegistry() - // register tlv header type - if err := AnalysisMsgRegistry.RegisterType(tlv.MessageTypeHeader, tlv.HeaderMessageDefinition); err != nil { - panic(err) - } + AnalysisMsgRegistry = message.NewRegistry([]*message.Definition{ + tlv.HeaderMessageDefinition, + HeartbeatMessageDefinition, + FPCHeartbeatMessageDefinition, + }) + // // register tlv header type + // if err := AnalysisMsgRegistry.RegisterType(tlv.MessageTypeHeader, tlv.HeaderMessageDefinition); err != nil { + // panic(err) + // } - // analysis plugin specific types (msgType > 0) - if err := AnalysisMsgRegistry.RegisterType(MessageTypeHeartbeat, HeartbeatMessageDefinition); err != nil { - panic(err) - } -} \ No newline at end of file + // // analysis plugin specific types (msgType > 0) + // if err := AnalysisMsgRegistry.RegisterType(MessageTypeHeartbeat, HeartbeatMessageDefinition); err != nil { + // panic(err) + // } + + // if err := AnalysisMsgRegistry.RegisterType(MessageTypeFPCHeartbeat, FPCHeartbeatMessageDefinition); err != nil { + // panic(err) + // } +} diff --git a/plugins/analysis/packet/types.go b/plugins/analysis/packet/types.go new file mode 100644 index 0000000000000000000000000000000000000000..589f4eeab6f3b39df8e2950e52bdeee4afa446b5 --- /dev/null +++ b/plugins/analysis/packet/types.go @@ -0,0 +1,8 @@ +package packet + +import "github.com/iotaledger/hive.go/protocol/message" + +const ( + MessageTypeHeartbeat message.Type = iota + 1 + MessageTypeFPCHeartbeat +) diff --git a/plugins/analysis/server/events.go b/plugins/analysis/server/events.go index 985e685a2c5b81da50c047369f845c3653e683d4..95006ccd77a3f4098adf7142688252db08c5acf5 100644 --- a/plugins/analysis/server/events.go +++ b/plugins/analysis/server/events.go @@ -19,6 +19,8 @@ var Events = struct { Error *events.Event // Heartbeat triggers when an heartbeat has been received. Heartbeat *events.Event + // FPCHeartbeat triggers when an FPC heartbeat has been received. + FPCHeartbeat *events.Event }{ events.NewEvent(stringCaller), events.NewEvent(stringCaller), @@ -26,6 +28,7 @@ var Events = struct { events.NewEvent(stringStringCaller), events.NewEvent(errorCaller), events.NewEvent(heartbeatPacketCaller), + events.NewEvent(fpcHeartbeatPacketCaller), } func stringCaller(handler interface{}, params ...interface{}) { @@ -43,3 +46,7 @@ func errorCaller(handler interface{}, params ...interface{}) { func heartbeatPacketCaller(handler interface{}, params ...interface{}) { handler.(func(heartbeat *packet.Heartbeat))(params[0].(*packet.Heartbeat)) } + +func fpcHeartbeatPacketCaller(handler interface{}, params ...interface{}) { + handler.(func(hb *packet.FPCHeartbeat))(params[0].(*packet.FPCHeartbeat)) +} diff --git a/plugins/analysis/server/plugin.go b/plugins/analysis/server/plugin.go index ce93828929eb2b72932a037e68a281b2145d246c..685cf1ca2a88b3bbce5cbd6b0f1c3552e6829fa9 100644 --- a/plugins/analysis/server/plugin.go +++ b/plugins/analysis/server/plugin.go @@ -109,6 +109,9 @@ func wireUp(p *protocol.Protocol) { p.Events.Received[packet.MessageTypeHeartbeat].Attach(events.NewClosure(func(data []byte) { processHeartbeatPacket(data, p) })) + p.Events.Received[packet.MessageTypeFPCHeartbeat].Attach(events.NewClosure(func(data []byte) { + processFPCHeartbeatPacket(data, p) + })) } // processHeartbeatPacket parses the serialized data into a Heartbeat packet and triggers its event @@ -121,3 +124,14 @@ func processHeartbeatPacket(data []byte, p *protocol.Protocol) { } Events.Heartbeat.Trigger(heartbeatPacket) } + +// processHeartbeatPacket parses the serialized data into a Heartbeat packet and triggers its event +func processFPCHeartbeatPacket(data []byte, p *protocol.Protocol) { + hb, err := packet.ParseFPCHeartbeat(data) + if err != nil { + Events.Error.Trigger(err) + p.CloseConnection() + return + } + Events.FPCHeartbeat.Trigger(hb) +} diff --git a/plugins/analysis/webinterface/plugin.go b/plugins/analysis/webinterface/plugin.go index 4252674fce4639ca10243e7738092e8b18a8faca..e741b7a91d9649af6a892466860b209028954844 100644 --- a/plugins/analysis/webinterface/plugin.go +++ b/plugins/analysis/webinterface/plugin.go @@ -47,7 +47,7 @@ func configure(plugin *node.Plugin) { } engine.GET("/datastream", echo.WrapHandler(websocket.Handler(dataStream))) - configureEventsRecording(plugin) + configureEventsRecording() } func run(_ *node.Plugin) { diff --git a/plugins/analysis/webinterface/recorded_events.go b/plugins/analysis/webinterface/recorded_events.go index 538400b49503e35f2571cb1e97a0a9d3a7d79840..daa69054ffe232d9a7f1be582d154b31819f0255 100644 --- a/plugins/analysis/webinterface/recorded_events.go +++ b/plugins/analysis/webinterface/recorded_events.go @@ -11,7 +11,6 @@ import ( "github.com/iotaledger/goshimmer/plugins/analysis/server" "github.com/iotaledger/hive.go/daemon" "github.com/iotaledger/hive.go/events" - "github.com/iotaledger/hive.go/node" ) // the period in which we scan and delete old data. @@ -26,87 +25,98 @@ var ( ) // configures the event recording by attaching to the analysis server's heartbeat event. -func configureEventsRecording(plugin *node.Plugin) { - server.Events.Heartbeat.Attach(events.NewClosure(func(hb *packet.Heartbeat) { - var out strings.Builder - for _, value := range hb.OutboundIDs { - out.WriteString(hex.EncodeToString(value)) - } - var in strings.Builder - for _, value := range hb.InboundIDs { - in.WriteString(hex.EncodeToString(value)) - } - plugin.Node.Logger.Debugw( - "Heartbeat", - "nodeId", hex.EncodeToString(hb.OwnID), - "outboundIds", out.String(), - "inboundIds", in.String(), - ) - lock.Lock() - defer lock.Unlock() - - nodeIDString := hex.EncodeToString(hb.OwnID) - timestamp := time.Now() - - // when node is new, add to graph - if _, isAlready := nodes[nodeIDString]; !isAlready { - server.Events.AddNode.Trigger(nodeIDString) +func configureEventsRecording() { + server.Events.Heartbeat.Attach(events.NewClosure(onHeartbeatReceived)) + server.Events.FPCHeartbeat.Attach(events.NewClosure(onFPCHeartbeatReceived)) +} + +func onHeartbeatReceived(hb *packet.Heartbeat) { + var out strings.Builder + for _, value := range hb.OutboundIDs { + out.WriteString(hex.EncodeToString(value)) + } + var in strings.Builder + for _, value := range hb.InboundIDs { + in.WriteString(hex.EncodeToString(value)) + } + log.Debugw( + "Heartbeat", + "nodeId", hex.EncodeToString(hb.OwnID), + "outboundIds", out.String(), + "inboundIds", in.String(), + ) + lock.Lock() + defer lock.Unlock() + + nodeIDString := hex.EncodeToString(hb.OwnID) + timestamp := time.Now() + + // when node is new, add to graph + if _, isAlready := nodes[nodeIDString]; !isAlready { + server.Events.AddNode.Trigger(nodeIDString) + } + // save it + update timestamp + nodes[nodeIDString] = timestamp + + // outgoing neighbor links update + for _, outgoingNeighbor := range hb.OutboundIDs { + outgoingNeighborString := hex.EncodeToString(outgoingNeighbor) + // do we already know about this neighbor? + // if no, add it and set it online + if _, isAlready := nodes[outgoingNeighborString]; !isAlready { + // first time we see this particular node + server.Events.AddNode.Trigger(outgoingNeighborString) } - // save it + update timestamp - nodes[nodeIDString] = timestamp - - // outgoing neighbor links update - for _, outgoingNeighbor := range hb.OutboundIDs { - outgoingNeighborString := hex.EncodeToString(outgoingNeighbor) - // do we already know about this neighbor? - // if no, add it and set it online - if _, isAlready := nodes[outgoingNeighborString]; !isAlready { - // first time we see this particular node - server.Events.AddNode.Trigger(outgoingNeighborString) - } - // we have indirectly heard about the neighbor. - nodes[outgoingNeighborString] = timestamp + // we have indirectly heard about the neighbor. + nodes[outgoingNeighborString] = timestamp - // do we have any links already with src=nodeIdString? - if _, isAlready := links[nodeIDString]; !isAlready { - // nope, so we have to allocate an empty map to be nested in links for nodeIdString - links[nodeIDString] = make(map[string]time.Time) - } + // do we have any links already with src=nodeIdString? + if _, isAlready := links[nodeIDString]; !isAlready { + // nope, so we have to allocate an empty map to be nested in links for nodeIdString + links[nodeIDString] = make(map[string]time.Time) + } - // update graph when connection hasn't been seen before - if _, isAlready := links[nodeIDString][outgoingNeighborString]; !isAlready { - server.Events.ConnectNodes.Trigger(nodeIDString, outgoingNeighborString) - } - // update links - links[nodeIDString][outgoingNeighborString] = timestamp + // update graph when connection hasn't been seen before + if _, isAlready := links[nodeIDString][outgoingNeighborString]; !isAlready { + server.Events.ConnectNodes.Trigger(nodeIDString, outgoingNeighborString) } + // update links + links[nodeIDString][outgoingNeighborString] = timestamp + } - // incoming neighbor links update - for _, incomingNeighbor := range hb.InboundIDs { - incomingNeighborString := hex.EncodeToString(incomingNeighbor) - // do we already know about this neighbor? - // if no, add it and set it online - if _, isAlready := nodes[incomingNeighborString]; !isAlready { - // First time we see this particular node - server.Events.AddNode.Trigger(incomingNeighborString) - } - // we have indirectly heard about the neighbor. - nodes[incomingNeighborString] = timestamp + // incoming neighbor links update + for _, incomingNeighbor := range hb.InboundIDs { + incomingNeighborString := hex.EncodeToString(incomingNeighbor) + // do we already know about this neighbor? + // if no, add it and set it online + if _, isAlready := nodes[incomingNeighborString]; !isAlready { + // First time we see this particular node + server.Events.AddNode.Trigger(incomingNeighborString) + } + // we have indirectly heard about the neighbor. + nodes[incomingNeighborString] = timestamp - // do we have any links already with src=incomingNeighborString? - if _, isAlready := links[incomingNeighborString]; !isAlready { - // nope, so we have to allocate an empty map to be nested in links for incomingNeighborString - links[incomingNeighborString] = make(map[string]time.Time) - } + // do we have any links already with src=incomingNeighborString? + if _, isAlready := links[incomingNeighborString]; !isAlready { + // nope, so we have to allocate an empty map to be nested in links for incomingNeighborString + links[incomingNeighborString] = make(map[string]time.Time) + } - // update graph when connection hasn't been seen before - if _, isAlready := links[incomingNeighborString][nodeIDString]; !isAlready { - server.Events.ConnectNodes.Trigger(incomingNeighborString, nodeIDString) - } - // update links map - links[incomingNeighborString][nodeIDString] = timestamp + // update graph when connection hasn't been seen before + if _, isAlready := links[incomingNeighborString][nodeIDString]; !isAlready { + server.Events.ConnectNodes.Trigger(incomingNeighborString, nodeIDString) } - })) + // update links map + links[incomingNeighborString][nodeIDString] = timestamp + } +} + +func onFPCHeartbeatReceived(hb *packet.FPCHeartbeat) { + log.Infow( + "FPCHeartbeat", + "nodeId", hex.EncodeToString(hb.OwnID), + "ActiveVoteContext", hb.RoundStats.ActiveVoteContexts, + ) } func runEventsRecordManager() {