github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/session/pingpong/consumer_balance_tracker.go (about) 1 /* 2 * Copyright (C) 2021 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package pingpong 19 20 import ( 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "math/big" 26 "sync" 27 "time" 28 29 "github.com/cenkalti/backoff/v4" 30 "github.com/ethereum/go-ethereum/common" 31 "github.com/rs/zerolog/log" 32 33 "github.com/mysteriumnetwork/node/config" 34 nodevent "github.com/mysteriumnetwork/node/core/node/event" 35 "github.com/mysteriumnetwork/node/eventbus" 36 "github.com/mysteriumnetwork/node/identity" 37 "github.com/mysteriumnetwork/node/identity/registry" 38 pevent "github.com/mysteriumnetwork/node/pilvytis" 39 "github.com/mysteriumnetwork/node/session/pingpong/event" 40 "github.com/mysteriumnetwork/payments/client" 41 "github.com/mysteriumnetwork/payments/units" 42 ) 43 44 type balanceKey string 45 46 func newBalanceKey(chainID int64, id identity.Identity) balanceKey { 47 return balanceKey(fmt.Sprintf("%v_%v", id.Address, chainID)) 48 } 49 50 type balances struct { 51 sync.Mutex 52 valuesMap map[balanceKey]ConsumerBalance 53 } 54 55 type transactorBounties struct { 56 sync.Mutex 57 valuesMap map[balanceKey]*big.Int 58 } 59 60 // ConsumerBalanceTracker keeps track of consumer balances. 61 // TODO: this needs to take into account the saved state. 62 type ConsumerBalanceTracker struct { 63 balances balances 64 transactorBounties transactorBounties 65 66 addressProvider addressProvider 67 registry registrationStatusProvider 68 consumerBalanceChecker consumerBalanceChecker 69 bus eventbus.EventBus 70 consumerGrandTotalsStorage consumerTotalsStorage 71 consumerInfoGetter consumerInfoGetter 72 transactorRegistrationStatusProvider transactorRegistrationStatusProvider 73 blockchainInfoProvider blockchainInfoProvider 74 stop chan struct{} 75 once sync.Once 76 77 fullBalanceUpdateThrottle map[string]struct{} 78 fullBalanceUpdateLock sync.Mutex 79 balanceSyncer *balanceSyncer 80 81 cfg ConsumerBalanceTrackerConfig 82 } 83 84 type transactorRegistrationStatusProvider interface { 85 FetchRegistrationFees(chainID int64) (registry.FeesResponse, error) 86 FetchRegistrationStatus(id string) ([]registry.TransactorStatusResponse, error) 87 } 88 89 type blockchainInfoProvider interface { 90 GetConsumerChannelsHermes(chainID int64, channelAddress common.Address) (client.ConsumersHermes, error) 91 } 92 93 // PollConfig sets the interval and timeout for polling. 94 type PollConfig struct { 95 Interval time.Duration 96 Timeout time.Duration 97 } 98 99 // ConsumerBalanceTrackerConfig represents the consumer balance tracker configuration. 100 type ConsumerBalanceTrackerConfig struct { 101 FastSync PollConfig 102 LongSync PollConfig 103 } 104 105 // NewConsumerBalanceTracker creates a new instance 106 func NewConsumerBalanceTracker( 107 publisher eventbus.EventBus, 108 consumerBalanceChecker consumerBalanceChecker, 109 consumerGrandTotalsStorage consumerTotalsStorage, 110 consumerInfoGetter consumerInfoGetter, 111 transactorRegistrationStatusProvider transactorRegistrationStatusProvider, 112 registry registrationStatusProvider, 113 addressProvider addressProvider, 114 blockchainInfoProvider blockchainInfoProvider, 115 cfg ConsumerBalanceTrackerConfig, 116 ) *ConsumerBalanceTracker { 117 return &ConsumerBalanceTracker{ 118 balances: balances{valuesMap: make(map[balanceKey]ConsumerBalance)}, 119 transactorBounties: transactorBounties{valuesMap: make(map[balanceKey]*big.Int)}, 120 consumerBalanceChecker: consumerBalanceChecker, 121 bus: publisher, 122 consumerGrandTotalsStorage: consumerGrandTotalsStorage, 123 consumerInfoGetter: consumerInfoGetter, 124 transactorRegistrationStatusProvider: transactorRegistrationStatusProvider, 125 blockchainInfoProvider: blockchainInfoProvider, 126 registry: registry, 127 addressProvider: addressProvider, 128 stop: make(chan struct{}), 129 cfg: cfg, 130 fullBalanceUpdateThrottle: make(map[string]struct{}), 131 balanceSyncer: newBalanceSyncer(), 132 } 133 } 134 135 type consumerInfoGetter interface { 136 GetConsumerData(chainID int64, id string, cacheDuration time.Duration) (HermesUserInfo, error) 137 } 138 139 type consumerBalanceChecker interface { 140 GetConsumerChannel(chainID int64, addr common.Address, mystSCAddress common.Address) (client.ConsumerChannel, error) 141 GetMystBalance(chainID int64, mystAddress, identity common.Address) (*big.Int, error) 142 } 143 144 var errBalanceNotOffchain = errors.New("balance is not offchain, can't use hermes to check") 145 146 // Subscribe subscribes the consumer balance tracker to relevant events 147 func (cbt *ConsumerBalanceTracker) Subscribe(bus eventbus.Subscriber) error { 148 err := bus.SubscribeAsync(registry.AppTopicIdentityRegistration, cbt.handleRegistrationEvent) 149 if err != nil { 150 return err 151 } 152 err = bus.SubscribeAsync(string(nodevent.StatusStopped), cbt.handleStopEvent) 153 if err != nil { 154 return err 155 } 156 err = bus.SubscribeAsync(event.AppTopicGrandTotalChanged, cbt.handleGrandTotalChanged) 157 if err != nil { 158 return err 159 } 160 err = bus.SubscribeAsync(pevent.AppTopicOrderUpdated, cbt.handleOrderUpdatedEvent) 161 if err != nil { 162 return err 163 } 164 err = bus.SubscribeAsync(event.AppTopicSettlementComplete, cbt.handleSettlementComplete) 165 if err != nil { 166 return err 167 } 168 err = bus.SubscribeAsync(event.AppTopicWithdrawalRequested, cbt.handleWithdrawalRequested) 169 if err != nil { 170 return err 171 } 172 return bus.SubscribeAsync(identity.AppTopicIdentityUnlock, cbt.handleUnlockEvent) 173 } 174 175 // settlements increase balance on the chain they are settled on. 176 func (cbt *ConsumerBalanceTracker) handleSettlementComplete(ev event.AppEventSettlementComplete) { 177 go cbt.aggressiveSync(ev.ChainID, ev.ProviderID, cbt.cfg.FastSync.Timeout, cbt.cfg.FastSync.Interval) 178 } 179 180 // withdrawals decrease balance on the from chain. 181 func (cbt *ConsumerBalanceTracker) handleWithdrawalRequested(ev event.AppEventWithdrawalRequested) { 182 go cbt.aggressiveSync(ev.FromChain, ev.ProviderID, cbt.cfg.FastSync.Timeout, cbt.cfg.FastSync.Interval) 183 } 184 185 func (cbt *ConsumerBalanceTracker) handleOrderUpdatedEvent(ev pevent.AppEventOrderUpdated) { 186 if !ev.Status.Paid() { 187 return 188 } 189 190 go cbt.aggressiveSync(config.GetInt64(config.FlagChainID), identity.FromAddress(ev.IdentityAddress), cbt.cfg.FastSync.Timeout, cbt.cfg.FastSync.Interval) 191 } 192 193 // Performs a more aggresive type of sync on BC for the given identity on the given chain. 194 // Should be used after events that cause a state change on blockchain. 195 func (cbt *ConsumerBalanceTracker) aggressiveSync(chainID int64, id identity.Identity, timeout, frequency time.Duration) { 196 b, ok := cbt.getBalance(chainID, id) 197 if ok && b.IsOffchain { 198 log.Info().Bool("is_offchain", b.IsOffchain).Msg("skipping aggresive sync") 199 return 200 } 201 202 cbt.startJob(chainID, id, timeout, frequency) 203 } 204 205 func (cbt *ConsumerBalanceTracker) formJobSyncKey(chainID int64, id identity.Identity, timeout, frequency time.Duration) string { 206 return fmt.Sprintf("%v%v%v%v", chainID, id.ToCommonAddress().Hex(), timeout, frequency) 207 } 208 209 // NeedsForceSync returns true if balance needs to be force synced. 210 func (cbt *ConsumerBalanceTracker) NeedsForceSync(chainID int64, id identity.Identity) bool { 211 v, ok := cbt.getBalance(chainID, id) 212 if !ok { 213 return true 214 } 215 216 // Offchain balances expire after configured amount of time and need to be resynced. 217 if v.OffchainNeedsSync() { 218 return true 219 } 220 221 // Balance doesn't always go to 0 but connections can still fail. 222 if v.BCBalance.Cmp(units.SingleGweiInWei()) < 0 { 223 return true 224 } 225 226 return false 227 } 228 229 // GetBalance gets the current balance for given identity 230 func (cbt *ConsumerBalanceTracker) GetBalance(chainID int64, id identity.Identity) *big.Int { 231 if v, ok := cbt.getBalance(chainID, id); ok { 232 return v.GetBalance() 233 } 234 return new(big.Int) 235 } 236 237 func (cbt *ConsumerBalanceTracker) publishChangeEvent(id identity.Identity, before, after *big.Int) { 238 if before == nil || after == nil || before.Cmp(after) == 0 { 239 return 240 } 241 242 cbt.bus.Publish(event.AppTopicBalanceChanged, event.AppEventBalanceChanged{ 243 Identity: id, 244 Previous: before, 245 Current: after, 246 }) 247 } 248 249 func (cbt *ConsumerBalanceTracker) handleUnlockEvent(data identity.AppEventIdentityUnlock) { 250 err := cbt.recoverGrandTotalPromised(data.ChainID, data.ID) 251 if err != nil { 252 log.Error().Err(err).Msg("Could not recover Grand Total Promised") 253 } 254 255 status, err := cbt.registry.GetRegistrationStatus(data.ChainID, data.ID) 256 if err != nil { 257 log.Error().Err(err).Msg("Could not recover get registration status") 258 } 259 260 switch status { 261 case registry.InProgress: 262 cbt.alignWithTransactor(data.ChainID, data.ID) 263 default: 264 cbt.ForceBalanceUpdate(data.ChainID, data.ID) 265 } 266 267 go cbt.lifetimeBCSync(data.ChainID, data.ID) 268 } 269 270 func (cbt *ConsumerBalanceTracker) handleGrandTotalChanged(ev event.AppEventGrandTotalChanged) { 271 if _, ok := cbt.getBalance(ev.ChainID, ev.ConsumerID); !ok { 272 cbt.ForceBalanceUpdate(ev.ChainID, ev.ConsumerID) 273 return 274 } 275 276 cbt.updateGrandTotal(ev.ChainID, ev.ConsumerID, ev.Current) 277 } 278 279 func (cbt *ConsumerBalanceTracker) getUnregisteredChannelBalance(chainID int64, id identity.Identity) (*big.Int, error) { 280 addr, err := cbt.addressProvider.GetActiveChannelAddress(chainID, id.ToCommonAddress()) 281 if err != nil { 282 return new(big.Int), err 283 } 284 285 myst, err := cbt.addressProvider.GetMystAddress(chainID) 286 if err != nil { 287 return new(big.Int), err 288 } 289 290 balance, err := cbt.consumerBalanceChecker.GetMystBalance(chainID, myst, addr) 291 if err != nil { 292 return new(big.Int), err 293 } 294 return balance, nil 295 } 296 297 func (cbt *ConsumerBalanceTracker) lifetimeBCSync(chainID int64, id identity.Identity) { 298 b, ok := cbt.getBalance(chainID, id) 299 if ok && b.IsOffchain { 300 log.Info().Bool("is_offchain", b.IsOffchain).Msg("skipping external channel top-up tracking") 301 return 302 } 303 304 // 100 years should be close enough to never 305 timeout := time.Hour * 24 * 365 * 100 306 cbt.startJob(chainID, id, timeout, cbt.cfg.LongSync.Interval) 307 } 308 309 func (cbt *ConsumerBalanceTracker) startJob(chainID int64, id identity.Identity, timeout, frequency time.Duration) { 310 job, exists := cbt.balanceSyncer.PeriodiclySyncBalance( 311 cbt.formJobSyncKey(chainID, id, timeout, frequency), 312 func(stop <-chan struct{}) { 313 cbt.periodicSync(stop, chainID, id, frequency) 314 }, 315 timeout, 316 ) 317 318 if exists { 319 return 320 } 321 322 go func() { 323 select { 324 case <-cbt.stop: 325 job.Stop() 326 return 327 case <-job.Done(): 328 return 329 } 330 }() 331 } 332 333 func (cbt *ConsumerBalanceTracker) periodicSync(stop <-chan struct{}, chainID int64, id identity.Identity, syncPeriod time.Duration) { 334 for { 335 select { 336 case <-stop: 337 return 338 case <-time.After(syncPeriod): 339 _ = cbt.ForceBalanceUpdate(chainID, id) 340 } 341 } 342 } 343 344 func (cbt *ConsumerBalanceTracker) alignWithHermes(chainID int64, id identity.Identity) (*big.Int, *big.Int, error) { 345 var boff backoff.BackOff 346 eback := backoff.NewExponentialBackOff() 347 eback.MaxElapsedTime = time.Second * 15 348 eback.InitialInterval = time.Second * 1 349 350 boff = backoff.WithMaxRetries(eback, 5) 351 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 352 defer cancel() 353 354 go func() { 355 select { 356 case <-cbt.stop: 357 cancel() 358 case <-ctx.Done(): 359 } 360 }() 361 362 boff = backoff.WithContext(boff, ctx) 363 balance := cbt.GetBalance(chainID, id) 364 promised := new(big.Int) 365 alignBalance := func() error { 366 consumer, err := cbt.consumerInfoGetter.GetConsumerData(chainID, id.Address, 5*time.Second) 367 if err != nil { 368 var syntax *json.SyntaxError 369 if errors.As(err, &syntax) { 370 cancel() 371 log.Err(err).Msg("hermes response is malformed JSON can't check if offchain") 372 return err 373 } 374 375 if errors.Is(err, ErrHermesNotFound) { 376 // Hermes doesn't know about this identity meaning it's not offchain. Cancel. 377 cancel() 378 return errBalanceNotOffchain 379 } 380 381 return err 382 } 383 if !consumer.IsOffchain { 384 // Hermes knows about this identity, but it's not offchain. Cancel. 385 cancel() 386 return errBalanceNotOffchain 387 } 388 389 if consumer.LatestPromise.Amount != nil { 390 promised = consumer.LatestPromise.Amount 391 } 392 393 if isSettledBiggerThanPromised(consumer.Settled, promised) { 394 promised, err = cbt.getPromisedWhenSettledIsBigger(consumer, promised, chainID, id.ToCommonAddress()) 395 if err != nil { 396 return err 397 } 398 } 399 400 previous, _ := cbt.getBalance(chainID, id) 401 cbt.setBalance(chainID, id, ConsumerBalance{ 402 BCBalance: consumer.Balance, 403 BCSettled: consumer.Settled, 404 GrandTotalPromised: promised, 405 IsOffchain: true, 406 LastOffchainSync: time.Now().UTC(), 407 }) 408 409 currentBalance, _ := cbt.getBalance(chainID, id) 410 go cbt.publishChangeEvent(id, previous.GetBalance(), currentBalance.GetBalance()) 411 balance = consumer.Balance 412 return nil 413 } 414 415 return balance, promised, backoff.Retry(alignBalance, boff) 416 } 417 418 // ForceBalanceUpdateCached forces a balance update for the given identity only if the last call to this func was done no sooner than a minute ago. 419 func (cbt *ConsumerBalanceTracker) ForceBalanceUpdateCached(chainID int64, id identity.Identity) *big.Int { 420 cbt.fullBalanceUpdateLock.Lock() 421 defer cbt.fullBalanceUpdateLock.Unlock() 422 423 key := getKeyForForceBalanceCache(chainID, id) 424 _, ok := cbt.fullBalanceUpdateThrottle[key] 425 if ok { 426 return cbt.GetBalance(chainID, id) 427 } 428 429 currentBalance := cbt.ForceBalanceUpdate(chainID, id) 430 cbt.fullBalanceUpdateThrottle[key] = struct{}{} 431 432 go func() { 433 select { 434 case <-time.After(time.Minute): 435 cbt.deleteCachedForceBalance(key) 436 case <-cbt.stop: 437 return 438 } 439 }() 440 441 return currentBalance 442 } 443 444 func (cbt *ConsumerBalanceTracker) deleteCachedForceBalance(key string) { 445 cbt.fullBalanceUpdateLock.Lock() 446 defer cbt.fullBalanceUpdateLock.Unlock() 447 448 delete(cbt.fullBalanceUpdateThrottle, key) 449 } 450 451 func getKeyForForceBalanceCache(chainID int64, id identity.Identity) string { 452 return fmt.Sprintf("%v_%v", id.ToCommonAddress().Hex(), chainID) 453 } 454 455 // ForceBalanceUpdate forces a balance update and returns the updated balance 456 func (cbt *ConsumerBalanceTracker) ForceBalanceUpdate(chainID int64, id identity.Identity) *big.Int { 457 fallback, ok := cbt.getBalance(chainID, id) 458 if !ok { 459 fallback.BCBalance = big.NewInt(0) 460 } 461 462 addr, err := cbt.addressProvider.GetActiveChannelAddress(chainID, id.ToCommonAddress()) 463 if err != nil { 464 log.Error().Err(err).Msg("Could not calculate channel address") 465 return fallback.BCBalance 466 } 467 468 myst, err := cbt.addressProvider.GetMystAddress(chainID) 469 if err != nil { 470 log.Error().Err(err).Msg("could not get myst address") 471 return new(big.Int) 472 } 473 474 balance, lastPromised, err := cbt.alignWithHermes(chainID, id) 475 if err != nil { 476 if !errors.Is(err, errBalanceNotOffchain) { 477 log.Error().Err(err).Msg("align with hermes failed with a critical error, offchain balance out of sync") 478 } 479 if !errors.Is(err, errBalanceNotOffchain) && fallback.IsOffchain { 480 log.Warn().Msg("offchain sync failed but found a cache entry, will return that") 481 return fallback.BCBalance 482 } 483 } else { 484 return balance 485 } 486 487 cc, err := cbt.consumerBalanceChecker.GetConsumerChannel(chainID, addr, myst) 488 if err != nil { 489 // This indicates we're not registered, check for transactor bounty first and then unregistered balance. 490 log.Warn().Err(err).Msg("Could not get consumer channel") 491 if client.IsErrConnectionFailed(err) { 492 log.Debug().Msg("tried to get consumer channel and got a connection error, will return last known balance") 493 return fallback.BCBalance 494 } 495 496 var unregisteredBalance *big.Int 497 // If registration is in progress, check transactor for bounty amount. 498 bountyAmount, ok := cbt.getTransactorBounty(chainID, id) 499 if ok { 500 // if bounty from transactor is 0 it will be the unregistered balance of the channel. 501 unregisteredBalance = bountyAmount 502 } else { 503 // If error was not for connection it indicates we're not registered, check for unregistered balance. 504 unregisteredBalance, err = cbt.getUnregisteredChannelBalance(chainID, id) 505 if err != nil { 506 log.Error().Err(err).Msg("could not get unregistered balance") 507 return fallback.BCBalance 508 } 509 } 510 511 cbt.setBalance(chainID, id, ConsumerBalance{ 512 BCBalance: unregisteredBalance, 513 BCSettled: new(big.Int), 514 GrandTotalPromised: new(big.Int), 515 }) 516 517 currentBalance, _ := cbt.getBalance(chainID, id) 518 go cbt.publishChangeEvent(id, new(big.Int), currentBalance.GetBalance()) 519 return unregisteredBalance 520 } 521 522 hermes, err := cbt.addressProvider.GetActiveHermes(chainID) 523 if err != nil { 524 log.Error().Err(err).Msg("could not get active hermes address") 525 return fallback.BCBalance 526 } 527 528 grandTotal, err := cbt.consumerGrandTotalsStorage.Get(chainID, id, hermes) 529 if errors.Is(err, ErrNotFound) || (err == nil && lastPromised != nil && grandTotal.Cmp(lastPromised) == -1) { 530 err := cbt.consumerGrandTotalsStorage.Store(chainID, id, hermes, lastPromised) 531 if err != nil { 532 log.Error().Err(err).Msg("Could not recover Grand Total Promised") 533 } 534 grandTotal = lastPromised 535 } 536 if err != nil && !errors.Is(err, ErrNotFound) { 537 log.Error().Err(err).Msg("Could not get consumer grand total promised") 538 return fallback.BCBalance 539 } 540 541 before := new(big.Int) 542 if v, ok := cbt.getBalance(chainID, id); ok { 543 before = v.GetBalance() 544 } 545 546 cbt.setBalance(chainID, id, ConsumerBalance{ 547 BCBalance: cc.Balance, 548 BCSettled: cc.Settled, 549 GrandTotalPromised: grandTotal, 550 }) 551 552 currentBalance, _ := cbt.getBalance(chainID, id) 553 go cbt.publishChangeEvent(id, before, currentBalance.GetBalance()) 554 return currentBalance.GetBalance() 555 } 556 557 func (cbt *ConsumerBalanceTracker) handleRegistrationEvent(event registry.AppEventIdentityRegistration) { 558 switch event.Status { 559 case registry.InProgress: 560 cbt.alignWithTransactor(event.ChainID, event.ID) 561 case registry.Registered: 562 cbt.removeTransactorBounty(event.ChainID, event.ID) 563 cbt.ForceBalanceUpdate(event.ChainID, event.ID) 564 } 565 } 566 567 func (cbt *ConsumerBalanceTracker) alignWithTransactor(chainID int64, id identity.Identity) { 568 balance, ok := cbt.getBalance(chainID, id) 569 if ok { 570 // do not override existing values with transactor data if it is not 0 571 if balance.BCBalance.Cmp(big.NewInt(0)) != 0 { 572 return 573 } 574 } 575 576 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 577 defer cancel() 578 579 go func() { 580 select { 581 case <-cbt.stop: 582 cancel() 583 case <-ctx.Done(): 584 } 585 }() 586 587 bountyAmount := cbt.getTransactorBalance(ctx, chainID, id) 588 if bountyAmount == nil { 589 return 590 } 591 592 c := ConsumerBalance{ 593 BCBalance: bountyAmount, 594 BCSettled: new(big.Int), 595 GrandTotalPromised: new(big.Int), 596 } 597 598 cbt.setBalance(chainID, id, c) 599 cbt.setTransactorBounty(chainID, id, bountyAmount) 600 go cbt.publishChangeEvent(id, balance.GetBalance(), c.GetBalance()) 601 } 602 603 func (cbt *ConsumerBalanceTracker) getTransactorBalance(ctx context.Context, chainID int64, id identity.Identity) *big.Int { 604 data, err := cbt.identityRegistrationStatus(ctx, id, chainID) 605 if err != nil { 606 log.Error().Err(fmt.Errorf("could not fetch registration status from transactor: %w", err)) 607 return nil 608 } 609 610 if data.Status != registry.TransactorRegistrationEntryStatusCreated && 611 data.Status != registry.TransactorRegistrationEntryStatusPriceIncreased { 612 return nil 613 } 614 615 if data.BountyAmount == nil || data.BountyAmount.Cmp(big.NewInt(0)) == 0 { 616 // if we've got no bounty, get myst balance from BC and use that as bounty 617 b, err := cbt.getUnregisteredChannelBalance(chainID, id) 618 if err != nil { 619 log.Error().Err(err).Msg("could not get unregistered balance") 620 return nil 621 } 622 623 data.BountyAmount = b 624 } 625 626 log.Debug().Msgf("Loaded transactor state, current balance: %v MYST", data.BountyAmount) 627 return data.BountyAmount 628 } 629 630 func (cbt *ConsumerBalanceTracker) recoverGrandTotalPromised(chainID int64, identity identity.Identity) error { 631 var boff backoff.BackOff 632 eback := backoff.NewExponentialBackOff() 633 eback.MaxElapsedTime = time.Second * 20 634 eback.InitialInterval = time.Second * 2 635 636 boff = backoff.WithMaxRetries(eback, 10) 637 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 638 defer cancel() 639 640 go func() { 641 select { 642 case <-cbt.stop: 643 cancel() 644 case <-ctx.Done(): 645 } 646 }() 647 648 var data HermesUserInfo 649 boff = backoff.WithContext(boff, ctx) 650 toRetry := func() error { 651 d, err := cbt.consumerInfoGetter.GetConsumerData(chainID, identity.Address, time.Minute) 652 if err != nil { 653 if !errors.Is(err, ErrHermesNotFound) { 654 return err 655 } 656 log.Debug().Msgf("No previous invoice grand total, assuming zero") 657 return nil 658 } 659 data = d 660 return nil 661 } 662 663 if err := backoff.Retry(toRetry, boff); err != nil { 664 return err 665 } 666 667 latestPromised := big.NewInt(0) 668 if data.LatestPromise.Amount != nil { 669 latestPromised = data.LatestPromise.Amount 670 } 671 672 if isSettledBiggerThanPromised(data.Settled, latestPromised) { 673 var err error 674 latestPromised, err = cbt.getPromisedWhenSettledIsBigger(data, latestPromised, chainID, identity.ToCommonAddress()) 675 if err != nil { 676 return err 677 } 678 } 679 680 log.Debug().Msgf("Loaded hermes state: already promised: %v", latestPromised) 681 682 hermes, err := cbt.addressProvider.GetActiveHermes(chainID) 683 if err != nil { 684 log.Error().Err(err).Msg("could not get hermes address") 685 return err 686 } 687 688 return cbt.consumerGrandTotalsStorage.Store(chainID, identity, hermes, latestPromised) 689 } 690 691 func (cbt *ConsumerBalanceTracker) handleStopEvent() { 692 cbt.once.Do(func() { 693 close(cbt.stop) 694 }) 695 } 696 697 func (cbt *ConsumerBalanceTracker) getBalance(chainID int64, id identity.Identity) (ConsumerBalance, bool) { 698 cbt.balances.Lock() 699 defer cbt.balances.Unlock() 700 701 if v, ok := cbt.balances.valuesMap[newBalanceKey(chainID, id)]; ok { 702 return v, true 703 } 704 705 return ConsumerBalance{ 706 BCBalance: new(big.Int), 707 BCSettled: new(big.Int), 708 GrandTotalPromised: new(big.Int), 709 }, false 710 } 711 712 func (cbt *ConsumerBalanceTracker) setBalance(chainID int64, id identity.Identity, balance ConsumerBalance) { 713 cbt.balances.Lock() 714 defer cbt.balances.Unlock() 715 716 cbt.balances.valuesMap[newBalanceKey(chainID, id)] = balance 717 } 718 719 func (cbt *ConsumerBalanceTracker) getTransactorBounty(chainID int64, id identity.Identity) (*big.Int, bool) { 720 cbt.transactorBounties.Lock() 721 defer cbt.transactorBounties.Unlock() 722 723 if v, ok := cbt.transactorBounties.valuesMap[newBalanceKey(chainID, id)]; ok { 724 return v, true 725 } 726 727 return nil, false 728 } 729 730 func (cbt *ConsumerBalanceTracker) setTransactorBounty(chainID int64, id identity.Identity, bountyAmount *big.Int) { 731 cbt.transactorBounties.Lock() 732 defer cbt.transactorBounties.Unlock() 733 734 cbt.transactorBounties.valuesMap[newBalanceKey(chainID, id)] = bountyAmount 735 } 736 737 func (cbt *ConsumerBalanceTracker) removeTransactorBounty(chainID int64, id identity.Identity) { 738 cbt.transactorBounties.Lock() 739 defer cbt.transactorBounties.Unlock() 740 741 delete(cbt.transactorBounties.valuesMap, newBalanceKey(chainID, id)) 742 } 743 744 func (cbt *ConsumerBalanceTracker) updateGrandTotal(chainID int64, id identity.Identity, current *big.Int) { 745 b, ok := cbt.getBalance(chainID, id) 746 if !ok || b.OffchainNeedsSync() { 747 cbt.ForceBalanceUpdate(chainID, id) 748 return 749 } 750 751 before := b.BCBalance 752 b.GrandTotalPromised = current 753 cbt.setBalance(chainID, id, b) 754 755 after, _ := cbt.getBalance(chainID, id) 756 go cbt.publishChangeEvent(id, before, after.GetBalance()) 757 } 758 759 // identityRegistrationStatus returns the registration status of a given identity. 760 func (cbt *ConsumerBalanceTracker) identityRegistrationStatus(ctx context.Context, id identity.Identity, chainID int64) (registry.TransactorStatusResponse, error) { 761 var data registry.TransactorStatusResponse 762 boff := backoff.WithContext(backoff.NewConstantBackOff(time.Millisecond*500), ctx) 763 toRetry := func() error { 764 resp, err := cbt.transactorRegistrationStatusProvider.FetchRegistrationStatus(id.Address) 765 if err != nil { 766 return err 767 } 768 769 var status *registry.TransactorStatusResponse 770 for _, v := range resp { 771 if v.ChainID == chainID { 772 status = &v 773 break 774 } 775 } 776 777 if status == nil { 778 err := fmt.Errorf("got response but failed to find status for id '%s' on chain '%d'", id.Address, chainID) 779 return backoff.Permanent(err) 780 } 781 782 data = *status 783 return nil 784 } 785 786 return data, backoff.Retry(toRetry, boff) 787 } 788 789 func (cbt *ConsumerBalanceTracker) getPromisedWhenSettledIsBigger(data HermesUserInfo, latestPromised *big.Int, chainID int64, identityAddress common.Address) (*big.Int, error) { 790 if data.IsOffchain { 791 return data.Settled, nil 792 } 793 794 activeChannelAddress, err := cbt.addressProvider.GetActiveChannelAddress(chainID, identityAddress) 795 if err != nil { 796 return nil, fmt.Errorf("error getting active channel address: %w", err) 797 } 798 799 consumerHermes, err := cbt.blockchainInfoProvider.GetConsumerChannelsHermes(chainID, activeChannelAddress) 800 if err != nil { 801 return nil, fmt.Errorf("error getting consumer channels hermes: %w", err) 802 } 803 804 return consumerHermes.Settled, nil 805 } 806 807 func safeSub(a, b *big.Int) *big.Int { 808 if a == nil || b == nil { 809 return new(big.Int) 810 } 811 812 if a.Cmp(b) >= 0 { 813 return new(big.Int).Sub(a, b) 814 } 815 return new(big.Int) 816 } 817 818 func isSettledBiggerThanPromised(settled, promised *big.Int) bool { 819 return settled != nil && settled.Cmp(promised) == 1 820 } 821 822 // ConsumerBalance represents the consumer balance 823 type ConsumerBalance struct { 824 BCBalance *big.Int 825 BCSettled *big.Int 826 GrandTotalPromised *big.Int 827 828 // IsOffchain is an optional indicator which marks an offchain balanace. 829 // Offchain balances receive no updates on the blockchain and their 830 // actual remaining balance should be retreived from hermes. 831 IsOffchain bool 832 LastOffchainSync time.Time 833 } 834 835 // OffchainNeedsSync returns true if balance is offchain and should be synced. 836 func (cb ConsumerBalance) OffchainNeedsSync() bool { 837 if !cb.IsOffchain { 838 return false 839 } 840 841 if cb.LastOffchainSync.IsZero() { 842 return true 843 } 844 845 expiresAfter := config.GetDuration(config.FlagOffchainBalanceExpiration) 846 now := time.Now().UTC() 847 return cb.LastOffchainSync.Add(expiresAfter).Before(now) 848 } 849 850 // GetBalance returns the current balance 851 func (cb ConsumerBalance) GetBalance() *big.Int { 852 // Balance (to spend) = BCBalance - (hermesPromised - BCSettled) 853 return safeSub(cb.BCBalance, safeSub(cb.GrandTotalPromised, cb.BCSettled)) 854 }