diff --git a/src/main/scala/com/wavesplatform/state2/BlockDiff.scala b/src/main/scala/com/wavesplatform/state2/BlockDiff.scala index cd7265c94d3..a081fccbed7 100644 --- a/src/main/scala/com/wavesplatform/state2/BlockDiff.scala +++ b/src/main/scala/com/wavesplatform/state2/BlockDiff.scala @@ -3,18 +3,27 @@ package com.wavesplatform.state2 import cats._ import cats.implicits._ import cats.Monoid +import scorex.account.Account + +import scala.collection.SortedMap case class BlockDiff(txsDiff: Diff, heightDiff: Int, - effectiveBalanceSnapshots: Set[EffectiveBalanceSnapshot]) + snapshots: Map[Account, SortedMap[Int, Snapshot]]) object BlockDiff { + + implicit def sortedMapForSnapshotsMonoid[A: Ordering, Snapshot]: Monoid[SortedMap[A, Snapshot]] = new Monoid[SortedMap[A, Snapshot]] { + def empty: SortedMap[A, Snapshot] = SortedMap.empty[A, Snapshot] + def combine(f1: SortedMap[A, Snapshot], f2: SortedMap[A, Snapshot]): SortedMap[A, Snapshot] = f1 ++ f2 + } + implicit val blockDiffMonoid = new Monoid[BlockDiff] { - override def empty: BlockDiff = BlockDiff(Monoid[Diff].empty, 0, Set.empty) + override def empty: BlockDiff = BlockDiff(Monoid[Diff].empty, 0, Map.empty) override def combine(older: BlockDiff, newer: BlockDiff): BlockDiff = BlockDiff( txsDiff = older.txsDiff.combine(newer.txsDiff), heightDiff = older.heightDiff + newer.heightDiff, - effectiveBalanceSnapshots = older.effectiveBalanceSnapshots ++ newer.effectiveBalanceSnapshots) + snapshots = older.snapshots.combine(newer.snapshots)) } } \ No newline at end of file diff --git a/src/main/scala/com/wavesplatform/state2/BlockchainUpdaterImpl.scala b/src/main/scala/com/wavesplatform/state2/BlockchainUpdaterImpl.scala index fb79b5ea69f..fb1dcf10982 100644 --- a/src/main/scala/com/wavesplatform/state2/BlockchainUpdaterImpl.scala +++ b/src/main/scala/com/wavesplatform/state2/BlockchainUpdaterImpl.scala @@ -13,15 +13,20 @@ import scorex.utils.ScorexLogging class BlockchainUpdaterImpl(persisted: StateWriter with StateReader, settings: FunctionalitySettings, bc: HistoryWriter with History) extends BlockchainUpdater with ScorexLogging { - private val MinInMemDiff = 100 - private val MaxInMemDiff = 200 + private val MinInMemDiff = 200 + private val MaxInMemDiff = MinInMemDiff * 2 private val unsafeDifferByRange: (StateReader, (Int, Int)) => BlockDiff = { case (sr, (from, to)) => log.debug(s"Reading blocks from $from to $to") - val blocks = Range(from, to).map(bc.blockAt(_).get) + val blocks = Range(from, to).foldLeft((List.empty[Block], 0)) { case ((acc, counter), i) => + if (counter % 1000 == 0) { + log.debug(s"Read block $counter of Range($from, $to)") + } + (bc.blockAt(i).get +: acc, counter + 1) + }._1.reverse log.debug(s"Blocks read from $from to $to") - val r = BlockDiffer.unsafeDiffMany(settings, m => log.info(m))(sr, blocks) + val r = BlockDiffer.unsafeDiffMany(settings)(sr, blocks) log.debug(s"Diff for Range($from, $to) rebuilt") r } @@ -42,7 +47,7 @@ class BlockchainUpdaterImpl(persisted: StateWriter with StateReader, settings: F } } - def currentState: StateReader = CompositeStateReader.proxy(persisted, () => inMemoryDiff) + def currentState: StateReader = new CompositeStateReader.Proxy(persisted, () => inMemoryDiff) private def updatePersistedAndInMemory(): Unit = { logHeights("State rebuild started:") diff --git a/src/main/scala/com/wavesplatform/state2/Diff.scala b/src/main/scala/com/wavesplatform/state2/Diff.scala index 1423cccf0e9..d5f41d15dab 100644 --- a/src/main/scala/com/wavesplatform/state2/Diff.scala +++ b/src/main/scala/com/wavesplatform/state2/Diff.scala @@ -5,15 +5,13 @@ import cats.implicits._ import scorex.account.{Account, Alias} import scorex.transaction.Transaction import scorex.transaction.assets.exchange.ExchangeTransaction -import scorex.transaction.lease.{LeaseCancelTransaction, LeaseTransaction} -case class EffectiveBalanceSnapshot(acc: Account, height: Int, prevEffectiveBalance: Long, effectiveBalance: Long) +case class Snapshot(prevHeight: Int, balance: Long, effectiveBalance: Long) case class LeaseInfo(leaseIn: Long, leaseOut: Long) object LeaseInfo { val empty = LeaseInfo(0, 0) - implicit val leaseInfoMonoid = new Monoid[LeaseInfo] { override def empty: LeaseInfo = LeaseInfo.empty @@ -38,35 +36,18 @@ case class Diff(transactions: Map[ByteArray, (Int, Transaction, Set[Account])], aliases: Map[Alias, Account], paymentTransactionIdsByHashes: Map[ByteArray, ByteArray], previousExchangeTxs: Map[ByteArray, Set[ExchangeTransaction]], - patchExtraLeaseIdsToCancel: Seq[ByteArray]) { + leaseState: Map[ByteArray, Boolean]) { lazy val accountTransactionIds: Map[Account, List[ByteArray]] = { - val map: List[(Account, List[(Int, Long, ByteArray)])] = transactions.toList - .flatMap { case (id, (h, tx, accs)) => accs.map(acc => acc -> List((h, tx.timestamp, id))) } - val groupedByAcc = map.foldLeft(Map.empty[Account, List[(Int, Long, ByteArray)]]) { case (m, (acc, list)) => - m.combine(Map(acc -> list)) + val map: List[(Account, Set[(Int, Long, ByteArray)])] = transactions.toList + .flatMap { case (id, (h, tx, accs)) => accs.map(acc => acc -> Set((h, tx.timestamp, id))) } + val groupedByAcc = map.foldLeft(Map.empty[Account, Set[(Int, Long, ByteArray)]]) { case (m, (acc, set)) => + m.combine(Map(acc -> set)) } groupedByAcc - .mapValues(l => l.sortBy { case ((h, t, id)) => (-h, -t) }) // fresh head ([h=2, h=1, h=0]) + .mapValues(l => l.toList.sortBy { case ((h, t, id)) => (-h, -t) }) // fresh head ([h=2, h=1, h=0]) .mapValues(_.map(_._3)) } - - lazy val effectiveLeaseTxUpdates: (Set[EqByteArray], Set[EqByteArray]) = { - val txs = transactions.values.map(_._2) - - val canceledLeaseIds: Set[EqByteArray] = txs - .collect { case (lctx: LeaseCancelTransaction) => EqByteArray(lctx.leaseId) } - .toSet - - val newLeaseIds = txs - .collect { case (ltx: LeaseTransaction) => EqByteArray(ltx.id) } - .toSet - - val effectiveNewCancels = (canceledLeaseIds ++ patchExtraLeaseIdsToCancel).diff(newLeaseIds) - val effectiveNewLeases = newLeaseIds.diff(canceledLeaseIds ++ patchExtraLeaseIdsToCancel) - (effectiveNewLeases, effectiveNewCancels) - } - } object Diff { @@ -75,7 +56,8 @@ object Diff { assetInfos: Map[ByteArray, AssetInfo] = Map.empty, aliases: Map[Alias, Account] = Map.empty, previousExchangeTxs: Map[ByteArray, Set[ExchangeTransaction]] = Map.empty, - paymentTransactionIdsByHashes: Map[ByteArray, ByteArray] = Map.empty + paymentTransactionIdsByHashes: Map[ByteArray, ByteArray] = Map.empty, + leaseState: Map[ByteArray, Boolean] = Map.empty ): Diff = Diff( transactions = Map(EqByteArray(tx.id) -> (height, tx, portfolios.keys.toSet)), portfolios = portfolios, @@ -83,14 +65,14 @@ object Diff { aliases = aliases, paymentTransactionIdsByHashes = paymentTransactionIdsByHashes, previousExchangeTxs = previousExchangeTxs, - patchExtraLeaseIdsToCancel = Seq.empty) + leaseState = leaseState) implicit class DiffExt(d: Diff) { - def asBlockDiff: BlockDiff = BlockDiff(d, 0, Set.empty) + def asBlockDiff: BlockDiff = BlockDiff(d, 0, Map.empty) } implicit val diffMonoid = new Monoid[Diff] { - override def empty: Diff = Diff(transactions = Map.empty, portfolios = Map.empty, issuedAssets = Map.empty, Map.empty, Map.empty, Map.empty, Seq.empty) + override def empty: Diff = Diff(transactions = Map.empty, portfolios = Map.empty, issuedAssets = Map.empty, Map.empty, Map.empty, Map.empty, Map.empty) override def combine(older: Diff, newer: Diff): Diff = Diff( transactions = older.transactions ++ newer.transactions, @@ -99,8 +81,6 @@ object Diff { aliases = older.aliases ++ newer.aliases, paymentTransactionIdsByHashes = older.paymentTransactionIdsByHashes ++ newer.paymentTransactionIdsByHashes, previousExchangeTxs = older.previousExchangeTxs ++ newer.previousExchangeTxs, - patchExtraLeaseIdsToCancel = newer.patchExtraLeaseIdsToCancel ++ older.patchExtraLeaseIdsToCancel - ) + leaseState = older.leaseState ++ newer.leaseState) } - } diff --git a/src/main/scala/com/wavesplatform/state2/StateStorage.scala b/src/main/scala/com/wavesplatform/state2/StateStorage.scala index 4acd35a4b4a..b5f1c53fd88 100644 --- a/src/main/scala/com/wavesplatform/state2/StateStorage.scala +++ b/src/main/scala/com/wavesplatform/state2/StateStorage.scala @@ -2,7 +2,10 @@ package com.wavesplatform.state2 import java.util +import com.google.common.primitives.Ints +import com.wavesplatform.state2.StateStorage.SnapshotKey import org.h2.mvstore.{MVMap, MVStore} +import scorex.account.Account class StateStorage(db: MVStore) { @@ -21,7 +24,7 @@ class StateStorage(db: MVStore) { val accountTransactionIds: util.Map[Array[Byte], List[Array[Byte]]] = db.openMap("accountTransactionIds") - val effectiveBalanceSnapshots: util.Map[(Array[Byte], Int), (Long, Long)] = db.openMap("effectiveBalanceUpdates") + val balanceSnapshots: util.Map[SnapshotKey, (Int, Long, Long)] = db.openMap("balanceSnapshots") val paymentTransactionHashes: util.Map[Array[Byte], Array[Byte]] = db.openMap("paymentTransactionHashes") @@ -29,7 +32,16 @@ class StateStorage(db: MVStore) { val exchangeTransactionsByOrder: util.Map[Array[Byte], Set[Array[Byte]]] = db.openMap("exchangeTransactionsByOrder") - val leaseState: util.Map[Array[Byte], Boolean] = db.openMap("leaseState") + val leaseState: util.Map[Array[Byte], Boolean] = db.openMap("leaseState") + + val lastUpdateHeight: MVMap[Array[Byte], Int] = db.openMap("lastUpdateHeight") def commit(): Unit = db.commit() + +} + +object StateStorage { + type SnapshotKey = Array[Byte] + + def snapshotKey(acc: Account, height: Int): SnapshotKey = acc.bytes ++ Ints.toByteArray(height) } diff --git a/src/main/scala/com/wavesplatform/state2/StateWriter.scala b/src/main/scala/com/wavesplatform/state2/StateWriter.scala index 47cce8779a5..20806f1aa90 100644 --- a/src/main/scala/com/wavesplatform/state2/StateWriter.scala +++ b/src/main/scala/com/wavesplatform/state2/StateWriter.scala @@ -78,20 +78,22 @@ class StateWriterImpl(p: StateStorage) extends StateReaderImpl(p) with StateWrit } } - measurePersist("effectiveBalanceSnapshots")(blockDiff.effectiveBalanceSnapshots) { - _.foreach { ebs => - p.effectiveBalanceSnapshots.put((ebs.acc.bytes, ebs.height), (ebs.prevEffectiveBalance, ebs.effectiveBalance)) - } - } + measurePersist("effectiveBalanceSnapshots")(blockDiff.snapshots)( + _.foreach { case (acc, snapshotsByHeight) => + snapshotsByHeight.foreach { case (h, snapshot) => + p.balanceSnapshots.put(StateStorage.snapshotKey(acc, h), (snapshot.prevHeight, snapshot.balance, snapshot.effectiveBalance)) + } + p.lastUpdateHeight.put(acc.bytes, snapshotsByHeight.keys.max) + }) + measurePersist("aliases")(blockDiff.txsDiff.aliases) { _.foreach { case (alias, acc) => p.aliasToAddress.put(alias.name, acc.bytes) } } - val (effectiveNewLeases, effectiveNewCancels) = blockDiff.txsDiff.effectiveLeaseTxUpdates - measurePersist("effectiveNewLeases")(effectiveNewLeases)(_.foreach(id => p.leaseState.put(id.arr, true))) - measurePersist("effectiveNewCancels")(effectiveNewCancels)(_.foreach(id => p.leaseState.put(id.arr, false))) + measurePersist("lease info")(blockDiff.txsDiff.leaseState)( + _.foreach { case (id, isActive) => p.leaseState.put(id.arr, isActive) }) p.setHeight(p.getHeight + blockDiff.heightDiff) p.commit() @@ -103,11 +105,12 @@ class StateWriterImpl(p: StateStorage) extends StateReaderImpl(p) with StateWrit p.portfolios.clear() p.assets.clear() p.accountTransactionIds.clear() - p.effectiveBalanceSnapshots.clear() + p.balanceSnapshots.clear() p.paymentTransactionHashes.clear() p.exchangeTransactionsByOrder.clear() p.aliasToAddress.clear() p.leaseState.clear() + p.lastUpdateHeight.clear() p.setHeight(0) p.commit() @@ -123,7 +126,7 @@ object StateWriterImpl extends ScorexLogging { (r, t1 - t0) } - def measurePersist[F[_] <: TraversableOnce[_], A](s: String)(fa: F[A])(f: F[A] => Unit): Unit = { + def measurePersist[F[_] <: TraversableOnce[_], A](s: String)(fa: => F[A])(f: F[A] => Unit): Unit = { val (_, time) = withTime(f(fa)) log.debug(s"Persisting $s(size=${fa.size}) took ${time}ms") } diff --git a/src/main/scala/com/wavesplatform/state2/diffs/BlockDiffer.scala b/src/main/scala/com/wavesplatform/state2/diffs/BlockDiffer.scala index 67bb92fdf1f..25446b0e19b 100644 --- a/src/main/scala/com/wavesplatform/state2/diffs/BlockDiffer.scala +++ b/src/main/scala/com/wavesplatform/state2/diffs/BlockDiffer.scala @@ -12,6 +12,8 @@ import scorex.block.Block import scorex.transaction.{AssetAcc, ValidationError} import scorex.utils.ScorexLogging +import scala.collection.SortedMap + object BlockDiffer extends ScorexLogging { @@ -19,7 +21,9 @@ object BlockDiffer extends ScorexLogging { def apply(settings: FunctionalitySettings)(s: StateReader, block: Block): Either[ValidationError, BlockDiff] = { - val txDiffer = TransactionDiffer(settings, block.timestamp, s.height + 1) _ + val currentBlockHeight = s.height + 1 + + val txDiffer = TransactionDiffer(settings, block.timestamp, currentBlockHeight) _ val accountPortfolioFeesMap: List[(Account, Portfolio)] = block.feesDistribution.toList.map { case (AssetAcc(account, maybeAssetId), feeVolume) => @@ -29,7 +33,7 @@ object BlockDiffer extends ScorexLogging { }) } val feeDiff = Monoid[Diff].combineAll(accountPortfolioFeesMap.map { case (acc, p) => - new Diff(Map.empty, Map(acc -> p), Map.empty, Map.empty, Map.empty, Map.empty, Seq.empty) + new Diff(Map.empty, Map(acc -> p), Map.empty, Map.empty, Map.empty, Map.empty, Map.empty) }) val txsDiffEi = block.transactionData.foldLeft(right(feeDiff)) { case (ei, tx) => ei.flatMap(diff => @@ -37,37 +41,32 @@ object BlockDiffer extends ScorexLogging { .map(newDiff => diff.combine(newDiff))) } - txsDiffEi - .map(d => if (s.height + 1 == settings.resetEffectiveBalancesAtHeight) + txsDiffEi.map { d => + val diff = if (currentBlockHeight == settings.resetEffectiveBalancesAtHeight) Monoid.combine(d, LeasePatch(new CompositeStateReader(s, d.asBlockDiff))) - else d) - .map(diff => { - val effectiveBalanceSnapshots = diff.portfolios - .filter { case (acc, portfolio) => portfolio.effectiveBalance != 0 } - .map { case (acc, portfolio) => (acc, s.accountPortfolio(acc).effectiveBalance, portfolio.effectiveBalance) } - .map { case (acc, oldEffBalance, effBalanceDiff) => - EffectiveBalanceSnapshot(acc = acc, - height = s.height + 1, - prevEffectiveBalance = if (s.height == 0) (oldEffBalance + effBalanceDiff) else oldEffBalance, - effectiveBalance = oldEffBalance + effBalanceDiff) - } - .toSet - - BlockDiff(diff, 1, effectiveBalanceSnapshots) - } - ) + else d + val newSnapshots = diff.portfolios + .collect { case (acc, portfolioDiff) if (portfolioDiff.balance != 0 || portfolioDiff.effectiveBalance != 0) => + val oldPortfolio = s.accountPortfolio(acc) + acc -> SortedMap(currentBlockHeight -> Snapshot( + prevHeight = s.lastUpdateHeight(acc).getOrElse(0), + balance = oldPortfolio.balance + portfolioDiff.balance, + effectiveBalance = oldPortfolio.effectiveBalance + portfolioDiff.effectiveBalance)) + } + BlockDiff(diff, 1, newSnapshots) + } } - def unsafeDiffMany(settings: FunctionalitySettings, log: (String) => Unit = _ => ())(s: StateReader, blocks: Seq[Block]): BlockDiff = { + def unsafeDiffMany(settings: FunctionalitySettings)(s: StateReader, blocks: Seq[Block]): BlockDiff = { val r = blocks.foldLeft(Monoid[BlockDiff].empty) { case (diff, block) => val blockDiff = apply(settings)(new CompositeStateReader(s, diff), block).explicitGet() if (diff.heightDiff % 1000 == 0) { - log(s"Rebuilt ${diff.heightDiff} blocks out of ${blocks.size}") + log.info(s"Rebuilt ${diff.heightDiff} blocks out of ${blocks.size}") } Monoid[BlockDiff].combine(diff, blockDiff) } - log(s"Rebuild of ${blocks.size} completed") + log.info(s"Rebuild of ${blocks.size} blocks completed") r } } diff --git a/src/main/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiff.scala b/src/main/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiff.scala index e990bb304ee..0eff63e8273 100644 --- a/src/main/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiff.scala +++ b/src/main/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiff.scala @@ -30,7 +30,7 @@ object LeaseTransactionsDiff { sender -> Portfolio(-tx.fee, LeaseInfo(0, tx.amount), Map.empty), recipient -> Portfolio(0, LeaseInfo(tx.amount, 0), Map.empty) ) - Right(Diff(height = height, tx = tx, portfolios = portfolioDiff)) + Right(Diff(height = height, tx = tx, portfolios = portfolioDiff, leaseState = Map(EqByteArray(tx.id) -> true))) } } } @@ -53,14 +53,14 @@ object LeaseTransactionsDiff { Right(Monoid.combine( Map(canceller -> Portfolio(-tx.fee, LeaseInfo(0, -lease.amount), Map.empty)), Map(recipient -> Portfolio(0, LeaseInfo(-lease.amount, 0), Map.empty)))) - } else if (time < settings.allowMultipleLeaseCancelTransactionUntilTimestamp) { // cancel of another acc + } else if (time < settings.allowMultipleLeaseCancelTransactionUntilTimestamp) { // cancel of another acc Right(Monoid.combine( Map(canceller -> Portfolio(-tx.fee, LeaseInfo(0, -lease.amount), Map.empty)), Map(recipient -> Portfolio(0, LeaseInfo(-lease.amount, 0), Map.empty)))) } else Left(TransactionValidationError(tx, s"LeaseTransaction was leased by other sender " + s"and time=$time > allowMultipleLeaseCancelTransactionUntilTimestamp=${settings.allowMultipleLeaseCancelTransactionUntilTimestamp}")) - } yield Diff(height = height, tx = tx, portfolios = portfolioDiff) + } yield Diff(height = height, tx = tx, portfolios = portfolioDiff, leaseState = Map(EqByteArray(lease.id) -> false)) } } diff --git a/src/main/scala/com/wavesplatform/state2/patch/LeasePatch.scala b/src/main/scala/com/wavesplatform/state2/patch/LeasePatch.scala index 8a4dd5fc71f..002eeae5140 100644 --- a/src/main/scala/com/wavesplatform/state2/patch/LeasePatch.scala +++ b/src/main/scala/com/wavesplatform/state2/patch/LeasePatch.scala @@ -6,11 +6,11 @@ import com.wavesplatform.state2.{Diff, LeaseInfo, Portfolio} object LeasePatch { def apply(s: StateReader): Diff = { - def invertLeaseInfo(l: LeaseInfo): LeaseInfo = LeaseInfo(-l.leaseIn, -l.leaseOut ) + def invertLeaseInfo(l: LeaseInfo): LeaseInfo = LeaseInfo(-l.leaseIn, -l.leaseOut) val portfolioUpd = s.accountPortfolios - .filter { case (_, pf) => pf.leaseInfo != LeaseInfo.empty } - .map { case (acc, pf) => acc -> Portfolio(0, invertLeaseInfo(pf.leaseInfo), Map.empty) } + .collect { case (acc, pf) if pf.leaseInfo != LeaseInfo.empty => + acc -> Portfolio(0, invertLeaseInfo(pf.leaseInfo), Map.empty) } Diff(transactions = Map.empty, portfolios = portfolioUpd, @@ -18,7 +18,7 @@ object LeasePatch { aliases = Map.empty, paymentTransactionIdsByHashes = Map.empty, previousExchangeTxs = Map.empty, - patchExtraLeaseIdsToCancel = s.activeLeases()) + leaseState = s.activeLeases().map(_ -> false).toMap) } } diff --git a/src/main/scala/com/wavesplatform/state2/reader/CompositeStateReader.scala b/src/main/scala/com/wavesplatform/state2/reader/CompositeStateReader.scala index 2cc9c8ae6fd..245ab9287b1 100644 --- a/src/main/scala/com/wavesplatform/state2/reader/CompositeStateReader.scala +++ b/src/main/scala/com/wavesplatform/state2/reader/CompositeStateReader.scala @@ -31,35 +31,8 @@ class CompositeStateReader(inner: StateReader, blockDiff: BlockDiff) extends Sta fromDiff ++ inner.accountTransactionIds(a) // fresh head ++ stale tail } - override def effectiveBalanceAtHeightWithConfirmations(acc: Account, height: Int, confs: Int): Long = { - val localEffectiveBalanceSnapshotsOfAccount = blockDiff.effectiveBalanceSnapshots - .filter(ebs => ebs.acc == acc) - - lazy val relatedUpdates = localEffectiveBalanceSnapshotsOfAccount.filter(_.height > height - confs) - lazy val storedEffectiveBalance = inner.effectiveBalanceAtHeightWithConfirmations(acc, height - blockDiff.heightDiff, confs - blockDiff.heightDiff) - - if (localEffectiveBalanceSnapshotsOfAccount.isEmpty) - storedEffectiveBalance - else { - if (confs < blockDiff.heightDiff) { - relatedUpdates.headOption match { - case None => - localEffectiveBalanceSnapshotsOfAccount.maxBy(_.height).effectiveBalance - case Some(relatedUpdate) => - Math.min(relatedUpdate.prevEffectiveBalance, relatedUpdates.map(_.effectiveBalance).min) - } - } - else { - val localMin = localEffectiveBalanceSnapshotsOfAccount.map(_.effectiveBalance).min - val prevEffBalance = if (inner.height == 0) - localEffectiveBalanceSnapshotsOfAccount.minBy(_.height).prevEffectiveBalance - else - storedEffectiveBalance - Math.min(prevEffBalance, localMin) - - } - } - } + override def snapshotAtHeight(acc: Account, h: Int): Option[Snapshot] = + blockDiff.snapshots.get(acc).flatMap(_.get(h)).orElse(inner.snapshotAtHeight(acc, h)) override def paymentTransactionIdByHash(hash: ByteArray): Option[ByteArray] = blockDiff.txsDiff.paymentTransactionIdsByHashes.get(hash) @@ -89,12 +62,14 @@ class CompositeStateReader(inner: StateReader, blockDiff: BlockDiff) extends Sta } override def activeLeases(): Seq[ByteArray] = { - blockDiff.txsDiff.effectiveLeaseTxUpdates._1.toSeq ++ inner.activeLeases() + blockDiff.txsDiff.leaseState.collect { case (id, isActive) if isActive => id }.toSeq ++ inner.activeLeases() } + + override def lastUpdateHeight(acc: Account): Option[Int] = blockDiff.snapshots.get(acc).map(_.keySet.max).orElse(inner.lastUpdateHeight(acc)) } object CompositeStateReader { - def proxy(inner: StateReader, blockDiff: () => BlockDiff): StateReader = new StateReader { + class Proxy(val inner: StateReader, blockDiff: () => BlockDiff) extends StateReader { override def paymentTransactionIdByHash(hash: ByteArray): Option[ByteArray] = new CompositeStateReader(inner, blockDiff()).paymentTransactionIdByHash(hash) @@ -114,9 +89,6 @@ object CompositeStateReader { override def transactionInfo(id: ByteArray): Option[(Int, Transaction)] = new CompositeStateReader(inner, blockDiff()).transactionInfo(id) - override def effectiveBalanceAtHeightWithConfirmations(acc: Account, height: Int, confs: Int): Long = - new CompositeStateReader(inner, blockDiff()).effectiveBalanceAtHeightWithConfirmations(acc, height, confs) - override def findPreviousExchangeTxs(orderId: EqByteArray): Set[ExchangeTransaction] = new CompositeStateReader(inner, blockDiff()).findPreviousExchangeTxs(orderId) @@ -134,5 +106,12 @@ object CompositeStateReader { override def activeLeases(): Seq[ByteArray] = new CompositeStateReader(inner, blockDiff()).activeLeases() + + override def lastUpdateHeight(acc: Account): Option[Int] = + new CompositeStateReader(inner, blockDiff()).lastUpdateHeight(acc) + + override def snapshotAtHeight(acc: Account, h: Int): Option[Snapshot] = + new CompositeStateReader(inner, blockDiff()).snapshotAtHeight(acc, h) } + } \ No newline at end of file diff --git a/src/main/scala/com/wavesplatform/state2/reader/StateReader.scala b/src/main/scala/com/wavesplatform/state2/reader/StateReader.scala index 664e27738fb..6908731da44 100644 --- a/src/main/scala/com/wavesplatform/state2/reader/StateReader.scala +++ b/src/main/scala/com/wavesplatform/state2/reader/StateReader.scala @@ -3,12 +3,15 @@ package com.wavesplatform.state2.reader import com.google.common.base.Charsets import com.wavesplatform.state2._ import scorex.account.{Account, AccountOrAlias, Alias} +import scorex.crypto.hash.FastCryptographicHash import scorex.transaction.ValidationError.AliasNotExists import scorex.transaction._ import scorex.transaction.assets.IssueTransaction import scorex.transaction.assets.exchange.{ExchangeTransaction, Order} import scorex.transaction.lease.LeaseTransaction +import scorex.utils.ScorexLogging +import scala.annotation.tailrec import scala.reflect.ClassTag import scala.util.Right @@ -26,8 +29,6 @@ trait StateReader { def accountTransactionIds(a: Account): Seq[ByteArray] - def effectiveBalanceAtHeightWithConfirmations(acc: Account, height: Int, confs: Int): Long - def paymentTransactionIdByHash(hash: ByteArray): Option[ByteArray] def aliasesOfAddress(a: Account): Seq[Alias] @@ -39,11 +40,15 @@ trait StateReader { def isLeaseActive(leaseTx: LeaseTransaction): Boolean def activeLeases(): Seq[ByteArray] + + def lastUpdateHeight(acc: Account): Option[Int] + + def snapshotAtHeight(acc: Account, h: Int): Option[Snapshot] } object StateReader { - implicit class StateReaderExt(s: StateReader) { + implicit class StateReaderExt(s: StateReader) extends ScorexLogging { def assetDistribution(assetId: ByteArray): Map[Account, Long] = s.accountPortfolios .mapValues(portfolio => portfolio.assets.get(assetId)) @@ -108,5 +113,58 @@ object StateReader { .map(tx => new String(tx.name, Charsets.UTF_8)) .getOrElse("Unknown") } + + def stateHash(): Int = (BigInt(FastCryptographicHash(s.accountPortfolios.toString().getBytes)) % Int.MaxValue).toInt + + private def minBySnapshot(acc: Account, atHeight: Int, confirmations: Int)(extractor: Snapshot => Long): Long = { + val bottomNotIncluded = atHeight - confirmations + + @tailrec + def loop(deeperHeight: Int, list: Seq[Snapshot]): Seq[Snapshot] = { + if (deeperHeight == 0) list + else { + val snapshot = s.snapshotAtHeight(acc, deeperHeight).get + if (deeperHeight <= bottomNotIncluded) + snapshot +: list + else if (deeperHeight > atHeight && snapshot.prevHeight > atHeight) { + loop(snapshot.prevHeight, list) + } else + loop(snapshot.prevHeight, snapshot +: list) + } + } + + val snapshots: Seq[Snapshot] = s.lastUpdateHeight(acc) match { + case None => Seq(Snapshot(0, 0, 0)) + case Some(h) if h < atHeight - confirmations => + val pf = s.accountPortfolio(acc) + Seq(Snapshot(h, pf.balance, pf.effectiveBalance)) + case Some(h) => loop(h, Seq.empty) + } + + snapshots.map(extractor).min + } + + def effectiveBalanceAtHeightWithConfirmations(acc: Account, atHeight: Int, confirmations: Int): Long = + minBySnapshot(acc, atHeight, confirmations)(_.effectiveBalance) + + def balanceWithConfirmations(acc: Account, confirmations: Int): Long = + minBySnapshot(acc, s.height, confirmations)(_.balance) + + def balanceAtHeight(acc: Account, height: Int): Long = { + + @tailrec + def loop(lookupHeight: Int): Long = s.snapshotAtHeight(acc, lookupHeight) match { + case None if lookupHeight == 0 => 0 + case Some(snapshot) if lookupHeight <= height => snapshot.balance + case Some(snapshot) => loop(snapshot.prevHeight) + case None => + throw new Exception(s"Cannot lookup account $acc for height $height(current=${s.height}). " + + s"No history found at requested lookupHeight=$lookupHeight") + + } + + loop(s.lastUpdateHeight(acc).getOrElse(0)) + } } + } diff --git a/src/main/scala/com/wavesplatform/state2/reader/StateReaderImpl.scala b/src/main/scala/com/wavesplatform/state2/reader/StateReaderImpl.scala index 21cd1841157..b9734b18d69 100644 --- a/src/main/scala/com/wavesplatform/state2/reader/StateReaderImpl.scala +++ b/src/main/scala/com/wavesplatform/state2/reader/StateReaderImpl.scala @@ -7,7 +7,7 @@ import scorex.transaction.assets.exchange.ExchangeTransaction import scorex.transaction.lease.LeaseTransaction import scorex.transaction.{Transaction, TransactionParser} -import scala.collection.JavaConverters.iterableAsScalaIterableConverter +import scala.collection.JavaConverters._ class StateReaderImpl(p: StateStorage) extends StateReader { @@ -32,25 +32,14 @@ class StateReaderImpl(p: StateStorage) extends StateReader { .map(EqByteArray) } - override def effectiveBalanceAtHeightWithConfirmations(acc: Account, atHeight: Int, confs: Int): Long = { - val bockNumberThatIsConfsOld = Math.max(1, atHeight - confs) - val confsOldMinimum: Seq[(Long, Long)] = Range(bockNumberThatIsConfsOld + 1, atHeight + 1) - .flatMap { height => Option(p.effectiveBalanceSnapshots.get((acc.bytes, height))) } - confsOldMinimum.headOption match { - case None => accountPortfolio(acc).effectiveBalance - case Some((oldest, _)) => Math.min(oldest, confsOldMinimum.map(_._2).min) - } - } - override def paymentTransactionIdByHash(hash: ByteArray): Option[ByteArray] = Option(p.paymentTransactionHashes.get(hash)).map(EqByteArray) override def aliasesOfAddress(a: Account): Seq[Alias] = - p.aliasToAddress.entrySet().asScala - .filter(_.getValue sameElements a.bytes) - .map(_.getKey) - .map(aliasStr => Alias.buildWithCurrentNetworkByte(aliasStr).explicitGet()) - .toSeq + p.aliasToAddress.asScala + .collect { case (aliasStr, addressBytes) if addressBytes sameElements a.bytes => + Alias.buildWithCurrentNetworkByte(aliasStr).explicitGet() + }.toSeq override def resolveAlias(a: Alias): Option[Account] = @@ -63,17 +52,22 @@ class StateReaderImpl(p: StateStorage) extends StateReader { .flatMap(id => this.findTransaction[ExchangeTransaction](id)) override def accountPortfolios: Map[Account, Portfolio] = - p.portfolios.entrySet().asScala - .map { entry => entry.getKey -> entry.getValue } - .map { case (acc, (b, (i, o), as)) => Account.fromBytes(acc).explicitGet() -> Portfolio(b, LeaseInfo(i, o), as.map { case (k, v) => EqByteArray(k) -> v }) } - .toMap + p.portfolios.asScala.map { + case (acc, (b, (i, o), as)) => Account.fromBytes(acc).explicitGet() -> Portfolio(b, LeaseInfo(i, o), as.map { + case (k, v) => EqByteArray(k) -> v + }) + }.toMap override def isLeaseActive(leaseTx: LeaseTransaction): Boolean = p.leaseState.getOrDefault(leaseTx.id, false) - override def activeLeases(): Seq[ByteArray] = p.leaseState.entrySet() + override def activeLeases(): Seq[ByteArray] = p.leaseState .asScala - .filter(_.getValue) - .map(_.getKey) - .map(EqByteArray) + .collect { case (leaseId, isActive) if isActive => EqByteArray(leaseId) } .toSeq + + override def lastUpdateHeight(acc: Account): Option[Int] = Option(p.lastUpdateHeight.get(acc.bytes)) + + override def snapshotAtHeight(acc: Account, h: Int): Option[Snapshot] = + Option(p.balanceSnapshots.get(StateStorage.snapshotKey(acc, h))) + .map { case (ph, b, eb) => Snapshot(ph, b, eb) } } diff --git a/src/main/scala/scorex/api/http/AddressApiRoute.scala b/src/main/scala/scorex/api/http/AddressApiRoute.scala index 81eb3b7e589..7633bfe95ea 100644 --- a/src/main/scala/scorex/api/http/AddressApiRoute.scala +++ b/src/main/scala/scorex/api/http/AddressApiRoute.scala @@ -27,7 +27,7 @@ case class AddressApiRoute(settings: RestAPISettings, wallet: Wallet, state: Sta override lazy val route = pathPrefix("addresses") { - validate ~ seed ~ balanceWithConfirmations ~ balanceDetails ~ balance ~ verify ~ sign ~ deleteAddress ~ verifyText ~ + validate ~ seed ~ balanceWithConfirmations ~ balanceDetails ~ balance ~ balanceWithConfirmations ~ verify ~ sign ~ deleteAddress ~ verifyText ~ signText ~ seq ~ publicKey ~ effectiveBalance ~ effectiveBalanceWithConfirmations } ~ root ~ create @@ -120,7 +120,7 @@ case class AddressApiRoute(settings: RestAPISettings, wallet: Wallet, state: Sta new ApiImplicitParam(name = "address", value = "Address", required = true, dataType = "string", paramType = "path") )) def balance: Route = (path("balance" / Segment) & get) { address => - complete(balanceJson(address, 0)) + complete(balanceJson(address)) } @Path("/balance/details/{address}") @@ -237,7 +237,15 @@ case class AddressApiRoute(settings: RestAPISettings, wallet: Wallet, state: Sta Account.fromString(address).right.map(acc => ToResponseMarshallable(Balance( acc.address, confirmations, - state.accountPortfolio(acc).balance + state.balanceWithConfirmations(acc, confirmations) + ))).getOrElse(InvalidAddress) + } + + private def balanceJson(address: String): ToResponseMarshallable = { + Account.fromString(address).right.map(acc => ToResponseMarshallable(Balance( + acc.address, + 0, + state.balance(acc) ))).getOrElse(InvalidAddress) } diff --git a/src/main/scala/scorex/app/RunnableApplication.scala b/src/main/scala/scorex/app/RunnableApplication.scala index c57f40846e5..ac058915ea4 100755 --- a/src/main/scala/scorex/app/RunnableApplication.scala +++ b/src/main/scala/scorex/app/RunnableApplication.scala @@ -23,9 +23,8 @@ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.reflect.runtime.universe.Type -import scala.util.Try +import scala.util.{Left, Try} -import com.wavesplatform.state2._ trait RunnableApplication extends Application with Shutdownable with ScorexLogging { @@ -121,13 +120,16 @@ trait RunnableApplication extends Application with Shutdownable with ScorexLoggi private def checkGenesis(): Unit = { if (transactionModule.blockStorage.history.isEmpty) { - val maybeGenesisSignature = Option(settings.blockchainSettings.genesisSettings.signature).filter(_.trim.nonEmpty) - transactionModule.blockStorage.blockchainUpdater.processBlock(Block.genesis(consensusModule.genesisData, transactionModule.genesisData, - settings.blockchainSettings.genesisSettings.blockTimestamp, maybeGenesisSignature)).explicitGet() + settings.blockchainSettings.genesisSettings.blockTimestamp, maybeGenesisSignature)) match { + case Left(value) => + log.error(value.toString) + System.exit(1) + case _ => + } log.info("Genesis block has been added to the state") } - }.ensuring(transactionModule.blockStorage.history.height() >= 1) + } } diff --git a/src/main/scala/scorex/network/UnconfirmedPoolSynchronizer.scala b/src/main/scala/scorex/network/UnconfirmedPoolSynchronizer.scala index dfb69194791..f38bd5a347f 100644 --- a/src/main/scala/scorex/network/UnconfirmedPoolSynchronizer.scala +++ b/src/main/scala/scorex/network/UnconfirmedPoolSynchronizer.scala @@ -27,7 +27,7 @@ class UnconfirmedPoolSynchronizer(private val transactionModule: TransactionModu log.debug(s"Got tx: $tx") transactionModule.putUnconfirmedIfNew(tx) match { case Right(_) => broadcastExceptOf(tx, remote) - case Left(err) => log.error(s"transaction $tx has been rejected by UTX pool. Reason: $err") + case Left(err) => log.error(s"Transaction $tx has been rejected by UTX pool. Reason: $err") } case BroadcastRandom => diff --git a/src/main/scala/scorex/waves/http/DebugApiRoute.scala b/src/main/scala/scorex/waves/http/DebugApiRoute.scala index 20e79c640d0..e86f04456c0 100644 --- a/src/main/scala/scorex/waves/http/DebugApiRoute.scala +++ b/src/main/scala/scorex/waves/http/DebugApiRoute.scala @@ -7,6 +7,7 @@ import com.wavesplatform.settings.RestAPISettings import com.wavesplatform.state2.reader.StateReader import io.swagger.annotations._ import play.api.libs.json.{JsArray, Json} +import scorex.account.Account import scorex.api.http._ import scorex.crypto.encode.Base58 import scorex.crypto.hash.FastCryptographicHash @@ -18,7 +19,7 @@ import scorex.wallet.Wallet case class DebugApiRoute(settings: RestAPISettings, wallet: Wallet, stateReader: StateReader, history: History) extends ApiRoute { override lazy val route = pathPrefix("debug") { - blocks ~ state ~ info + blocks ~ state ~ info ~ stateWaves } @Path("/blocks/{howMany}") @@ -51,6 +52,21 @@ case class DebugApiRoute(settings: RestAPISettings, wallet: Wallet, stateReader: ) } + + @Path("/stateWaves/{height}") + @ApiOperation(value = "State at block", notes = "Get state at specified height", httpMethod = "GET") + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "height", value = "height", required = true, dataType = "integer", paramType = "path") + )) + def stateWaves: Route = (path("stateWaves" / IntNumber) & get) { height => + val result = stateReader.accountPortfolios.keys + .map(acc => acc.stringRepr -> stateReader.balanceAtHeight(acc, height)) + .filter(_._2 != 0) + .toMap + complete(result) + } + + @Path("/info") @ApiOperation(value = "State", notes = "All info you need to debug", httpMethod = "GET") @ApiResponses(Array( diff --git a/src/test/scala/com/wavesplatform/http/ConsensusRouteSpec.scala b/src/test/scala/com/wavesplatform/http/ConsensusRouteSpec.scala index f85abab6cd5..1197ec83285 100644 --- a/src/test/scala/com/wavesplatform/http/ConsensusRouteSpec.scala +++ b/src/test/scala/com/wavesplatform/http/ConsensusRouteSpec.scala @@ -82,5 +82,4 @@ class ConsensusRouteSpec extends RouteSpec("/consensus") with RestAPISettingsHel } } } - } diff --git a/src/test/scala/com/wavesplatform/state2/diffs/GenesisTransactionDiffTest.scala b/src/test/scala/com/wavesplatform/state2/diffs/GenesisTransactionDiffTest.scala index 5bc6f18d438..b0981349ef6 100644 --- a/src/test/scala/com/wavesplatform/state2/diffs/GenesisTransactionDiffTest.scala +++ b/src/test/scala/com/wavesplatform/state2/diffs/GenesisTransactionDiffTest.scala @@ -27,7 +27,7 @@ class GenesisTransactionDiffTest extends PropSpec with PropertyChecks with Gener totalPortfolioDiff.assets shouldBe Map.empty gtxs.foreach { gtx => - blockDiff.effectiveBalanceSnapshots.contains(EffectiveBalanceSnapshot(gtx.recipient, 1, gtx.amount, gtx.amount)) + blockDiff.snapshots(gtx.recipient) shouldBe Map(1 -> Snapshot(0, gtx.amount, gtx.amount)) } } } diff --git a/src/test/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiffTest.scala b/src/test/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiffTest.scala index 573d32c5933..7c79bc60267 100644 --- a/src/test/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiffTest.scala +++ b/src/test/scala/com/wavesplatform/state2/diffs/LeaseTransactionsDiffTest.scala @@ -39,8 +39,7 @@ class LeaseTransactionsDiffTest extends PropSpec with PropertyChecks with Genera totalPortfolioDiff.effectiveBalance shouldBe 0 totalPortfolioDiff.assets.values.foreach(_ shouldBe 0) - val snapshot = EffectiveBalanceSnapshot(lease.recipient.asInstanceOf[Account], 2, 0, lease.amount) - totalDiff.effectiveBalanceSnapshots should contain(snapshot) + totalDiff.snapshots(lease.recipient.asInstanceOf[Account]) shouldBe Map(2 -> Snapshot(0, 0, lease.amount)) } assertDiffAndState(Seq(TestBlock(Seq(genesis, lease))), TestBlock(Seq(leaseCancel), miner)) { case (totalDiff, newState) => @@ -50,8 +49,7 @@ class LeaseTransactionsDiffTest extends PropSpec with PropertyChecks with Genera totalPortfolioDiff.effectiveBalance shouldBe 0 totalPortfolioDiff.assets.values.foreach(_ shouldBe 0) - val snapshot = EffectiveBalanceSnapshot(lease.recipient.asInstanceOf[Account], 2, lease.amount, 0) - totalDiff.effectiveBalanceSnapshots should contain(snapshot) + totalDiff.snapshots(lease.recipient.asInstanceOf[Account]) shouldBe Map(2 -> Snapshot(1, 0, 0)) newState.accountPortfolio(lease.sender).leaseInfo shouldBe LeaseInfo.empty newState.accountPortfolio(lease.recipient.asInstanceOf[Account]).leaseInfo shouldBe LeaseInfo.empty diff --git a/src/test/scala/com/wavesplatform/state2/reader/CompositeStateReaderEffectiveBalanceTest.scala b/src/test/scala/com/wavesplatform/state2/reader/CompositeStateReaderEffectiveBalanceTest.scala deleted file mode 100644 index e3ce26f62b1..00000000000 --- a/src/test/scala/com/wavesplatform/state2/reader/CompositeStateReaderEffectiveBalanceTest.scala +++ /dev/null @@ -1,103 +0,0 @@ -package com.wavesplatform.state2.reader - -import cats.kernel.Monoid -import com.wavesplatform.state2.{BlockDiff, Diff, EffectiveBalanceSnapshot} -import org.scalamock.scalatest.MockFactory -import org.scalatest.{FreeSpec, Matchers} -import scorex.account.Account - -class CompositeStateReaderEffectiveBalanceTest extends FreeSpec with MockFactory with Matchers { - - val acc: Account = Account.fromPublicKey(Array.emptyByteArray) - val innerHeight = 1000 - - "blockDiff contains no effective balance changes" in { - val heightDiff = 100 - val blockDiff = BlockDiff( - txsDiff = Monoid[Diff].empty, - heightDiff = heightDiff, - effectiveBalanceSnapshots = Set.empty) - - val innerEffectiveBalance = 10000 - - val inner = stub[StateReader] - (inner.effectiveBalanceAtHeightWithConfirmations _).when(*, *, *).returns(innerEffectiveBalance) - - val composite = new CompositeStateReader(inner, blockDiff) - composite.effectiveBalanceAtHeightWithConfirmations(acc, innerHeight + heightDiff, 30) shouldBe 10000 - } - - "blockDiff contains effective balance changes" - { - "confirmations required is less than blockDiff height" - { - "block diff contains records in the past in the requested range" in { - val heightDiff = 100 - val blockDiff = BlockDiff( - txsDiff = Monoid[Diff].empty, - heightDiff = heightDiff, - effectiveBalanceSnapshots = Set( - EffectiveBalanceSnapshot(acc, innerHeight + 80, 10000, 50000))) - - val inner = stub[StateReader] - (inner.height _).when().returns(innerHeight) - - val composite = new CompositeStateReader(inner, blockDiff) - composite.effectiveBalanceAtHeightWithConfirmations(acc, innerHeight + heightDiff, 30) shouldBe 10000 - } - - "block diff contains records but they are out of requested range(too old)" in { - val heightDiff = 100 - val blockDiff = BlockDiff( - txsDiff = Monoid[Diff].empty, - heightDiff = heightDiff, - effectiveBalanceSnapshots = Set( - EffectiveBalanceSnapshot(acc, innerHeight + 80, 20000, 4000), - EffectiveBalanceSnapshot(acc, innerHeight + 50, 50000, 20000), - EffectiveBalanceSnapshot(acc, innerHeight + 90, 4000, 10000))) - - val inner = stub[StateReader] - (inner.height _).when().returns(innerHeight) - - val composite = new CompositeStateReader(inner, blockDiff) - composite.effectiveBalanceAtHeightWithConfirmations(acc, innerHeight + heightDiff, 2) shouldBe 10000 - } - - } - - "confirmations required is greater or equal blockdiff.height" - { - - "nothing is stored(Genesis block snapshot results are in memory)" in { - val heightDiff = 100 - val blockDiff = BlockDiff( - txsDiff = Monoid[Diff].empty, - heightDiff = heightDiff, - effectiveBalanceSnapshots = Set( - EffectiveBalanceSnapshot(acc, 80, 2000, 10000), - EffectiveBalanceSnapshot(acc, 90, 10000, 50000))) - - val inner = stub[StateReader] - (inner.height _).when().returns(0) - - val composite = new CompositeStateReader(inner, blockDiff) - composite.effectiveBalanceAtHeightWithConfirmations(acc, heightDiff, heightDiff + 100) shouldBe 2000 - } - - "some history of acc is stored" in { - val heightDiff = 100 - val blockDiff = BlockDiff( - txsDiff = Monoid[Diff].empty, - heightDiff = heightDiff, - effectiveBalanceSnapshots = Set( - EffectiveBalanceSnapshot(acc, 80, 2000, 10000), - EffectiveBalanceSnapshot(acc, 90, 10000, 50000))) - - val inner = stub[StateReader] - (inner.height _).when().returns(innerHeight) - val innerEffectiveBalance = 500 - (inner.effectiveBalanceAtHeightWithConfirmations _).when(*, *, *).returns(innerEffectiveBalance) - - val composite = new CompositeStateReader(inner, blockDiff) - composite.effectiveBalanceAtHeightWithConfirmations(acc, heightDiff, heightDiff + 50) shouldBe 500 - } - } - } -} diff --git a/src/test/scala/com/wavesplatform/state2/reader/StateReaderEffectiveBalanceTest.scala b/src/test/scala/com/wavesplatform/state2/reader/StateReaderEffectiveBalanceTest.scala new file mode 100644 index 00000000000..7e3c4f408c9 --- /dev/null +++ b/src/test/scala/com/wavesplatform/state2/reader/StateReaderEffectiveBalanceTest.scala @@ -0,0 +1,64 @@ +package com.wavesplatform.state2.reader + +import com.wavesplatform.state2.{StateStorage} +import org.h2.mvstore.MVStore +import org.scalatest.{FunSuite, Matchers} +import scorex.account.Account + + +class StateReaderEffectiveBalanceTest extends FunSuite with Matchers { + + val acc: Account = Account.fromPublicKey(Array.emptyByteArray) + val stateHeight = 100 + + test("exposes minimum of all 'current' and one 'previous' of oldest record") { + + val storage = new StateStorage(new MVStore.Builder().open()) + + storage.setHeight(stateHeight) + + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 20), (0, 0, 1)) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 75), (20, 0, 200)) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 90), (75, 0, 100)) + storage.lastUpdateHeight.put(acc.bytes, 90) + + new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, stateHeight, 50) shouldBe 1 + } + + test("exposes current effective balance if no records in past N blocks are made") { + + val storage = new StateStorage(new MVStore.Builder().open()) + + storage.setHeight(stateHeight) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 20), (0, 0, 1)) + storage.portfolios.put(acc.bytes, (1, (0, 0), Map.empty)) + storage.lastUpdateHeight.put(acc.bytes, 20) + + new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, stateHeight, 50) shouldBe 1 + } + + test("doesn't include info older than N blocks") { + val storage = new StateStorage(new MVStore.Builder().open()) + + storage.setHeight(stateHeight) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 20), (0, 0, 1000)) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 50), (20, 0, 50000)) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 75), (50, 0, 100000)) + storage.lastUpdateHeight.put(acc.bytes, 75) + + + new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, stateHeight, 50) shouldBe 50000 + } + + test("includes most recent update") { + val storage = new StateStorage(new MVStore.Builder().open()) + + storage.setHeight(stateHeight) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 20), (0, 0, 1000)) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 51), (20, 0, 50000)) + storage.balanceSnapshots.put(StateStorage.snapshotKey(acc, 100), (51, 0, 1)) + storage.lastUpdateHeight.put(acc.bytes, 100) + + new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, stateHeight, 50) shouldBe 1 + } +} \ No newline at end of file diff --git a/src/test/scala/com/wavesplatform/state2/reader/StateReaderImplEffectiveBalanceTest.scala b/src/test/scala/com/wavesplatform/state2/reader/StateReaderImplEffectiveBalanceTest.scala deleted file mode 100644 index 7f99b8d512a..00000000000 --- a/src/test/scala/com/wavesplatform/state2/reader/StateReaderImplEffectiveBalanceTest.scala +++ /dev/null @@ -1,60 +0,0 @@ -package com.wavesplatform.state2.reader - -import com.wavesplatform.state2.StateStorage -import org.h2.mvstore.MVStore -import org.scalatest.{FunSuite, Matchers} -import scorex.account.Account - - -class StateReaderImplEffectiveBalanceTest extends FunSuite with Matchers { - - val acc: Account = Account.fromPublicKey(Array.emptyByteArray) - val stateHeight = 100 - - private def mkStorage() = new StateStorage(new MVStore.Builder().open()) - - test("exposes minimum of all 'current' and one 'previous' of oldest record") { - - val storage = mkStorage() - - storage.setHeight(stateHeight) - - storage.effectiveBalanceSnapshots.put((acc.bytes, 20), (0, 1)) - storage.effectiveBalanceSnapshots.put((acc.bytes, 75), (1, 200)) - storage.effectiveBalanceSnapshots.put((acc.bytes, 90), (200, 100)) - - new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, 100, 50) shouldBe 1 - } - - test("exposes current effective balance if no records in past N blocks are made") { - - val storage = mkStorage() - - storage.setHeight(stateHeight) - storage.effectiveBalanceSnapshots.put((acc.bytes, 20), (0, 1)) - storage.portfolios.put(acc.bytes, (1, (0, 0), Map.empty)) - new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, 100, 50) shouldBe 1 - } - - test("doesn't include info older than N blocks") { - val storage = mkStorage() - - storage.setHeight(stateHeight) - storage.effectiveBalanceSnapshots.put((acc.bytes, 20), (0, 1000)) - storage.effectiveBalanceSnapshots.put((acc.bytes, 50), (1000, 50000)) - storage.effectiveBalanceSnapshots.put((acc.bytes, 75), (50000, 100000)) - - new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, 100, 50) shouldBe 50000 - } - - test("includes most recent update") { - val storage = mkStorage() - - storage.setHeight(stateHeight) - storage.effectiveBalanceSnapshots.put((acc.bytes, 20), (0, 1000)) - storage.effectiveBalanceSnapshots.put((acc.bytes, 51), (1000, 50000)) - storage.effectiveBalanceSnapshots.put((acc.bytes, 100), (50000, 1)) - - new StateReaderImpl(storage).effectiveBalanceAtHeightWithConfirmations(acc, 100, 50) shouldBe 1 - } -}