diff --git a/README.md b/README.md index 782ce71647..83aed14e54 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ To run specific Ergo version `` as a service with custom config `/path/ -e MAX_HEAP=3G \ ergoplatform/ergo: -- -c /etc/myergo.conf -Available versions can be found on [Ergo Docker image page](https://hub.docker.com/r/ergoplatform/ergo/tags), for example, `v4.0.35`. +Available versions can be found on [Ergo Docker image page](https://hub.docker.com/r/ergoplatform/ergo/tags), for example, `v4.0.36`. This will connect to the Ergo mainnet or testnet following your configuration passed in `myergo.conf` and network flag `--`. Every default config value would be overwritten with corresponding value in `myergo.conf`. `MAX_HEAP` variable can be used to control how much memory can the node consume. diff --git a/src/main/resources/api/openapi.yaml b/src/main/resources/api/openapi.yaml index 28fb20ea5b..5a158159ce 100644 --- a/src/main/resources/api/openapi.yaml +++ b/src/main/resources/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.2" info: - version: "4.0.35" + version: "4.0.36" title: Ergo Node API description: API docs for Ergo Node. Models are shared between all Ergo products contact: diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index fa78e5cab1..5bd1d0bc48 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -391,7 +391,7 @@ scorex { nodeName = "ergo-node" # Network protocol version to be sent in handshakes - appVersion = 4.0.35 + appVersion = 4.0.36 # Network agent name. May contain information about client code # stack, starting from core code-base up to the end graphical interface. diff --git a/src/main/resources/mainnet.conf b/src/main/resources/mainnet.conf index c5f85eb13f..b58dde2ff1 100644 --- a/src/main/resources/mainnet.conf +++ b/src/main/resources/mainnet.conf @@ -49,8 +49,8 @@ ergo { # The node still applying transactions to UTXO set and so checks UTXO set digests for each block. # Block at checkpoint height is to be checked against expected one. checkpoint = { - height = 798128 - blockId = "e7f9066562fa123cff39d4f30f8e205591f55f6ae4ac802efa850fd54d5507f1" + height = 804900 + blockId = "9d377e888dc7753ca5de3a7bc879afdc01993fd24f27bfe9c270d4828273c1a4" } # List with hex-encoded identifiers of transactions banned from getting into memory pool @@ -65,7 +65,7 @@ scorex { network { magicBytes = [1, 0, 2, 4] bindAddress = "0.0.0.0:9030" - nodeName = "ergo-mainnet-4.0.35" + nodeName = "ergo-mainnet-4.0.36" nodeName = ${?NODENAME} knownPeers = [ "213.239.193.208:9030", @@ -81,10 +81,6 @@ scorex { "176.9.65.58:9130", "213.152.106.56:9030" ] - - # Max number of delivery checks. Stop expecting modifier (and penalize peer) if it was not delivered after that - # number of delivery attempts - maxDeliveryChecks = 3 } restApi { diff --git a/src/main/resources/testnet.conf b/src/main/resources/testnet.conf index 09ad2312f9..8f06e53e70 100644 --- a/src/main/resources/testnet.conf +++ b/src/main/resources/testnet.conf @@ -77,7 +77,7 @@ scorex { network { magicBytes = [2, 0, 0, 2] bindAddress = "0.0.0.0:9020" - nodeName = "ergo-testnet-4.0.35" + nodeName = "ergo-testnet-4.0.36" nodeName = ${?NODENAME} knownPeers = [ "213.239.193.208:9020", diff --git a/src/main/scala/org/ergoplatform/http/api/ErgoBaseApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/ErgoBaseApiRoute.scala index 66d845eeef..d54cf93a64 100644 --- a/src/main/scala/org/ergoplatform/http/api/ErgoBaseApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/ErgoBaseApiRoute.scala @@ -1,20 +1,23 @@ package org.ergoplatform.http.api import akka.actor.ActorRef -import akka.http.scaladsl.server.{Directive1, ValidationRejection} +import akka.http.scaladsl.server.{Directive1, Route, ValidationRejection} import org.ergoplatform.modifiers.mempool.ErgoTransaction import org.ergoplatform.nodeView.ErgoReadersHolder.{GetReaders, Readers} import org.ergoplatform.nodeView.mempool.ErgoMemPoolReader import org.ergoplatform.nodeView.state.{ErgoStateReader, UtxoStateReader} import org.ergoplatform.settings.{Algos, ErgoSettings} -import scorex.core.api.http.ApiRoute +import scorex.core.api.http.{ApiError, ApiRoute} import scorex.util.{ModifierId, bytesToId} import akka.pattern.ask +import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction +import org.ergoplatform.nodeView.mempool.ErgoMemPool.ProcessingOutcome +import org.ergoplatform.nodeView.mempool.ErgoMemPool.ProcessingOutcome._ import scala.concurrent.{ExecutionContextExecutor, Future} import scala.util.{Success, Try} -trait ErgoBaseApiRoute extends ApiRoute { +trait ErgoBaseApiRoute extends ApiRoute with ApiCodecs { implicit val ec: ExecutionContextExecutor = context.dispatcher @@ -36,6 +39,25 @@ trait ErgoBaseApiRoute extends ApiRoute { } } + /** + * Send local transaction to ErgoNodeViewHolder + * @return Transaction Id with status OK(200), or BadRequest(400) + */ + protected def sendLocalTransactionRoute(nodeViewActorRef: ActorRef, tx: ErgoTransaction): Route = { + val resultFuture = + (nodeViewActorRef ? LocallyGeneratedTransaction(tx)) + .mapTo[ProcessingOutcome] + .flatMap { + case Accepted => Future.successful(tx.id) + case DoubleSpendingLoser(_) => Future.failed(new IllegalArgumentException("Double spending attempt")) + case Declined(ex) => Future.failed(ex) + case Invalidated(ex) => Future.failed(ex) + } + completeOrRecoverWith(resultFuture) { ex => + ApiError.BadRequest(ex.getMessage) + } + } + /** * Helper method to verify transaction against UTXO set (and unconfirmed outputs in the mempool), or check * transaction syntax only if UTXO set is not available (the node is running in "digest" mode) diff --git a/src/main/scala/org/ergoplatform/http/api/TransactionsApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/TransactionsApiRoute.scala index 8fd7b08f76..ee3e28ab9b 100644 --- a/src/main/scala/org/ergoplatform/http/api/TransactionsApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/TransactionsApiRoute.scala @@ -9,7 +9,6 @@ import org.ergoplatform.modifiers.mempool.ErgoTransaction import org.ergoplatform.nodeView.ErgoReadersHolder.{GetReaders, Readers} import org.ergoplatform.nodeView.mempool.ErgoMemPoolReader import org.ergoplatform.nodeView.mempool.HistogramStats.getFeeHistogram -import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction import org.ergoplatform.settings.ErgoSettings import scorex.core.api.http.ApiError.BadRequest import scorex.core.api.http.ApiResponse @@ -41,7 +40,7 @@ case class TransactionsApiRoute(readersHolder: ActorRef, p.getAll.slice(offset, offset + limit).map(_.asJson).asJson } - private def validateTransactionAndProcess(tx: ErgoTransaction)(processFn: ErgoTransaction => Any): Route = { + private def validateTransactionAndProcess(tx: ErgoTransaction)(processFn: ErgoTransaction => Route): Route = { if (tx.size > ergoSettings.nodeSettings.maxTransactionSize) { BadRequest(s"Transaction $tx has too large size ${tx.size}") } else { @@ -50,10 +49,7 @@ case class TransactionsApiRoute(readersHolder: ActorRef, } { _.fold( e => BadRequest(s"Malformed transaction: ${e.getMessage}"), - _ => { - processFn(tx) - ApiResponse(tx.id) - } + _ => processFn(tx) ) } } @@ -61,13 +57,11 @@ case class TransactionsApiRoute(readersHolder: ActorRef, def sendTransactionR: Route = (pathEnd & post & entity(as[ErgoTransaction])) { tx => - validateTransactionAndProcess(tx) { tx => - nodeViewActorRef ! LocallyGeneratedTransaction(tx) + validateTransactionAndProcess(tx)(validTx => sendLocalTransactionRoute(nodeViewActorRef, validTx)) } - } def checkTransactionR: Route = (path("check") & post & entity(as[ErgoTransaction])) { tx => - validateTransactionAndProcess(tx) { tx => tx } + validateTransactionAndProcess(tx)(validTx => ApiResponse(validTx.id)) } def getUnconfirmedTransactionsR: Route = (path("unconfirmed") & get & txPaging) { (offset, limit) => diff --git a/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala index d84658b04d..ed61fe5589 100644 --- a/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala @@ -16,7 +16,6 @@ import org.ergoplatform.wallet.interface4j.SecretString import org.ergoplatform.wallet.Constants import org.ergoplatform.wallet.Constants.ScanId import org.ergoplatform.wallet.boxes.ErgoBoxSerializer -import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction import scorex.core.api.http.ApiError.{BadRequest, NotExists} import scorex.core.api.http.ApiResponse import scorex.core.settings.RESTApiSettings @@ -158,10 +157,10 @@ case class WalletApiRoute(readersHolder: ActorRef, dataInputsRaw: Seq[String], verifyFn: ErgoTransaction => Future[Try[ErgoTransaction]], processFn: ErgoTransaction => Route): Route = { - withWalletOp(_.generateTransaction(requests, inputsRaw, dataInputsRaw).flatMap(txTry => txTry match { + withWalletOp(_.generateTransaction(requests, inputsRaw, dataInputsRaw).flatMap { case Success(tx) => verifyFn(tx) case f: Failure[ErgoTransaction] => Future(f) - })) { + }) { case Failure(e) => BadRequest(s"Bad request $requests. ${Option(e.getMessage).getOrElse(e.toString)}") case Success(tx) => processFn(tx) } @@ -187,10 +186,8 @@ case class WalletApiRoute(readersHolder: ActorRef, dataInputsRaw: Seq[String]): Route = { generateTransactionAndProcess(requests, inputsRaw, dataInputsRaw, tx => verifyTransaction(tx, readersHolder, ergoSettings), - { tx => - nodeViewActorRef ! LocallyGeneratedTransaction(tx) - ApiResponse(tx.id) - }) + validTx => sendLocalTransactionRoute(nodeViewActorRef, validTx) + ) } def sendTransactionR: Route = diff --git a/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala b/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala index c6dbc2867d..7899b3af33 100644 --- a/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala +++ b/src/main/scala/org/ergoplatform/local/ErgoStatsCollector.scala @@ -147,6 +147,8 @@ class ErgoStatsCollector(readersHolder: ActorRef, nodeInfo = nodeInfo.copy( stateRoot = Some(Algos.encode(fb.header.stateRoot)), stateVersion = Some(fb.encodedId)) + case SemanticallySuccessfulModifier(_) => + // Ignore other modifiers } } diff --git a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala index 3e5be95b0c..d36d3b0ecb 100644 --- a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala +++ b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala @@ -25,7 +25,6 @@ import org.ergoplatform.nodeView.wallet.ErgoWalletReader import scorex.core.network.message.{InvSpec, MessageSpec, ModifiersSpec, RequestModifierSpec} import scorex.core.network._ import scorex.core.network.message.{InvData, Message, ModifiersData} -import scorex.core.network.{ConnectedPeer, ModifiersStatus, SendToPeer, SendToPeers} import scorex.core.serialization.ScorexSerializer import scorex.core.settings.NetworkSettings import scorex.core.transaction.Transaction @@ -232,13 +231,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, * Processing sync V1 message `syncInfo` got from neighbour peer `remote` */ protected def processSyncV1(hr: ErgoHistory, syncInfo: ErgoSyncInfoV1, remote: ConnectedPeer): Unit = { - val comparison = hr.compare(syncInfo) - log.debug(s"Comparison with $remote having starting points ${syncInfo.lastHeaderIds}. " + - s"Comparison result is $comparison.") - - val oldStatus = syncTracker.getStatus(remote).getOrElse(Unknown) - val status = comparison - syncTracker.updateStatus(remote, status, height = None) + val (status, syncSendNeeded) = syncTracker.updateStatus(remote, syncInfo, hr) status match { case Unknown => @@ -279,7 +272,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, log.debug(s"$remote has equal header-chain") } - if ((oldStatus != status) || syncTracker.notSyncedOrOutdated(remote) || status == Older || status == Fork) { + if (syncSendNeeded) { val ownSyncInfo = getV1SyncInfo(hr) sendSyncToPeer(remote, ownSyncInfo) } @@ -289,12 +282,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, * Processing sync V2 message `syncInfo` got from neighbour peer `remote` (supporting sync v2) */ protected def processSyncV2(hr: ErgoHistory, syncInfo: ErgoSyncInfoV2, remote: ConnectedPeer): Unit = { - val oldStatus = syncTracker.getStatus(remote).getOrElse(Unknown) - val status = hr.compare(syncInfo) - syncTracker.updateStatus(remote, status, syncInfo.height) - - log.debug(s"Comparison with $remote having starting points ${syncInfo.lastHeaders}. " + - s"Comparison result is $status.") + val (status, syncSendNeeded) = syncTracker.updateStatus(remote, syncInfo, hr) status match { case Unknown => @@ -330,7 +318,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, log.debug(s"$remote has equal header-chain") } - if ((oldStatus != status) || syncTracker.notSyncedOrOutdated(remote) || status == Older || status == Fork) { + if (syncSendNeeded) { val ownSyncInfo = getV2SyncInfo(hr, full = true) sendSyncToPeer(remote, ownSyncInfo) } @@ -775,7 +763,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, networkControllerRef ! PenalizePeer(peer.connectionId.remoteAddress, PenaltyType.MisbehaviorPenalty) } - protected def penalizeMaliciousPeer(peer: ConnectedPeer): Unit = { + override protected def penalizeMaliciousPeer(peer: ConnectedPeer): Unit = { networkControllerRef ! PenalizePeer(peer.connectionId.remoteAddress, PenaltyType.PermanentPenalty) } diff --git a/src/main/scala/org/ergoplatform/network/ErgoSyncTracker.scala b/src/main/scala/org/ergoplatform/network/ErgoSyncTracker.scala index 95daae1246..3c0fcec684 100644 --- a/src/main/scala/org/ergoplatform/network/ErgoSyncTracker.scala +++ b/src/main/scala/org/ergoplatform/network/ErgoSyncTracker.scala @@ -1,7 +1,7 @@ package org.ergoplatform.network -import org.ergoplatform.nodeView.history.ErgoHistory +import org.ergoplatform.nodeView.history.{ErgoHistory, ErgoHistoryReader, ErgoSyncInfo, ErgoSyncInfoV1, ErgoSyncInfoV2} import org.ergoplatform.nodeView.history.ErgoHistory.Height import scorex.core.consensus.{Fork, PeerChainStatus, Older, Unknown} import scorex.core.network.ConnectedPeer @@ -18,6 +18,11 @@ final case class ErgoSyncTracker(networkSettings: NetworkSettings, timeProvider: private val MinSyncInterval: FiniteDuration = 20.seconds private val SyncThreshold: FiniteDuration = 1.minute + /** + * After this timeout we clear peer's status + */ + private val ClearThreshold: FiniteDuration = 3.minutes + private[network] val statuses = mutable.Map[ConnectedPeer, ErgoPeerStatus]() def fullInfo(): Iterable[ErgoPeerStatus] = statuses.values @@ -42,6 +47,28 @@ final case class ErgoSyncTracker(networkSettings: NetworkSettings, timeProvider: notSyncedOrMissing || outdated } + /** + * Obtains peer sync status from `syncInfo` network message and updates statuses table with it + * + * @return (new peer status, should our node send sync message to the peer) + */ + def updateStatus(peer: ConnectedPeer, + syncInfo: ErgoSyncInfo, + hr: ErgoHistoryReader): (PeerChainStatus, Boolean) = { + val oldStatus = getStatus(peer).getOrElse(Unknown) + val status = hr.compare(syncInfo) + + val height = syncInfo match { + case _: ErgoSyncInfoV1 => None + case sv2: ErgoSyncInfoV2 => sv2.height + } + updateStatus(peer, status, height) + + val syncSendNeeded = (oldStatus != status) || notSyncedOrOutdated(peer) || status == Older || status == Fork + + (status, syncSendNeeded) + } + def updateStatus(peer: ConnectedPeer, status: PeerChainStatus, height: Option[Height]): Unit = { @@ -55,7 +82,6 @@ final case class ErgoSyncTracker(networkSettings: NetworkSettings, timeProvider: val seniorsAfter = numOfSeniors() - // todo: we should also send NoBetterNeighbour signal when all the peers around are not seniors initially if (seniorsBefore > 0 && seniorsAfter == 0) { log.info("Syncing is done, switching to stable regime") // todo: update neighbours status ? @@ -88,7 +114,22 @@ final case class ErgoSyncTracker(networkSettings: NetworkSettings, timeProvider: } } - protected[network] def outdatedPeers: IndexedSeq[ConnectedPeer] = { + /** + * Helper method to clear statuses of peers not updated for long enough + */ + private[network] def clearOldStatuses(): Unit = { + val currentTime = timeProvider.time() + val peersToClear = statuses.filter { case (_, status) => + status.lastSyncSentTime.exists(syncTime => (currentTime - syncTime).millis > ClearThreshold) + }.keys + if (peersToClear.nonEmpty) { + log.debug(s"Clearing stalled statuses for $peersToClear") + // we set status to `Unknown` and reset peer's height + peersToClear.foreach(p => updateStatus(p, Unknown, None)) + } + } + + private[network] def outdatedPeers: IndexedSeq[ConnectedPeer] = { val currentTime = timeProvider.time() statuses.filter { case (_, status) => status.lastSyncSentTime.exists(syncTime => (currentTime - syncTime).millis > SyncThreshold) @@ -121,6 +162,7 @@ final case class ErgoSyncTracker(networkSettings: NetworkSettings, timeProvider: * Updates lastSyncSentTime for all returned peers as a side effect */ def peersToSyncWith(): IndexedSeq[ConnectedPeer] = { + clearOldStatuses() val outdated = outdatedPeers val peers = if (outdated.nonEmpty) { @@ -130,8 +172,13 @@ final case class ErgoSyncTracker(networkSettings: NetworkSettings, timeProvider: val unknowns = statuses.filter(_._2.status == Unknown).toVector val forks = statuses.filter(_._2.status == Fork).toVector val elders = statuses.filter(_._2.status == Older).toVector - val nonOutdated = - (if (elders.nonEmpty) elders(scala.util.Random.nextInt(elders.size)) +: unknowns else unknowns) ++ forks + + val eldersAndUnknown = if (elders.nonEmpty) { + elders(scala.util.Random.nextInt(elders.size)) +: unknowns + } else { + unknowns + } + val nonOutdated = eldersAndUnknown ++ forks nonOutdated.filter { case (_, status) => (currentTime - status.lastSyncSentTime.getOrElse(0L)).millis >= MinSyncInterval }.map(_._1) diff --git a/src/main/scala/org/ergoplatform/nodeView/ErgoNodeViewHolder.scala b/src/main/scala/org/ergoplatform/nodeView/ErgoNodeViewHolder.scala index 1ef4340e85..a0a27fd890 100644 --- a/src/main/scala/org/ergoplatform/nodeView/ErgoNodeViewHolder.scala +++ b/src/main/scala/org/ergoplatform/nodeView/ErgoNodeViewHolder.scala @@ -257,22 +257,24 @@ abstract class ErgoNodeViewHolder[State <: ErgoState[State]](settings: ErgoSetti } } - protected def txModify(tx: ErgoTransaction): Unit = { - memoryPool().process(tx, minimalState()) match { - case (newPool, ProcessingOutcome.Accepted) => + protected def txModify(tx: ErgoTransaction): ProcessingOutcome = { + val (newPool, processingOutcome) = memoryPool().process(tx, minimalState()) + processingOutcome match { + case ProcessingOutcome.Accepted => 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)) - case (newPool, ProcessingOutcome.Invalidated(e)) => + case ProcessingOutcome.Invalidated(e) => log.debug(s"Transaction $tx invalidated. Cause: ${e.getMessage}") updateNodeView(updatedMempool = Some(newPool)) context.system.eventStream.publish(FailedTransaction(tx.id, e, immediateFailure = true)) - case (_, ProcessingOutcome.DoubleSpendingLoser(winnerTxs)) => // do nothing + case ProcessingOutcome.DoubleSpendingLoser(winnerTxs) => // do nothing log.debug(s"Transaction $tx declined, as other transactions $winnerTxs are paying more") - case (_, ProcessingOutcome.Declined(e)) => // do nothing + case ProcessingOutcome.Declined(e) => // do nothing log.debug(s"Transaction $tx declined, reason: ${e.getMessage}") } + processingOutcome } /** @@ -577,8 +579,10 @@ abstract class ErgoNodeViewHolder[State <: ErgoState[State]](settings: ErgoSetti } protected def transactionsProcessing: Receive = { - case newTxs: NewTransactions => - newTxs.txs.foreach(txModify) + case TransactionsFromRemote(txs) => + txs.foreach(txModify) + case LocallyGeneratedTransaction(tx) => + sender() ! txModify(tx) case EliminateTransactions(ids) => val updatedPool = memoryPool().filter(tx => !ids.contains(tx.id)) updateNodeView(updatedMempool = Some(updatedPool)) diff --git a/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala b/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala index a57872e57a..564c14e13b 100644 --- a/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala +++ b/src/main/scala/org/ergoplatform/nodeView/history/ErgoHistoryReader.scala @@ -147,13 +147,13 @@ trait ErgoHistoryReader val otherHeaders = info.lastHeaders val otherLastHeader = otherHeaders.head // always available val otherHeight = otherLastHeader.height - // todo: check PoW of otherLastHeader if (otherHeight == myHeight) { if (otherLastHeader.id == myLastHeader.id) { // Last headers are the same => chains are equal Equal } else { + // todo: check PoW of otherLastHeader if (commonPoint(otherHeaders.tail).isDefined) { Fork } else { diff --git a/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala b/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala index 3300c25c94..4d70d344bf 100644 --- a/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala +++ b/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala @@ -184,7 +184,7 @@ class UtxoState(override val persistentProver: PersistentBatchAVLProver[Digest32 } stateTry.recoverWith[UtxoState] { case e => log.warn(s"Error while applying full block with header ${fb.header.encodedId} to UTXOState with root" + - s" ${Algos.encode(inRoot)}, reason: ${LoggingUtil.getReasonMsg(e)} ") + s" ${Algos.encode(inRoot)}, reason: ${LoggingUtil.getReasonMsg(e)} ", e) persistentProver.rollback(inRoot) .ensuring(java.util.Arrays.equals(persistentProver.digest, inRoot)) Failure(e) diff --git a/src/main/scala/scorex/core/network/PeerConnectionHandler.scala b/src/main/scala/scorex/core/network/PeerConnectionHandler.scala index 0e7b0e3255..618ba7656c 100644 --- a/src/main/scala/scorex/core/network/PeerConnectionHandler.scala +++ b/src/main/scala/scorex/core/network/PeerConnectionHandler.scala @@ -1,6 +1,6 @@ package scorex.core.network -import akka.actor.{Actor, ActorRef, ActorSystem, Cancellable, Props, SupervisorStrategy} +import akka.actor.{Actor, ActorRef, Cancellable, Props, SupervisorStrategy} import akka.io.Tcp import akka.io.Tcp._ import akka.util.{ByteString, CompactByteString} @@ -137,7 +137,9 @@ class PeerConnectionHandler(scorexSettings: ScorexSettings, "closed by the peer" } else if (cc.isAborted) { "aborted locally" - } else "" + } else { + "" + } log.info(s"Connection closed to $connectionId, reason: " + reason) context stop self } @@ -256,11 +258,7 @@ class PeerConnectionHandler(scorexSettings: ScorexSettings, object PeerConnectionHandler { object ReceivableMessages { - - private[PeerConnectionHandler] object HandshakeDone - - case object StartInteraction - + case object HandshakeTimeout case object CloseConnection @@ -272,6 +270,7 @@ object PeerConnectionHandler { } object PeerConnectionHandlerRef { + def props(settings: ScorexSettings, networkControllerRef: ActorRef, scorexContext: ScorexContext, @@ -279,18 +278,4 @@ object PeerConnectionHandlerRef { )(implicit ec: ExecutionContext): Props = Props(new PeerConnectionHandler(settings, networkControllerRef, scorexContext, connectionDescription)) - def apply(settings: ScorexSettings, - networkControllerRef: ActorRef, - scorexContext: ScorexContext, - connectionDescription: ConnectionDescription) - (implicit system: ActorSystem, ec: ExecutionContext): ActorRef = - system.actorOf(props(settings, networkControllerRef, scorexContext, connectionDescription)) - - def apply(name: String, - settings: ScorexSettings, - networkControllerRef: ActorRef, - scorexContext: ScorexContext, - connectionDescription: ConnectionDescription) - (implicit system: ActorSystem, ec: ExecutionContext): ActorRef = - system.actorOf(props(settings, networkControllerRef, scorexContext, connectionDescription), name) } diff --git a/src/main/scala/scorex/core/network/PeerSynchronizer.scala b/src/main/scala/scorex/core/network/PeerSynchronizer.scala index e72a82eb75..60fd6acee8 100644 --- a/src/main/scala/scorex/core/network/PeerSynchronizer.scala +++ b/src/main/scala/scorex/core/network/PeerSynchronizer.scala @@ -102,7 +102,6 @@ object PeerSynchronizerRef { featureSerializers: PeerFeature.Serializers)(implicit ec: ExecutionContext): Props = Props(new PeerSynchronizer(networkControllerRef, peerManager, settings, featureSerializers)) - def apply(name: String, networkControllerRef: ActorRef, peerManager: ActorRef, settings: NetworkSettings, featureSerializers: PeerFeature.Serializers)(implicit system: ActorSystem, ec: ExecutionContext): ActorRef = system.actorOf(props(networkControllerRef, peerManager, settings, featureSerializers), name) diff --git a/src/test/scala/org/ergoplatform/http/routes/TransactionApiRouteSpec.scala b/src/test/scala/org/ergoplatform/http/routes/TransactionApiRouteSpec.scala index 6d1a6d49c8..aef347df8a 100644 --- a/src/test/scala/org/ergoplatform/http/routes/TransactionApiRouteSpec.scala +++ b/src/test/scala/org/ergoplatform/http/routes/TransactionApiRouteSpec.scala @@ -61,6 +61,15 @@ class TransactionApiRouteSpec extends AnyFlatSpec } } + it should "fail when posting invalid transaction" in { + val failingNodeViewRef = system.actorOf(NodeViewStub.failingProps()) + val failingRoute: Route = TransactionsApiRoute(digestReadersRef, failingNodeViewRef, settings).route + + Post(prefix, tx.asJson) ~> failingRoute ~> check { + status shouldBe StatusCodes.BadRequest + } + } + it should "post chained transactions" in { Post(prefix, chainedTx.asJson) ~> route ~> check { status shouldBe StatusCodes.BadRequest diff --git a/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala b/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala index b8b240eaf1..5c08932ea3 100644 --- a/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala +++ b/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala @@ -35,6 +35,8 @@ class WalletApiRouteSpec extends AnyFlatSpec val ergoSettings: ErgoSettings = ErgoSettings.read( Args(userConfigPathOpt = Some("src/test/resources/application.conf"), networkTypeOpt = None)) val route: Route = WalletApiRoute(digestReadersRef, nodeViewRef, settings).route + val failingNodeViewRef = system.actorOf(NodeViewStub.failingProps()) + val failingRoute: Route = WalletApiRoute(digestReadersRef, failingNodeViewRef, settings).route val utxoRoute: Route = WalletApiRoute(utxoReadersRef, nodeViewRef, settings).route @@ -88,6 +90,12 @@ class WalletApiRouteSpec extends AnyFlatSpec } } + it should "fail when sent transaction is invalid" in { + Post(prefix + "/transaction/send", requestsHolder.asJson) ~> failingRoute ~> check { + status shouldBe StatusCodes.BadRequest + } + } + it should "sign a transaction" in { val digest = Random.nextBoolean() val (tsr, r) = if (digest) { @@ -108,6 +116,12 @@ class WalletApiRouteSpec extends AnyFlatSpec } } + it should "fail when payment is invalid" in { + Post(prefix + "/payment/send", Seq(paymentRequest).asJson) ~> failingRoute ~> check { + status shouldBe StatusCodes.BadRequest + } + } + it should "return addresses" in { Get(prefix + "/addresses") ~> route ~> check { status shouldBe StatusCodes.OK diff --git a/src/test/scala/org/ergoplatform/local/MempoolAuditorSpec.scala b/src/test/scala/org/ergoplatform/local/MempoolAuditorSpec.scala index 7fd092dd83..d5799b60ed 100644 --- a/src/test/scala/org/ergoplatform/local/MempoolAuditorSpec.scala +++ b/src/test/scala/org/ergoplatform/local/MempoolAuditorSpec.scala @@ -12,6 +12,7 @@ import org.scalatest.flatspec.AnyFlatSpec import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction import scorex.core.network.NetworkController.ReceivableMessages.SendToNetwork import org.ergoplatform.network.ErgoNodeViewSynchronizer.ReceivableMessages.{ChangedMempool, ChangedState, FailedTransaction, SuccessfulTransaction} +import org.ergoplatform.nodeView.mempool.ErgoMemPool.ProcessingOutcome import sigmastate.Values.ErgoTree import sigmastate.eval.{IRContext, RuntimeIRContext} import sigmastate.interpreter.Interpreter.emptyEnv @@ -66,8 +67,12 @@ class MempoolAuditorSpec extends AnyFlatSpec with NodeViewTestOps with ErgoTestH subscribeEvents(classOf[FailedTransaction]) nodeViewHolderRef ! LocallyGeneratedTransaction(validTx) testProbe.expectMsgClass(cleanupDuration, newTx) + expectMsgType[ProcessingOutcome.Accepted.type] + nodeViewHolderRef ! LocallyGeneratedTransaction(temporarilyValidTx) testProbe.expectMsgClass(cleanupDuration, newTx) + expectMsgType[ProcessingOutcome.Accepted.type] + getPoolSize shouldBe 2 val _: ActorRef = MempoolAuditorRef(nodeViewHolderRef, nodeViewHolderRef, settingsToTest) diff --git a/src/test/scala/org/ergoplatform/nodeView/viewholder/ErgoNodeViewHolderSpec.scala b/src/test/scala/org/ergoplatform/nodeView/viewholder/ErgoNodeViewHolderSpec.scala index c5c034bd1b..9f4fcf591d 100644 --- a/src/test/scala/org/ergoplatform/nodeView/viewholder/ErgoNodeViewHolderSpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/viewholder/ErgoNodeViewHolderSpec.scala @@ -13,6 +13,7 @@ import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages._ import org.ergoplatform.network.ErgoNodeViewSynchronizer.ReceivableMessages._ import org.ergoplatform.nodeView.ErgoNodeViewHolder import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.ChainProgress +import org.ergoplatform.nodeView.mempool.ErgoMemPool.ProcessingOutcome.Accepted import scorex.crypto.authds.{ADKey, SerializedAdProof} import scorex.testkit.utils.NoShrink import scorex.util.{ModifierId, bytesToId} @@ -146,7 +147,7 @@ class ErgoNodeViewHolderSpec extends ErgoPropertyTest with HistoryTestHelpers wi val tx = validTransactionFromBoxes(boxes.toIndexedSeq) subscribeEvents(classOf[FailedTransaction]) nodeViewHolderRef ! LocallyGeneratedTransaction(tx) - expectNoMsg() + expectMsg(Accepted) getPoolSize shouldBe 1 } } diff --git a/src/test/scala/org/ergoplatform/utils/Stubs.scala b/src/test/scala/org/ergoplatform/utils/Stubs.scala index ccdc94653f..a2c4aea085 100644 --- a/src/test/scala/org/ergoplatform/utils/Stubs.scala +++ b/src/test/scala/org/ergoplatform/utils/Stubs.scala @@ -9,9 +9,11 @@ import org.ergoplatform.mining.{AutolykosSolution, CandidateGenerator, ErgoMiner import org.ergoplatform.modifiers.ErgoFullBlock import org.ergoplatform.modifiers.history.header.Header import org.ergoplatform.modifiers.mempool.ErgoTransaction +import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction import org.ergoplatform.nodeView.ErgoReadersHolder.{GetDataFromHistory, GetReaders, Readers} import org.ergoplatform.nodeView.history.ErgoHistory import org.ergoplatform.nodeView.mempool.ErgoMemPool +import org.ergoplatform.nodeView.mempool.ErgoMemPool.ProcessingOutcome.{Accepted, Invalidated} import org.ergoplatform.nodeView.state.wrapped.WrappedUtxoState import org.ergoplatform.nodeView.state.{DigestState, ErgoStateContext, StateType} import org.ergoplatform.nodeView.wallet.ErgoWalletActor._ @@ -117,12 +119,23 @@ trait Stubs extends ErgoGenerators with ErgoTestHelpers with ChainGenerator with class NodeViewStub extends Actor { def receive: Receive = { + case LocallyGeneratedTransaction(_) => + sender() ! Accepted + case _ => + } + } + + class FailingNodeViewStub extends Actor { + def receive: Receive = { + case LocallyGeneratedTransaction(_) => + sender() ! Invalidated(new Error("Transaction invalid")) case _ => } } object NodeViewStub { def props(): Props = Props(new NodeViewStub) + def failingProps(): Props = Props(new FailingNodeViewStub) } class NetworkControllerStub extends Actor {