Refactor and simplification of Transaction#processNodes

* fixes #14183

CHANGELOG_BEGIN

- [DAML Studio] Refactor and simplification of Transaction#processNodes to avoid need for custom state during processing. Refactor of Transaction#processTransaction to make processing workflow more transparent and easier to unit test. See https://github.com/digital-asset/daml/issues/14183 for details.

CHANGELOG_END
This commit is contained in:
Carl Pulley 2022-06-28 12:49:41 +01:00 committed by GitHub
parent 8c02057eb8
commit 6ed88105bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 610 additions and 169 deletions

View File

@ -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."

View File

@ -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

View File

@ -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 dont 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
}
}
}
// ----------------------------------------------------------------

View File

@ -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 theyve 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

View File

@ -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 {