mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
Engine: move partialTransaction to interpreter (#4617)
* Engine: move partialTransaction to interpreter CHANGELOG_BEGIN CHANGELOG_END * formating * fix
This commit is contained in:
parent
726b692797
commit
3a8139d9c1
@ -7,9 +7,12 @@ import scala.collection.JavaConverters._
|
||||
import com.digitalasset.daml.lf.data.{Numeric, Ref}
|
||||
import com.digitalasset.daml.lf.scenario.api.v1
|
||||
import com.digitalasset.daml.lf.scenario.api.v1.{List => _, _}
|
||||
import com.digitalasset.daml.lf.speedy.SError
|
||||
import com.digitalasset.daml.lf.speedy.Speedy
|
||||
import com.digitalasset.daml.lf.speedy.SValue
|
||||
import com.digitalasset.daml.lf.speedy.{
|
||||
SError,
|
||||
PartialTransaction => SPartialTransaction,
|
||||
Speedy,
|
||||
SValue
|
||||
}
|
||||
import com.digitalasset.daml.lf.transaction.{Node => N, Transaction => Tx}
|
||||
import com.digitalasset.daml.lf.types.Ledger
|
||||
import com.digitalasset.daml.lf.types.Ledger.ScenarioNodeId
|
||||
@ -384,7 +387,7 @@ case class Conversions(homePackageId: Ref.PackageId) {
|
||||
.build
|
||||
}
|
||||
|
||||
def convertPartialTransaction(ptx: Tx.PartialTransaction): PartialTransaction = {
|
||||
def convertPartialTransaction(ptx: SPartialTransaction): PartialTransaction = {
|
||||
val builder = PartialTransaction.newBuilder
|
||||
.addAllNodes(ptx.nodes.map(Function.tupled(convertTxNode)).asJava)
|
||||
.addAllRoots(
|
||||
@ -392,8 +395,8 @@ case class Conversions(homePackageId: Ref.PackageId) {
|
||||
)
|
||||
|
||||
ptx.context match {
|
||||
case Tx.PartialTransaction.ContextRoot(_, _) =>
|
||||
case Tx.PartialTransaction.ContextExercise(ctx, _) =>
|
||||
case SPartialTransaction.ContextRoot(_, _) =>
|
||||
case SPartialTransaction.ContextExercise(ctx, _) =>
|
||||
val ecBuilder = ExerciseContext.newBuilder
|
||||
.setTargetId(mkContractRef(ctx.targetId, ctx.templateId))
|
||||
.setChoiceId(ctx.choiceId)
|
||||
|
@ -12,7 +12,6 @@ import com.digitalasset.daml.lf.transaction.Node._
|
||||
import com.digitalasset.daml.lf.types.{Ledger => L}
|
||||
import com.digitalasset.daml.lf.data.Ref._
|
||||
import com.digitalasset.daml.lf.transaction.Transaction
|
||||
import com.digitalasset.daml.lf.transaction.Transaction.PartialTransaction
|
||||
import com.digitalasset.daml.lf.speedy.SError._
|
||||
import com.digitalasset.daml.lf.speedy.SValue._
|
||||
import com.digitalasset.daml.lf.speedy.SBuiltin._
|
||||
|
@ -1012,9 +1012,9 @@ object SBuiltin {
|
||||
val observers = extractParties(args.get(2))
|
||||
val stakeholders = observers union signatories
|
||||
val contextActors = machine.ptx.context match {
|
||||
case Tx.PartialTransaction.ContextExercise(ctx, _) =>
|
||||
case PartialTransaction.ContextExercise(ctx, _) =>
|
||||
ctx.actingParties union ctx.signatories
|
||||
case Tx.PartialTransaction.ContextRoot(_, _) =>
|
||||
case PartialTransaction.ContextRoot(_, _) =>
|
||||
machine.committers
|
||||
}
|
||||
|
||||
@ -1181,7 +1181,7 @@ object SBuiltin {
|
||||
def clearCommit(): Unit = {
|
||||
machine.committers = Set.empty
|
||||
machine.commitLocation = None
|
||||
machine.ptx = Tx.PartialTransaction.initial()
|
||||
machine.ptx = PartialTransaction.initial()
|
||||
}
|
||||
|
||||
args.get(0) match {
|
||||
@ -1227,7 +1227,7 @@ object SBuiltin {
|
||||
callback = newValue => {
|
||||
machine.committers = Set.empty
|
||||
machine.commitLocation = None
|
||||
machine.ptx = Tx.PartialTransaction.initial()
|
||||
machine.ptx = PartialTransaction.initial()
|
||||
machine.ctrl = CtrlValue(newValue)
|
||||
},
|
||||
),
|
||||
@ -1490,7 +1490,7 @@ object SBuiltin {
|
||||
* throw if so. The partial transaction abort status must be
|
||||
* checked after every operation on it.
|
||||
*/
|
||||
private def checkAborted(ptx: Tx.PartialTransaction): Unit =
|
||||
private def checkAborted(ptx: PartialTransaction): Unit =
|
||||
ptx.aborted match {
|
||||
case Some(Tx.ContractNotActive(coid, tid, consumedBy)) =>
|
||||
throw DamlELocalContractNotActive(coid, tid, consumedBy)
|
||||
|
@ -12,13 +12,11 @@ import com.digitalasset.daml.lf.speedy.SError._
|
||||
import com.digitalasset.daml.lf.speedy.SExpr._
|
||||
import com.digitalasset.daml.lf.speedy.SResult._
|
||||
import com.digitalasset.daml.lf.speedy.SValue._
|
||||
import com.digitalasset.daml.lf.transaction.Transaction._
|
||||
import com.digitalasset.daml.lf.value.{Value => V}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import java.util
|
||||
|
||||
import com.digitalasset.daml.lf.CompiledPackages
|
||||
import com.digitalasset.daml.lf.value.Value.AbsoluteContractId
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
|
@ -0,0 +1,420 @@
|
||||
// Copyright (c) 2020 The DAML Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.daml.lf.speedy
|
||||
|
||||
import com.digitalasset.daml.lf.crypto
|
||||
import com.digitalasset.daml.lf.data.Ref.{ChoiceName, Location, Party, TypeConName}
|
||||
import com.digitalasset.daml.lf.data.{BackStack, ImmArray, Ref, Time}
|
||||
import com.digitalasset.daml.lf.transaction.{GenTransaction, Node, Transaction => Tx}
|
||||
import com.digitalasset.daml.lf.value.Value
|
||||
|
||||
import scala.collection.breakOut
|
||||
import scala.collection.immutable.HashMap
|
||||
|
||||
object PartialTransaction {
|
||||
|
||||
type NodeIdx = Value.NodeIdx
|
||||
|
||||
/** Contexts of the transaction graph builder, which we use to record
|
||||
* the sub-transaction structure due to 'exercises' statements.
|
||||
*/
|
||||
sealed abstract class Context extends Product with Serializable {
|
||||
def contextSeed: Option[crypto.Hash]
|
||||
|
||||
def children: BackStack[Value.NodeId]
|
||||
|
||||
def addChild(child: Value.NodeId): Context
|
||||
}
|
||||
|
||||
/** The root context, which is what we use when we are not exercising
|
||||
* a choice.
|
||||
*/
|
||||
final case class ContextRoot(
|
||||
contextSeed: Option[crypto.Hash],
|
||||
children: BackStack[Value.NodeId] = BackStack.empty,
|
||||
) extends Context {
|
||||
override def addChild(child: Value.NodeId): ContextRoot = copy(children = children :+ child)
|
||||
}
|
||||
|
||||
/** Context when creating a sub-transaction due to an exercises. */
|
||||
final case class ContextExercise(
|
||||
ctx: ExercisesContext,
|
||||
children: BackStack[Value.NodeId] = BackStack.empty,
|
||||
) extends Context {
|
||||
override def addChild(child: Value.NodeId): ContextExercise =
|
||||
copy(children = children :+ child)
|
||||
|
||||
override def contextSeed: Option[crypto.Hash] = ctx.contextSeed
|
||||
}
|
||||
|
||||
/** Context information to remember when building a sub-transaction
|
||||
* due to an 'exercises' statement.
|
||||
*
|
||||
* @param targetId Contract-id referencing the contract-instance on
|
||||
* which we are exercising a choice.
|
||||
* @param templateId Template-id referencing the template of the
|
||||
* contract on which we are exercising a choice.
|
||||
* @param contractKey Optional contract key, if defined for the
|
||||
* contract on which we are exercising a choice.
|
||||
* @param choiceId Label of the choice that we are exercising.
|
||||
* @param consuming True if the choice consumes the contract.
|
||||
* @param actingParties The parties exercising the choice.
|
||||
* @param chosenValue The chosen value.
|
||||
* @param signatories The signatories of the contract.
|
||||
* @param stakeholders The stakeholders of the contract.
|
||||
* @param controllers The controllers of the choice.
|
||||
* @param nodeId The node to be inserted once we've
|
||||
* finished this sub-transaction.
|
||||
* @param parent The context in which the exercises is
|
||||
* happening.
|
||||
*/
|
||||
case class ExercisesContext(
|
||||
contextSeed: Option[crypto.Hash],
|
||||
targetId: Value.ContractId,
|
||||
templateId: TypeConName,
|
||||
contractKey: Option[Node.KeyWithMaintainers[Tx.Value[Nothing]]],
|
||||
choiceId: ChoiceName,
|
||||
optLocation: Option[Location],
|
||||
consuming: Boolean,
|
||||
actingParties: Set[Party],
|
||||
chosenValue: Tx.Value[Value.ContractId],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
controllers: Set[Party],
|
||||
nodeId: Value.NodeId,
|
||||
parent: Context,
|
||||
)
|
||||
|
||||
def initial(seedWithTime: Option[(crypto.Hash, Time.Timestamp)] = None) =
|
||||
PartialTransaction(
|
||||
seedWithTime.map(_._2),
|
||||
nextNodeIdx = 0,
|
||||
nodes = HashMap.empty,
|
||||
consumedBy = Map.empty,
|
||||
context = ContextRoot(seedWithTime.map(_._1)),
|
||||
aborted = None,
|
||||
keys = Map.empty,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/** A transaction under construction
|
||||
*
|
||||
* @param nodes The nodes of the transaction graph being built up.
|
||||
* @param consumedBy 'ContractId's of all contracts that have
|
||||
* been consumed by nodes up to now.
|
||||
* @param context The context of what sub-transaction is being
|
||||
* built.
|
||||
* @param aborted The error that lead to aborting the building of
|
||||
* this transaction. We inline this error to allow
|
||||
* reporting the error jointly with the state that
|
||||
* the transaction was in when aborted. It is up to
|
||||
* the caller to check for 'isAborted' after every
|
||||
* change to a transaction.
|
||||
* @param keys A local store of the contract keys. Note that this contains
|
||||
* info both about relative and absolute contract ids. We must
|
||||
* do this because absolute contract ids can be archived as
|
||||
* part of execution, and we must record these archivals locally.
|
||||
* Note: it is important for keys that we know to not be present
|
||||
* to be present as [[None]]. The reason for this is that we must
|
||||
* record the "no key" information for absolute contract ids that
|
||||
* we archive. This is not an optimization and is required for
|
||||
* correct semantics, since otherwise lookups for keys for
|
||||
* locally archived absolute contract ids will succeed wrongly.
|
||||
*/
|
||||
case class PartialTransaction(
|
||||
submissionTime: Option[Time.Timestamp],
|
||||
nextNodeIdx: Int,
|
||||
nodes: HashMap[Value.NodeId, Tx.Node],
|
||||
consumedBy: Map[Value.ContractId, Value.NodeId],
|
||||
context: PartialTransaction.Context,
|
||||
aborted: Option[Tx.TransactionError],
|
||||
keys: Map[Node.GlobalKey, Option[Value.ContractId]],
|
||||
) {
|
||||
|
||||
import PartialTransaction._
|
||||
|
||||
def nodesToString: String =
|
||||
if (nodes.isEmpty) "<empty transaction>"
|
||||
else {
|
||||
val sb = new StringBuilder()
|
||||
|
||||
def addToStringBuilder(
|
||||
nid: Value.NodeId,
|
||||
node: Node.GenNode.WithTxValue[Value.NodeId, Value.ContractId],
|
||||
rootPrefix: String,
|
||||
): Unit = {
|
||||
sb.append(rootPrefix)
|
||||
.append("node ")
|
||||
.append(nid)
|
||||
.append(": ")
|
||||
.append(node.toString)
|
||||
.append(", ")
|
||||
()
|
||||
}
|
||||
|
||||
def removeTrailingComma(): Unit = {
|
||||
if (sb.length >= 2) sb.setLength(sb.length - 2) // remove trailing ", "
|
||||
}
|
||||
|
||||
// roots field is not initialized when this method is executed on a failed transaction,
|
||||
// so we need to compute them.
|
||||
val rootNodes = {
|
||||
val allChildNodeIds: Set[Value.NodeId] = nodes.values.flatMap {
|
||||
case _: Node.LeafOnlyNode[_, _] => Nil
|
||||
case ex: Node.NodeExercises[Value.NodeId, _, _] => ex.children.toSeq
|
||||
}(breakOut)
|
||||
|
||||
nodes.keySet diff allChildNodeIds
|
||||
}
|
||||
val tx = GenTransaction(nodes, ImmArray(rootNodes), None)
|
||||
|
||||
tx.foreach { (nid, node) =>
|
||||
val rootPrefix = if (rootNodes.contains(nid)) "root " else ""
|
||||
addToStringBuilder(nid, node, rootPrefix)
|
||||
}
|
||||
removeTrailingComma()
|
||||
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def resolveCidDiscriminator(node: Tx.Node) =
|
||||
node.resolveRelCid(cid =>
|
||||
Ref.ContractIdString.assertFromString("0" + cid.discriminator.get.toHexaString))
|
||||
|
||||
/** Finish building a transaction; i.e., try to extract a complete
|
||||
* transaction from the given 'PartialTransaction'. This fails if
|
||||
* the 'PartialTransaction' is not yet complete or has been
|
||||
* aborted.
|
||||
*/
|
||||
def finish: Either[PartialTransaction, Tx.Transaction] =
|
||||
context match {
|
||||
case ContextRoot(transactionSeed, children) if aborted.isEmpty =>
|
||||
Right(
|
||||
GenTransaction(
|
||||
nodes =
|
||||
if (transactionSeed.isEmpty) nodes
|
||||
else nodes.transform((_, v) => resolveCidDiscriminator(v)),
|
||||
roots = children.toImmArray,
|
||||
optUsedPackages = None,
|
||||
transactionSeed = transactionSeed,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
Left(this)
|
||||
}
|
||||
|
||||
/** Lookup the contract associated to 'Value.RelativeContractId'.
|
||||
* Return the contract instance and the node in which it was
|
||||
* consumed if any.
|
||||
*/
|
||||
def lookupLocalContract(
|
||||
lcoid: Value.RelativeContractId,
|
||||
): Option[(Value.ContractInst[Tx.Value[Value.ContractId]], Option[Value.NodeId])] =
|
||||
for {
|
||||
node <- nodes.get(lcoid.txnid)
|
||||
coinst <- node match {
|
||||
case create: Node.NodeCreate.WithTxValue[Value.ContractId] =>
|
||||
Some((create.coinst, consumedBy.get(lcoid)))
|
||||
case _: Node.NodeExercises[_, _, _] | _: Node.NodeFetch[_] |
|
||||
_: Node.NodeLookupByKey[_, _] =>
|
||||
None
|
||||
}
|
||||
} yield coinst
|
||||
|
||||
/** Extend the 'PartialTransaction' with a node for creating a
|
||||
* contract instance.
|
||||
*/
|
||||
def insertCreate(
|
||||
coinst: Value.ContractInst[Tx.Value[Value.ContractId]],
|
||||
optLocation: Option[Location],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
key: Option[Node.KeyWithMaintainers[Tx.Value[Nothing]]],
|
||||
): Either[String, (Value.ContractId, PartialTransaction)] = {
|
||||
val serializableErrs = serializable(coinst.arg)
|
||||
if (serializableErrs.nonEmpty) {
|
||||
Left(
|
||||
s"""Trying to create a contract with a non-serializable value: ${serializableErrs.iterator
|
||||
.mkString(",")}""",
|
||||
)
|
||||
} else {
|
||||
val nodeSeed = deriveChildSeed
|
||||
val contractDiscriminator =
|
||||
for {
|
||||
seed <- nodeSeed
|
||||
time <- submissionTime
|
||||
} yield crypto.Hash.deriveContractDiscriminator(seed, time, stakeholders)
|
||||
val cid = Value.RelativeContractId(Value.NodeId(nextNodeIdx), contractDiscriminator)
|
||||
val createNode = Node.NodeCreate(
|
||||
nodeSeed,
|
||||
cid,
|
||||
coinst,
|
||||
optLocation,
|
||||
signatories,
|
||||
stakeholders,
|
||||
key,
|
||||
)
|
||||
val nid = Value.NodeId(nextNodeIdx)
|
||||
val ptx = copy(
|
||||
nextNodeIdx = nextNodeIdx + 1,
|
||||
context = context.addChild(nid),
|
||||
nodes = nodes.updated(nid, createNode),
|
||||
)
|
||||
|
||||
// if we have a contract key being added, include it in the list of
|
||||
// active keys
|
||||
key match {
|
||||
case None => Right((cid, ptx))
|
||||
case Some(kWithM) =>
|
||||
val ck = Node.GlobalKey(coinst.template, kWithM.key.value)
|
||||
Right((cid, ptx.copy(keys = ptx.keys.updated(ck, Some(cid)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def serializable(a: Tx.Value[Value.ContractId]): ImmArray[String] = a.value.serializable()
|
||||
|
||||
def insertFetch(
|
||||
coid: Value.ContractId,
|
||||
templateId: TypeConName,
|
||||
optLocation: Option[Location],
|
||||
actingParties: Set[Party],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
): PartialTransaction =
|
||||
mustBeActive(
|
||||
coid,
|
||||
templateId,
|
||||
insertLeafNode(
|
||||
Node
|
||||
.NodeFetch(coid, templateId, optLocation, Some(actingParties), signatories, stakeholders),
|
||||
),
|
||||
)
|
||||
|
||||
def insertLookup(
|
||||
templateId: TypeConName,
|
||||
optLocation: Option[Location],
|
||||
key: Node.KeyWithMaintainers[Tx.Value[Nothing]],
|
||||
result: Option[Value.ContractId],
|
||||
): PartialTransaction =
|
||||
insertLeafNode(Node.NodeLookupByKey(templateId, optLocation, key, result))
|
||||
|
||||
def beginExercises(
|
||||
targetId: Value.ContractId,
|
||||
templateId: TypeConName,
|
||||
choiceId: ChoiceName,
|
||||
optLocation: Option[Location],
|
||||
consuming: Boolean,
|
||||
actingParties: Set[Party],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
controllers: Set[Party],
|
||||
mbKey: Option[Node.KeyWithMaintainers[Tx.Value[Nothing]]],
|
||||
chosenValue: Tx.Value[Value.ContractId],
|
||||
): Either[String, PartialTransaction] = {
|
||||
val serializableErrs = serializable(chosenValue)
|
||||
if (serializableErrs.nonEmpty) {
|
||||
Left(
|
||||
s"""Trying to exercise a choice with a non-serializable value: ${serializableErrs.iterator
|
||||
.mkString(",")}""",
|
||||
)
|
||||
} else {
|
||||
val nid = Value.NodeId(nextNodeIdx)
|
||||
Right(
|
||||
mustBeActive(
|
||||
targetId,
|
||||
templateId,
|
||||
copy(
|
||||
nextNodeIdx = nextNodeIdx + 1,
|
||||
context = ContextExercise(
|
||||
ExercisesContext(
|
||||
contextSeed = deriveChildSeed,
|
||||
targetId = targetId,
|
||||
templateId = templateId,
|
||||
contractKey = mbKey,
|
||||
choiceId = choiceId,
|
||||
optLocation = optLocation,
|
||||
consuming = consuming,
|
||||
actingParties = actingParties,
|
||||
chosenValue = chosenValue,
|
||||
signatories = signatories,
|
||||
stakeholders = stakeholders,
|
||||
controllers = controllers,
|
||||
nodeId = nid,
|
||||
parent = context,
|
||||
),
|
||||
),
|
||||
// important: the semantics of DAML dictate that contracts are immediately
|
||||
// inactive as soon as you exercise it. therefore, mark it as consumed now.
|
||||
consumedBy = if (consuming) consumedBy.updated(targetId, nid) else consumedBy,
|
||||
keys = mbKey match {
|
||||
case Some(kWithM) if consuming =>
|
||||
keys.updated(Node.GlobalKey(templateId, kWithM.key.value), None)
|
||||
case _ => keys
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def endExercises(value: Tx.Value[Value.ContractId]): PartialTransaction =
|
||||
context match {
|
||||
case ContextExercise(ec, children) =>
|
||||
val exerciseNode = Node.NodeExercises(
|
||||
nodeSeed = ec.contextSeed,
|
||||
targetCoid = ec.targetId,
|
||||
templateId = ec.templateId,
|
||||
choiceId = ec.choiceId,
|
||||
optLocation = ec.optLocation,
|
||||
consuming = ec.consuming,
|
||||
actingParties = ec.actingParties,
|
||||
chosenValue = ec.chosenValue,
|
||||
stakeholders = ec.stakeholders,
|
||||
signatories = ec.signatories,
|
||||
controllers = ec.controllers,
|
||||
children = children.toImmArray,
|
||||
exerciseResult = Some(value),
|
||||
key = ec.contractKey,
|
||||
)
|
||||
val nodeId = ec.nodeId
|
||||
copy(context = ec.parent.addChild(nodeId), nodes = nodes.updated(nodeId, exerciseNode))
|
||||
case ContextRoot(_, _) =>
|
||||
noteAbort(Tx.EndExerciseInRootContext)
|
||||
}
|
||||
|
||||
/** Note that the transaction building failed due to the given error */
|
||||
def noteAbort(err: Tx.TransactionError): PartialTransaction = copy(aborted = Some(err))
|
||||
|
||||
/** `True` iff the given `ContractId` has been consumed already */
|
||||
def isConsumed(coid: Value.ContractId): Boolean = consumedBy.contains(coid)
|
||||
|
||||
/** Guard the execution of a step with the unconsumedness of a
|
||||
* `ContractId`
|
||||
*/
|
||||
def mustBeActive(
|
||||
coid: Value.ContractId,
|
||||
templateId: TypeConName,
|
||||
f: => PartialTransaction,
|
||||
): PartialTransaction =
|
||||
consumedBy.get(coid) match {
|
||||
case None => f
|
||||
case Some(nid) => noteAbort(Tx.ContractNotActive(coid, templateId, nid))
|
||||
}
|
||||
|
||||
/** Insert the given `LeafNode` under a fresh node-id, and return it */
|
||||
def insertLeafNode(node: Tx.LeafNode): PartialTransaction = {
|
||||
val nid = Value.NodeId(nextNodeIdx)
|
||||
copy(
|
||||
nextNodeIdx = nextNodeIdx + 1,
|
||||
context = context.addChild(nid),
|
||||
nodes = nodes.updated(nid, node),
|
||||
)
|
||||
}
|
||||
|
||||
def deriveChildSeed: Option[crypto.Hash] =
|
||||
context.contextSeed.map(crypto.Hash.deriveNodeSeed(_, nodes.size))
|
||||
|
||||
}
|
@ -9,11 +9,11 @@ import com.digitalasset.daml.lf.data._
|
||||
import com.digitalasset.daml.lf.language.LanguageVersion
|
||||
import com.digitalasset.daml.lf.transaction.Node._
|
||||
import com.digitalasset.daml.lf.value.Value
|
||||
import com.digitalasset.daml.lf.value.Value._
|
||||
|
||||
import scalaz.Equal
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.breakOut
|
||||
|
||||
import scala.collection.immutable.HashMap
|
||||
|
||||
case class VersionedTransaction[Nid, Cid](
|
||||
@ -69,10 +69,6 @@ case class VersionedTransaction[Nid, Cid](
|
||||
* This is a hint for what packages are required to validate
|
||||
* the transaction using the current interpreter.
|
||||
* The used packages are not serialized using [[TransactionCoder]].
|
||||
* @param transactionSeed master hash used to derived node and relative contractId
|
||||
* discriminators. If it is undefined, the discriminators have not be
|
||||
* generated and have be let undefined in the nodes and the relative
|
||||
* contractIds 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`.
|
||||
@ -393,414 +389,4 @@ object Transaction {
|
||||
final case class ContractNotActive(coid: TContractId, templateId: TypeConName, consumedBy: NodeId)
|
||||
extends TransactionError
|
||||
|
||||
object PartialTransaction {
|
||||
|
||||
type NodeIdx = Value.NodeIdx
|
||||
|
||||
/** Contexts of the transaction graph builder, which we use to record
|
||||
* the sub-transaction structure due to 'exercises' statements.
|
||||
*/
|
||||
sealed abstract class Context extends Product with Serializable {
|
||||
def contextSeed: Option[crypto.Hash]
|
||||
|
||||
def children: BackStack[NodeId]
|
||||
|
||||
def addChild(child: NodeId): Context
|
||||
}
|
||||
|
||||
/** The root context, which is what we use when we are not exercising
|
||||
* a choice.
|
||||
*/
|
||||
final case class ContextRoot(
|
||||
val contextSeed: Option[crypto.Hash],
|
||||
children: BackStack[NodeId] = BackStack.empty,
|
||||
) extends Context {
|
||||
override def addChild(child: NodeId): ContextRoot = copy(children = children :+ child)
|
||||
}
|
||||
|
||||
/** Context when creating a sub-transaction due to an exercises. */
|
||||
final case class ContextExercise(
|
||||
ctx: ExercisesContext,
|
||||
children: BackStack[NodeId] = BackStack.empty,
|
||||
) extends Context {
|
||||
override def addChild(child: NodeId): ContextExercise =
|
||||
copy(children = children :+ child)
|
||||
|
||||
override def contextSeed: Option[crypto.Hash] = ctx.contextSeed
|
||||
}
|
||||
|
||||
/** Context information to remember when building a sub-transaction
|
||||
* due to an 'exercises' statement.
|
||||
*
|
||||
* @param targetId Contract-id referencing the contract-instance on
|
||||
* which we are exercising a choice.
|
||||
* @param templateId Template-id referencing the template of the
|
||||
* contract on which we are exercising a choice.
|
||||
* @param contractKey Optional contract key, if defined for the
|
||||
* contract on which we are exercising a choice.
|
||||
* @param choiceId Label of the choice that we are exercising.
|
||||
* @param consuming True if the choice consumes the contract.
|
||||
* @param actingParties The parties exercising the choice.
|
||||
* @param chosenValue The chosen value.
|
||||
* @param signatories The signatories of the contract.
|
||||
* @param stakeholders The stakeholders of the contract.
|
||||
* @param controllers The controllers of the choice.
|
||||
* @param nodeId The node to be inserted once we've
|
||||
* finished this sub-transaction.
|
||||
* @param parent The context in which the exercises is
|
||||
* happening.
|
||||
*/
|
||||
case class ExercisesContext(
|
||||
contextSeed: Option[crypto.Hash],
|
||||
targetId: TContractId,
|
||||
templateId: TypeConName,
|
||||
contractKey: Option[KeyWithMaintainers[Value[Nothing]]],
|
||||
choiceId: ChoiceName,
|
||||
optLocation: Option[Location],
|
||||
consuming: Boolean,
|
||||
actingParties: Set[Party],
|
||||
chosenValue: Value[TContractId],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
controllers: Set[Party],
|
||||
nodeId: NodeId,
|
||||
parent: Context,
|
||||
)
|
||||
|
||||
/** The initial transaction from which we start building. It does not
|
||||
* contain any nodes and is not marked as aborted.
|
||||
* @param transactionSeed is the master hash used to derive nodes and
|
||||
* contractIds discriminator. If let undefined no
|
||||
* discriminators should be generate.
|
||||
*/
|
||||
def initial(seedWithTime: Option[(crypto.Hash, Time.Timestamp)] = None) =
|
||||
PartialTransaction(
|
||||
seedWithTime.map(_._2),
|
||||
nextNodeIdx = 0,
|
||||
nodes = HashMap.empty,
|
||||
consumedBy = Map.empty,
|
||||
context = ContextRoot(seedWithTime.map(_._1)),
|
||||
aborted = None,
|
||||
keys = Map.empty,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/** A transaction under construction
|
||||
*
|
||||
* @param nodes The nodes of the transaction graph being built up.
|
||||
* @param consumedBy 'ContractId's of all contracts that have
|
||||
* been consumed by nodes up to now.
|
||||
* @param context The context of what sub-transaction is being
|
||||
* built.
|
||||
* @param aborted The error that lead to aborting the building of
|
||||
* this transaction. We inline this error to allow
|
||||
* reporting the error jointly with the state that
|
||||
* the transaction was in when aborted. It is up to
|
||||
* the caller to check for 'isAborted' after every
|
||||
* change to a transaction.
|
||||
* @param keys A local store of the contract keys. Note that this contains
|
||||
* info both about relative and absolute contract ids. We must
|
||||
* do this because absolute contract ids can be archived as
|
||||
* part of execution, and we must record these archivals locally.
|
||||
* Note: it is important for keys that we know to not be present
|
||||
* to be present as [[None]]. The reason for this is that we must
|
||||
* record the "no key" information for absolute contract ids that
|
||||
* we archive. This is not an optimization and is required for
|
||||
* correct semantics, since otherwise lookups for keys for
|
||||
* locally archived absolute contract ids will succeed wrongly.
|
||||
*/
|
||||
case class PartialTransaction(
|
||||
submissionTime: Option[Time.Timestamp],
|
||||
nextNodeIdx: Int,
|
||||
nodes: HashMap[NodeId, Node],
|
||||
consumedBy: Map[TContractId, NodeId],
|
||||
context: PartialTransaction.Context,
|
||||
aborted: Option[TransactionError],
|
||||
keys: Map[GlobalKey, Option[TContractId]],
|
||||
) {
|
||||
|
||||
import PartialTransaction._
|
||||
|
||||
def nodesToString: String =
|
||||
if (nodes.isEmpty) "<empty transaction>"
|
||||
else {
|
||||
val sb = new StringBuilder()
|
||||
|
||||
def addToStringBuilder(
|
||||
nid: NodeId,
|
||||
node: GenNode.WithTxValue[NodeId, TContractId],
|
||||
rootPrefix: String,
|
||||
): Unit = {
|
||||
sb.append(rootPrefix)
|
||||
.append("node ")
|
||||
.append(nid.index)
|
||||
.append(": ")
|
||||
.append(node.toString)
|
||||
.append(", ")
|
||||
()
|
||||
}
|
||||
|
||||
def removeTrailingComma(): Unit = {
|
||||
if (sb.length >= 2) sb.setLength(sb.length - 2) // remove trailing ", "
|
||||
}
|
||||
|
||||
// roots field is not initialized when this method is executed on a failed transaction,
|
||||
// so we need to compute them.
|
||||
val rootNodes = {
|
||||
val allChildNodeIds: Set[NodeId] = nodes.values.flatMap {
|
||||
case _: LeafOnlyNode[_, _] => Nil
|
||||
case ex: NodeExercises[NodeId, _, _] => ex.children.toSeq
|
||||
}(breakOut)
|
||||
|
||||
nodes.keySet diff allChildNodeIds
|
||||
}
|
||||
val tx = GenTransaction(nodes, ImmArray(rootNodes), None)
|
||||
|
||||
tx.foreach { (nid, node) =>
|
||||
val rootPrefix = if (rootNodes.contains(nid)) "root " else ""
|
||||
addToStringBuilder(nid, node, rootPrefix)
|
||||
}
|
||||
removeTrailingComma()
|
||||
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def resolveCidDiscriminator(node: Node) =
|
||||
node.resolveRelCid(cid =>
|
||||
Ref.ContractIdString.assertFromString("0" + cid.discriminator.get.toHexaString))
|
||||
|
||||
/** Finish building a transaction; i.e., try to extract a complete
|
||||
* transaction from the given 'PartialTransaction'. This fails if
|
||||
* the 'PartialTransaction' is not yet complete or has been
|
||||
* aborted.
|
||||
*/
|
||||
def finish: Either[PartialTransaction, Transaction] =
|
||||
context match {
|
||||
case ContextRoot(transactionSeed, children) if aborted.isEmpty =>
|
||||
Right(
|
||||
GenTransaction(
|
||||
nodes =
|
||||
if (transactionSeed.isEmpty) nodes
|
||||
else nodes.transform((_, v) => resolveCidDiscriminator(v)),
|
||||
roots = children.toImmArray,
|
||||
optUsedPackages = None,
|
||||
transactionSeed = transactionSeed,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
Left(this)
|
||||
}
|
||||
|
||||
/** Lookup the contract associated to 'RelativeContractId'.
|
||||
* Return the contract instance and the node in which it was
|
||||
* consumed if any.
|
||||
*/
|
||||
def lookupLocalContract(
|
||||
lcoid: RelativeContractId,
|
||||
): Option[(ContractInst[Transaction.Value[TContractId]], Option[NodeId])] =
|
||||
for {
|
||||
node <- nodes.get(lcoid.txnid)
|
||||
coinst <- node match {
|
||||
case create: NodeCreate.WithTxValue[TContractId] =>
|
||||
Some((create.coinst, consumedBy.get(lcoid)))
|
||||
case _: NodeExercises[_, _, _] | _: NodeFetch[_] | _: NodeLookupByKey[_, _] => None
|
||||
}
|
||||
} yield coinst
|
||||
|
||||
/** Extend the 'PartialTransaction' with a node for creating a
|
||||
* contract instance.
|
||||
*/
|
||||
def insertCreate(
|
||||
coinst: ContractInst[Value[TContractId]],
|
||||
optLocation: Option[Location],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
key: Option[KeyWithMaintainers[Value[Nothing]]],
|
||||
): Either[String, (TContractId, PartialTransaction)] = {
|
||||
val serializableErrs = serializable(coinst.arg)
|
||||
if (serializableErrs.nonEmpty) {
|
||||
Left(
|
||||
s"""Trying to create a contract with a non-serializable value: ${serializableErrs.iterator
|
||||
.mkString(",")}""",
|
||||
)
|
||||
} else {
|
||||
val nodeSeed = deriveChildSeed
|
||||
val nodeId = NodeId(nextNodeIdx)
|
||||
val contractDiscriminator =
|
||||
for {
|
||||
seed <- nodeSeed
|
||||
time <- submissionTime
|
||||
} yield crypto.Hash.deriveContractDiscriminator(seed, time, stakeholders)
|
||||
val cid = RelativeContractId(nodeId, contractDiscriminator)
|
||||
val createNode = NodeCreate(
|
||||
nodeSeed,
|
||||
cid,
|
||||
coinst,
|
||||
optLocation,
|
||||
signatories,
|
||||
stakeholders,
|
||||
key,
|
||||
)
|
||||
val ptx = copy(
|
||||
nextNodeIdx = nextNodeIdx + 1,
|
||||
context = context.addChild(nodeId),
|
||||
nodes = nodes.updated(nodeId, createNode),
|
||||
)
|
||||
|
||||
// if we have a contract key being added, include it in the list of
|
||||
// active keys
|
||||
key match {
|
||||
case None => Right((cid, ptx))
|
||||
case Some(kWithM) =>
|
||||
val ck = GlobalKey(coinst.template, kWithM.key.value)
|
||||
Right((cid, ptx.copy(keys = ptx.keys.updated(ck, Some(cid)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def serializable(a: Value[TContractId]): ImmArray[String] = a.value.serializable()
|
||||
|
||||
def insertFetch(
|
||||
coid: TContractId,
|
||||
templateId: TypeConName,
|
||||
optLocation: Option[Location],
|
||||
actingParties: Set[Party],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
): PartialTransaction =
|
||||
mustBeActive(
|
||||
coid,
|
||||
templateId,
|
||||
insertLeafNode(
|
||||
NodeFetch(coid, templateId, optLocation, Some(actingParties), signatories, stakeholders),
|
||||
),
|
||||
)
|
||||
|
||||
def insertLookup(
|
||||
templateId: TypeConName,
|
||||
optLocation: Option[Location],
|
||||
key: KeyWithMaintainers[Value[Nothing]],
|
||||
result: Option[TContractId],
|
||||
): PartialTransaction =
|
||||
insertLeafNode(NodeLookupByKey(templateId, optLocation, key, result))
|
||||
|
||||
def beginExercises(
|
||||
targetId: TContractId,
|
||||
templateId: TypeConName,
|
||||
choiceId: ChoiceName,
|
||||
optLocation: Option[Location],
|
||||
consuming: Boolean,
|
||||
actingParties: Set[Party],
|
||||
signatories: Set[Party],
|
||||
stakeholders: Set[Party],
|
||||
controllers: Set[Party],
|
||||
mbKey: Option[KeyWithMaintainers[Value[Nothing]]],
|
||||
chosenValue: Value[TContractId],
|
||||
): Either[String, PartialTransaction] = {
|
||||
val serializableErrs = serializable(chosenValue)
|
||||
if (serializableErrs.nonEmpty) {
|
||||
Left(
|
||||
s"""Trying to exercise a choice with a non-serializable value: ${serializableErrs.iterator
|
||||
.mkString(",")}""",
|
||||
)
|
||||
} else {
|
||||
val nodeId = NodeId(nextNodeIdx)
|
||||
Right(
|
||||
mustBeActive(
|
||||
targetId,
|
||||
templateId,
|
||||
copy(
|
||||
nextNodeIdx = nextNodeIdx + 1,
|
||||
context = ContextExercise(
|
||||
ExercisesContext(
|
||||
contextSeed = deriveChildSeed,
|
||||
targetId = targetId,
|
||||
templateId = templateId,
|
||||
contractKey = mbKey,
|
||||
choiceId = choiceId,
|
||||
optLocation = optLocation,
|
||||
consuming = consuming,
|
||||
actingParties = actingParties,
|
||||
chosenValue = chosenValue,
|
||||
signatories = signatories,
|
||||
stakeholders = stakeholders,
|
||||
controllers = controllers,
|
||||
nodeId = nodeId,
|
||||
parent = context,
|
||||
),
|
||||
),
|
||||
// important: the semantics of DAML dictate that contracts are immediately
|
||||
// inactive as soon as you exercise it. therefore, mark it as consumed now.
|
||||
consumedBy = if (consuming) consumedBy.updated(targetId, nodeId) else consumedBy,
|
||||
keys = mbKey match {
|
||||
case Some(kWithM) if consuming =>
|
||||
keys.updated(GlobalKey(templateId, kWithM.key.value), None)
|
||||
case _ => keys
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def endExercises(value: Value[TContractId]): PartialTransaction =
|
||||
context match {
|
||||
case ContextExercise(ec, children) =>
|
||||
val exerciseNode: Transaction.Node = NodeExercises(
|
||||
nodeSeed = ec.contextSeed,
|
||||
targetCoid = ec.targetId,
|
||||
templateId = ec.templateId,
|
||||
choiceId = ec.choiceId,
|
||||
optLocation = ec.optLocation,
|
||||
consuming = ec.consuming,
|
||||
actingParties = ec.actingParties,
|
||||
chosenValue = ec.chosenValue,
|
||||
stakeholders = ec.stakeholders,
|
||||
signatories = ec.signatories,
|
||||
controllers = ec.controllers,
|
||||
children = children.toImmArray,
|
||||
exerciseResult = Some(value),
|
||||
key = ec.contractKey,
|
||||
)
|
||||
val nodeId = ec.nodeId
|
||||
copy(context = ec.parent.addChild(nodeId), nodes = nodes.updated(nodeId, exerciseNode))
|
||||
case ContextRoot(_, _) =>
|
||||
noteAbort(EndExerciseInRootContext)
|
||||
}
|
||||
|
||||
/** Note that the transaction building failed due to the given error */
|
||||
def noteAbort(err: TransactionError): PartialTransaction = copy(aborted = Some(err))
|
||||
|
||||
/** `True` iff the given `ContractId` has been consumed already */
|
||||
def isConsumed(coid: TContractId): Boolean = consumedBy.contains(coid)
|
||||
|
||||
/** Guard the execution of a step with the unconsumedness of a
|
||||
* `ContractId`
|
||||
*/
|
||||
def mustBeActive(
|
||||
coid: TContractId,
|
||||
templateId: TypeConName,
|
||||
f: => PartialTransaction,
|
||||
): PartialTransaction =
|
||||
consumedBy.get(coid) match {
|
||||
case None => f
|
||||
case Some(nid) => noteAbort(ContractNotActive(coid, templateId, nid))
|
||||
}
|
||||
|
||||
/** Insert the given `LeafNode` under a fresh node-id, and return it */
|
||||
def insertLeafNode(node: LeafNode): PartialTransaction = {
|
||||
val nodeId = NodeId(nextNodeIdx)
|
||||
copy(
|
||||
nextNodeIdx = nextNodeIdx + 1,
|
||||
context = context.addChild(nodeId),
|
||||
nodes = nodes.updated(nodeId, node),
|
||||
)
|
||||
}
|
||||
|
||||
def deriveChildSeed: Option[crypto.Hash] =
|
||||
context.contextSeed.map(crypto.Hash.deriveNodeSeed(_, nodes.size))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user