diff --git a/compiler/damlc/lib/DA/Cli/Damlc/Test.hs b/compiler/damlc/lib/DA/Cli/Damlc/Test.hs index 0f52625408c..0377338f443 100644 --- a/compiler/damlc/lib/DA/Cli/Damlc/Test.hs +++ b/compiler/damlc/lib/DA/Cli/Damlc/Test.hs @@ -263,8 +263,7 @@ prettyErr lfVersion err = case err of prettyResult :: SS.ScenarioResult -> DA.Pretty.Doc Pretty.SyntaxClass prettyResult result = let nTx = length (SS.scenarioResultScenarioSteps result) - activeContracts = S.fromList (V.toList (SS.scenarioResultActiveContracts result)) - nActive = length $ filter (SS.isActive activeContracts) (V.toList (SS.scenarioResultNodes result)) + nActive = length $ filter (SS.isActive (SS.activeContractsFromScenarioResult result)) (V.toList (SS.scenarioResultNodes result)) in DA.Pretty.typeDoc_ "ok, " <> DA.Pretty.int nActive <> DA.Pretty.typeDoc_ " active contracts, " <> DA.Pretty.int nTx <> DA.Pretty.typeDoc_ " transactions." diff --git a/compiler/scenario-service/client/src/DA/Daml/LF/PrettyScenario.hs b/compiler/scenario-service/client/src/DA/Daml/LF/PrettyScenario.hs index 2bbf00b9e89..ca0d394c100 100644 --- a/compiler/scenario-service/client/src/DA/Daml/LF/PrettyScenario.hs +++ b/compiler/scenario-service/client/src/DA/Daml/LF/PrettyScenario.hs @@ -4,7 +4,8 @@ -- | Pretty-printing of scenario results module DA.Daml.LF.PrettyScenario - ( prettyScenarioResult + ( activeContractsFromScenarioResult + , prettyScenarioResult , prettyScenarioError , prettyBriefScenarioError , prettyWarningMessage @@ -132,6 +133,14 @@ parseNodeId = where dropHash s = fromMaybe s $ stripPrefix "#" s +activeContractsFromScenarioResult :: ScenarioResult -> S.Set TL.Text +activeContractsFromScenarioResult result = + S.fromList (V.toList (scenarioResultActiveContracts result)) + +activeContractsFromScenarioError :: ScenarioError -> S.Set TL.Text +activeContractsFromScenarioError err = + S.fromList (V.toList (scenarioErrorActiveContracts err)) + prettyScenarioResult :: LF.World -> S.Set TL.Text -> ScenarioResult -> Doc SyntaxClass prettyScenarioResult world activeContracts (ScenarioResult steps nodes retValue _finaltime traceLog warnings _) = @@ -1094,9 +1103,8 @@ renderScenarioError world err = TL.toStrict $ Blaze.renderHtml $ do H.style $ H.text Pretty.highlightStylesheet H.script "" H.! A.src "$webviewSrc" H.link H.! A.rel "stylesheet" H.! A.href "$webviewCss" - let activeContracts = S.fromList (V.toList (scenarioErrorActiveContracts err)) let tableView = do - table <- renderTableView world activeContracts (scenarioErrorNodes err) + table <- renderTableView world (activeContractsFromScenarioError err) (scenarioErrorNodes err) pure $ H.div H.! A.class_ "table" $ do Pretty.renderHtml 128 $ annotateSC ErrorSC "Script execution failed, displaying state before failing transaction" table diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/scenario/ScenarioLedger.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/scenario/ScenarioLedger.scala index 77a38be7046..e123ecb9e21 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/scenario/ScenarioLedger.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/scenario/ScenarioLedger.scala @@ -21,7 +21,6 @@ import Value._ import com.daml.scalautil.Statement.discard -import scala.annotation.tailrec import scala.collection.immutable /** An in-memory representation of a ledger for scenarios */ @@ -375,142 +374,25 @@ object ScenarioLedger { case class UniqueKeyViolation(gk: GlobalKey) - private def processTransaction( + /** Functions for updating the ledger with new transactional information. + * + * @param trId transaction identity + * @param richTr (enriched) transaction + * @param locationInfo location map + */ + class TransactionProcessor( trId: TransactionId, richTr: RichTransaction, locationInfo: Map[NodeId, Location], - ledgerData: LedgerData, - ): Either[UniqueKeyViolation, LedgerData] = { + ) { - final case class RollbackBeginState( - activeContracts: Set[ContractId], - activeKeys: Map[GlobalKey, ContractId], - ) - - final case class ProcessingNode( - // The id of the direct parent or None. - mbParentId: Option[NodeId], - // The id of the nearest rollback ancestor. If this is - // a rollback node itself, it points to itself. - mbRollbackAncestorId: Option[NodeId], - children: List[NodeId], - // For rollback nodes, we store the previous state here and restore it. - // For exercise nodes, we don’t need to restore anything. - prevState: Option[RollbackBeginState], - ) - - @tailrec - def processNodes( - cache0: LedgerData, - enps: List[ProcessingNode], - ): LedgerData = enps match { - case Nil => cache0 - case ProcessingNode(_, _, Nil, optPrevState) :: restENPs => { - val cache1 = optPrevState.fold(cache0) { case prevState => - cache0.copy( - activeContracts = prevState.activeContracts, - activeKeys = prevState.activeKeys, - ) + def duplicateKeyCheck(ledgerData: LedgerData): Either[UniqueKeyViolation, Unit] = { + val inactiveKeys = richTr.transaction.contractKeyInputs + .fold(error => crash(s"$error: inconsistent transaction"), identity) + .collect { case (key, _: Tx.KeyInactive) => + key } - processNodes(cache1, restENPs) - } - case (processingNode @ ProcessingNode( - mbParentId, - mbRollbackAncestorId, - nodeId :: restOfNodeIds, - optPrevState, - )) :: restENPs => - val eventId = EventId(trId.id, nodeId) - richTr.transaction.nodes.get(nodeId) match { - case None => - crash(s"processTransaction: non-existent node '$eventId'.") - case Some(node) => - val newLedgerNodeInfo = LedgerNodeInfo( - node = node, - optLocation = locationInfo.get(nodeId), - transaction = trId, - effectiveAt = richTr.effectiveAt, - disclosures = Map.empty, - referencedBy = Set.empty, - consumedBy = None, - rolledbackBy = mbRollbackAncestorId, - parent = mbParentId.map(EventId(trId.id, _)), - ) - val newCache = - cache0.copy(nodeInfos = cache0.nodeInfos + (eventId -> newLedgerNodeInfo)) - val idsToProcess = processingNode.copy(children = restOfNodeIds) :: restENPs - node match { - case rollback: Node.Rollback => - val rollbackState = - RollbackBeginState(newCache.activeContracts, newCache.activeKeys) - processNodes( - newCache, - ProcessingNode( - Some(nodeId), - Some(nodeId), - rollback.children.toList, - Some(rollbackState), - ) :: idsToProcess, - ) - - case nc: Node.Create => - val newCache1 = newCache.createdIn(nc.coid, eventId) - processNodes(newCache1, idsToProcess) - - case Node.Fetch(referencedCoid, templateId @ _, _, _, _, _, _, _) => - val newCacheP = - newCache.updateLedgerNodeInfo(referencedCoid)(info => - info.copy(referencedBy = info.referencedBy + eventId) - ) - - processNodes(newCacheP, idsToProcess) - - case ex: Node.Exercise => - val newCache0 = - newCache.updateLedgerNodeInfo(ex.targetCoid)(info => - info.copy( - referencedBy = info.referencedBy + eventId, - consumedBy = optPrevState match { - // consuming exercise outside a rollback node - case None if ex.consuming => Some(eventId) - case _ => info.consumedBy - }, - ) - ) - - processNodes( - newCache0, - ProcessingNode( - Some(nodeId), - mbRollbackAncestorId, - ex.children.toList, - None, - ) :: idsToProcess, - ) - - case nlkup: Node.LookupByKey => - nlkup.result match { - case None => - processNodes(newCache, idsToProcess) - case Some(referencedCoid) => - val newCacheP = - newCache.updateLedgerNodeInfo(referencedCoid)(info => - info.copy(referencedBy = info.referencedBy + eventId) - ) - - processNodes(newCacheP, idsToProcess) - } - } - } - } - - val inactiveKeys = richTr.transaction.contractKeyInputs - .fold(error => crash(s"$error: inconsistent transaction"), identity) - .collect { case (key, _: Tx.KeyInactive) => - key - } - val duplicateKeyCheck: Either[UniqueKeyViolation, Unit] = inactiveKeys.find(ledgerData.activeKeys.contains(_)) match { case Some(duplicateKey) => Left(UniqueKeyViolation(duplicateKey)) @@ -518,45 +400,185 @@ object ScenarioLedger { case None => Right(()) } + } - for { - _ <- duplicateKeyCheck - } yield { - val cacheAfterProcess = processNodes( - ledgerData, - List(ProcessingNode(None, None, richTr.transaction.roots.toList, None)), + def addNewLedgerNodes(historicalLedgerData: LedgerData): LedgerData = + richTr.transaction.transaction.fold[LedgerData](historicalLedgerData) { + case (ledgerData, (nodeId, node)) => + val eventId = EventId(trId.id, nodeId) + val newLedgerNodeInfo = LedgerNodeInfo( + node = node, + optLocation = locationInfo.get(nodeId), + transaction = trId, + effectiveAt = richTr.effectiveAt, + // Following fields will be updated by additional calls to node processing code + disclosures = Map.empty, + referencedBy = Set.empty, + consumedBy = None, + rolledbackBy = None, + parent = None, + ) + + ledgerData.copy(nodeInfos = ledgerData.nodeInfos + (eventId -> newLedgerNodeInfo)) + } + + def createdInAndReferenceByUpdates(historicalLedgerData: LedgerData): LedgerData = + richTr.transaction.transaction.fold[LedgerData](historicalLedgerData) { + case (ledgerData, (nodeId, createNode: Node.Create)) => + ledgerData.createdIn(createNode.coid, EventId(trId.id, nodeId)) + + case (ledgerData, (nodeId, exerciseNode: Node.Exercise)) => + ledgerData.updateLedgerNodeInfo(exerciseNode.targetCoid)(ledgerNodeInfo => + ledgerNodeInfo.copy(referencedBy = + ledgerNodeInfo.referencedBy + EventId(trId.id, nodeId) + ) + ) + + case (ledgerData, (nodeId, fetchNode: Node.Fetch)) => + ledgerData.updateLedgerNodeInfo(fetchNode.coid)(ledgerNodeInfo => + ledgerNodeInfo.copy(referencedBy = + ledgerNodeInfo.referencedBy + EventId(trId.id, nodeId) + ) + ) + + case (ledgerData, (nodeId, lookupNode: Node.LookupByKey)) => + lookupNode.result match { + case None => + ledgerData + + case Some(referencedCoid) => + ledgerData.updateLedgerNodeInfo(referencedCoid)(ledgerNodeInfo => + ledgerNodeInfo.copy(referencedBy = + ledgerNodeInfo.referencedBy + EventId(trId.id, nodeId) + ) + ) + } + + case (ledgerData, (_, _: Node)) => + ledgerData + } + + def parentUpdates(historicalLedgerData: LedgerData): LedgerData = + richTr.transaction.transaction.fold[LedgerData](historicalLedgerData) { + case (ledgerData, (nodeId, exerciseNode: Node.Exercise)) => + exerciseNode.children.foldLeft[LedgerData](ledgerData) { + case (updatedLedgerData, childNodeId) => + updatedLedgerData.updateLedgerNodeInfo(EventId(trId.id, childNodeId))( + ledgerNodeInfo => ledgerNodeInfo.copy(parent = Some(EventId(trId.id, nodeId))) + ) + } + + case (ledgerData, (nodeId, rollbackNode: Node.Rollback)) => + rollbackNode.children.foldLeft[LedgerData](ledgerData) { + case (updatedLedgerData, childNodeId) => + updatedLedgerData.updateLedgerNodeInfo(EventId(trId.id, childNodeId))( + ledgerNodeInfo => ledgerNodeInfo.copy(parent = Some(EventId(trId.id, nodeId))) + ) + } + + case (ledgerData, (_, _: Node)) => + ledgerData + } + + def consumedByUpdates(ledgerData: LedgerData): LedgerData = { + var ledgerDataResult = ledgerData + + for ((contractId, nodeId) <- richTr.transaction.transaction.consumedBy) { + ledgerDataResult = ledgerDataResult.updateLedgerNodeInfo(contractId) { ledgerNodeInfo => + ledgerNodeInfo.copy(consumedBy = Some(EventId(trId.id, nodeId))) + } + } + + ledgerDataResult + } + + def rolledbackByUpdates(ledgerData: LedgerData): LedgerData = { + var ledgerDataResult = ledgerData + + for ((nodeId, rollbackNodeId) <- richTr.transaction.transaction.rolledbackBy) { + ledgerDataResult = ledgerDataResult.updateLedgerNodeInfo(EventId(trId.id, nodeId)) { + ledgerNodeInfo => + ledgerNodeInfo.copy(rolledbackBy = Some(rollbackNodeId)) + } + } + + ledgerDataResult + } + + def activeContractAndKeyUpdates(ledgerData: LedgerData): LedgerData = { + ledgerData.copy( + activeContracts = + ledgerData.activeContracts ++ richTr.transaction.localContracts.keySet -- richTr.transaction.inactiveContracts, + activeKeys = richTr.transaction.updatedContractKeys.foldLeft(ledgerData.activeKeys) { + case (activeKeys, (key, Some(cid))) => + activeKeys + (key -> cid) + + case (activeKeys, (key, None)) => + activeKeys - key + }, ) - val cacheActiveness = - cacheAfterProcess.copy( - activeContracts = - cacheAfterProcess.activeContracts ++ richTr.transaction.localContracts.keySet -- richTr.transaction.inactiveContracts, - activeKeys = - richTr.transaction.updatedContractKeys.foldLeft(cacheAfterProcess.activeKeys) { - case (activeKs, (key, Some(cid))) => - activeKs + (key -> cid) - case (activeKs, (key, None)) => - activeKs - key - }, - ) + } + + def disclosureUpdates(ledgerData: LedgerData): LedgerData = { // NOTE(MH): Since `addDisclosures` is biased towards existing // disclosures, we need to add the "stronger" explicit ones first. - val cacheWithExplicitDisclosures = - richTr.blindingInfo.disclosure.foldLeft(cacheActiveness) { - case (cacheP, (nodeId, witnesses)) => - cacheP.updateLedgerNodeInfo(EventId(richTr.transactionId, nodeId))( - _.addDisclosures(witnesses.map(_ -> Disclosure(since = trId, explicit = true)).toMap) - ) - } + richTr.blindingInfo.disclosure.foldLeft(ledgerData) { case (cacheP, (nodeId, witnesses)) => + cacheP.updateLedgerNodeInfo(EventId(richTr.transactionId, nodeId))( + _.addDisclosures(witnesses.map(_ -> Disclosure(since = trId, explicit = true)).toMap) + ) + } + } - richTr.blindingInfo.divulgence.foldLeft(cacheWithExplicitDisclosures) { - case (cacheP, (coid, divulgees)) => - cacheP.updateLedgerNodeInfo(cacheWithExplicitDisclosures.coidToNodeId(coid))( - _.addDisclosures(divulgees.map(_ -> Disclosure(since = trId, explicit = false)).toMap) - ) + def divulgenceUpdates(ledgerData: LedgerData): LedgerData = { + richTr.blindingInfo.divulgence.foldLeft(ledgerData) { case (cacheP, (coid, divulgees)) => + cacheP.updateLedgerNodeInfo(ledgerData.coidToNodeId(coid))( + _.addDisclosures(divulgees.map(_ -> Disclosure(since = trId, explicit = false)).toMap) + ) } } } + /** Update the ledger (which records information on all historical transactions) with new transaction information. + * + * @param trId transaction identity + * @param richTr (enriched) transaction + * @param locationInfo location map + * @param ledgerData ledger recording all historical transaction that have been processed + * @return updated ledger with new transaction information + */ + private def processTransaction( + trId: TransactionId, + richTr: RichTransaction, + locationInfo: Map[NodeId, Location], + ledgerData: LedgerData, + ): Either[UniqueKeyViolation, LedgerData] = { + + val processor: TransactionProcessor = new TransactionProcessor(trId, richTr, locationInfo) + + for { + _ <- processor.duplicateKeyCheck(ledgerData) + } yield { + // Update ledger data with new transaction node information *before* performing any other updates + var cachedLedgerData: LedgerData = processor.addNewLedgerNodes(ledgerData) + + // Update ledger data with any new created in and referenced by information + cachedLedgerData = processor.createdInAndReferenceByUpdates(cachedLedgerData) + // Update ledger data with any new parent information + cachedLedgerData = processor.parentUpdates(cachedLedgerData) + // Update ledger data with any new consumed by information + cachedLedgerData = processor.consumedByUpdates(cachedLedgerData) + // Update ledger data with any new rolled back by information + cachedLedgerData = processor.rolledbackByUpdates(cachedLedgerData) + // Update ledger data with any new active contract information + cachedLedgerData = processor.activeContractAndKeyUpdates(cachedLedgerData) + // Update ledger data with any new disclosure information + cachedLedgerData = processor.disclosureUpdates(cachedLedgerData) + // Update ledger data with any new divulgence information + cachedLedgerData = processor.divulgenceUpdates(cachedLedgerData) + + cachedLedgerData + } + } } // ---------------------------------------------------------------- diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala index 91f216c743d..94cbad11406 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala @@ -52,6 +52,7 @@ final case class VersionedTransaction private[lf] ( * * @param nodes The nodes of this transaction. * @param roots References to the root nodes of the transaction. + * * Users of this class may assume that all instances are well-formed, i.e., `isWellFormed.isEmpty`. * For performance reasons, users are not required to call `isWellFormed`. * Therefore, it is '''forbidden''' to create ill-formed instances, i.e., instances with `!isWellFormed.isEmpty`. @@ -255,7 +256,7 @@ sealed abstract class HasTxNodes { def roots: ImmArray[NodeId] - /** The union of the informees of a all the action nodes. */ + /** The union of the informees of all the action nodes. */ lazy val informees: Set[Ref.Party] = nodes.values.foldLeft(Set.empty[Ref.Party]) { case (acc, node: Node.Action) => acc | node.informeesOfNode @@ -350,8 +351,8 @@ sealed abstract class HasTxNodes { } /** Returns the IDs of all the consumed contracts. - * This includes transient contracts but it does not include contracts - * consumed in rollback nodes. + * This includes transient contracts but it does not include contracts + * consumed in rollback nodes. */ final def consumedContracts[Cid2 >: ContractId]: Set[Cid2] = foldInExecutionOrder(Set.empty[Cid2])( @@ -465,6 +466,60 @@ sealed abstract class HasTxNodes { } } + /** Keys are contracts (that have been consumed) and values are the nodes where the contract was consumed. + * Nodes under rollbacks (both exercises and creates) are ignored (as they have been rolled back). + * The result includes both local contracts created in the transaction (if they’ve been consumed) as well as global + * contracts created in previous transactions. It does not include local contracts created under a rollback. + */ + final def consumedBy: Map[ContractId, NodeId] = + foldInExecutionOrder[Map[ContractId, NodeId]](HashMap.empty)( + exerciseBegin = (consumedByMap, nodeId, exerciseNode) => { + if (exerciseNode.consuming) { + (consumedByMap + (exerciseNode.targetCoid -> nodeId), ChildrenRecursion.DoRecurse) + } else { + (consumedByMap, ChildrenRecursion.DoRecurse) + } + }, + rollbackBegin = (consumedByMap, _, _) => { + (consumedByMap, ChildrenRecursion.DoNotRecurse) + }, + leaf = (consumedByMap, _, _) => consumedByMap, + exerciseEnd = (consumedByMap, _, _) => consumedByMap, + rollbackEnd = (consumedByMap, _, _) => consumedByMap, + ) + + /** Keys are nodes under a rollback and values are the "nearest" (i.e. most recent) rollback node. + */ + final def rolledbackBy: Map[NodeId, NodeId] = { + val rolledbackByMapUpdate + : ((Map[NodeId, NodeId], Seq[NodeId]), NodeId) => (Map[NodeId, NodeId], Seq[NodeId]) = { + case ((rolledbackMap, rollbackStack @ (rollbackNode +: _)), nodeId) => + (rolledbackMap + (nodeId -> rollbackNode), rollbackStack) + + case (state, _) => + state + } + + foldInExecutionOrder[(Map[NodeId, NodeId], Seq[NodeId])]((HashMap.empty, Vector.empty))( + exerciseBegin = + (state, nodeId, _) => (rolledbackByMapUpdate(state, nodeId), ChildrenRecursion.DoRecurse), + rollbackBegin = { case ((rolledbackMap, rollbackStack), nodeId, _) => + ((rolledbackMap, nodeId +: rollbackStack), ChildrenRecursion.DoRecurse) + }, + leaf = (state, nodeId, _) => rolledbackByMapUpdate(state, nodeId), + exerciseEnd = (state, _, _) => state, + rollbackEnd = { + case ((rolledbackMap, _ +: rollbackStack), _, _) => + (rolledbackMap, rollbackStack) + + case _ => + throw new IllegalStateException( + "Impossible case: rollbackBegin should already have pushed to the rollback stack" + ) + }, + )._1 + } + /** Return the expected contract key inputs (i.e. the state before the transaction) * for this transaction or an error if the transaction contains a * duplicate key error or has an inconsistent mapping for a key. For diff --git a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala index 2c406c31751..d792ef700b4 100644 --- a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala +++ b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala @@ -675,6 +675,363 @@ class TransactionSpec Map(key("key0") -> Some(cid0), key("key1") -> None, key("key2") -> Some(cid3)) } } + + "consumedBy and rolledbackBy" - { + "non-consuming transaction with no rollbacks" - { + "no nodes" in { + val builder = TransactionBuilder() + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe Map.empty + } + + "one node" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + + builder.add(createNode0) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe Map.empty + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val fetchNode0 = builder.fetch(createNode0, true) + + builder.add(fetchNode0) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe Map.empty + } + } + + "multiple nodes" - { + "only create nodes" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + builder.add(createNode0) + builder.add(createNode1) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe Map.empty + } + + "create and non-consuming exercise nodes" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + + builder.add(createNode0) + builder.add(exercise(builder, createNode0, parties, false)) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe Map.empty + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + + builder.add(exercise(builder, createNode0, parties, false)) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe Map.empty + } + } + } + } + + "consuming transaction with no rollbacks" - { + "one excercise" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (cid0, createNode0) = create(builder, parties, Some("key0")) + + builder.add(createNode0) + val exerciseId0 = builder.add(exercise(builder, createNode0, parties, true)) + val transaction = builder.build() + + transaction.consumedBy shouldBe + Map(cid0 -> exerciseId0) + transaction.rolledbackBy shouldBe Map.empty + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (cid0, createNode0) = create(builder, parties, Some("key0")) + + val exerciseId0 = builder.add(exercise(builder, createNode0, parties, true)) + val transaction = builder.build() + + transaction.consumedBy shouldBe + Map(cid0 -> exerciseId0) + transaction.rolledbackBy shouldBe Map.empty + } + } + + "multiple exercises" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (cid0, createNode0) = create(builder, parties, Some("key0")) + val (cid1, createNode1) = create(builder, parties, Some("key1")) + + builder.add(createNode0) + builder.add(createNode1) + val exerciseId0 = builder.add(exercise(builder, createNode0, parties, true)) + val exerciseId1 = builder.add(exercise(builder, createNode1, parties, true)) + val transaction = builder.build() + + transaction.consumedBy shouldBe + Map(cid0 -> exerciseId0, cid1 -> exerciseId1) + transaction.rolledbackBy shouldBe Map.empty + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (cid0, createNode0) = create(builder, parties, Some("key0")) + val (cid1, createNode1) = create(builder, parties, Some("key1")) + + val exerciseId0 = builder.add(exercise(builder, createNode0, parties, true)) + val exerciseId1 = builder.add(exercise(builder, createNode1, parties, true)) + val transaction = builder.build() + + transaction.consumedBy shouldBe + Map(cid0 -> exerciseId0, cid1 -> exerciseId1) + transaction.rolledbackBy shouldBe Map.empty + } + } + } + + "consuming transaction with rollbacks" - { + "one rollback" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (cid0, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + builder.add(createNode0) + builder.add(createNode1) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true)) + val rollbackId = builder.add(builder.rollback()) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId) + val transaction = builder.build() + + transaction.consumedBy shouldBe + Map(cid0 -> nodeId0) + transaction.rolledbackBy shouldBe + Map(nodeId1 -> rollbackId) + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (cid0, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true)) + val rollbackId = builder.add(builder.rollback()) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId) + val transaction = builder.build() + + transaction.consumedBy shouldBe + Map(cid0 -> nodeId0) + transaction.rolledbackBy shouldBe + Map(nodeId1 -> rollbackId) + } + } + + "multiple rollbacks" - { + "sequential rollbacks" - { + "with local wontracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + builder.add(createNode0) + builder.add(createNode1) + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback()) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1) + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback()) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1) + } + } + + "nested rollbacks" - { + "2 deep and 2 rollbacks" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + builder.add(createNode0) + builder.add(createNode1) + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback(), rollbackId0) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1) + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback(), rollbackId0) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1) + } + } + + "2 deep and 3 rollbacks" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + val (_, createNode2) = create(builder, parties, Some("key2")) + + builder.add(createNode0) + builder.add(createNode1) + builder.add(createNode2) + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback(), rollbackId0) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val rollbackId2 = builder.add(builder.rollback(), rollbackId0) + val nodeId2 = builder.add(exercise(builder, createNode2, parties, true), rollbackId2) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1, nodeId2 -> rollbackId2) + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + val (_, createNode2) = create(builder, parties, Some("key2")) + + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback(), rollbackId0) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val rollbackId2 = builder.add(builder.rollback(), rollbackId0) + val nodeId2 = builder.add(exercise(builder, createNode2, parties, true), rollbackId2) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1, nodeId2 -> rollbackId2) + } + } + + "3 deep" - { + "with local contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + val (_, createNode2) = create(builder, parties, Some("key2")) + + builder.add(createNode0) + builder.add(createNode1) + builder.add(createNode2) + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback(), rollbackId0) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val rollbackId2 = builder.add(builder.rollback(), rollbackId1) + val nodeId2 = builder.add(exercise(builder, createNode2, parties, true), rollbackId2) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1, nodeId2 -> rollbackId2) + } + + "with global contracts" in { + val builder = TransactionBuilder() + val parties = Seq("Alice") + val (_, createNode0) = create(builder, parties, Some("key0")) + val (_, createNode1) = create(builder, parties, Some("key1")) + val (_, createNode2) = create(builder, parties, Some("key2")) + + val rollbackId0 = builder.add(builder.rollback()) + val nodeId0 = builder.add(exercise(builder, createNode0, parties, true), rollbackId0) + val rollbackId1 = builder.add(builder.rollback(), rollbackId0) + val nodeId1 = builder.add(exercise(builder, createNode1, parties, true), rollbackId1) + val rollbackId2 = builder.add(builder.rollback(), rollbackId1) + val nodeId2 = builder.add(exercise(builder, createNode2, parties, true), rollbackId2) + val transaction = builder.build() + + transaction.consumedBy shouldBe Map.empty + transaction.rolledbackBy shouldBe + Map(nodeId0 -> rollbackId0, nodeId1 -> rollbackId1, nodeId2 -> rollbackId2) + } + } + } + } + } + } } object TransactionSpec {