mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 16:57:40 +03:00
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:
parent
8c02057eb8
commit
6ed88105bb
@ -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."
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user