diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..32c78f052b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI +on: + pull_request: + push: +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.12.10, 2.11.12] + java: [adopt@1.8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Java and Scala + uses: olafurpg/setup-scala@v10 + with: + java-version: ${{ matrix.java }} + + - name: Cache sbt + uses: actions/cache@v2 + with: + path: | + ~/.sbt + ~/.ivy2/cache + ~/.coursier/cache/v1 + ~/.cache/coursier/v1 + ~/AppData/Local/Coursier/Cache/v1 + ~/Library/Caches/Coursier/v1 + key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + + - name: Runs tests for the wallet + run: sbt +ergoWallet/test + + - name: Runs tests + run: sbt -Denv=test clean test + + - name: Publish a wallet snapshot ${{ github.ref }} + run: sbt ++${{ matrix.scala }} +ergoWallet/publish + env: + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..987c720c4d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Publish a release + +on: + release: + types: [published] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + publish_release: + name: Publish release to Sonatype + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup Java and Scala + uses: olafurpg/setup-scala@v10 + with: + java-version: adopt@1.8 + + - name: Cache sbt + uses: actions/cache@v2 + with: + path: | + ~/.sbt + ~/.ivy2/cache + ~/.coursier/cache/v1 + ~/.cache/coursier/v1 + ~/AppData/Local/Coursier/Cache/v1 + ~/Library/Caches/Coursier/v1 + key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + + - name: Publish release ${{ github.ref }} + run: ci/publish_release_gpg2.sh + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} \ No newline at end of file diff --git a/README.md b/README.md index 58947ac215..edc47e7725 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.12`. +Available versions can be found on [Ergo Docker image page](https://hub.docker.com/r/ergoplatform/ergo/tags), for example, `v4.0.13`. 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/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala index 42a8400e08..14d284b081 100644 --- a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala +++ b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/interpreter/ErgoInterpreter.scala @@ -119,8 +119,8 @@ object ErgoInterpreter { * Preforms pre-compilation of the given scripts during instantiation. * Keeps pre-compiled data structures for the lifetime of JVM. */ - val scriptProcessor: PrecompiledScriptProcessor = { - /** Script compilation requires an instance of [[SigmaValidationSettings]]. + lazy val scriptProcessor: PrecompiledScriptProcessor = { + /** Script compilation requires an instance of [[org.ergoplatform.validation.SigmaValidationSettings]]. * The only way to pass it to the CacheLoader is via cache key. * So here we augment each script bytes with the instance of validation settings. */ diff --git a/src/main/resources/api/openapi.yaml b/src/main/resources/api/openapi.yaml index 39c820cbbc..b849ab612b 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.12" + version: "4.0.13" title: Ergo Node API description: API docs for Ergo Node. Models are shared between all Ergo products contact: @@ -1114,7 +1114,7 @@ components: $ref: '#/components/schemas/ErgoAddress' ergValue: description: Optional, amount of ergs to be put into box with issued assets - type: integer + type: int64 amount: description: Supply amount type: integer @@ -1180,7 +1180,7 @@ components: type: string example: '0000000000000000' d: - type: integer + type: number example: 987654321 BlockHeaderWithoutPow: @@ -1530,9 +1530,24 @@ components: - currentNetworkTime properties: lastIncomingMessage: - $ref: '#/components/schemas/Timestamp' + type: integer + allOf: + - $ref: '#/components/schemas/Timestamp' currentNetworkTime: - $ref: '#/components/schemas/Timestamp' + type: integer + allOf: + - $ref: '#/components/schemas/Timestamp' + + BlacklistedPeers: + type: object + required: + - addresses + properties: + addresses: + type: array + items: + type: string + description: Blacklisted node address NodeInfo: type: object @@ -1871,7 +1886,7 @@ components: Timestamp: description: Basic timestamp definition type: integer - format: int32 + format: int64 example: 1524143059077 EmissionInfo: @@ -2730,10 +2745,8 @@ paths: content: application/json: schema: - type: array - items: - type: string - description: Blacklisted node address + $ref: '#/components/schemas/BlacklistedPeers' + default: description: Error content: diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 97c77b1821..000a5dbb01 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -242,7 +242,15 @@ ergo { } } +bounded-mailbox { + mailbox-type = "akka.dispatch.NonBlockingBoundedMailbox" + mailbox-capacity = 5000 +} + akka { + actor.mailbox.requirements { + "akka.dispatch.BoundedMessageQueueSemantics" = bounded-mailbox + } http { server { request-timeout = 1 minute diff --git a/src/main/resources/mainnet.conf b/src/main/resources/mainnet.conf index 9e1d62731e..672043cde1 100644 --- a/src/main/resources/mainnet.conf +++ b/src/main/resources/mainnet.conf @@ -38,7 +38,7 @@ scorex { network { magicBytes = [1, 0, 2, 4] bindAddress = "0.0.0.0:9030" - nodeName = "ergo-mainnet-4.0.12" + nodeName = "ergo-mainnet-4.0.13" nodeName = ${?NODENAME} knownPeers = [ "46.4.112.10:9030", diff --git a/src/main/resources/testnet.conf b/src/main/resources/testnet.conf index 136b55d14c..476df6ef9f 100644 --- a/src/main/resources/testnet.conf +++ b/src/main/resources/testnet.conf @@ -52,7 +52,7 @@ scorex { network { magicBytes = [2, 0, 0, 2] bindAddress = "0.0.0.0:9020" - nodeName = "ergo-testnet-4.0.12" + nodeName = "ergo-testnet-4.0.13" nodeName = ${?NODENAME} knownPeers = [ "213.239.193.208:9020", diff --git a/src/main/scala/org/ergoplatform/mining/ErgoMiner.scala b/src/main/scala/org/ergoplatform/mining/ErgoMiner.scala index bad406401a..c2e231b2b4 100644 --- a/src/main/scala/org/ergoplatform/mining/ErgoMiner.scala +++ b/src/main/scala/org/ergoplatform/mining/ErgoMiner.scala @@ -1,6 +1,7 @@ package org.ergoplatform.mining import akka.actor.{Actor, ActorRef, ActorRefFactory, PoisonPill, Props} +import akka.dispatch.{BoundedMessageQueueSemantics, RequiresMessageQueue} import akka.pattern.{StatusReply, ask} import akka.util.Timeout import com.google.common.primitives.Longs @@ -52,7 +53,7 @@ class ErgoMiner(ergoSettings: ErgoSettings, viewHolderRef: ActorRef, readersHolderRef: ActorRef, timeProvider: NetworkTimeProvider, - inSecretKeyOpt: Option[DLogProverInput]) extends Actor with ScorexLogging { + inSecretKeyOpt: Option[DLogProverInput]) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ScorexLogging { import ErgoMiner._ @@ -77,6 +78,9 @@ class ErgoMiner(ergoSettings: ErgoSettings, // Flag which is set when a future with block candidate generation is running private var candidateGenerating: Boolean = false + // duration to wait before new prepare candidate attempt (adjusted based on feedback from previous execution) + private var prepareCandidateRetryDelay: FiniteDuration = 100.millis + // cached block candidate private var candidateOpt: Option[CandidateCache] = None @@ -248,7 +252,7 @@ class ErgoMiner(ergoSettings: ErgoSettings, // helper method to update cached candidate block and corresponding message for external miners private def updateCandidate(candidate: CandidateBlock, pk: ProveDlog, - txsToInclude: Seq[ErgoTransaction]): CandidateCache = { + txsToInclude: Seq[ErgoTransaction]): Try[CandidateCache] = Try { val ext = powScheme.deriveExternalCandidate(candidate, pk, txsToInclude.map(_.id)) log.info(s"New candidate with msg ${Base16.encode(ext.msg)} generated") log.debug(s"Got candidate block at height ${ErgoHistory.heightOf(candidate.parentOpt) + 1}" + @@ -259,10 +263,14 @@ class ErgoMiner(ergoSettings: ErgoSettings, candCache } + /** + * @return None if chain is not synced or Some of attempt to create candidate + */ private def generateCandidate(h: ErgoHistoryReader, m: ErgoMemPoolReader, s: UtxoStateReader, - txsToInclude: Seq[ErgoTransaction]): Try[CandidateCache] = { + pk: ProveDlog, + txsToInclude: Seq[ErgoTransaction]): Option[Try[CandidateCache]] = { //mandatory transactions to include into next block taken from the previous candidate val unspentTxsToInclude = txsToInclude.filter { tx => inputsNotSpent(tx, s) @@ -273,23 +281,21 @@ class ErgoMiner(ergoSettings: ErgoSettings, solvedBlock = None } - publicKeyOpt match { - case Some(pk) => - if (solvedBlock.isEmpty && h.bestFullBlockOpt.map(_.id) == s.stateContext.lastHeaderOpt.map(_.id)) { - createCandidate(pk, h, m, desiredUpdate, s, unspentTxsToInclude) match { - case Success(candidate) => - Success(updateCandidate(candidate, pk, unspentTxsToInclude)) - case Failure(e) => - log.warn("Failed to produce candidate block.", e) - //candidate cleared, including its mandatory transactions - candidateOpt = None - Failure(new Exception("Failed to produce candidate block.", e)) - } - } else { - Failure(new Exception("Can not generate block candidate: chain not synced (maybe last block not fully applied yet")) - } - case None => - Failure(new Exception("Candidate could not be generated: no public key available")) + def chainSynced = solvedBlock.isEmpty && h.bestFullBlockOpt.map(_.id) == s.stateContext.lastHeaderOpt.map(_.id) + + if (chainSynced) { + createCandidate(pk, h, m, desiredUpdate, s, unspentTxsToInclude) match { + case Success(candidate) => + Some(updateCandidate(candidate, pk, unspentTxsToInclude)) + case Failure(e) => + log.warn("Failed to produce candidate block.", e) + //candidate cleared, including its mandatory transactions + candidateOpt = None + Some(Failure(new Exception("Failed to produce candidate block.", e))) + } + } else { + // should not be synced probably due to racing condition when last block is not fully applied yet + None } } @@ -299,10 +305,13 @@ class ErgoMiner(ergoSettings: ErgoSettings, sender() ! StatusReply.error("Candidate creation is not supported when mining is disabled") } - case PrepareCandidate(txsToInclude, reply) => + case prepareCandidate@PrepareCandidate(txsToInclude, reply) => val msgSender = if (reply) Some(sender()) else None if (candidateGenerating) { - msgSender.foreach(_ ! StatusReply.error("Skipping candidate generation, one is already in progress")) + msgSender.foreach(s => context.system.scheduler.scheduleOnce(prepareCandidateRetryDelay, self, prepareCandidate)(context.system.dispatcher, s)) + } else if (publicKeyOpt.isEmpty) { + // Could happen if wallet is not initialized + msgSender.foreach(_ ! StatusReply.error("Candidate could not be generated, public key not available")) } else { candidateGenerating = true if (cachedFor(txsToInclude)) { @@ -313,17 +322,24 @@ class ErgoMiner(ergoSettings: ErgoSettings, } candidateGenerating = false } else { - log.info("Generating new candidate requested by miner") + val start = System.currentTimeMillis() (readersHolderRef ? GetReaders).mapTo[Readers] .onComplete { case Success(Readers(h, s: UtxoStateReader, m, _)) => - val candidateVersionReply = - generateCandidate(h, m, s, txsToInclude).fold( - ex => StatusReply.error(ex), - candidate => StatusReply.success(candidate.externalVersion) - ) + generateCandidate(h, m, s, publicKeyOpt.get, txsToInclude) match { + case Some(Success(candidate)) => + val generationTook = System.currentTimeMillis() - start + prepareCandidateRetryDelay = generationTook.millis + log.info(s"Generated new candidate requested by miner in $generationTook ms") + msgSender.foreach(_ ! StatusReply.success(candidate.externalVersion)) + case Some(Failure(ex)) => + log.error("Failed to generate new candidate", ex) + msgSender.foreach(_ ! StatusReply.error(ex)) + case None => + log.warn("Can not generate block candidate: chain not synced (maybe last block not fully applied yet") + msgSender.foreach(s => context.system.scheduler.scheduleOnce(prepareCandidateRetryDelay, self, prepareCandidate)(context.system.dispatcher, s)) + } candidateGenerating = false - msgSender.foreach(_ ! candidateVersionReply) case _ => candidateGenerating = false msgSender.foreach(_ ! StatusReply.error("Invalid readers state, mining is possible in UTXO mode only")) @@ -331,7 +347,7 @@ class ErgoMiner(ergoSettings: ErgoSettings, } } - // solution found externally (by e.g. GPU miner) + // solution found by external (GPU) or internal miner case preSolution: AutolykosSolution => // Inject node pk if it is not externally set (in Autolykos 2) val solution = if (preSolution.pk.isInfinity) { @@ -371,7 +387,7 @@ class ErgoMiner(ergoSettings: ErgoSettings, StatusReply.error("Invalid miner state") } } - log.debug(s"Processed solution $solution with the result result $result") + log.debug(s"Processed solution $solution with the result $result") if (externalMinerMode) sender() ! result } @@ -392,11 +408,12 @@ class ErgoMiner(ergoSettings: ErgoSettings, * * @param minerPk - public key of the miner * @param history - blockchain reader (to extract parent) - * @param pool - memory pool reader - * @param proposedUpdate - votes for parameters and soft-fork + * @param pool - memory pool reader (to read transactions) + * @param proposedUpdate - votes for parameters update or/and soft-fork * @param state - UTXO set reader * @param prioritizedTransactions - transactions which are going into the block in the first place - * (before transactions from the pool). No guarantee of inclusion in general case. + * (before transactions from the pool). + * Inclusion of all the prioritized transactions is not guaranteed in general case. * @return - block candidate or an error */ private def createCandidate(minerPk: ProveDlog, @@ -405,6 +422,7 @@ class ErgoMiner(ergoSettings: ErgoSettings, proposedUpdate: ErgoValidationSettingsUpdate, state: UtxoStateReader, prioritizedTransactions: Seq[ErgoTransaction]): Try[CandidateBlock] = Try { + // Extract best header and extension of a best block user their data for assembling a new block val bestHeaderOpt: Option[Header] = history.bestFullBlockOpt.map(_.header) val bestExtensionOpt: Option[Extension] = bestHeaderOpt @@ -621,9 +639,13 @@ object ErgoMiner extends ScorexLogging { maxTransactionComplexity: Int, us: UtxoStateReader, upcomingContext: ErgoStateContext, - transactions: Iterable[ErgoTransaction]) + transactions: Seq[ErgoTransaction]) (implicit vs: ValidationSettings): (Seq[ErgoTransaction], Seq[ModifierId]) = { + val currentHeight = us.stateContext.currentHeight + + log.info(s"Assembling a block candidate for block #$currentHeight from ${transactions.length} transactions available") + @tailrec def loop(mempoolTxs: Iterable[ErgoTransaction], acc: Seq[CostedTransaction], @@ -640,7 +662,7 @@ object ErgoMiner extends ScorexLogging { if (!inputsNotSpent(tx, stateWithTxs) || doublespend(current, tx)) { //mark transaction as invalid if it tries to do double-spending or trying to spend outputs not present //do these checks before validating the scripts to save time - current -> (invalidTxs :+ tx.id) + loop(mempoolTxs.tail, acc, lastFeeTx, invalidTxs :+ tx.id) } else { implicit val verifier: ErgoInterpreter = ErgoInterpreter(us.stateContext.currentParameters) // check validity and calculate transaction cost @@ -650,7 +672,7 @@ object ErgoMiner extends ScorexLogging { val newBoxes = newTxs.flatMap(_._1.outputs) val emissionRules = stateWithTxs.constants.settings.chainSettings.emissionRules - ErgoMiner.collectFees(stateWithTxs.stateContext.currentHeight, newTxs.map(_._1), minerPk, emissionRules) match { + ErgoMiner.collectFees(currentHeight, newTxs.map(_._1), minerPk, emissionRules) match { case Some(feeTx) => val boxesToSpend = feeTx.inputs.flatMap(i => newBoxes.find(b => java.util.Arrays.equals(b.id, i.boxId))) feeTx.statefulValidity(boxesToSpend, IndexedSeq(), upcomingContext) match { @@ -662,11 +684,12 @@ object ErgoMiner extends ScorexLogging { current -> invalidTxs } case Failure(e) => - log.debug(s"Fee collecting tx is invalid, return current: ${e.getMessage} from ${stateWithTxs.stateContext}") + log.warn(s"Fee collecting tx is invalid, not including it, " + + s"details: ${e.getMessage} from ${stateWithTxs.stateContext}") current -> invalidTxs } case None => - log.debug(s"No fee proposition found in txs ${newTxs.map(_._1.id)} ") + log.info(s"No fee proposition found in txs ${newTxs.map(_._1.id)} ") val blockTxs: Seq[CostedTransaction] = newTxs ++ lastFeeTx.toSeq if (correctLimits(blockTxs, maxBlockCost, maxBlockSize)) { loop(mempoolTxs.tail, blockTxs, lastFeeTx, invalidTxs) @@ -684,7 +707,10 @@ object ErgoMiner extends ScorexLogging { } } - loop(transactions, Seq.empty, None, Seq.empty) + val res = loop(transactions, Seq.empty, None, Seq.empty) + log.info(s"Collected ${res._1.length} transactions For block #$currentHeight, " + + s"${res._2.length} transactions turned out to be invalid") + res } //Checks that transaction "tx" is not spending outputs spent already by transactions "txs" diff --git a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala index 27f8d30f7f..23bd83b2bf 100644 --- a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala +++ b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala @@ -175,7 +175,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, * @param maxModifiers maximum modifiers to download * @param minModifiersPerBucket minimum modifiers to download per bucket * @param maxModifiersPerBucket maximum modifiers to download per bucket - * @param getPeersOpt optionally get peers to download from, all peers have the same [[PeerSyncState]] + * @param getPeersOpt optionally get peers to download from, all peers have the same PeerSyncState * @param fetchMax function that fetches modifiers, it is passed how many of them tops */ protected def requestDownload(maxModifiers: Int, minModifiersPerBucket: Int, maxModifiersPerBucket: Int) diff --git a/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala b/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala index c613c0fd58..f389a50c36 100644 --- a/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala +++ b/src/main/scala/org/ergoplatform/nodeView/state/UtxoState.scala @@ -189,6 +189,9 @@ object UtxoState { new UtxoState(persistentProver, version, store, constants) } + /** + * Used in tests and to generate a genesis state. + */ @SuppressWarnings(Array("OptionGet", "TryGet")) def fromBoxHolder(bh: BoxHolder, currentEmissionBoxOpt: Option[ErgoBox], diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala index 9922de855b..e863239696 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala @@ -190,7 +190,13 @@ class ErgoWalletActor(settings: ErgoSettings, historyReader.bestFullBlockAt(blockHeight) match { case Some(block) => log.info(s"Wallet is going to scan a block ${block.id} in the past at height ${block.height}") - ergoWalletService.scanBlockUpdate(state, block) + ergoWalletService.scanBlockUpdate(state, block) match { + case Failure(ex) => + log.error("Scanning past block update failed", ex) + state + case Success(updatedState) => + updatedState + } case None => state // We may do not have a block if, for example, the blockchain is pruned. This is okay, just skip it. } @@ -206,7 +212,15 @@ class ErgoWalletActor(settings: ErgoSettings, val nextBlockHeight = state.expectedNextBlockHeight(newBlock.height, settings.nodeSettings.isFullBlocksPruned) if (nextBlockHeight == newBlock.height) { log.info(s"Wallet is going to scan a block ${newBlock.id} on chain at height ${newBlock.height}") - context.become(loadedWallet(ergoWalletService.scanBlockUpdate(state, newBlock))) + val newState = + ergoWalletService.scanBlockUpdate(state, newBlock) match { + case Failure(ex) => + log.error("Scanning new block update failed", ex) + state + case Success(updatedState) => + updatedState + } + context.become(loadedWallet(newState)) } else if (nextBlockHeight < newBlock.height) { log.warn(s"Wallet: skipped blocks found starting from $nextBlockHeight, going back to scan them") self ! ScanInThePast(nextBlockHeight) diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala index e6d97b2723..d47ac32684 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala @@ -179,7 +179,7 @@ trait ErgoWalletService { * @param state current wallet state * @param block - block to scan */ - def scanBlockUpdate(state: ErgoWalletState, block: ErgoFullBlock): ErgoWalletState + def scanBlockUpdate(state: ErgoWalletState, block: ErgoFullBlock): Try[ErgoWalletState] /** * Sign a transaction @@ -520,11 +520,9 @@ class ErgoWalletServiceImpl extends ErgoWalletService with ErgoWalletSupport { Failure(new Exception("Unable to derive key, wallet is not initialized")) } - def scanBlockUpdate(state: ErgoWalletState, block: ErgoFullBlock): ErgoWalletState = { - val (reg, offReg, updatedOutputsFilter) = + def scanBlockUpdate(state: ErgoWalletState, block: ErgoFullBlock): Try[ErgoWalletState] = WalletScanLogic.scanBlockTransactions(state.registry, state.offChainRegistry, state.stateContext, state.walletVars, block, state.outputsFilter) - state.copy(registry = reg, offChainRegistry = offReg, outputsFilter = Some(updatedOutputsFilter)) - } + .map { case (reg, offReg, updatedOutputsFilter) => state.copy(registry = reg, offChainRegistry = offReg, outputsFilter = Some(updatedOutputsFilter)) } def updateUtxoState(state: ErgoWalletState): ErgoWalletState = { (state.mempoolReaderOpt, state.stateReaderOpt) match { diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/WalletScanLogic.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/WalletScanLogic.scala index d02fccaa38..e2d30f1b8f 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/WalletScanLogic.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/WalletScanLogic.scala @@ -13,6 +13,7 @@ import scorex.util.{ModifierId, ScorexLogging} import scorex.util.bytesToId import scala.collection.mutable +import scala.util.Try /** * Functions which do scan boxes, transactions and blocks to find boxes which belong to wallet's keys. @@ -45,7 +46,7 @@ object WalletScanLogic extends ScorexLogging { walletVars: WalletVars, block: ErgoFullBlock, cachedOutputsFilter: Option[BloomFilter[Array[Byte]]] - ): (WalletRegistry, OffChainRegistry, BloomFilter[Array[Byte]]) = { + ): Try[(WalletRegistry, OffChainRegistry, BloomFilter[Array[Byte]])] = { scanBlockTransactions( registry, offChainRegistry, stateContext, walletVars, block.height, block.id, block.transactions, cachedOutputsFilter) @@ -72,7 +73,7 @@ object WalletScanLogic extends ScorexLogging { blockId: ModifierId, transactions: Seq[ErgoTransaction], cachedOutputsFilter: Option[BloomFilter[Array[Byte]]] - ): (WalletRegistry, OffChainRegistry, BloomFilter[Array[Byte]]) = { + ): Try[(WalletRegistry, OffChainRegistry, BloomFilter[Array[Byte]])] = { // Take unspent wallet outputs Bloom Filter from cache // or recreate it from unspent outputs stored in the database @@ -167,13 +168,14 @@ object WalletScanLogic extends ScorexLogging { // function effects: updating registry and offchainRegistry datasets registry.updateOnBlock(scanRes, blockId, height) + .map { _ => + //data needed to update the offchain-registry + val walletUnspent = registry.walletUnspentBoxes() + val newOnChainIds = scanRes.outputs.map(x => encodedBoxId(x.box.id)) + val updatedOffchainRegistry = offChainRegistry.updateOnBlock(height, walletUnspent, newOnChainIds) - //data needed to update the offchain-registry - val walletUnspent = registry.walletUnspentBoxes() - val newOnChainIds = scanRes.outputs.map(x => encodedBoxId(x.box.id)) - val updatedOffchainRegistry = offChainRegistry.updateOnBlock(height, walletUnspent, newOnChainIds) - - (registry, updatedOffchainRegistry, outputsFilter) + (registry, updatedOffchainRegistry, outputsFilter) + } } diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala index 4d2bbb3560..2de7d51045 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala @@ -207,7 +207,7 @@ class WalletRegistry(store: LDBVersionedStore)(ws: WalletSettings) extends Score * @param blockId - block identifier * @param blockHeight - block height */ - def updateOnBlock(scanResults: ScanResults, blockId: ModifierId, blockHeight: Int): Unit = { + def updateOnBlock(scanResults: ScanResults, blockId: ModifierId, blockHeight: Int): Try[Unit] = Try { // first, put newly created outputs and related transactions into key-value bag val bag1 = putBoxes(KeyValuePairsBag.empty, scanResults.outputs) diff --git a/src/test/scala/org/ergoplatform/mining/ErgoMinerSpec.scala b/src/test/scala/org/ergoplatform/mining/ErgoMinerSpec.scala index 548e1ab32a..bfd3a34b3c 100644 --- a/src/test/scala/org/ergoplatform/mining/ErgoMinerSpec.scala +++ b/src/test/scala/org/ergoplatform/mining/ErgoMinerSpec.scala @@ -39,10 +39,8 @@ class ErgoMinerSpec extends AnyFlatSpec with ErgoTestHelpers with ValidBlocksGen val newBlockSignal: Class[SemanticallySuccessfulModifier[_]] = classOf[SemanticallySuccessfulModifier[_]] val newBlockDelay: FiniteDuration = 30 seconds - private def getWorkMessage(minerRef: ActorRef, mandatoryTransactions: Seq[ErgoTransaction]): WorkMessage = { - implicit val patienceConfig: PatienceConfig = PatienceConfig(1.seconds, 50.millis) - eventually(await(minerRef.askWithStatus(PrepareCandidate(mandatoryTransactions)).mapTo[WorkMessage])) - } + private def getWorkMessage(minerRef: ActorRef, mandatoryTransactions: Seq[ErgoTransaction]): WorkMessage = + await(minerRef.askWithStatus(PrepareCandidate(mandatoryTransactions)).mapTo[WorkMessage]) val defaultSettings: ErgoSettings = { val empty = ErgoSettings.read() @@ -277,7 +275,8 @@ class ErgoMinerSpec extends AnyFlatSpec with ErgoTestHelpers with ValidBlocksGen val passiveMiner: ActorRef = minerRef - val wm = await(passiveMiner.askWithStatus(PrepareCandidate(Seq.empty)).mapTo[WorkMessage]) + implicit val patienceConfig: PatienceConfig = PatienceConfig(1.second, 100.millis) // it takes a while before PK is set + val wm = eventually(await(passiveMiner.askWithStatus(PrepareCandidate(Seq.empty)).mapTo[WorkMessage])) wm.isInstanceOf[WorkMessage] shouldBe true system.terminate() } diff --git a/src/test/scala/org/ergoplatform/nodeView/state/UtxoStateSpecification.scala b/src/test/scala/org/ergoplatform/nodeView/state/UtxoStateSpecification.scala index dfe09bafbd..c58a545c13 100644 --- a/src/test/scala/org/ergoplatform/nodeView/state/UtxoStateSpecification.scala +++ b/src/test/scala/org/ergoplatform/nodeView/state/UtxoStateSpecification.scala @@ -22,7 +22,8 @@ import sigmastate.basics.DLogProtocol.{DLogProverInput, ProveDlog} import sigmastate.interpreter.ProverResult import sigmastate.helpers.TestingHelpers._ -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor, Future} import scala.util.{Random, Try} @@ -189,7 +190,7 @@ class UtxoStateSpecification extends ErgoPropertyTest with ErgoTransactionGenera var us2 = createUtxoState(BoxHolder(Seq(genesisEmissionBox))) val stateReader = us2.getReader.asInstanceOf[UtxoState] // parallel thread that generates proofs - Future { + val f = Future { (0 until 1000) foreach { _ => Try { val boxes = stateReader.randomBox().toSeq @@ -202,6 +203,7 @@ class UtxoStateSpecification extends ErgoPropertyTest with ErgoTransactionGenera chain.foreach { fb => us2 = us2.applyModifier(fb).get } + Await.result(f, Duration.Inf); } property("proofsForTransactions() to be deterministic") { diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala index 338f69c5e9..d442402396 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala @@ -129,7 +129,7 @@ class ErgoWalletServiceSpec extends ErgoPropertyTest with WalletTestOps with Erg val unspentBoxes = boxes.map(bx => bx.copy(spendingHeightOpt = None, spendingTxIdOpt = None, scans = Set(PaymentsScanId))) val spentBox = boxes.head.copy(spendingHeightOpt = Some(10000), spendingTxIdOpt = Some(txId), scans = Set(PaymentsScanId)) val allBoxes = unspentBoxes :+ spentBox - wState.registry.updateOnBlock(ScanResults(allBoxes, Seq.empty, Seq.empty), blockId, 100) + wState.registry.updateOnBlock(ScanResults(allBoxes, Seq.empty, Seq.empty), blockId, 100).get val walletService = new ErgoWalletServiceImpl val actualUnspentOnlyWalletBoxes = walletService.getWalletBoxes(wState, unspentOnly = true, considerUnconfirmed = false).toList diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/WalletScanLogicSpec.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/WalletScanLogicSpec.scala index 413526efad..35b60708bd 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/WalletScanLogicSpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/WalletScanLogicSpec.scala @@ -118,7 +118,7 @@ class WalletScanLogicSpec extends ErgoPropertyTest with DBSpec with WalletTestOp val height0 = 5 //simplest case - we're scanning an empty block val (r0, o0, f0) = - scanBlockTransactions(emptyReg, emptyOff, emptyStateContext, walletVars, height0, blockId, Seq.empty, None) + scanBlockTransactions(emptyReg, emptyOff, emptyStateContext, walletVars, height0, blockId, Seq.empty, None).get val r0digest = r0.fetchDigest() r0digest.walletBalance shouldBe 0 r0digest.walletAssetBalances.size shouldBe 0 @@ -142,7 +142,7 @@ class WalletScanLogicSpec extends ErgoPropertyTest with DBSpec with WalletTestOp val offDigestBefore = off.digest.walletBalance val (r1, o1, f1) = - scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1, blockId, txs, Some(f0)) + scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1, blockId, txs, Some(f0)).get val r1digest = r1.fetchDigest() r1digest.walletBalance shouldBe (regDigestBefore + trackedTransaction.paymentValues.sum) r1digest.walletAssetBalances.size shouldBe 0 @@ -162,7 +162,7 @@ class WalletScanLogicSpec extends ErgoPropertyTest with DBSpec with WalletTestOp val spendingTx = ErgoTransaction(inputs, IndexedSeq.empty, tx.outputCandidates) val (r2, o2, f2) = - scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1 + 1, blockId, Seq(spendingTx), Some(f1)) + scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1 + 1, blockId, Seq(spendingTx), Some(f1)).get val r2digest = r2.fetchDigest() r2digest.walletBalance shouldBe (regDigestBefore + trackedTransaction.paymentValues.sum) @@ -183,7 +183,7 @@ class WalletScanLogicSpec extends ErgoPropertyTest with DBSpec with WalletTestOp val spendingTx2 = new ErgoTransaction(inputs2, IndexedSeq.empty, outputs2) val (r3, o3, f3) = - scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1 + 2, blockId, Seq(spendingTx2), Some(f2)) + scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1 + 2, blockId, Seq(spendingTx2), Some(f2)).get val r3digest = r3.fetchDigest() r3digest.walletBalance shouldBe regDigestBefore @@ -201,7 +201,8 @@ class WalletScanLogicSpec extends ErgoPropertyTest with DBSpec with WalletTestOp //applying all the three previous transactions val threeTxs = Seq(creatingTx, spendingTx, spendingTx2) - val (r4, o4, f4) = scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1 + 3, blockId, threeTxs, Some(f3)) + val (r4, o4, _) = + scanBlockTransactions(registry, off, emptyStateContext, walletVars, height1 + 3, blockId, threeTxs, Some(f3)).get val r4digest = r4.fetchDigest() r4digest.walletBalance shouldBe regDigestBefore diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistryBenchmark.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistryBenchmark.scala index 0718716619..5e132c30f4 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistryBenchmark.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistryBenchmark.scala @@ -54,7 +54,7 @@ object WalletRegistryBenchmark extends App with ErgoTestConstants { } val scanResults0 = ScanResults(boxes, Seq.empty, Seq.empty) - registry.updateOnBlock(scanResults0, ModifierId @@ Base16.encode(Array.fill(32)(0: Byte)), 1) + registry.updateOnBlock(scanResults0, ModifierId @@ Base16.encode(Array.fill(32)(0: Byte)), 1).get println("keys: " + walletVars.proverOpt.get.secretKeys.size) val bts0 = System.currentTimeMillis() @@ -73,7 +73,7 @@ object WalletRegistryBenchmark extends App with ErgoTestConstants { } val scanResults1 = ScanResults(Seq.empty, Seq.empty, txs) - registry.updateOnBlock(scanResults1, ModifierId @@ Base16.encode(Array.fill(32)(1: Byte)), 2) + registry.updateOnBlock(scanResults1, ModifierId @@ Base16.encode(Array.fill(32)(1: Byte)), 2).get val tts0 = System.currentTimeMillis() val txsRead = registry.allWalletTxs() diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala index 61c11f02bd..6637cafc21 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala @@ -108,7 +108,7 @@ class WalletRegistrySpec val registry = new WalletRegistry(store)(settings.walletSettings) val blockId = modifierIdGen.sample.get val unspentBoxes = boxes.map(bx => bx.copy(spendingHeightOpt = None, spendingTxIdOpt = None, scans = walletBoxStatus)) - registry.updateOnBlock(ScanResults(unspentBoxes, Seq.empty, Seq.empty), blockId, 100) + registry.updateOnBlock(ScanResults(unspentBoxes, Seq.empty, Seq.empty), blockId, 100).get registry.walletUnspentBoxes().toList should contain theSameElementsAs unspentBoxes } } @@ -123,7 +123,7 @@ class WalletRegistrySpec bx.copy(spendingHeightOpt = None, spendingTxIdOpt = None, scans = walletBoxStatus) } val inputs = outs.map(tb => SpentInputData(fakeTxId, tb)) - registry.updateOnBlock(ScanResults(outs, inputs, Seq.empty), blockId, 100) + registry.updateOnBlock(ScanResults(outs, inputs, Seq.empty), blockId, 100).get registry.walletUnspentBoxes() shouldBe Seq.empty } }