code.vegaprotocol.io/vega@v0.79.0/core/validators/topology.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 "encoding/hex" 21 "errors" 22 "fmt" 23 "math" 24 "math/rand" 25 "sort" 26 "sync" 27 "time" 28 29 "code.vegaprotocol.io/vega/core/events" 30 "code.vegaprotocol.io/vega/core/types" 31 "code.vegaprotocol.io/vega/libs/crypto" 32 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 33 "code.vegaprotocol.io/vega/libs/num" 34 "code.vegaprotocol.io/vega/logging" 35 proto "code.vegaprotocol.io/vega/protos/vega" 36 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 37 v1 "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 38 39 abcitypes "github.com/cometbft/cometbft/abci/types" 40 "github.com/ethereum/go-ethereum/common" 41 "golang.org/x/exp/maps" 42 ) 43 44 var ( 45 ErrVegaNodeAlreadyRegisterForChain = errors.New("a vega node is already registered with the blockchain node") 46 ErrIssueSignaturesUnexpectedKind = errors.New("unexpected node-signature kind") 47 ) 48 49 // Broker needs no mocks. 50 type Broker interface { 51 Send(event events.Event) 52 SendBatch(events []events.Event) 53 } 54 55 type Wallet interface { 56 PubKey() crypto.PublicKey 57 ID() crypto.PublicKey 58 Signer 59 } 60 61 type MultiSigTopology interface { 62 IsSigner(address string) bool 63 ExcessSigners(addresses []string) bool 64 GetSigners() []string 65 GetThreshold() uint32 66 ChainID() string 67 } 68 69 type ValidatorPerformance interface { 70 ValidatorPerformanceScore(address string, votingPower, totalPower int64, performanceScalingFactor num.Decimal) num.Decimal 71 BeginBlock(ctx context.Context, proposer string) 72 Serialize() *v1.ValidatorPerformance 73 Deserialize(*v1.ValidatorPerformance) 74 Reset() 75 } 76 77 // Notary ... 78 type Notary interface { 79 StartAggregate(resID string, kind types.NodeSignatureKind, signature []byte) 80 IsSigned(ctx context.Context, id string, kind types.NodeSignatureKind) ([]types.NodeSignature, bool) 81 OfferSignatures(kind types.NodeSignatureKind, f func(resources string) []byte) 82 } 83 84 type ValidatorData struct { 85 ID string `json:"id"` 86 VegaPubKey string `json:"vega_pub_key"` 87 VegaPubKeyIndex uint32 `json:"vega_pub_key_index"` 88 EthereumAddress string `json:"ethereum_address"` 89 TmPubKey string `json:"tm_pub_key"` 90 InfoURL string `json:"info_url"` 91 Country string `json:"country"` 92 Name string `json:"name"` 93 AvatarURL string `json:"avatar_url"` 94 FromEpoch uint64 `json:"from_epoch"` 95 SubmitterAddress string `json:"submitter_address"` 96 } 97 98 func (v ValidatorData) IsValid() bool { 99 if len(v.ID) <= 0 || len(v.VegaPubKey) <= 0 || 100 len(v.EthereumAddress) <= 0 || len(v.TmPubKey) <= 0 { 101 return false 102 } 103 return true 104 } 105 106 // HashVegaPubKey returns hash VegaPubKey encoded as hex string. 107 func (v ValidatorData) HashVegaPubKey() (string, error) { 108 decoded, err := hex.DecodeString(v.VegaPubKey) 109 if err != nil { 110 return "", fmt.Errorf("couldn't decode public key: %w", err) 111 } 112 113 return hex.EncodeToString(vgcrypto.Hash(decoded)), nil 114 } 115 116 // ValidatorMapping maps a tendermint pubkey with a vega pubkey. 117 type ValidatorMapping map[string]ValidatorData 118 119 type validators map[string]*valState 120 121 type Topology struct { 122 log *logging.Logger 123 cfg Config 124 wallets NodeWallets 125 broker Broker 126 timeService TimeService 127 validatorPerformance ValidatorPerformance 128 primaryMultisig MultiSigTopology 129 secondaryMultisig MultiSigTopology 130 131 // vega pubkey to validator data 132 validators validators 133 134 chainValidators []string 135 136 // this is the runtime information 137 // has the validator been added to the validator set 138 isValidator bool 139 140 // this is about the node setup, 141 // is the node configured to be a validator 142 isValidatorSetup bool 143 144 // Vega key rotations 145 pendingPubKeyRotations pendingKeyRotationMapping 146 pubKeyChangeListeners []func(ctx context.Context, oldPubKey, newPubKey string) 147 148 // Ethereum key rotations 149 // pending are those lined up to happen in a future block, unresolved are ones 150 // that have happened but we are waiting to see the old key has been removed from the contract 151 pendingEthKeyRotations pendingEthereumKeyRotationMapping 152 unresolvedEthKeyRotations map[string]PendingEthereumKeyRotation 153 154 mu sync.RWMutex 155 156 tss *topologySnapshotState 157 158 rng *rand.Rand // random generator seeded by block 159 currentBlockHeight uint64 160 161 // net params 162 numberOfTendermintValidators int 163 numberOfErsatzValidators int 164 validatorIncumbentBonusFactor num.Decimal 165 ersatzValidatorsFactor num.Decimal 166 minimumStake *num.Uint 167 minimumEthereumEventsForNewValidator uint64 168 numberEthMultisigSigners int 169 170 // transient data for updating tendermint on validator voting power changes. 171 validatorPowerUpdates []abcitypes.ValidatorUpdate 172 epochSeq uint64 173 newEpochStarted bool 174 175 cmd Commander 176 checkpointLoaded bool 177 notary Notary 178 signatures Signatures 179 180 // validator heartbeat parameters 181 blocksToKeepMalperforming int64 182 timeBetweenHeartbeats time.Duration 183 timeToSendHeartbeat time.Duration 184 185 performanceScalingFactor num.Decimal 186 } 187 188 func (t *Topology) OnEpochEvent(ctx context.Context, epoch types.Epoch) { 189 t.epochSeq = epoch.Seq 190 if epoch.Action == proto.EpochAction_EPOCH_ACTION_START { 191 t.newEpochStarted = true 192 // this is needed because when we load a checkpoint on genesis t.rng is not initialised as it's done before calling beginBlock 193 // so we need to initialise the rng to something. 194 if t.rng == nil { 195 t.rng = rand.New(rand.NewSource(epoch.StartTime.Unix())) 196 } 197 } 198 // this is a workaround to the topology loaded from checkpoint before the epoch. 199 if t.checkpointLoaded { 200 evts := make([]events.Event, 0, len(t.validators)) 201 seq := num.NewUint(t.epochSeq).String() 202 t.checkpointLoaded = false 203 nodeIDs := make([]string, 0, len(t.validators)) 204 for k := range t.validators { 205 nodeIDs = append(nodeIDs, k) 206 } 207 sort.Strings(nodeIDs) 208 for _, nid := range nodeIDs { 209 node := t.validators[nid] 210 if node.rankingScore == nil { 211 continue 212 } 213 evts = append(evts, events.NewValidatorRanking(ctx, seq, node.data.ID, node.rankingScore.StakeScore, node.rankingScore.PerformanceScore, node.rankingScore.RankingScore, protoStatusToString(node.rankingScore.PreviousStatus), protoStatusToString(node.rankingScore.Status), int(node.rankingScore.VotingPower))) 214 } 215 // send ranking events for all loaded validators so data node knows the current ranking 216 t.broker.SendBatch(evts) 217 } 218 } 219 220 func NewTopology( 221 log *logging.Logger, 222 cfg Config, 223 wallets NodeWallets, 224 broker Broker, 225 isValidatorSetup bool, 226 cmd Commander, 227 primaryMultisig MultiSigTopology, 228 secondaryMultisig MultiSigTopology, 229 timeService TimeService, 230 ) *Topology { 231 log = log.Named(namedLogger) 232 log.SetLevel(cfg.Level.Get()) 233 234 t := &Topology{ 235 log: log, 236 cfg: cfg, 237 wallets: wallets, 238 broker: broker, 239 timeService: timeService, 240 validators: map[string]*valState{}, 241 chainValidators: []string{}, 242 tss: &topologySnapshotState{}, 243 pendingPubKeyRotations: pendingKeyRotationMapping{}, 244 pendingEthKeyRotations: pendingEthereumKeyRotationMapping{}, 245 unresolvedEthKeyRotations: map[string]PendingEthereumKeyRotation{}, 246 isValidatorSetup: isValidatorSetup, 247 validatorPerformance: NewValidatorPerformance(log), 248 validatorIncumbentBonusFactor: num.DecimalZero(), 249 ersatzValidatorsFactor: num.DecimalZero(), 250 primaryMultisig: primaryMultisig, 251 secondaryMultisig: secondaryMultisig, 252 cmd: cmd, 253 signatures: &noopSignatures{log}, 254 } 255 256 return t 257 } 258 259 // OnEpochLengthUpdate updates the duration of an epoch - which is used to calculate the number of blocks to keep a malperforming validators. 260 // The number of blocks is calculated as 10 epochs x duration of epoch in seconds, assuming block time is 1s. 261 func (t *Topology) OnEpochLengthUpdate(ctx context.Context, l time.Duration) error { 262 t.blocksToKeepMalperforming = int64(10 * l.Seconds()) 263 // set time between hearbeats to 1% of the epoch duration in seconds as blocks 264 // e.g. if epoch is 1 day = 86400 seconds (blocks) then time between hb becomes 864 265 // if epoch is 300 seconds then blocks becomes 50 (lower bound applied). 266 blocks := int64(math.Max(l.Seconds()*0.01, 50.0)) 267 t.timeBetweenHeartbeats = time.Duration(blocks * int64(time.Second)) 268 t.timeToSendHeartbeat = time.Duration(blocks * int64(time.Second) / 2) 269 return nil 270 } 271 272 // SetNotary this is not good, the topology depends on the notary 273 // which in return also depends on the topology... Luckily they 274 // do not require recursive calls as for each calls are one offs... 275 // anyway we may want to extract the code requiring the notary somewhere 276 // else or have different pattern somehow... 277 func (t *Topology) SetNotary(notary Notary) { 278 t.signatures = NewSignatures(t.log, t.primaryMultisig, t.secondaryMultisig, notary, t.wallets, t.broker, t.isValidatorSetup) 279 t.notary = notary 280 } 281 282 // SetSignatures this is not good, same issue as for SetNotary method. 283 // This is only used as a helper for testing.. 284 func (t *Topology) SetSignatures(signatures Signatures) { 285 t.signatures = signatures 286 } 287 288 // SetIsValidator will set the flag for `self` so that it is considered a real validator 289 // for example, when a node has announced itself and is accepted as a PENDING validator. 290 func (t *Topology) SetIsValidator() { 291 t.isValidator = true 292 } 293 294 // ReloadConf updates the internal configuration. 295 func (t *Topology) ReloadConf(cfg Config) { 296 t.log.Info("reloading configuration") 297 if t.log.GetLevel() != cfg.Level.Get() { 298 t.log.Info("updating log level", 299 logging.String("old", t.log.GetLevel().String()), 300 logging.String("new", cfg.Level.String()), 301 ) 302 t.log.SetLevel(cfg.Level.Get()) 303 } 304 305 t.cfg = cfg 306 } 307 308 func (t *Topology) IsValidator() bool { 309 return t.isValidatorSetup && t.isValidator 310 } 311 312 // Len return the number of validators with status Tendermint, the only validators that matter. 313 func (t *Topology) Len() int { 314 t.mu.RLock() 315 defer t.mu.RUnlock() 316 317 count := 0 318 for _, v := range t.validators { 319 if v.status == ValidatorStatusTendermint { 320 count++ 321 } 322 } 323 return count 324 } 325 326 // Get returns validator data based on validator master public key. 327 func (t *Topology) Get(key string) *ValidatorData { 328 t.mu.RLock() 329 defer t.mu.RUnlock() 330 331 if data, ok := t.validators[key]; ok { 332 return &data.data 333 } 334 335 return nil 336 } 337 338 // AllVegaPubKeys returns all the validators vega public keys. 339 func (t *Topology) AllVegaPubKeys() []string { 340 t.mu.RLock() 341 defer t.mu.RUnlock() 342 keys := make([]string, 0, len(t.validators)) 343 for _, data := range t.validators { 344 keys = append(keys, data.data.VegaPubKey) 345 } 346 return keys 347 } 348 349 // AllNodeIDs returns all the validators node IDs keys. 350 func (t *Topology) AllNodeIDs() []string { 351 t.mu.RLock() 352 defer t.mu.RUnlock() 353 keys := make([]string, 0, len(t.validators)) 354 for k := range t.validators { 355 keys = append(keys, k) 356 } 357 return keys 358 } 359 360 func (t *Topology) SelfVegaPubKey() string { 361 if !t.isValidatorSetup { 362 return "" 363 } 364 return t.wallets.GetVega().PubKey().Hex() 365 } 366 367 func (t *Topology) SelfNodeID() string { 368 if !t.isValidatorSetup { 369 return "" 370 } 371 return t.wallets.GetVega().ID().Hex() 372 } 373 374 // IsValidatorNodeID takes a nodeID and returns true if the node is a validator node. 375 func (t *Topology) IsValidatorNodeID(nodeID string) bool { 376 t.mu.RLock() 377 defer t.mu.RUnlock() 378 _, ok := t.validators[nodeID] 379 return ok 380 } 381 382 // IsValidatorVegaPubKey returns true if the given key is a Vega validator public key. 383 func (t *Topology) IsValidatorVegaPubKey(pubkey string) (ok bool) { 384 defer func() { 385 if t.log.GetLevel() <= logging.DebugLevel { 386 s := "requested non-existing validator" 387 if ok { 388 s = "requested existing validator" 389 } 390 t.log.Debug(s, 391 logging.Strings("validators", t.AllVegaPubKeys()), 392 logging.String("pubkey", pubkey), 393 ) 394 } 395 }() 396 397 t.mu.RLock() 398 defer t.mu.RUnlock() 399 400 for _, data := range t.validators { 401 if data.data.VegaPubKey == pubkey { 402 return true 403 } 404 } 405 406 return false 407 } 408 409 func (t *Topology) IsSelfTendermintValidator() bool { 410 return t.IsTendermintValidator(t.SelfVegaPubKey()) 411 } 412 413 func (t *Topology) GetTotalVotingPower() int64 { 414 t.mu.RLock() 415 defer t.mu.RUnlock() 416 total := int64(0) 417 for _, data := range t.validators { 418 total += data.validatorPower 419 } 420 return total 421 } 422 423 func (t *Topology) GetVotingPower(pubkey string) int64 { 424 t.mu.RLock() 425 defer t.mu.RUnlock() 426 for _, data := range t.validators { 427 if data.data.VegaPubKey == pubkey && data.status == ValidatorStatusTendermint { 428 return data.validatorPower 429 } 430 } 431 432 return int64(0) 433 } 434 435 // IsValidatorVegaPubKey returns true if the given key is a Vega validator public key and the validators is of status Tendermint. 436 func (t *Topology) IsTendermintValidator(pubkey string) (ok bool) { 437 t.mu.RLock() 438 defer t.mu.RUnlock() 439 440 for _, data := range t.validators { 441 if data.data.VegaPubKey == pubkey && data.status == ValidatorStatusTendermint { 442 return true 443 } 444 } 445 446 return false 447 } 448 449 func (t *Topology) NumberOfTendermintValidators() uint { 450 t.mu.RLock() 451 defer t.mu.RUnlock() 452 453 count := uint(0) 454 for _, data := range t.validators { 455 if data.status == ValidatorStatusTendermint { 456 count++ 457 } 458 } 459 return count 460 } 461 462 func (t *Topology) BeginBlock(ctx context.Context, blockHeight uint64, proposer string) { 463 // we're not adding or removing nodes only potentially changing their state so should be safe 464 t.mu.RLock() 465 defer t.mu.RUnlock() 466 467 // resetting the seed every block, to both get some more unpredictability and still deterministic 468 // and play nicely with snapshot 469 currentTime := t.timeService.GetTimeNow() 470 t.rng = rand.New(rand.NewSource(currentTime.Unix())) 471 472 t.checkHeartbeat(ctx) 473 t.validatorPerformance.BeginBlock(ctx, proposer) 474 t.currentBlockHeight = blockHeight 475 476 t.signatures.SetNonce(currentTime) 477 t.signatures.ClearStaleSignatures() 478 t.signatures.OfferSignatures() 479 t.keyRotationBeginBlockLocked(ctx) 480 t.ethereumKeyRotationBeginBlockLocked(ctx) 481 } 482 483 // OnPerformanceScalingChanged updates the network parameter for performance scaling factor. 484 func (t *Topology) OnPerformanceScalingChanged(ctx context.Context, scalingFactor num.Decimal) error { 485 t.performanceScalingFactor = scalingFactor 486 return nil 487 } 488 489 func (t *Topology) AddNewNode(ctx context.Context, nr *commandspb.AnnounceNode, status ValidatorStatus) error { 490 // write lock! 491 t.mu.Lock() 492 defer t.mu.Unlock() 493 494 if _, ok := t.validators[nr.Id]; ok { 495 return ErrVegaNodeAlreadyRegisterForChain 496 } 497 498 data := ValidatorData{ 499 ID: nr.Id, 500 VegaPubKey: nr.VegaPubKey, 501 VegaPubKeyIndex: nr.VegaPubKeyIndex, 502 EthereumAddress: nr.EthereumAddress, 503 TmPubKey: nr.ChainPubKey, 504 InfoURL: nr.InfoUrl, 505 Country: nr.Country, 506 Name: nr.Name, 507 AvatarURL: nr.AvatarUrl, 508 FromEpoch: nr.FromEpoch, 509 SubmitterAddress: nr.SubmitterAddress, 510 } 511 512 // then add it to the topology 513 t.validators[nr.Id] = &valState{ 514 data: data, 515 status: status, 516 blockAdded: int64(t.currentBlockHeight), 517 statusChangeBlock: int64(t.currentBlockHeight), 518 lastBlockWithPositiveRanking: -1, 519 numberOfEthereumEventsForwarded: 0, 520 heartbeatTracker: &validatorHeartbeatTracker{}, 521 } 522 523 if status == ValidatorStatusTendermint { 524 t.validators[nr.Id].validatorPower = 10 525 } 526 527 rankingScoreStatus := statusToProtoStatus(ValidatorStatusToName[status]) 528 t.validators[nr.Id].rankingScore = &proto.RankingScore{ 529 StakeScore: "0", 530 PerformanceScore: "0", 531 RankingScore: "0", 532 Status: rankingScoreStatus, 533 PreviousStatus: statusToProtoStatus("pending"), 534 VotingPower: uint32(t.validators[nr.Id].validatorPower), 535 } 536 537 // Send event to notify core about new validator 538 t.sendValidatorUpdateEvent(ctx, data, true) 539 // Send an event to notify the new validator ranking 540 epochSeq := num.NewUint(t.epochSeq).String() 541 t.broker.Send(events.NewValidatorRanking(ctx, epochSeq, nr.Id, "0", "0", "0", "pending", ValidatorStatusToName[status], int(t.validators[nr.Id].validatorPower))) 542 t.log.Info("new node registration successful", 543 logging.String("id", nr.Id), 544 logging.String("vega-key", nr.VegaPubKey), 545 logging.String("eth-addr", nr.EthereumAddress), 546 logging.String("tm-key", nr.ChainPubKey)) 547 return nil 548 } 549 550 func (t *Topology) sendValidatorUpdateEvent(ctx context.Context, data ValidatorData, added bool) { 551 t.broker.Send(events.NewValidatorUpdateEvent( 552 ctx, 553 data.ID, 554 data.VegaPubKey, 555 data.VegaPubKeyIndex, 556 data.EthereumAddress, 557 data.TmPubKey, 558 data.InfoURL, 559 data.Country, 560 data.Name, 561 data.AvatarURL, 562 data.FromEpoch, 563 added, 564 t.epochSeq, 565 )) 566 } 567 568 func (t *Topology) LoadValidatorsOnGenesis(ctx context.Context, rawstate []byte) (err error) { 569 t.log.Debug("Entering validators.Topology.LoadValidatorsOnGenesis") 570 defer func() { 571 t.log.Debug("Leaving validators.Topology.LoadValidatorsOnGenesis without error") 572 if err != nil { 573 t.log.Debug("Failure in validators.Topology.LoadValidatorsOnGenesis", logging.Error(err)) 574 } 575 }() 576 577 state, err := LoadGenesisState(rawstate) 578 if err != nil { 579 return err 580 } 581 582 // tm is base64 encoded, vega is hex 583 keys := maps.Keys(state) 584 sort.Strings(keys) 585 for _, tm := range keys { 586 data := state[tm] 587 if !data.IsValid() { 588 return fmt.Errorf("missing required field from validator data: %#v", data) 589 } 590 591 // this node is started and expect to be a validator 592 // but so far we haven't seen ourselves as validators for 593 // this network. 594 if t.isValidatorSetup && !t.isValidator { 595 t.checkValidatorDataWithSelfWallets(data) 596 } 597 598 nr := &commandspb.AnnounceNode{ 599 Id: data.ID, 600 VegaPubKey: data.VegaPubKey, 601 VegaPubKeyIndex: data.VegaPubKeyIndex, 602 EthereumAddress: data.EthereumAddress, 603 ChainPubKey: tm, 604 InfoUrl: data.InfoURL, 605 Country: data.Country, 606 Name: data.Name, 607 AvatarUrl: data.AvatarURL, 608 } 609 if err := t.AddNewNode(ctx, nr, ValidatorStatusTendermint); err != nil { 610 return err 611 } 612 } 613 614 return nil 615 } 616 617 // checkValidatorDataWithSelfWallets in the genesis file, validators data 618 // are a mapping of a tendermint pubkey to validator info. 619 // in here we are going to check if: 620 // - the tm pubkey is the same as the one stored in the nodewallet 621 // - if no we return straight away and consider ourself as non validator 622 // - if yes then we do the following checks 623 // 624 // - check that all pubkeys / addresses matches what's in the node wallet 625 // - if they all match, we are a validator! 626 // - if they don't, we panic, that's a missconfiguration from the checkValidatorDataWithSelfWallets, ever the genesis or the node is misconfigured 627 func (t *Topology) checkValidatorDataWithSelfWallets(data ValidatorData) { 628 if data.TmPubKey != t.wallets.GetTendermintPubkey() { 629 return 630 } 631 632 // if any of these are wrong, the nodewallet didn't import 633 // the keys set in the genesis block 634 hasError := t.wallets.GetVega().ID().Hex() != data.ID || 635 t.wallets.GetVega().PubKey().Hex() != data.VegaPubKey || 636 common.HexToAddress(t.wallets.GetEthereumAddress()) != common.HexToAddress(data.EthereumAddress) 637 638 if hasError { 639 t.log.Panic("invalid node wallet configurations, the genesis validator mapping differ to the wallets imported by the nodewallet", 640 logging.String("genesis-tendermint-pubkey", data.TmPubKey), 641 logging.String("nodewallet-tendermint-pubkey", t.wallets.GetTendermintPubkey()), 642 logging.String("genesis-vega-pubkey", data.VegaPubKey), 643 logging.String("nodewallet-vega-pubkey", t.wallets.GetVega().PubKey().Hex()), 644 logging.String("genesis-vega-id", data.ID), 645 logging.String("nodewallet-vega-id", t.wallets.GetVega().ID().Hex()), 646 logging.String("genesis-ethereum-address", data.EthereumAddress), 647 logging.String("nodewallet-ethereum-address", t.wallets.GetEthereumAddress()), 648 ) 649 } 650 651 t.isValidator = true 652 } 653 654 func (t *Topology) IssueSignatures(ctx context.Context, submitter, nodeID, chainID string, kind types.NodeSignatureKind) error { 655 t.log.Debug("received IssueSignatures txn", logging.String("submitter", submitter), logging.String("nodeID", nodeID)) 656 currentTime := t.timeService.GetTimeNow() 657 switch kind { 658 case types.NodeSignatureKindERC20MultiSigSignerAdded: 659 return t.signatures.EmitValidatorAddedSignatures(ctx, submitter, nodeID, chainID, currentTime) 660 case types.NodeSignatureKindERC20MultiSigSignerRemoved: 661 return t.signatures.EmitValidatorRemovedSignatures(ctx, submitter, nodeID, chainID, currentTime) 662 default: 663 return ErrIssueSignaturesUnexpectedKind 664 } 665 }