All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wavesplatform.network.RxExtensionLoader.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.network

import com.google.common.cache.{Cache, CacheBuilder}
import com.wavesplatform.block.Block
import com.wavesplatform.block.Block.BlockId
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.metrics.BlockStats
import com.wavesplatform.network.RxExtensionLoader.ApplierState.Buffer
import com.wavesplatform.network.RxExtensionLoader.LoaderState.WithPeer
import com.wavesplatform.network.RxScoreObserver.{ChannelClosedAndSyncWith, SyncWith}
import com.wavesplatform.state.ParSignatureChecker
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.utils.ScorexLogging
import io.netty.channel.*
import monix.eval.{Coeval, Task}
import monix.execution.CancelableFuture
import monix.execution.schedulers.SchedulerService
import monix.reactive.subjects.{ConcurrentSubject, Subject}
import monix.reactive.{Observable, Observer}

import java.util.concurrent.TimeUnit
import scala.concurrent.duration.*

case class ExtensionBlocks(remoteScore: BigInt, blocks: Seq[Block], snapshots: Map[BlockId, BlockSnapshotResponse]) {
  override def toString: String = s"ExtensionBlocks($remoteScore, ${formatSignatures(blocks.map(_.id()))}"
}

object RxExtensionLoader extends ScorexLogging {

  type ApplyExtensionResult = Either[ValidationError, Option[BigInt]]
  private val dummy = new Object()

  type BlockWithSnapshot = (Channel, Block, Option[BlockSnapshotResponse])

