sparkz.core.NodeViewHolder.scala Maven / Gradle / Ivy
The newest version!
package sparkz.core
import akka.actor.Actor
import sparkz.core.consensus.History.ProgressInfo
import sparkz.core.consensus.{History, SyncInfo}
import sparkz.core.network.NodeViewSynchronizer.ReceivableMessages.NodeViewHolderEvent
import sparkz.core.settings.SparkzSettings
import sparkz.core.transaction._
import sparkz.core.transaction.state.{MinimalState, TransactionValidation}
import sparkz.core.transaction.wallet.Vault
import sparkz.util.SparkzEncoding
import sparkz.util.SparkzLogging
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
/**
* Composite local view of the node
*
* Contains instances for History, MinimalState, Vault, MemoryPool.
* The instances are read-only for external world.
* Updates of the composite view(the instances are to be performed atomically.
*
* @tparam TX
* @tparam PMOD
*/
trait NodeViewHolder[TX <: Transaction, PMOD <: PersistentNodeViewModifier]
extends Actor with SparkzLogging with SparkzEncoding {
import NodeViewHolder.ReceivableMessages._
import NodeViewHolder._
import sparkz.core.network.NodeViewSynchronizer.ReceivableMessages._
type SI <: SyncInfo
type HIS <: History[PMOD, SI, HIS]
type MS <: MinimalState[PMOD, MS]
type VL <: Vault[TX, PMOD, VL]
type MP <: MemoryPool[TX, MP]
type NodeView = (HIS, MS, VL, MP)
case class UpdateInformation(history: HIS,
state: MS,
failedMod: Option[PMOD],
alternativeProgressInfo: Option[ProgressInfo[PMOD]],
suffix: IndexedSeq[PMOD])
val sparksSettings: SparkzSettings
/**
* Cache for modifiers. If modifiers are coming out-of-order, they are to be stored in this cache.
*/
protected lazy val modifiersCache: ModifiersCache[PMOD, HIS] =
new DefaultModifiersCache[PMOD, HIS](sparksSettings.network.maxModifiersCacheSize)
/**
* The main data structure a node software is taking care about, a node view consists
* of four elements to be updated atomically: history (log of persistent modifiers),
* state (result of log's modifiers application to pre-historical(genesis) state,
* user-specific information stored in vault (it could be e.g. a wallet), and a memory pool.
*/
private var nodeView: NodeView = restoreState().getOrElse(genesisState)
/**
* Restore a local view during a node startup. If no any stored view found
* (e.g. if it is a first launch of a node) None is to be returned
*/
def restoreState(): Option[NodeView]
/**
* Hard-coded initial view all the honest nodes in a network are making progress from.
*/
protected def genesisState: NodeView
protected def history(): HIS = nodeView._1
protected def minimalState(): MS = nodeView._2
protected def vault(): VL = nodeView._3
protected def memoryPool(): MP = nodeView._4
protected def txModify(tx: TX): Unit = {
//todo: async validation?
val errorOpt: Option[Throwable] = minimalState() match {
case txValidator: TransactionValidation[TX @unchecked] =>
txValidator.validate(tx) match {
case Success(_) => None
case Failure(e) => Some(e)
}
case _ => None
}
errorOpt match {
case None =>
memoryPool().put(tx) match {
case Success(newPool) =>
log.debug(s"Unconfirmed transaction $tx added to the memory pool")
val newVault = vault().scanOffchain(tx)
updateNodeView(updatedVault = Some(newVault), updatedMempool = Some(newPool))
context.system.eventStream.publish(SuccessfulTransaction[TX](tx))
case Failure(e) =>
context.system.eventStream.publish(FailedTransaction(tx.id, e, immediateFailure = true))
}
case Some(e) =>
context.system.eventStream.publish(FailedTransaction(tx.id, e, immediateFailure = true))
}
}
/**
* Update NodeView with new components and notify subscribers of changed components
*
* @param updatedHistory
* @param updatedState
* @param updatedVault
* @param updatedMempool
*/
protected def updateNodeView(updatedHistory: Option[HIS] = None,
updatedState: Option[MS] = None,
updatedVault: Option[VL] = None,
updatedMempool: Option[MP] = None): Unit = {
val newNodeView = (updatedHistory.getOrElse(history()),
updatedState.getOrElse(minimalState()),
updatedVault.getOrElse(vault()),
updatedMempool.getOrElse(memoryPool()))
if (updatedHistory.nonEmpty) {
context.system.eventStream.publish(ChangedHistory(newNodeView._1.getReader))
}
if (updatedState.nonEmpty) {
context.system.eventStream.publish(ChangedState(newNodeView._2.getReader))
}
if (updatedVault.nonEmpty) {
context.system.eventStream.publish(ChangedVault(newNodeView._3.getReader))
}
if (updatedMempool.nonEmpty) {
context.system.eventStream.publish(ChangedMempool(newNodeView._4.getReader))
}
nodeView = newNodeView
}
protected def extractTransactions(mod: PMOD): Seq[TX] = mod match {
case tcm: TransactionsCarryingPersistentNodeViewModifier[TX @unchecked] => tcm.transactions
case _ => Seq()
}
//todo: this method causes delays in a block processing as it removes transactions from mempool and checks
//todo: validity of remaining transactions in a synchronous way. Do this job async!
protected def updateMemPool(blocksRemoved: Seq[PMOD], blocksApplied: Seq[PMOD], memPool: MP, state: MS): MP = {
if (sparksSettings.network.handlingTransactionsEnabled){
val rolledBackTxs = blocksRemoved.flatMap(extractTransactions)
val appliedTxs = blocksApplied.flatMap(extractTransactions)
memPool.putWithoutCheck(rolledBackTxs).filter { tx =>
!appliedTxs.exists(t => t.id == tx.id) && {
state match {
case v: TransactionValidation[TX @unchecked] => v.validate(tx).isSuccess
case _ => true
}
}
}
}
else
memPool
}
private def trimChainSuffix(suffix: IndexedSeq[PMOD], rollbackPoint: sparkz.util.ModifierId): IndexedSeq[PMOD] = {
val idx = suffix.indexWhere(_.id == rollbackPoint)
if (idx == -1) IndexedSeq() else suffix.drop(idx)
}
/**
*
* Assume that history knows the following blocktree:
*
* G
* / \
* * G
* / \
* * G
*
* where path with G-s is about canonical chain (G means semantically valid modifier), path with * is sidechain (* means
* that semantic validity is unknown). New modifier is coming to the sidechain, it sends rollback to the root +
* application of the sidechain to the state. Assume that state is finding that some modifier in the sidechain is
* incorrect:
*
* G
* / \
* G G
* / \
* B G
* /
* *
*
* In this case history should be informed about the bad modifier and it should retarget state
*
* //todo: improve the comment below
*
* We assume that we apply modifiers sequentially (on a single modifier coming from the network or generated locally),
* and in case of failed application of some modifier in a progressInfo, rollback point in an alternative should be not
* earlier than a rollback point of an initial progressInfo.
* */
@tailrec
protected final def updateState(history: HIS,
state: MS,
progressInfo: ProgressInfo[PMOD],
suffixApplied: IndexedSeq[PMOD]): (HIS, Try[MS], Seq[PMOD]) = {
val (stateToApplyTry: Try[MS], suffixTrimmed: IndexedSeq[PMOD]) = if (progressInfo.chainSwitchingNeeded) {
@SuppressWarnings(Array("org.wartremover.warts.OptionPartial"))
val branchingPoint = progressInfo.branchPoint.get //todo: .get
if (state.version != branchingPoint) {
state.rollbackTo(idToVersion(branchingPoint)) -> trimChainSuffix(suffixApplied, branchingPoint)
} else Success(state) -> IndexedSeq()
} else Success(state) -> suffixApplied
stateToApplyTry match {
case Success(stateToApply) =>
applyState(history, stateToApply, suffixTrimmed, progressInfo) match {
case Success(stateUpdateInfo) =>
stateUpdateInfo.failedMod match {
case Some(_) =>
@SuppressWarnings(Array("org.wartremover.warts.OptionPartial"))
val alternativeProgressInfo = stateUpdateInfo.alternativeProgressInfo.get
updateState(stateUpdateInfo.history, stateUpdateInfo.state, alternativeProgressInfo, stateUpdateInfo.suffix)
case None =>
(stateUpdateInfo.history, Success(stateUpdateInfo.state), stateUpdateInfo.suffix)
}
case Failure(ex) =>
(history, Failure(ex), suffixTrimmed)
}
case Failure(e) =>
log.error("Rollback failed: ", e)
context.system.eventStream.publish(RollbackFailed)
//todo: what to return here? the situation is totally wrong
???
}
}
private def applyState(history: HIS,
stateToApply: MS,
suffixTrimmed: IndexedSeq[PMOD],
progressInfo: ProgressInfo[PMOD]): Try[UpdateInformation] = {
val updateInfoSample = UpdateInformation(history, stateToApply, None, None, suffixTrimmed)
progressInfo.toApply.foldLeft[Try[UpdateInformation]](Success(updateInfoSample)) {
case (f@Failure(ex), _) =>
log.error("Reporting modifier failed", ex)
f
case (success@Success(updateInfo), modToApply) =>
if (updateInfo.failedMod.isEmpty) {
updateInfo.state.applyModifier(modToApply) match {
case Success(stateAfterApply) =>
history.reportModifierIsValid(modToApply).map { newHis =>
context.system.eventStream.publish(SemanticallySuccessfulModifier(modToApply))
UpdateInformation(newHis, stateAfterApply, None, None, updateInfo.suffix :+ modToApply)
}
case Failure(e) =>
history.reportModifierIsInvalid(modToApply, progressInfo).map { case (newHis, newProgressInfo) =>
context.system.eventStream.publish(SemanticallyFailedModification(modToApply, e))
UpdateInformation(newHis, updateInfo.state, Some(modToApply), Some(newProgressInfo), updateInfo.suffix)
}
}
} else success
}
}
//todo: update state in async way?
protected def pmodModify(pmod: PMOD): Unit =
if (!history().contains(pmod.id)) {
context.system.eventStream.publish(StartingPersistentModifierApplication(pmod))
log.info(s"Apply modifier ${pmod.encodedId} of type ${pmod.modifierTypeId} to nodeViewHolder")
history().append(pmod) match {
case Success((historyBeforeStUpdate, progressInfo)) =>
log.debug(s"Going to apply modifications to the state: $progressInfo")
context.system.eventStream.publish(SyntacticallySuccessfulModifier(pmod))
context.system.eventStream.publish(NewOpenSurface(historyBeforeStUpdate.openSurfaceIds()))
if (progressInfo.toApply.nonEmpty) {
val (newHistory, newStateTry, blocksApplied) =
updateState(historyBeforeStUpdate, minimalState(), progressInfo, IndexedSeq())
newStateTry match {
case Success(newMinState) =>
val newMemPool = updateMemPool(progressInfo.toRemove, blocksApplied, memoryPool(), newMinState)
//we consider that vault always able to perform a rollback needed
@SuppressWarnings(Array("org.wartremover.warts.OptionPartial"))
val newVault = if (progressInfo.chainSwitchingNeeded) {
vault().rollback(idToVersion(progressInfo.branchPoint.get)).get
} else vault()
blocksApplied.foreach(newVault.scanPersistent)
log.info(s"Persistent modifier ${pmod.encodedId} applied successfully")
updateNodeView(Some(newHistory), Some(newMinState), Some(newVault), Some(newMemPool))
case Failure(e) =>
log.warn(s"Can`t apply persistent modifier (id: ${pmod.encodedId}, contents: $pmod) to minimal state", e)
// not publishing SemanticallyFailedModification as this is an internal error
updateNodeView(updatedHistory = Some(newHistory))
}
} else {
updateNodeView(updatedHistory = Some(historyBeforeStUpdate))
}
case Failure(e) =>
log.warn(s"Can`t apply persistent modifier (id: ${pmod.encodedId}, contents: $pmod) to history", e)
context.system.eventStream.publish(SyntacticallyFailedModification(pmod, e))
}
} else {
log.warn(s"Trying to apply modifier ${pmod.encodedId} that's already in history")
}
/**
* Process new modifiers from remote.
* Put all candidates to modifiersCache and then try to apply as much modifiers from cache as possible.
* Clear cache if it's size exceeds size limit.
* Publish `ModifiersProcessingResult` message with all just applied and removed from cache modifiers.
*/
protected def processRemoteModifiers: Receive = {
case ModifiersFromRemote(mods: Seq[PMOD @unchecked]) =>
mods.foreach(m => modifiersCache.put(m.id, m))
log.debug(s"Cache size before: ${modifiersCache.size}")
@tailrec
def applyLoop(applied: Seq[PMOD]): Seq[PMOD] = {
modifiersCache.popCandidate(history()) match {
case Some(mod) =>
pmodModify(mod)
applyLoop(mod +: applied)
case None =>
applied
}
}
val applied = applyLoop(Seq())
val cleared = modifiersCache.cleanOverfull()
context.system.eventStream.publish(ModifiersProcessingResult(applied, cleared))
log.debug(s"Cache size after: ${modifiersCache.size}")
}
protected def transactionsProcessing: Receive = {
case newTxs: NewTransactions[TX @unchecked] =>
if (sparksSettings.network.handlingTransactionsEnabled)
newTxs.txs.foreach(txModify)
else
newTxs.txs.foreach(tx => context.system.eventStream.publish(
FailedTransaction(tx.id, new Exception("Transactions handling disabled"), immediateFailure = false)))
case EliminateTransactions(ids) =>
val updatedPool = memoryPool().filter(tx => !ids.contains(tx.id))
updateNodeView(updatedMempool = Some(updatedPool))
ids.foreach { id =>
val e = new Exception("Became invalid")
context.system.eventStream.publish(FailedTransaction(id, e, immediateFailure = false))
}
}
protected def processLocallyGeneratedModifiers: Receive = {
case lm: LocallyGeneratedModifier[PMOD @unchecked] =>
log.info(s"Got locally generated modifier ${lm.pmod.encodedId} of type ${lm.pmod.modifierTypeId}")
pmodModify(lm.pmod)
}
protected def getCurrentInfo: Receive = {
case GetDataFromCurrentView(f) =>
sender() ! f(CurrentView(history(), minimalState(), vault(), memoryPool()))
}
protected def getNodeViewChanges: Receive = {
case GetNodeViewChanges(history, state, vault, mempool) =>
if (history) sender() ! ChangedHistory(nodeView._1.getReader)
if (state) sender() ! ChangedState(nodeView._2.getReader)
if (vault) sender() ! ChangedVault(nodeView._3.getReader)
if (mempool) sender() ! ChangedMempool(nodeView._4.getReader)
}
override def receive: Receive =
processRemoteModifiers orElse
processLocallyGeneratedModifiers orElse
transactionsProcessing orElse
getCurrentInfo orElse
getNodeViewChanges orElse {
case a: Any => log.error("Strange input: " + a)
}
}
object NodeViewHolder {
object ReceivableMessages {
// Explicit request of NodeViewChange events of certain types.
case class GetNodeViewChanges(history: Boolean, state: Boolean, vault: Boolean, mempool: Boolean)
case class GetDataFromCurrentView[HIS, MS, VL, MP, A](f: CurrentView[HIS, MS, VL, MP] => A)
// Modifiers received from the remote peer with new elements in it
case class ModifiersFromRemote[PM <: PersistentNodeViewModifier](modifiers: Iterable[PM])
sealed trait NewTransactions[TX <: Transaction] {
val txs: Iterable[TX]
}
case class LocallyGeneratedTransaction[TX <: Transaction](tx: TX) extends NewTransactions[TX] {
override val txs: Iterable[TX] = Iterable(tx)
}
case class TransactionsFromRemote[TX <: Transaction](txs: Iterable[TX]) extends NewTransactions[TX]
case class LocallyGeneratedModifier[PMOD <: PersistentNodeViewModifier](pmod: PMOD)
case class EliminateTransactions(ids: Seq[sparkz.util.ModifierId])
}
// fixme: No actor is expecting this ModificationApplicationStarted and DownloadRequest messages
// fixme: Even more, ModificationApplicationStarted seems not to be sent at all
// fixme: should we delete these messages?
case class ModificationApplicationStarted[PMOD <: PersistentNodeViewModifier](modifier: PMOD)
extends NodeViewHolderEvent
case class CurrentView[HIS, MS, VL, MP](history: HIS, state: MS, vault: VL, pool: MP)
}