Engine: move partialTransaction to interpreter (#4617)

* Engine: move partialTransaction to interpreter

CHANGELOG_BEGIN
CHANGELOG_END

* formating

* fix
This commit is contained in:
Remy 2020-02-20 17:48:47 +01:00 committed by GitHub
parent 726b692797
commit 3a8139d9c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 436 additions and 430 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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