  def apply(
      syncTimeOut: FiniteDuration,
      processedBlocksCacheTimeout: FiniteDuration,
      isLightMode: Boolean,
      lastBlockIds: Coeval[Seq[ByteStr]],
      peerDatabase: PeerDatabase,
      invalidBlocks: InvalidBlockStorage,
      blocks: Observable[(Channel, Block)],
      signatures: Observable[(Channel, Signatures)],
      snapshots: Observable[(Channel, BlockSnapshotResponse)],
      syncWithChannelClosed: Observable[ChannelClosedAndSyncWith],
      scheduler: SchedulerService,
      timeoutSubject: Subject[Channel, Channel]
  )(
      extensionApplier: (Channel, ExtensionBlocks) => Task[ApplyExtensionResult]
  ): (Observable[BlockWithSnapshot], Coeval[State], RxExtensionLoaderShutdownHook) = {

    implicit val schdlr: SchedulerService = scheduler

    val extensions: ConcurrentSubject[(Channel, ExtensionBlocks), (Channel, ExtensionBlocks)] = ConcurrentSubject.publish[(Channel, ExtensionBlocks)]
    val simpleBlocksWithSnapshot
        : ConcurrentSubject[BlockWithSnapshot, BlockWithSnapshot] =
      ConcurrentSubject.publish[BlockWithSnapshot]
    @volatile var stateValue: State            = State(LoaderState.Idle, ApplierState.Idle)
    val lastSyncWith: Coeval[Option[SyncWith]] = lastObserved(syncWithChannelClosed.map(_.syncWith))

    val pendingBlocks     = cache[(Channel, BlockId), Block](syncTimeOut)
    val receivedSnapshots = cache[BlockId, Object](processedBlocksCacheTimeout)

    def scheduleBlacklist(ch: Channel, reason: String): Task[Unit] =
      Task {
        timeoutSubject.onNext(ch)
        peerDatabase.blacklistAndClose(ch, reason)
      }.delayExecution(syncTimeOut)

    def syncNext(state: State, syncWith: SyncWith = lastSyncWith().flatten): State =
      syncWith match {
        case None =>
          log.trace("Last bestChannel is None, state is up to date")
          state.withIdleLoader
        case Some(best) =>
          state.loaderState match {
            case wp: WithPeer =>
              log.trace(s"${id(wp.channel.channel)} Already syncing, no need to sync next, $state")
              state
            case LoaderState.Idle =>
              val maybeKnownSigs = state.applierState match {
                case ApplierState.Idle                => Some((lastBlockIds(), false))
                case ApplierState.Applying(None, ext) => Some((ext.blocks.map(_.id()).reverse, true))
                case _                                => None
              }
              maybeKnownSigs match {
                case Some((knownSigs, optimistic)) =>
                  val ch = best.channel
                  log.debug(
                    s"${id(ch)} Requesting signatures${if (optimistic) " optimistically" else ""}, last ${knownSigs.length} are ${formatSignatures(knownSigs)}"
                  )

                  val blacklisting = scheduleBlacklist(ch, s"Timeout loading extension").runAsyncLogErr
                  ch.writeAndFlush(GetSignatures(knownSigs)).addListener { (f: ChannelFuture) =>
                    if (!f.isSuccess) log.trace(s"Error requesting signatures: $ch", f.cause())
                  }

                  state.withLoaderState(LoaderState.ExpectingSignatures(best, knownSigs, blacklisting))
                case None =>
                  log.trace(s"Holding on requesting next sigs, $state")
                  state
              }
          }
      }

    def onNewSyncWithChannelClosed(state: State, cc: ChannelClosedAndSyncWith): State = {
      cc match {
        case ChannelClosedAndSyncWith(_, None) =>
          state.loaderState match {
            case _: LoaderState.WithPeer => state.withIdleLoader
            case _                       => state
          }
        case ChannelClosedAndSyncWith(None, Some(bestChannel)) =>
          log.trace(s"New SyncWith: $bestChannel, currentState = $state")
          syncNext(state, Some(bestChannel))
        case ChannelClosedAndSyncWith(Some(closedChannel), Some(bestChannel)) =>
          state.loaderState match {
            case wp: LoaderState.WithPeer if closedChannel != wp.channel.channel => state
            case _ =>
              log.trace(s"Switching to next best channel: state=$state, cc=$cc, bestChannel=$bestChannel")
              syncNext(state.withIdleLoader, Some(bestChannel))
          }
      }
    }

    def onNewSignatures(state: State, ch: Channel, sigs: Signatures): State = {
      state.loaderState match {
        case LoaderState.ExpectingSignatures(c, _, _) if c.channel == ch && sigs.signatures.isEmpty =>
          peerDatabase.blacklistAndClose(ch, s"Peer did not return any signatures and is likely on a fork")
          syncNext(state.withIdleLoader)
        case LoaderState.ExpectingSignatures(c, known, _) if c.channel == ch =>
          val (_, unknown) = sigs.signatures.span(id => known.contains(id))

          val firstInvalid = sigs.signatures.view.flatMap { sig =>
            invalidBlocks.find(sig).map(sig -> _)
          }.headOption

          firstInvalid match {
            case Some((invalidBlock, reason)) =>
              peerDatabase.blacklistAndClose(ch, s"Signatures contain invalid block(s): $invalidBlock, $reason")
              syncNext(state.withIdleLoader)
            case None =>
              if (unknown.isEmpty) {
                log.trace(s"${id(ch)} Received empty extension signatures list, sync with node complete")
                state.withIdleLoader
              } else {
                log.trace(s"${id(ch)} Requesting ${unknown.size} blocks")
                val blacklistingAsync = scheduleBlacklist(ch, "Timeout loading first requested block").runAsyncLogErr
                unknown.foreach { s =>
                  ch.write(GetBlock(s))
                  if (isLightMode) ch.write(GetSnapshot(s))
                }
                ch.flush()
                state.withLoaderState(
                  LoaderState.ExpectingBlocksWithSnapshots(
                    c,
                    unknown,
                    unknown.toSet,
                    Set.empty,
                    if (isLightMode) unknown.toSet else Set.empty,
                    Map.empty,
                    blacklistingAsync
                  )
                )
              }
          }
        case _ =>
          log.trace(s"${id(ch)} Received unexpected signatures ${formatSignatures(sigs.signatures)}, ignoring at $state")
          state
      }
    }

    def onBlock(state: State, ch: Channel, block: Block): State = {
      state.loaderState match {
        case LoaderState.ExpectingBlocksWithSnapshots(c, requested, expectedBlocks, receivedBlocks, expectedSnapshots, receivedSnapshots, _)
            if c.channel == ch && expectedBlocks.contains(block.id()) =>
          val updatedExpectedBlocks = expectedBlocks - block.id()

          BlockStats.received(block, BlockStats.Source.Ext, ch)
          ParSignatureChecker.checkBlockSignature(block)

          if (updatedExpectedBlocks.isEmpty && expectedSnapshots.isEmpty) {
            val blockById = (receivedBlocks + block).map(b => b.id() -> b).toMap
            val ext       = ExtensionBlocks(c.score, requested.map(blockById), receivedSnapshots)
            log.debug(s"${id(ch)} $ext successfully received")
            extensionLoadingFinished(state.withIdleLoader, ext, ch)
          } else {
            val blacklistAsync = scheduleBlacklist(
              ch,
              timeoutMsg(isLightMode, updatedExpectedBlocks.size, expectedSnapshots.size, requested)
            ).runAsyncLogErr

            state.withLoaderState(
              LoaderState.ExpectingBlocksWithSnapshots(
                c,
                requested,
                updatedExpectedBlocks,
                receivedBlocks + block,
                expectedSnapshots,
                receivedSnapshots,
                blacklistAsync
              )
            )
          }
        case _ =>
          BlockStats.received(block, BlockStats.Source.Broadcast, ch)
          if (!isLightMode || block.transactionData.isEmpty) {
            simpleBlocksWithSnapshot.onNext((ch, block, None))
          } else {
            val blockId = block.id()
            if (Option(receivedSnapshots.getIfPresent(blockId)).isEmpty) {
              pendingBlocks.put((ch, blockId), block)
              ch.writeAndFlush(GetSnapshot(blockId))
            }
          }
          state
      }
    }

    def onSnapshot(state: State, ch: Channel, snapshot: BlockSnapshotResponse): State = {
      if (isLightMode) {
        state.loaderState match {
          case LoaderState.ExpectingBlocksWithSnapshots(c, requested, expectedBlocks, receivedBlocks, expectedSnapshots, receivedSnapshots, _)
              if c.channel == ch && expectedSnapshots.contains(snapshot.blockId) =>
            val updatedExpectedSnapshots = expectedSnapshots - snapshot.blockId

            BlockStats.receivedSnapshot(snapshot.blockId, BlockStats.Source.Ext, ch)

            if (updatedExpectedSnapshots.isEmpty && expectedBlocks.isEmpty) {
              val blockById = receivedBlocks.map(b => b.id() -> b).toMap
              val ext       = ExtensionBlocks(c.score, requested.map(blockById), receivedSnapshots.updated(snapshot.blockId, snapshot))
              log.debug(s"${id(ch)} $ext successfully received")
              extensionLoadingFinished(state.withIdleLoader, ext, ch)
            } else {
              val blacklistAsync = scheduleBlacklist(
                ch,
                timeoutMsg(isLightMode, expectedBlocks.size, updatedExpectedSnapshots.size, requested)
              ).runAsyncLogErr
              state.withLoaderState(
                LoaderState.ExpectingBlocksWithSnapshots(
                  c,
                  requested,
                  expectedBlocks,
                  receivedBlocks,
                  updatedExpectedSnapshots,
                  receivedSnapshots.updated(snapshot.blockId, snapshot),
                  blacklistAsync
                )
              )
            }
          case _ =>
            BlockStats.receivedSnapshot(snapshot.blockId, BlockStats.Source.Broadcast, ch)
            Option(receivedSnapshots.getIfPresent(snapshot.blockId)) match {
              case Some(_) =>
                pendingBlocks.invalidate(snapshot.blockId)
                log.trace(s"${id(ch)} Received snapshot for processed block ${snapshot.blockId}, ignoring at $state")
              case _ =>
                Option(pendingBlocks.getIfPresent((ch, snapshot.blockId))) match {
                  case Some(block) =>
                    simpleBlocksWithSnapshot.onNext((ch, block, Some(snapshot)))
                    receivedSnapshots.put(snapshot.blockId, dummy)
                    pendingBlocks.invalidate(snapshot.blockId)
                  case None =>
                    log.trace(s"${id(ch)} Received unexpected snapshot ${snapshot.blockId}, ignoring at $state")
                }
            }

            state
        }
      } else {
        log.trace(s"${id(ch)} Received unexpected snapshot ${snapshot.blockId}, ignoring at $state")
        state
      }
    }

    def extensionLoadingFinished(state: State, extension: ExtensionBlocks, ch: Channel): State = {
      state.applierState match {
        case ApplierState.Idle =>
          extensions.onNext(ch -> extension)
          syncNext(state.copy(applierState = ApplierState.Applying(None, extension)))
        case s @ ApplierState.Applying(None, applying) =>
          log.trace(s"An optimistic extension was received: $extension, but applying $applying now")
          state.copy(applierState = s.copy(buf = Some(Buffer(ch, extension))))
        case _ =>
          log.warn(s"Overflow, discarding $extension")
          state
      }
    }

    def onExtensionApplied(state: State, extension: ExtensionBlocks, applicationResult: ApplyExtensionResult): State = {
      log.trace(s"Applying $extension finished with $applicationResult")
      state.applierState match {
        case ApplierState.Idle =>
          log.warn(s"Applied $extension but ApplierState is Idle")
          state
        case ApplierState.Applying(maybeBuffer, applying) =>
          if (applying != extension) log.warn(s"Applied $extension doesn't match expected $applying")
          maybeBuffer match {
            case None => state.copy(applierState = ApplierState.Idle)
            case Some(Buffer(nextChannel, nextExtension)) =>
              applicationResult match {
                case Left(_) =>
                  log.debug(s"Failed to apply $extension, discarding cached as well")
                  syncNext(state.copy(applierState = ApplierState.Idle))
                case Right(_) =>
                  log.trace(s"Successfully applied $extension, starting to apply an optimistically loaded one: $nextExtension")
                  extensions.onNext(nextChannel -> nextExtension)
                  syncNext(state.copy(applierState = ApplierState.Applying(None, nextExtension)))
              }
          }
      }
    }

    def appliedExtensions: Observable[(Channel, ExtensionBlocks, ApplyExtensionResult)] = {
      def apply(x: (Channel, ExtensionBlocks)): Task[ApplyExtensionResult] = Function.tupled(extensionApplier)(x)

      extensions.mapEval { x =>
        apply(x)
          .asyncBoundary(scheduler)
          .onErrorHandle { err =>
            log.error("Error during extension applying", err)
            Left(GenericError(err))
          }
          .map((x._1, x._2, _))
      }
    }

    Observable(
      signatures.observeOn(scheduler).map { case (ch, sigs) => stateValue = onNewSignatures(stateValue, ch, sigs) },
      blocks.observeOn(scheduler).map { case (ch, block) => stateValue = onBlock(stateValue, ch, block) },
      snapshots.observeOn(scheduler).map { case (ch, snapshot) => stateValue = onSnapshot(stateValue, ch, snapshot) },
      syncWithChannelClosed.observeOn(scheduler).map { ch =>
        stateValue = onNewSyncWithChannelClosed(stateValue, ch)
      },
      appliedExtensions.map { case (_, extensionBlocks, ar) => stateValue = onExtensionApplied(stateValue, extensionBlocks, ar) }
    ).merge
      .map { _ =>
        log.trace(s"Current state: $stateValue")
      }
      .logErr
      .subscribe()

    (simpleBlocksWithSnapshot, Coeval.eval(stateValue), RxExtensionLoaderShutdownHook(extensions, simpleBlocksWithSnapshot))
  }

