github.com/status-im/status-go@v1.1.0/protocol/communities/manager_archive.go (about) 1 //go:build !disable_torrent 2 // +build !disable_torrent 3 4 // Attribution to Pascal Precht, for further context please view the below issues 5 // - https://github.com/status-im/status-go/issues/2563 6 // - https://github.com/status-im/status-go/issues/2565 7 // - https://github.com/status-im/status-go/issues/2567 8 // - https://github.com/status-im/status-go/issues/2568 9 10 package communities 11 12 import ( 13 "crypto/ecdsa" 14 "errors" 15 "fmt" 16 "net" 17 "os" 18 "path" 19 "sort" 20 "sync" 21 "time" 22 23 "github.com/status-im/status-go/eth-node/types" 24 "github.com/status-im/status-go/params" 25 "github.com/status-im/status-go/protocol/encryption" 26 "github.com/status-im/status-go/protocol/transport" 27 "github.com/status-im/status-go/signal" 28 29 "github.com/anacrolix/torrent" 30 "github.com/anacrolix/torrent/metainfo" 31 "go.uber.org/zap" 32 ) 33 34 type archiveMDSlice []*archiveMetadata 35 36 type archiveMetadata struct { 37 hash string 38 from uint64 39 } 40 41 func (md archiveMDSlice) Len() int { 42 return len(md) 43 } 44 45 func (md archiveMDSlice) Swap(i, j int) { 46 md[i], md[j] = md[j], md[i] 47 } 48 49 func (md archiveMDSlice) Less(i, j int) bool { 50 return md[i].from > md[j].from 51 } 52 53 type EncodedArchiveData struct { 54 padding int 55 bytes []byte 56 } 57 58 type ArchiveManager struct { 59 torrentConfig *params.TorrentConfig 60 torrentClient *torrent.Client 61 torrentTasks map[string]metainfo.Hash 62 historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask 63 historyArchiveTasksWaitGroup sync.WaitGroup 64 historyArchiveTasks sync.Map // stores `chan struct{}` 65 66 logger *zap.Logger 67 persistence *Persistence 68 transport *transport.Transport 69 identity *ecdsa.PrivateKey 70 encryptor *encryption.Protocol 71 72 *ArchiveFileManager 73 publisher Publisher 74 } 75 76 // NewArchiveManager this function is only built and called when the "disable_torrent" build tag is not set 77 // In this case this version of NewArchiveManager will return the full Desktop ArchiveManager ensuring that the 78 // build command will import and build the torrent deps for the Desktop OSes. 79 // NOTE: It is intentional that this file contains the identical function name as in "manager_archive_nop.go" 80 func NewArchiveManager(amc *ArchiveManagerConfig) *ArchiveManager { 81 return &ArchiveManager{ 82 torrentConfig: amc.TorrentConfig, 83 torrentTasks: make(map[string]metainfo.Hash), 84 historyArchiveDownloadTasks: make(map[string]*HistoryArchiveDownloadTask), 85 86 logger: amc.Logger, 87 persistence: amc.Persistence, 88 transport: amc.Transport, 89 identity: amc.Identity, 90 encryptor: amc.Encryptor, 91 92 publisher: amc.Publisher, 93 ArchiveFileManager: NewArchiveFileManager(amc), 94 } 95 } 96 97 func (m *ArchiveManager) SetOnline(online bool) { 98 if online { 99 if m.torrentConfig != nil && m.torrentConfig.Enabled && !m.torrentClientStarted() { 100 err := m.StartTorrentClient() 101 if err != nil { 102 m.logger.Error("couldn't start torrent client", zap.Error(err)) 103 } 104 } 105 } 106 } 107 108 func (m *ArchiveManager) SetTorrentConfig(config *params.TorrentConfig) { 109 m.torrentConfig = config 110 m.ArchiveFileManager.torrentConfig = config 111 } 112 113 // getTCPandUDPport will return the same port number given if != 0, 114 // otherwise, it will attempt to find a free random tcp and udp port using 115 // the same number for both protocols 116 func (m *ArchiveManager) getTCPandUDPport(portNumber int) (int, error) { 117 if portNumber != 0 { 118 return portNumber, nil 119 } 120 121 // Find free port 122 for i := 0; i < 10; i++ { 123 port := func() int { 124 tcpAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort("localhost", "0")) 125 if err != nil { 126 m.logger.Warn("unable to resolve tcp addr: %v", zap.Error(err)) 127 return 0 128 } 129 130 tcpListener, err := net.ListenTCP("tcp", tcpAddr) 131 if err != nil { 132 m.logger.Warn("unable to listen on addr", zap.Stringer("addr", tcpAddr), zap.Error(err)) 133 return 0 134 } 135 defer tcpListener.Close() 136 137 port := tcpListener.Addr().(*net.TCPAddr).Port 138 139 udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort("localhost", fmt.Sprintf("%d", port))) 140 if err != nil { 141 m.logger.Warn("unable to resolve udp addr: %v", zap.Error(err)) 142 return 0 143 } 144 145 udpListener, err := net.ListenUDP("udp", udpAddr) 146 if err != nil { 147 m.logger.Warn("unable to listen on addr", zap.Stringer("addr", udpAddr), zap.Error(err)) 148 return 0 149 } 150 defer udpListener.Close() 151 152 return port 153 }() 154 155 if port != 0 { 156 return port, nil 157 } 158 } 159 160 return 0, fmt.Errorf("no free port found") 161 } 162 163 func (m *ArchiveManager) StartTorrentClient() error { 164 if m.torrentConfig == nil { 165 return fmt.Errorf("can't start torrent client: missing torrentConfig") 166 } 167 168 if m.torrentClientStarted() { 169 return nil 170 } 171 172 port, err := m.getTCPandUDPport(m.torrentConfig.Port) 173 if err != nil { 174 return err 175 } 176 177 config := torrent.NewDefaultClientConfig() 178 config.SetListenAddr(":" + fmt.Sprint(port)) 179 config.Seed = true 180 181 config.DataDir = m.torrentConfig.DataDir 182 183 if _, err := os.Stat(m.torrentConfig.DataDir); os.IsNotExist(err) { 184 err := os.MkdirAll(m.torrentConfig.DataDir, 0700) 185 if err != nil { 186 return err 187 } 188 } 189 190 m.logger.Info("Starting torrent client", zap.Any("port", port)) 191 // Instantiating the client will make it bootstrap and listen eagerly, 192 // so no go routine is needed here 193 client, err := torrent.NewClient(config) 194 if err != nil { 195 return err 196 } 197 m.torrentClient = client 198 return nil 199 } 200 201 func (m *ArchiveManager) Stop() error { 202 if m.torrentClientStarted() { 203 m.stopHistoryArchiveTasksIntervals() 204 m.logger.Info("Stopping torrent client") 205 errs := m.torrentClient.Close() 206 if len(errs) > 0 { 207 return errors.Join(errs...) 208 } 209 m.torrentClient = nil 210 } 211 return nil 212 } 213 214 func (m *ArchiveManager) torrentClientStarted() bool { 215 return m.torrentClient != nil 216 } 217 218 func (m *ArchiveManager) IsReady() bool { 219 // Simply checking for `torrentConfig.Enabled` isn't enough 220 // as there's a possibility that the torrent client couldn't 221 // be instantiated (for example in case of port conflicts) 222 return m.torrentConfig != nil && 223 m.torrentConfig.Enabled && 224 m.torrentClientStarted() 225 } 226 227 func (m *ArchiveManager) GetCommunityChatsFilters(communityID types.HexBytes) ([]*transport.Filter, error) { 228 chatIDs, err := m.persistence.GetCommunityChatIDs(communityID) 229 if err != nil { 230 return nil, err 231 } 232 233 filters := []*transport.Filter{} 234 for _, cid := range chatIDs { 235 filters = append(filters, m.transport.FilterByChatID(cid)) 236 } 237 return filters, nil 238 } 239 240 func (m *ArchiveManager) GetCommunityChatsTopics(communityID types.HexBytes) ([]types.TopicType, error) { 241 filters, err := m.GetCommunityChatsFilters(communityID) 242 if err != nil { 243 return nil, err 244 } 245 246 topics := []types.TopicType{} 247 for _, filter := range filters { 248 topics = append(topics, filter.ContentTopic) 249 } 250 251 return topics, nil 252 } 253 254 func (m *ArchiveManager) getOldestWakuMessageTimestamp(topics []types.TopicType) (uint64, error) { 255 return m.persistence.GetOldestWakuMessageTimestamp(topics) 256 } 257 258 func (m *ArchiveManager) getLastMessageArchiveEndDate(communityID types.HexBytes) (uint64, error) { 259 return m.persistence.GetLastMessageArchiveEndDate(communityID) 260 } 261 262 func (m *ArchiveManager) GetHistoryArchivePartitionStartTimestamp(communityID types.HexBytes) (uint64, error) { 263 filters, err := m.GetCommunityChatsFilters(communityID) 264 if err != nil { 265 m.logger.Error("failed to get community chats filters", zap.Error(err)) 266 return 0, err 267 } 268 269 if len(filters) == 0 { 270 // If we don't have chat filters, we likely don't have any chats 271 // associated to this community, which means there's nothing more 272 // to do here 273 return 0, nil 274 } 275 276 topics := []types.TopicType{} 277 278 for _, filter := range filters { 279 topics = append(topics, filter.ContentTopic) 280 } 281 282 lastArchiveEndDateTimestamp, err := m.getLastMessageArchiveEndDate(communityID) 283 if err != nil { 284 m.logger.Error("failed to get last archive end date", zap.Error(err)) 285 return 0, err 286 } 287 288 if lastArchiveEndDateTimestamp == 0 { 289 // If we don't have a tracked last message archive end date, it 290 // means we haven't created an archive before, which means 291 // the next thing to look at is the oldest waku message timestamp for 292 // this community 293 lastArchiveEndDateTimestamp, err = m.getOldestWakuMessageTimestamp(topics) 294 if err != nil { 295 m.logger.Error("failed to get oldest waku message timestamp", zap.Error(err)) 296 return 0, err 297 } 298 if lastArchiveEndDateTimestamp == 0 { 299 // This means there's no waku message stored for this community so far 300 // (even after requesting possibly missed messages), so no messages exist yet that can be archived 301 m.logger.Debug("can't find valid `lastArchiveEndTimestamp`") 302 return 0, nil 303 } 304 } 305 306 return lastArchiveEndDateTimestamp, nil 307 } 308 309 func (m *ArchiveManager) CreateAndSeedHistoryArchive(communityID types.HexBytes, topics []types.TopicType, startDate time.Time, endDate time.Time, partition time.Duration, encrypt bool) error { 310 m.UnseedHistoryArchiveTorrent(communityID) 311 _, err := m.ArchiveFileManager.CreateHistoryArchiveTorrentFromDB(communityID, topics, startDate, endDate, partition, encrypt) 312 if err != nil { 313 return err 314 } 315 return m.SeedHistoryArchiveTorrent(communityID) 316 } 317 318 func (m *ArchiveManager) StartHistoryArchiveTasksInterval(community *Community, interval time.Duration) { 319 id := community.IDString() 320 if _, exists := m.historyArchiveTasks.Load(id); exists { 321 m.logger.Error("history archive tasks interval already in progress", zap.String("id", id)) 322 return 323 } 324 325 cancel := make(chan struct{}) 326 m.historyArchiveTasks.Store(id, cancel) 327 m.historyArchiveTasksWaitGroup.Add(1) 328 329 ticker := time.NewTicker(interval) 330 defer ticker.Stop() 331 332 m.logger.Debug("starting history archive tasks interval", zap.String("id", id)) 333 for { 334 select { 335 case <-ticker.C: 336 m.logger.Debug("starting archive task...", zap.String("id", id)) 337 lastArchiveEndDateTimestamp, err := m.GetHistoryArchivePartitionStartTimestamp(community.ID()) 338 if err != nil { 339 m.logger.Error("failed to get last archive end date", zap.Error(err)) 340 continue 341 } 342 343 if lastArchiveEndDateTimestamp == 0 { 344 // This means there are no waku messages for this community, 345 // so nothing to do here 346 m.logger.Debug("couldn't determine archive start date - skipping") 347 continue 348 } 349 350 topics, err := m.GetCommunityChatsTopics(community.ID()) 351 if err != nil { 352 m.logger.Error("failed to get community chat topics ", zap.Error(err)) 353 continue 354 } 355 356 ts := time.Now().Unix() 357 to := time.Unix(ts, 0) 358 lastArchiveEndDate := time.Unix(int64(lastArchiveEndDateTimestamp), 0) 359 360 err = m.CreateAndSeedHistoryArchive(community.ID(), topics, lastArchiveEndDate, to, interval, community.Encrypted()) 361 if err != nil { 362 m.logger.Error("failed to create and seed history archive", zap.Error(err)) 363 continue 364 } 365 case <-cancel: 366 m.UnseedHistoryArchiveTorrent(community.ID()) 367 m.historyArchiveTasks.Delete(id) 368 m.historyArchiveTasksWaitGroup.Done() 369 return 370 } 371 } 372 } 373 374 func (m *ArchiveManager) stopHistoryArchiveTasksIntervals() { 375 m.historyArchiveTasks.Range(func(_, task interface{}) bool { 376 close(task.(chan struct{})) // Need to cast to the chan 377 return true 378 }) 379 // Stoping archive interval tasks is async, so we need 380 // to wait for all of them to be closed before we shutdown 381 // the torrent client 382 m.historyArchiveTasksWaitGroup.Wait() 383 } 384 385 func (m *ArchiveManager) StopHistoryArchiveTasksInterval(communityID types.HexBytes) { 386 task, exists := m.historyArchiveTasks.Load(communityID.String()) 387 if exists { 388 m.logger.Info("Stopping history archive tasks interval", zap.Any("id", communityID.String())) 389 close(task.(chan struct{})) // Need to cast to the chan 390 } 391 } 392 393 func (m *ArchiveManager) SeedHistoryArchiveTorrent(communityID types.HexBytes) error { 394 m.UnseedHistoryArchiveTorrent(communityID) 395 396 id := communityID.String() 397 torrentFile := torrentFile(m.torrentConfig.TorrentDir, id) 398 399 metaInfo, err := metainfo.LoadFromFile(torrentFile) 400 if err != nil { 401 return err 402 } 403 404 info, err := metaInfo.UnmarshalInfo() 405 if err != nil { 406 return err 407 } 408 409 hash := metaInfo.HashInfoBytes() 410 m.torrentTasks[id] = hash 411 412 if err != nil { 413 return err 414 } 415 416 torrent, err := m.torrentClient.AddTorrent(metaInfo) 417 if err != nil { 418 return err 419 } 420 421 torrent.DownloadAll() 422 423 m.publisher.publish(&Subscription{ 424 HistoryArchivesSeedingSignal: &signal.HistoryArchivesSeedingSignal{ 425 CommunityID: communityID.String(), 426 }, 427 }) 428 429 magnetLink := metaInfo.Magnet(nil, &info).String() 430 431 m.logger.Debug("seeding torrent", zap.String("id", id), zap.String("magnetLink", magnetLink)) 432 return nil 433 } 434 435 func (m *ArchiveManager) UnseedHistoryArchiveTorrent(communityID types.HexBytes) { 436 id := communityID.String() 437 438 hash, exists := m.torrentTasks[id] 439 440 if exists { 441 torrent, ok := m.torrentClient.Torrent(hash) 442 if ok { 443 m.logger.Debug("Unseeding and dropping torrent for community: ", zap.Any("id", id)) 444 torrent.Drop() 445 delete(m.torrentTasks, id) 446 447 m.publisher.publish(&Subscription{ 448 HistoryArchivesUnseededSignal: &signal.HistoryArchivesUnseededSignal{ 449 CommunityID: id, 450 }, 451 }) 452 } 453 } 454 } 455 456 func (m *ArchiveManager) IsSeedingHistoryArchiveTorrent(communityID types.HexBytes) bool { 457 id := communityID.String() 458 hash := m.torrentTasks[id] 459 torrent, ok := m.torrentClient.Torrent(hash) 460 return ok && torrent.Seeding() 461 } 462 463 func (m *ArchiveManager) GetHistoryArchiveDownloadTask(communityID string) *HistoryArchiveDownloadTask { 464 return m.historyArchiveDownloadTasks[communityID] 465 } 466 467 func (m *ArchiveManager) AddHistoryArchiveDownloadTask(communityID string, task *HistoryArchiveDownloadTask) { 468 m.historyArchiveDownloadTasks[communityID] = task 469 } 470 471 func (m *ArchiveManager) DownloadHistoryArchivesByMagnetlink(communityID types.HexBytes, magnetlink string, cancelTask chan struct{}) (*HistoryArchiveDownloadTaskInfo, error) { 472 473 id := communityID.String() 474 475 ml, err := metainfo.ParseMagnetUri(magnetlink) 476 if err != nil { 477 return nil, err 478 } 479 480 m.logger.Debug("adding torrent via magnetlink for community", zap.String("id", id), zap.String("magnetlink", magnetlink)) 481 torrent, err := m.torrentClient.AddMagnet(magnetlink) 482 if err != nil { 483 return nil, err 484 } 485 486 downloadTaskInfo := &HistoryArchiveDownloadTaskInfo{ 487 TotalDownloadedArchivesCount: 0, 488 TotalArchivesCount: 0, 489 Cancelled: false, 490 } 491 492 m.torrentTasks[id] = ml.InfoHash 493 timeout := time.After(20 * time.Second) 494 495 m.logger.Debug("fetching torrent info", zap.String("magnetlink", magnetlink)) 496 select { 497 case <-timeout: 498 return nil, ErrTorrentTimedout 499 case <-cancelTask: 500 m.logger.Debug("cancelled fetching torrent info") 501 downloadTaskInfo.Cancelled = true 502 return downloadTaskInfo, nil 503 case <-torrent.GotInfo(): 504 505 files := torrent.Files() 506 507 i, ok := findIndexFile(files) 508 if !ok { 509 // We're dealing with a malformed torrent, so don't do anything 510 return nil, errors.New("malformed torrent data") 511 } 512 513 indexFile := files[i] 514 indexFile.Download() 515 516 m.logger.Debug("downloading history archive index") 517 ticker := time.NewTicker(100 * time.Millisecond) 518 defer ticker.Stop() 519 520 for { 521 select { 522 case <-cancelTask: 523 m.logger.Debug("cancelled downloading archive index") 524 downloadTaskInfo.Cancelled = true 525 return downloadTaskInfo, nil 526 case <-ticker.C: 527 if indexFile.BytesCompleted() == indexFile.Length() { 528 529 index, err := m.ArchiveFileManager.LoadHistoryArchiveIndexFromFile(m.identity, communityID) 530 if err != nil { 531 return nil, err 532 } 533 534 existingArchiveIDs, err := m.persistence.GetDownloadedMessageArchiveIDs(communityID) 535 if err != nil { 536 return nil, err 537 } 538 539 if len(existingArchiveIDs) == len(index.Archives) { 540 m.logger.Debug("download cancelled, no new archives") 541 return downloadTaskInfo, nil 542 } 543 544 downloadTaskInfo.TotalDownloadedArchivesCount = len(existingArchiveIDs) 545 downloadTaskInfo.TotalArchivesCount = len(index.Archives) 546 547 archiveHashes := make(archiveMDSlice, 0, downloadTaskInfo.TotalArchivesCount) 548 549 for hash, metadata := range index.Archives { 550 archiveHashes = append(archiveHashes, &archiveMetadata{hash: hash, from: metadata.Metadata.From}) 551 } 552 553 sort.Sort(sort.Reverse(archiveHashes)) 554 555 m.publisher.publish(&Subscription{ 556 DownloadingHistoryArchivesStartedSignal: &signal.DownloadingHistoryArchivesStartedSignal{ 557 CommunityID: communityID.String(), 558 }, 559 }) 560 561 for _, hd := range archiveHashes { 562 563 hash := hd.hash 564 hasArchive := false 565 566 for _, existingHash := range existingArchiveIDs { 567 if existingHash == hash { 568 hasArchive = true 569 break 570 } 571 } 572 if hasArchive { 573 continue 574 } 575 576 metadata := index.Archives[hash] 577 startIndex := int(metadata.Offset) / pieceLength 578 endIndex := startIndex + int(metadata.Size)/pieceLength 579 580 downloadMsg := fmt.Sprintf("downloading data for message archive (%d/%d)", downloadTaskInfo.TotalDownloadedArchivesCount+1, downloadTaskInfo.TotalArchivesCount) 581 m.logger.Debug(downloadMsg, zap.String("hash", hash)) 582 m.logger.Debug("pieces (start, end)", zap.Any("startIndex", startIndex), zap.Any("endIndex", endIndex-1)) 583 torrent.DownloadPieces(startIndex, endIndex) 584 585 piecesCompleted := make(map[int]bool) 586 for i = startIndex; i < endIndex; i++ { 587 piecesCompleted[i] = false 588 } 589 590 psc := torrent.SubscribePieceStateChanges() 591 downloadTicker := time.NewTicker(1 * time.Second) 592 defer downloadTicker.Stop() 593 594 downloadLoop: 595 for { 596 select { 597 case <-downloadTicker.C: 598 done := true 599 for i = startIndex; i < endIndex; i++ { 600 piecesCompleted[i] = torrent.PieceState(i).Complete 601 if !piecesCompleted[i] { 602 done = false 603 } 604 } 605 if done { 606 psc.Close() 607 break downloadLoop 608 } 609 case <-cancelTask: 610 m.logger.Debug("downloading archive data interrupted") 611 downloadTaskInfo.Cancelled = true 612 return downloadTaskInfo, nil 613 } 614 } 615 downloadTaskInfo.TotalDownloadedArchivesCount++ 616 err = m.persistence.SaveMessageArchiveID(communityID, hash) 617 if err != nil { 618 m.logger.Error("couldn't save message archive ID", zap.Error(err)) 619 continue 620 } 621 m.publisher.publish(&Subscription{ 622 HistoryArchiveDownloadedSignal: &signal.HistoryArchiveDownloadedSignal{ 623 CommunityID: communityID.String(), 624 From: int(metadata.Metadata.From), 625 To: int(metadata.Metadata.To), 626 }, 627 }) 628 } 629 m.publisher.publish(&Subscription{ 630 HistoryArchivesSeedingSignal: &signal.HistoryArchivesSeedingSignal{ 631 CommunityID: communityID.String(), 632 }, 633 }) 634 m.logger.Debug("finished downloading archives") 635 return downloadTaskInfo, nil 636 } 637 } 638 } 639 } 640 } 641 642 func (m *ArchiveManager) TorrentFileExists(communityID string) bool { 643 _, err := os.Stat(torrentFile(m.torrentConfig.TorrentDir, communityID)) 644 return err == nil 645 } 646 647 func topicsAsByteArrays(topics []types.TopicType) [][]byte { 648 var topicsAsByteArrays [][]byte 649 for _, t := range topics { 650 topic := types.TopicTypeToByteArray(t) 651 topicsAsByteArrays = append(topicsAsByteArrays, topic) 652 } 653 return topicsAsByteArrays 654 } 655 656 func findIndexFile(files []*torrent.File) (index int, ok bool) { 657 for i, f := range files { 658 if f.DisplayPath() == "index" { 659 return i, true 660 } 661 } 662 return 0, false 663 } 664 665 func torrentFile(torrentDir, communityID string) string { 666 return path.Join(torrentDir, communityID+".torrent") 667 }