diff --git a/node/src/main/scala/com/wavesplatform/state/NewTransactionInfo.scala b/node/src/main/scala/com/wavesplatform/state/NewTransactionInfo.scala index fb8668ecab..5008b04da8 100644 --- a/node/src/main/scala/com/wavesplatform/state/NewTransactionInfo.scala +++ b/node/src/main/scala/com/wavesplatform/state/NewTransactionInfo.scala @@ -44,6 +44,7 @@ object NewTransactionInfo { elidedAffectedAddresses(tx, blockchain) ++ maybeDApp else snapshot.balances.keySet.map(_._1) ++ + snapshot.zeroBalanceAffected ++ snapshot.leaseBalances.keySet ++ snapshot.accountData.keySet ++ calledScripts ++ diff --git a/node/src/main/scala/com/wavesplatform/state/StateSnapshot.scala b/node/src/main/scala/com/wavesplatform/state/StateSnapshot.scala index 5f5a7f2228..284f9997b4 100644 --- a/node/src/main/scala/com/wavesplatform/state/StateSnapshot.scala +++ b/node/src/main/scala/com/wavesplatform/state/StateSnapshot.scala @@ -37,6 +37,7 @@ case class StateSnapshot( accountData: Map[Address, Map[String, DataEntry[?]]] = Map(), scriptResults: Map[ByteStr, InvokeScriptResult] = Map(), ethereumTransactionMeta: Map[ByteStr, EthereumTransactionMeta] = Map(), + zeroBalanceAffected: Set[Address] = Set(), scriptsComplexity: Long = 0 ) { import com.wavesplatform.protobuf.snapshot.TransactionStateSnapshot as S @@ -106,7 +107,7 @@ case class StateSnapshot( def addBalances(portfolios: Map[Address, Portfolio], blockchain: Blockchain): Either[String, StateSnapshot] = StateSnapshot .balances(portfolios, SnapshotBlockchain(blockchain, this)) - .map(b => copy(balances = balances ++ b)) + .map { case (a, b) => copy(zeroBalanceAffected = zeroBalanceAffected ++ a, balances = balances ++ b) } def withTransaction(tx: NewTransactionInfo): StateSnapshot = copy(transactions + (tx.transaction.id() -> tx)) @@ -270,13 +271,13 @@ object StateSnapshot { ): Either[ValidationError, StateSnapshot] = { val r = for { - b <- balances(portfolios, blockchain) - lb <- leaseBalances(portfolios, blockchain) - of <- this.orderFills(orderFills, blockchain) + (affected, balances) <- balances(portfolios, blockchain) + leaseBalances <- leaseBalances(portfolios, blockchain) + orderFills <- this.orderFills(orderFills, blockchain) } yield StateSnapshot( transactions, - b, - lb, + balances, + leaseBalances, assetStatics(issuedAssets), assetVolumes(blockchain, issuedAssets, updatedAssets), assetNamesAndDescriptions(issuedAssets, updatedAssets), @@ -284,41 +285,41 @@ object StateSnapshot { sponsorships.collect { case (asset, value: SponsorshipValue) => (asset, value) }, resolvedLeaseStates(blockchain, leaseStates, aliases), aliases, - of, + orderFills, accountScripts, accountData, scriptResults, ethereumTransactionMeta, + affected, scriptsComplexity ) r.leftMap(GenericError(_)) } // ignores lease balances from portfolios - private def balances(portfolios: Map[Address, Portfolio], blockchain: Blockchain): Either[String, VectorMap[(Address, Asset), Long]] = + private def balances( + portfolios: Map[Address, Portfolio], + blockchain: Blockchain + ): Either[String, (Set[Address], VectorMap[(Address, Asset), Long])] = flatTraverse(portfolios) { case (address, Portfolio(wavesAmount, _, assets)) => - val assetBalancesE = flatTraverse(assets) { + flatTraverse(assets.asInstanceOf[VectorMap[Asset, Long]] + (Waves -> wavesAmount)) { case (_, 0) => - Right(VectorMap[(Address, Asset), Long]()) - case (assetId, balance) => - safeSum(blockchain.balance(address, assetId), balance, s"$address -> Asset balance") - .map(newBalance => VectorMap((address, assetId: Asset) -> newBalance)) + Right((Set(address), VectorMap[(Address, Asset), Long]())) + case (asset, balance) => + val error = if (asset == Waves) "Waves balance" else "Asset balance" + safeSum(blockchain.balance(address, asset), balance, s"$address -> $error") + .map(newBalance => (Set(), VectorMap((address, asset) -> newBalance))) } - if (wavesAmount != 0) - for { - assetBalances <- assetBalancesE - newWavesBalance <- safeSum(blockchain.balance(address), wavesAmount, s"$address -> Waves balance") - } yield assetBalances + ((address, Waves) -> newWavesBalance) - else - assetBalancesE } - private def flatTraverse[E, K1, V1, K2, V2](m: Map[K1, V1])(f: (K1, V1) => Either[E, VectorMap[K2, V2]]): Either[E, VectorMap[K2, V2]] = - m.foldLeft(VectorMap[K2, V2]().asRight[E]) { + private def flatTraverse[E, A, K1, V1, K2, V2]( + m: Map[K1, V1] + )(f: (K1, V1) => Either[E, (Set[A], VectorMap[K2, V2])]): Either[E, (Set[A], VectorMap[K2, V2])] = + m.foldLeft((Set[A](), VectorMap[K2, V2]()).asRight[E]) { case (e @ Left(_), _) => e - case (Right(acc), (k, v)) => - f(k, v).map(acc ++ _) + case (Right((s, m)), (k, v)) => + f(k, v).map { case (ns, nm) => (s ++ ns, m ++ nm) } } def ofLeaseBalances(balances: Map[Address, LeaseBalance], blockchain: Blockchain): Either[String, StateSnapshot] = @@ -426,6 +427,7 @@ object StateSnapshot { combineDataEntries(s1.accountData, s2.accountData), s1.scriptResults |+| s2.scriptResults, s1.ethereumTransactionMeta ++ s2.ethereumTransactionMeta, + s1.zeroBalanceAffected ++ s2.zeroBalanceAffected, s1.scriptsComplexity + s2.scriptsComplexity ) diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/ExchangeTransactionDiffTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/ExchangeTransactionDiffTest.scala index 858e6f25ed..754f2a79e6 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/ExchangeTransactionDiffTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/ExchangeTransactionDiffTest.scala @@ -27,10 +27,12 @@ import com.wavesplatform.test.* import com.wavesplatform.test.DomainPresets.* import com.wavesplatform.transaction.* import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves} +import com.wavesplatform.transaction.TxHelpers.* import com.wavesplatform.transaction.TxValidationError.{AccountBalanceError, GenericError} import com.wavesplatform.transaction.assets.IssueTransaction import com.wavesplatform.transaction.assets.exchange.* import com.wavesplatform.transaction.assets.exchange.OrderPriceMode.{AssetDecimals, FixedDecimals, Default as DefaultPriceMode} +import com.wavesplatform.transaction.assets.exchange.OrderType.{BUY, SELL} import com.wavesplatform.transaction.smart.script.ScriptCompiler import com.wavesplatform.transaction.transfer.MassTransferTransaction.ParsedTransfer import com.wavesplatform.transaction.transfer.{MassTransferTransaction, TransferTransaction} @@ -336,7 +338,7 @@ class ExchangeTransactionDiffTest extends PropSpec with Inside with WithDomain w val totalPortfolioDiff: Portfolio = blockDiff.portfolios.values.fold(Portfolio())(_.combine(_).explicitGet()) totalPortfolioDiff.balance shouldBe 0 totalPortfolioDiff.effectiveBalance(false).explicitGet() shouldBe 0 - totalPortfolioDiff.assets.values.toSet should (be (Set()) or be (Set(0))) + totalPortfolioDiff.assets.values.toSet should (be(Set()) or be(Set(0))) blockDiff.portfolios(exchange.sender.toAddress).balance shouldBe exchange.buyMatcherFee + exchange.sellMatcherFee - exchange.fee.value } @@ -937,7 +939,7 @@ class ExchangeTransactionDiffTest extends PropSpec with Inside with WithDomain w BlockchainFeatures.FairPoS -> 0 ) - private val RideV6 = DomainPresets.RideV6.blockchainSettings.functionalitySettings + private val RideV6FS = DomainPresets.RideV6.blockchainSettings.functionalitySettings private def createSettings(preActivatedFeatures: (BlockchainFeature, Int)*): FunctionalitySettings = TestFunctionalitySettings.Enabled @@ -997,7 +999,7 @@ class ExchangeTransactionDiffTest extends PropSpec with Inside with WithDomain w } yield { val (genesis, transfers, issueAndScripts, exchangeTx, _) = smartTradePreconditions(buyerScriptSrc, sellerScriptSrc, txScript) val preconBlocks = Seq(TestBlock.create(Seq(genesis)), TestBlock.create(transfers), TestBlock.create(issueAndScripts)) - assertLeft(preconBlocks, TestBlock.create(Seq(exchangeTx)), RideV6)("TransactionNotAllowedByScript") + assertLeft(preconBlocks, TestBlock.create(Seq(exchangeTx)), RideV6FS)("TransactionNotAllowedByScript") } } @@ -1009,7 +1011,7 @@ class ExchangeTransactionDiffTest extends PropSpec with Inside with WithDomain w } yield { val (genesis, transfers, issueAndScripts, exchangeTx, _) = smartTradePreconditions(buyerScriptSrc, sellerScriptSrc, txScript) val preconBlocks = Seq(TestBlock.create(Seq(genesis)), TestBlock.create(transfers), TestBlock.create(issueAndScripts)) - assertLeft(preconBlocks, TestBlock.create(Seq(exchangeTx)), RideV6)("TransactionNotAllowedByScript") + assertLeft(preconBlocks, TestBlock.create(Seq(exchangeTx)), RideV6FS)("TransactionNotAllowedByScript") } } @@ -1021,7 +1023,7 @@ class ExchangeTransactionDiffTest extends PropSpec with Inside with WithDomain w } yield { val (genesis, transfers, issueAndScripts, exchangeTx, _) = smartTradePreconditions(buyerScriptSrc, sellerScriptSrc, txScript) val preconBlocks = Seq(TestBlock.create(Seq(genesis)), TestBlock.create(transfers), TestBlock.create(issueAndScripts)) - assertLeft(preconBlocks, TestBlock.create(Seq(exchangeTx)), RideV6)("TransactionNotAllowedByScript") + assertLeft(preconBlocks, TestBlock.create(Seq(exchangeTx)), RideV6FS)("TransactionNotAllowedByScript") } } @@ -2022,6 +2024,21 @@ class ExchangeTransactionDiffTest extends PropSpec with Inside with WithDomain w } } + property("tx belongs to matcher on zero balance diff") { + val matcher = signer(2) + withDomain(RideV6, Seq(AddrWithBalance(matcher.toAddress))) { d => + val issueTx = issue() + val asset = IssuedAsset(issueTx.id()) + val order1 = order(BUY, Waves, asset, fee = TestValues.fee / 2, matcher = matcher) + val order2 = order(SELL, Waves, asset, fee = TestValues.fee / 2, matcher = matcher) + val exchange = exchangeFromOrders(order1, order2, matcher = matcher, TestValues.fee) + d.appendBlock(issueTx) + d.appendBlock(exchange) + d.liquidDiff.portfolios.get(matcher.toAddress) shouldBe None + d.liquidDiff.transaction(exchange.id()).get.affected shouldBe Set(matcher.toAddress, defaultAddress) + } + } + def script(caseType: String, v: Boolean, complex: Boolean = false): Seq[String] = Seq(true, false).map { full => val expr = s""" diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/ci/InvokeAffectedAddressTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/ci/InvokeAffectedAddressTest.scala index 27df11b5e2..fda25667e3 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/ci/InvokeAffectedAddressTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/ci/InvokeAffectedAddressTest.scala @@ -62,4 +62,23 @@ class InvokeAffectedAddressTest extends PropSpec with WithDomain { } } } + + property("tx belongs to invoker on zero balance diff") { + def dApp(fee: Long) = + TestCompiler(V5).compileContract( + s""" + | @Callable(i) + | func default() = [ScriptTransfer(i.caller, $fee, unit)] + """.stripMargin + ) + + val invoker = signer(2) + withDomain(RideV5, AddrWithBalance.enoughBalances(secondSigner, invoker)) { d => + val invokeTx = invoke(invoker = invoker) + d.appendBlock(setScript(secondSigner, dApp(invokeTx.fee.value))) + d.appendAndAssertSucceed(invokeTx) + d.liquidDiff.portfolios.get(invoker.toAddress) shouldBe None + d.liquidDiff.transaction(invokeTx.id()).get.affected shouldBe Set(invoker.toAddress, secondAddress) + } + } }