  private def timeoutMsg(isLightMode: Boolean, totalLeftBlocks: Int, totalLeftSnapshots: Int, requested: Seq[BlockId]): String = {
    val snapshotShortMsg = if (isLightMode) " or snapshots" else ""
    val snapshotsInfo =
      if (isLightMode)
        s", non-received snapshots: ${if (totalLeftSnapshots == 1) s"one=${requested.last.trim}" else s"total=$totalLeftSnapshots"}"
      else ""
    s"Timeout loading one of requested blocks$snapshotShortMsg, non-received blocks: ${if (totalLeftBlocks == 1) s"one=${requested.last.trim}"
    else s"total=$totalLeftBlocks"}$snapshotsInfo"
  }

  private def cache[K <: AnyRef, V <: AnyRef](timeout: FiniteDuration): Cache[K, V] =
    CacheBuilder
      .newBuilder()
      .expireAfterWrite(timeout.toMillis, TimeUnit.MILLISECONDS)
      .build[K, V]()

  sealed trait LoaderState

  object LoaderState {

    sealed trait WithPeer extends LoaderState {
      def channel: BestChannel
      def timeout: CancelableFuture[Unit]
    }

    case object Idle extends LoaderState

    case class ExpectingSignatures(channel: BestChannel, known: Seq[BlockId], timeout: CancelableFuture[Unit]) extends WithPeer {
      override def toString: String = s"ExpectingSignatures($channel)"
    }

    case class ExpectingBlocksWithSnapshots(
        channel: BestChannel,
        allBlocks: Seq[BlockId],
        expectedBlocks: Set[BlockId],
        receivedBlocks: Set[Block],
        expectedSnapshots: Set[BlockId],
        receivedSnapshots: Map[BlockId, BlockSnapshotResponse],
        timeout: CancelableFuture[Unit]
    ) extends WithPeer {
      override def toString: String =
        s"ExpectingBlocks($channel,totalBlocks=${allBlocks.size},received=${receivedBlocks.size},expected=${if (expectedBlocks.size == 1) expectedBlocks.head.trim
        else expectedBlocks.size})"
    }

  }

  case class RxExtensionLoaderShutdownHook(
      extensionChannel: Observer[(Channel, ExtensionBlocks)],
      simpleBlocksWithSnapshotChannel: Observer[BlockWithSnapshot]
  ) {
    def shutdown(): Unit = {
      extensionChannel.onComplete()
      simpleBlocksWithSnapshotChannel.onComplete()
    }
  }

  case class State(loaderState: LoaderState, applierState: ApplierState) {
    def withLoaderState(newLoaderState: LoaderState): State = {
      loaderState match {
        case wp: WithPeer => wp.timeout.cancel()
        case _            =>
      }
      State(newLoaderState, applierState)
    }

    def withIdleLoader: State = withLoaderState(LoaderState.Idle)
  }

  sealed trait ApplierState

  object ApplierState {

    case object Idle extends ApplierState

    case class Buffer(ch: Channel, ext: ExtensionBlocks) {
      override def toString: String = s"Buffer($ext from ${id(ch)})"
    }

    case class Applying(buf: Option[Buffer], applying: ExtensionBlocks) extends ApplierState

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy