[LF] Drop kv-support (#16927)

This commit is contained in:
Remy 2023-05-30 16:53:21 +02:00 committed by GitHub
parent 4845ddf48d
commit 198a81f9c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 34 additions and 2130 deletions

View File

@ -1,61 +0,0 @@
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
load(
"//bazel_tools:scala.bzl",
"da_scala_library",
"da_scala_test_suite",
"lf_scalacopts",
"lf_scalacopts_stricter",
)
da_scala_library(
name = "kv-support",
srcs = glob(["src/main/**/*.scala"]),
scala_deps = [
"@maven//:org_scalaz_scalaz_core",
],
scalacopts = lf_scalacopts_stricter,
tags = ["maven_coordinates=com.daml:daml-lf-kv-support:__VERSION__"],
deps = [
"//daml-lf/archive:daml_lf_1.dev_archive_proto_java",
"//daml-lf/archive:daml_lf_archive_reader",
"//daml-lf/data",
"//daml-lf/language",
"//daml-lf/transaction",
"//daml-lf/transaction:transaction_proto_java",
"//daml-lf/transaction:value_proto_java",
"//libs-scala/safe-proto",
"@maven//:com_google_protobuf_protobuf_java",
],
)
da_scala_test_suite(
name = "test",
srcs = glob(["src/test/**/*.scala"]),
scala_deps = [
"@maven//:org_scalaz_scalaz_core",
"@maven//:org_scalacheck_scalacheck",
"@maven//:org_scalatest_scalatest_core",
"@maven//:org_scalatest_scalatest_matchers_core",
"@maven//:org_scalatest_scalatest_shouldmatchers",
"@maven//:org_scalatest_scalatest_wordspec",
"@maven//:org_scalatestplus_scalacheck_1_15",
],
scalacopts = lf_scalacopts,
deps = [
":kv-support",
"//daml-lf/archive:daml_lf_1.dev_archive_proto_java",
"//daml-lf/archive:daml_lf_archive_reader",
"//daml-lf/data",
"//daml-lf/encoder",
"//daml-lf/language",
"//daml-lf/parser",
"//daml-lf/transaction",
"//daml-lf/transaction:transaction_proto_java",
"//daml-lf/transaction:value_proto_java",
"//daml-lf/transaction-test-lib",
"@maven//:com_google_protobuf_protobuf_java",
"@maven//:org_scalatest_scalatest_compatible",
],
)

View File

@ -1,7 +0,0 @@
The KV Daml-LF library
======================
This package contains LF utilities specifically tailored for the KV
integration kit. This library does not define a stable public API, and
should not be used outside of the KV integration kit.
Breaking changes to this library will be made without notice.

View File

@ -1,20 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv
import com.daml.lf.value.ValueCoder
sealed abstract class ConversionError(val errorMessage: String)
extends RuntimeException(errorMessage)
object ConversionError {
final case class ParseError(override val errorMessage: String)
extends ConversionError(errorMessage)
final case class DecodeError(cause: ValueCoder.DecodeError)
extends ConversionError(cause.errorMessage)
final case class EncodeError(cause: ValueCoder.EncodeError)
extends ConversionError(cause.errorMessage)
final case class InternalError(override val errorMessage: String)
extends ConversionError(errorMessage)
}

View File

@ -1,55 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.archives
import com.daml.SafeProto
import com.daml.lf.archive.{ArchiveParser, Decode, Error => ArchiveError}
import com.daml.lf.data.Ref
import com.daml.lf.data.Ref.PackageId
import com.daml.lf.language.Ast
object ArchiveConversions {
def parsePackageId(rawArchive: RawArchive): Either[ArchiveError, Ref.PackageId] =
for {
archive <- ArchiveParser.fromByteString(rawArchive.byteString)
packageId <- Ref.PackageId
.fromString(archive.getHash)
.left
.map(ArchiveError.Parsing)
} yield packageId
def parsePackageIdsAndRawArchives(
archives: List[com.daml.daml_lf_dev.DamlLf.Archive]
): Either[ArchiveError, Map[Ref.PackageId, RawArchive]] =
archives.partitionMap { archive =>
for {
pkgId <- Ref.PackageId.fromString(archive.getHash).left.map(ArchiveError.Parsing)
bytes <- SafeProto.toByteString(archive).left.map(ArchiveError.Encoding)
} yield pkgId -> RawArchive(bytes)
} match {
case (Nil, hashesAndRawArchives) => Right(hashesAndRawArchives.toMap)
case (errors, _) => Left(errors.head)
}
def decodePackages(
hashesAndArchives: Iterable[RawArchive]
): Either[ArchiveError, Map[Ref.PackageId, Ast.Package]] = {
type Result = Either[ArchiveError, Map[Ref.PackageId, Ast.Package]]
hashesAndArchives
.foldLeft[Result](Right(Map.empty)) { (acc, rawArchive) =>
for {
result <- acc
packageAst <- decodePackage(rawArchive)
} yield result + packageAst
}
}
def decodePackage(
rawArchive: RawArchive
): Either[ArchiveError, (PackageId, Ast.Package)] =
ArchiveParser
.fromByteString(rawArchive.byteString)
.flatMap(archive => Decode.decodeArchive(archive))
}

View File

@ -1,9 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.archives
import com.google.protobuf.ByteString
/** Stores [[com.daml.daml_lf_dev.DamlLf.Archive]] as a [[ByteString]]. */
case class RawArchive(byteString: ByteString) extends AnyVal

View File

@ -1,34 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.contracts
import com.daml.SafeProto
import com.daml.lf.kv.ConversionError
import com.daml.lf.transaction.{TransactionCoder, TransactionOuterClass, Versioned}
import com.daml.lf.value.{Value, ValueCoder}
import scala.util.{Failure, Success, Try}
object ContractConversions {
def encodeContractInstance(
coinst: Versioned[Value.ContractInstanceWithAgreement]
): Either[ValueCoder.EncodeError, RawContractInstance] =
for {
message <- TransactionCoder.encodeContractInstance(ValueCoder.CidEncoder, coinst)
bytes <- SafeProto.toByteString(message).left.map(ValueCoder.EncodeError(_))
} yield RawContractInstance(bytes)
def decodeContractInstance(
rawContractInstance: RawContractInstance
): Either[ConversionError, Versioned[Value.ContractInstanceWithAgreement]] =
Try(TransactionOuterClass.ContractInstance.parseFrom(rawContractInstance.byteString)) match {
case Success(contractInstance) =>
TransactionCoder
.decodeVersionedContractInstance(ValueCoder.CidDecoder, contractInstance)
.left
.map(ConversionError.DecodeError)
case Failure(throwable) => Left(ConversionError.ParseError(throwable.getMessage))
}
}

View File

@ -1,9 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.contracts
import com.google.protobuf.ByteString
/** Stores [[com.daml.lf.transaction.TransactionOuterClass.ContractInstance]] as a [[ByteString]]. */
case class RawContractInstance(byteString: ByteString) extends AnyVal

View File

@ -1,18 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.google.protobuf.ByteString
/** Stores [[com.daml.lf.transaction.TransactionOuterClass.Transaction]] as a [[ByteString]]. */
case class RawTransaction(byteString: ByteString) extends AnyVal
object RawTransaction {
/** Stores [[com.daml.lf.transaction.TransactionOuterClass.Node]] as a [[ByteString]]. */
case class Node(byteString: ByteString) extends AnyVal
/** We store node IDs as strings (see [[com.daml.ledger.participant.state.kvutils.store.events.DamlTransactionBlindingInfo.DisclosureEntry]]). */
case class NodeId(value: String) extends AnyVal
}

View File

@ -1,273 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.daml.SafeProto
import com.daml.lf.data.{FrontStack, ImmArray}
import com.daml.lf.kv.ConversionError
import com.daml.lf.transaction.TransactionOuterClass.Node.NodeTypeCase
import com.daml.lf.transaction.{
GlobalKey,
NodeId,
TransactionCoder,
TransactionOuterClass,
VersionedTransaction,
}
import com.daml.lf.value.{Value, ValueCoder}
import scala.annotation.tailrec
import scala.collection.mutable
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try}
object TransactionConversions {
def encodeTransaction(
tx: VersionedTransaction
): Either[ValueCoder.EncodeError, RawTransaction] =
for {
msg <-
TransactionCoder.encodeTransaction(TransactionCoder.NidEncoder, ValueCoder.CidEncoder, tx)
bytes <- SafeProto.toByteString(msg).left.map(ValueCoder.EncodeError(_))
} yield RawTransaction(bytes)
def decodeTransaction(
rawTx: RawTransaction
): Either[ConversionError, VersionedTransaction] =
Try(TransactionOuterClass.Transaction.parseFrom(rawTx.byteString)) match {
case Success(transaction) =>
TransactionCoder
.decodeTransaction(
TransactionCoder.NidDecoder,
ValueCoder.CidDecoder,
transaction,
)
.left
.map(ConversionError.DecodeError)
case Failure(throwable) => Left(ConversionError.ParseError(throwable.getMessage))
}
def encodeTransactionNodeId(nodeId: NodeId): RawTransaction.NodeId =
RawTransaction.NodeId(nodeId.index.toString)
def decodeTransactionNodeId(transactionNodeId: RawTransaction.NodeId): NodeId =
NodeId(transactionNodeId.value.toInt)
def extractTransactionVersion(rawTransaction: RawTransaction): String =
TransactionOuterClass.Transaction.parseFrom(rawTransaction.byteString).getVersion
def extractNodeId(rawTransactionNode: RawTransaction.Node): RawTransaction.NodeId =
RawTransaction.NodeId(
TransactionOuterClass.Node.parseFrom(rawTransactionNode.byteString).getNodeId
)
@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
def reconstructTransaction(
transactionVersion: String,
nodesWithIds: Seq[TransactionNodeIdWithNode],
): Either[ConversionError, RawTransaction] = {
import scalaz.std.either._
import scalaz.std.list._
import scalaz.syntax.traverse._
// Reconstruct roots by considering the transaction nodes in order and
// marking all child nodes as non-roots and skipping over them.
val nonRoots = mutable.HashSet.empty[RawTransaction.NodeId]
val transactionBuilder =
TransactionOuterClass.Transaction.newBuilder.setVersion(transactionVersion)
nodesWithIds
.map { case TransactionNodeIdWithNode(rawNodeId, rawNode) =>
Try(TransactionOuterClass.Node.parseFrom(rawNode.byteString))
.map { node =>
transactionBuilder.addNodes(node)
if (!nonRoots.contains(rawNodeId)) {
transactionBuilder.addRoots(rawNodeId.value)
}
if (node.hasExercise) {
val children =
node.getExercise.getChildrenList.asScala.map(RawTransaction.NodeId).toSet
nonRoots ++= children
}
}
.toEither
.left
.map(throwable => ConversionError.ParseError(throwable.getMessage))
}
.toList
.sequence_
.flatMap(_ =>
SafeProto.toByteString(transactionBuilder.build()) match {
case Right(bytes) =>
Right(RawTransaction(bytes))
case Left(msg) =>
Left(ConversionError.EncodeError(ValueCoder.EncodeError(msg)))
}
)
}
/** Decodes and extracts outputs of a submitted transaction, that is the IDs and keys of contracts created or updated
* by processing a submission.
*
* The results, among the others, may include contract IDs of transient contracts (created and archived within the same transaction),
* contract IDs that may cause divulgence and keys that are not modified. Actual outputs must be a subset of,
* or the same as, computed outputs and we currently relax this check by widening the latter set,
* treating a node the same regardless of whether it was a child of a rollback node or not, for example.
* Computed outputs that are not actual outputs can be safely trimmed.
*/
def extractTransactionOutputs(
rawTransaction: RawTransaction
): Either[ConversionError, Set[ContractIdOrKey]] =
Try(TransactionOuterClass.Transaction.parseFrom(rawTransaction.byteString)) match {
case Failure(throwable) =>
Left(ConversionError.ParseError(throwable.getMessage))
case Success(transaction) =>
TransactionCoder
.decodeVersion(transaction.getVersion)
.flatMap { txVersion =>
transaction.getNodesList.asScala
.foldLeft[Either[ValueCoder.DecodeError, Set[ContractIdOrKey]]](Right(Set.empty)) {
case (Right(contractIdsOrKeys), node) =>
TransactionCoder.decodeNodeVersion(txVersion, node).flatMap { nodeVersion =>
node.getNodeTypeCase match {
case NodeTypeCase.ROLLBACK =>
// Nodes under a rollback node may potentially produce outputs such as divulgence.
Right(contractIdsOrKeys)
case NodeTypeCase.CREATE =>
val protoCreate = node.getCreate
for {
newContractIdsOrKeys <- TransactionCoder
.nodeKey(nodeVersion, protoCreate)
.map {
case Some(key) => contractIdsOrKeys + ContractIdOrKey.Key(key)
case None => contractIdsOrKeys
}
contractId <- ValueCoder.CidDecoder
.decode(protoCreate.getContractIdStruct)
} yield newContractIdsOrKeys + ContractIdOrKey.Id(contractId)
case NodeTypeCase.EXERCISE =>
val protoExercise = node.getExercise
for {
newContractIdsOrKeys <- TransactionCoder
.nodeKey(nodeVersion, protoExercise)
.map {
case Some(key) => contractIdsOrKeys + ContractIdOrKey.Key(key)
case None => contractIdsOrKeys
}
contractId <- ValueCoder.CidDecoder
.decode(protoExercise.getContractIdStruct)
} yield newContractIdsOrKeys + ContractIdOrKey.Id(contractId)
case NodeTypeCase.FETCH =>
// A fetch may cause divulgence, which is why the target contract is a potential output.
ValueCoder.CidDecoder.decode(node.getFetch.getContractIdStruct).map {
contractId => contractIdsOrKeys + ContractIdOrKey.Id(contractId)
}
case NodeTypeCase.LOOKUP_BY_KEY =>
// Contract state only modified on divulgence, in which case we'll have a fetch node,
// so no outputs from lookup node.
Right(contractIdsOrKeys)
case NodeTypeCase.NODETYPE_NOT_SET =>
Left(ValueCoder.DecodeError("NODETYPE_NOT_SET not supported"))
}
}
case (Left(error), _) => Left(error)
}
}
.left
.map(ConversionError.DecodeError)
}
/** Removes `Fetch`, `LookupByKey` and `Rollback` nodes (including their children) from a transaction tree. */
def keepCreateAndExerciseNodes(
rawTransaction: RawTransaction
): Either[ConversionError, RawTransaction] =
Try(TransactionOuterClass.Transaction.parseFrom(rawTransaction.byteString)) match {
case Failure(throwable) => Left(ConversionError.ParseError(throwable.getMessage))
case Success(transaction) =>
val nodes = transaction.getNodesList.asScala
val nodeMap: Map[String, TransactionOuterClass.Node] =
nodes.view.map(n => n.getNodeId -> n).toMap
@tailrec
def goNodesToKeep(
toVisit: FrontStack[String],
result: Set[String],
): Either[ConversionError, Set[String]] = toVisit.pop match {
case None => Right(result)
case Some((nodeId, previousToVisit)) =>
nodeMap.get(nodeId) match {
case Some(node) =>
node.getNodeTypeCase match {
case NodeTypeCase.CREATE =>
goNodesToKeep(previousToVisit, result + nodeId)
case NodeTypeCase.EXERCISE =>
goNodesToKeep(
node.getExercise.getChildrenList.asScala.to(ImmArray) ++: previousToVisit,
result + nodeId,
)
case NodeTypeCase.ROLLBACK | NodeTypeCase.FETCH | NodeTypeCase.LOOKUP_BY_KEY |
NodeTypeCase.NODETYPE_NOT_SET =>
goNodesToKeep(previousToVisit, result)
}
case None =>
Left(ConversionError.InternalError(s"Invalid transaction node id $nodeId"))
}
}
goNodesToKeep(transaction.getRootsList.asScala.to(FrontStack), Set.empty).flatMap {
nodesToKeep =>
val filteredRoots = transaction.getRootsList.asScala.filter(nodesToKeep)
val filteredNodes = nodes.collect {
case node if nodesToKeep(node.getNodeId) =>
if (node.hasExercise) {
val exerciseNode = node.getExercise
val keptChildren =
exerciseNode.getChildrenList.asScala.filter(nodesToKeep)
val newExerciseNode = exerciseNode.toBuilder
.clearChildren()
.addAllChildren(keptChildren.asJavaCollection)
.build()
node.toBuilder
.setExercise(newExerciseNode)
.build()
} else {
node
}
}
val newTransaction = transaction
.newBuilderForType()
.addAllRoots(filteredRoots.asJavaCollection)
.addAllNodes(filteredNodes.asJavaCollection)
.setVersion(transaction.getVersion)
.build()
SafeProto.toByteString(newTransaction) match {
case Right(bytes) =>
Right(RawTransaction(bytes))
case Left(msg) =>
// Should not happen as removing nodes should results into a smaller transaction.
Left(ConversionError.InternalError(msg))
}
}
}
}
final case class TransactionNodeIdWithNode(
nodeId: RawTransaction.NodeId,
node: RawTransaction.Node,
)
sealed abstract class ContractIdOrKey extends Product with Serializable
object ContractIdOrKey {
final case class Id(id: Value.ContractId) extends ContractIdOrKey
final case class Key(key: GlobalKey) extends ContractIdOrKey
}

View File

@ -1,50 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.daml.lf.transaction.Transaction.ChildrenRecursion
import com.daml.lf.transaction.{CommittedTransaction, Node, NodeId, VersionedTransaction}
object TransactionNormalizer {
// KV specific normalization.
// Drop Fetch, Lookup and Rollback nodes from a transaction, keeping Create and Exercise.
// Also drop everything contained with a rollback node
// Ignore Authority node, but traverse them
def normalize(
tx: CommittedTransaction
): CommittedTransaction = {
val keepNids: Set[NodeId] =
tx.foldInExecutionOrder[Set[NodeId]](Set.empty)(
exerciseBegin = (acc, nid, _) => (acc + nid, ChildrenRecursion.DoRecurse),
rollbackBegin = (acc, _, _) => (acc, ChildrenRecursion.DoNotRecurse),
leaf = (acc, nid, node) =>
node match {
case _: Node.Create => acc + nid
case _: Node.Fetch => acc
case _: Node.LookupByKey => acc
},
exerciseEnd = (acc, _, _) => acc,
rollbackEnd = (acc, _, _) => acc,
)
val filteredNodes =
tx.nodes
.filter { case (nid, _) => keepNids.contains(nid) }
.transform {
case (_, node: Node.Exercise) =>
node.copy(children = node.children.filter(keepNids.contains))
case (_, node: Node.Rollback) =>
node.copy(children = node.children.filter(keepNids.contains))
case (_, keep) =>
keep
}
val filteredRoots = tx.roots.filter(keepNids.contains)
CommittedTransaction(
VersionedTransaction(tx.version, filteredNodes, filteredRoots)
)
}
}

View File

@ -1,210 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.daml.lf.data.Ref.Party
import com.daml.lf.data.{FrontStack, ImmArray, Ref}
import com.daml.lf.kv.ConversionError
import com.daml.lf.transaction.TransactionOuterClass.Node
import com.daml.lf.transaction.{TransactionCoder, TransactionOuterClass, TransactionVersion}
import com.daml.lf.value.ValueOuterClass
import scala.annotation.tailrec
import scala.jdk.CollectionConverters._
import scala.util.Try
import com.daml.lf.value.ValueCoder.DecodeError
object TransactionTraversal {
// Helper to traverse the transaction, top-down, while keeping track of the
// witnessing parties of each node.
def traverseTransactionWithWitnesses(rawTx: RawTransaction)(
f: (RawTransaction.NodeId, RawTransaction.Node, Set[Ref.Party]) => Unit
): Either[ConversionError, Unit] =
for {
parsedTransaction <- parseTransaction(rawTx)
(txVersion, nodes, initialToVisit) = parsedTransaction
_ <- traverseWitnesses(f, txVersion, nodes, initialToVisit)
} yield ()
// Helper to traverse the transaction, top-down, while keeping track of the
// witnessing parties of each package.
def extractPerPackageWitnesses(
rawTx: RawTransaction
): Either[ConversionError, Map[String, Set[Ref.Party]]] =
for {
parsedTransaction <- parseTransaction(rawTx)
(txVersion, nodes, initialToVisit) = parsedTransaction
result <- traverseWitnessesWithPackages(txVersion, nodes, initialToVisit)
} yield result
private def parseTransaction(rawTx: RawTransaction): Either[
ConversionError,
(TransactionVersion, Map[String, Node], FrontStack[(RawTransaction.NodeId, Set[Party])]),
] = {
for {
tx <- Try(TransactionOuterClass.Transaction.parseFrom(rawTx.byteString)).toEither.left.map(
throwable => ConversionError.ParseError(throwable.getMessage)
)
txVersion <- TransactionVersion.fromString(tx.getVersion).left.map(ConversionError.ParseError)
nodes = tx.getNodesList.iterator.asScala.map(node => node.getNodeId -> node).toMap
initialToVisit = tx.getRootsList.asScala.view
.map(RawTransaction.NodeId(_) -> Set.empty[Ref.Party])
.to(FrontStack)
} yield { (txVersion, nodes, initialToVisit) }
}
@tailrec
private def traverseWitnessesWithPackages(
txVersion: TransactionVersion,
nodes: Map[String, Node],
toVisit: FrontStack[(RawTransaction.NodeId, Set[Ref.Party])],
packagesToParties: Map[String, Set[Ref.Party]] = Map.empty,
): Either[ConversionError, Map[String, Set[Ref.Party]]] = {
toVisit.pop match {
case None => Right(packagesToParties)
case Some(((nodeId, parentWitnesses), toVisit)) =>
val node = nodes(nodeId.value)
lazy val witnesses = informeesOfNode(txVersion, node).map(_ ++ parentWitnesses)
node.getNodeTypeCase match {
case Node.NodeTypeCase.EXERCISE =>
witnesses match {
case Left(value) => Left(value)
case Right(witnesses) =>
val exercise = node.getExercise
// Recurse into children (if any).
val next = exercise.getChildrenList.asScala.view
.map(RawTransaction.NodeId(_) -> witnesses)
.to(ImmArray)
val packagesToPartiesWithTemplateParties =
addWitnessesToPackage(packagesToParties, exercise.getTemplateId, witnesses)
val currentNodePackagesWithWitnesses =
Option
.when(exercise.hasInterfaceId)(
exercise.getInterfaceId
)
.map(addWitnessesToPackage(packagesToPartiesWithTemplateParties, _, witnesses))
.getOrElse(packagesToPartiesWithTemplateParties)
traverseWitnessesWithPackages(
txVersion,
nodes,
next ++: toVisit,
currentNodePackagesWithWitnesses,
)
}
case Node.NodeTypeCase.FETCH =>
val templateId = node.getFetch.getTemplateId
witnesses match {
case Left(error) => Left(error)
case Right(witnesses) =>
traverseWitnessesWithPackages(
txVersion,
nodes,
toVisit,
addWitnessesToPackage(packagesToParties, templateId, witnesses),
)
}
case Node.NodeTypeCase.CREATE =>
val templateId = node.getCreate.getTemplateId
witnesses match {
case Left(error) => Left(error)
case Right(witnesses) =>
traverseWitnessesWithPackages(
txVersion,
nodes,
toVisit,
addWitnessesToPackage(packagesToParties, templateId, witnesses),
)
}
case Node.NodeTypeCase.LOOKUP_BY_KEY =>
val templateId = node.getLookupByKey.getTemplateId
witnesses match {
case Left(error) => Left(error)
case Right(witnesses) =>
traverseWitnessesWithPackages(
txVersion,
nodes,
toVisit,
addWitnessesToPackage(packagesToParties, templateId, witnesses),
)
}
case Node.NodeTypeCase.ROLLBACK =>
// Rollback nodes have only the parent witnesses.
val rollback = node.getRollback
// Recurse into children (if any).
val next = rollback.getChildrenList.asScala.view
.map(RawTransaction.NodeId(_) -> parentWitnesses)
.to(ImmArray)
traverseWitnessesWithPackages(
txVersion,
nodes,
next ++: toVisit,
packagesToParties,
)
case Node.NodeTypeCase.NODETYPE_NOT_SET =>
Left(ConversionError.DecodeError(DecodeError("NodeType not set.")))
}
}
}
private def addWitnessesToPackage(
packagesToParties: Map[String, Set[Ref.Party]],
templateId: ValueOuterClass.Identifier,
witnesses: Set[Party],
) = {
val packageExistingParties = packagesToParties.get(templateId.getPackageId)
val packageWithAddedParties = packageExistingParties.getOrElse(Set.empty) ++ witnesses
packagesToParties + (templateId.getPackageId -> packageWithAddedParties)
}
@tailrec
private def traverseWitnesses(
f: (RawTransaction.NodeId, RawTransaction.Node, Set[Ref.Party]) => Unit,
txVersion: TransactionVersion,
nodes: Map[String, Node],
toVisit: FrontStack[(RawTransaction.NodeId, Set[Ref.Party])],
): Either[ConversionError, Unit] = {
toVisit.pop match {
case None => Right(())
case Some(((nodeId, parentWitnesses), toVisit)) =>
val node = nodes(nodeId.value)
informeesOfNode(txVersion, node) match {
case Left(error) => Left(error)
case Right(nodeWitnesses) =>
val witnesses = parentWitnesses union nodeWitnesses
// Here node.toByteString is safe.
// Indeed node is a submessage of the transaction `rawTx` we got serialized
// as input of `traverseTransactionWithWitnesses` and successfully decoded, i.e.
// `rawTx` requires less than 2GB to be serialized, so does <node`.
// See com.daml.SafeProto for more details about issues with the toByteString method.
f(nodeId, RawTransaction.Node(node.toByteString), witnesses)
// Recurse into children (if any).
node.getNodeTypeCase match {
case Node.NodeTypeCase.EXERCISE =>
val next = node.getExercise.getChildrenList.asScala.view
.map(RawTransaction.NodeId(_) -> witnesses)
.to(ImmArray)
traverseWitnesses(f, txVersion, nodes, next ++: toVisit)
case Node.NodeTypeCase.NODETYPE_NOT_SET =>
Left(ConversionError.DecodeError(DecodeError("NodeType not set.")))
case Node.NodeTypeCase.ROLLBACK =>
Left(ConversionError.DecodeError(DecodeError("Cannot traverse rollback nodes.")))
case _ =>
traverseWitnesses(f, txVersion, nodes, toVisit)
}
}
}
}
private[this] def informeesOfNode(
txVersion: TransactionVersion,
node: TransactionOuterClass.Node,
): Either[ConversionError.DecodeError, Set[Party]] =
TransactionCoder
.protoActionNodeInfo(txVersion, node)
.map(_.informeesOfNode)
.left
.map(ConversionError.DecodeError)
}

View File

@ -1,114 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.archives
import com.daml.daml_lf_dev.DamlLf.Archive
import com.daml.lf.archive.testing.Encode
import com.daml.lf.archive.{Error => ArchiveError}
import com.daml.lf.data.Ref
import com.daml.lf.language.Ast
import com.daml.lf.testing.parser.Implicits._
import com.daml.lf.testing.parser.Implicits.defaultParserParameters
import com.google.protobuf.ByteString
import org.scalatest.Inside
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class ArchiveConversionsSpec extends AnyWordSpec with Matchers with Inside {
import ArchiveConversionsSpec._
"parsePackageId" should {
"successfully parse a package ID" in {
ArchiveConversions.parsePackageId(
RawArchive(Archive.newBuilder().setHash("aPackageId").build().toByteString)
) shouldBe Right(Ref.PackageId.assertFromString("aPackageId"))
}
"fail on a broken archive" in {
inside(ArchiveConversions.parsePackageId(RawArchive(ByteString.copyFromUtf8("wrong")))) {
case Left(error: ArchiveError.IO) =>
error.msg shouldBe "IO error: com.google.protobuf.InvalidProtocolBufferException$InvalidWireTypeException: Protocol message tag had invalid wire type."
}
}
"fail on a broken package ID" in {
ArchiveConversions.parsePackageId(
RawArchive(Archive.newBuilder().setHash("???").build().toByteString)
) shouldBe Left(
ArchiveError.Parsing("non expected character 0x3f in Daml-LF Package ID \"???\"")
)
}
}
"parsePackageIdsAndRawArchives" should {
"successfully parse package IDs and raw archives" in {
val packageId1 = Ref.PackageId.assertFromString("packageId1")
val archive1 = Archive.newBuilder().setHash(packageId1).build()
val packageId2 = Ref.PackageId.assertFromString("packageId2")
val archive2 = Archive.newBuilder().setHash(packageId2).build()
ArchiveConversions.parsePackageIdsAndRawArchives(List(archive1, archive2)) shouldBe Right(
Map(
packageId1 -> RawArchive(archive1.toByteString),
packageId2 -> RawArchive(archive2.toByteString),
)
)
}
"fail on a broken package ID" in {
val archive = Archive.newBuilder().setHash("???").build()
ArchiveConversions.parsePackageIdsAndRawArchives(List(archive)) shouldBe Left(
ArchiveError.Parsing("non expected character 0x3f in Daml-LF Package ID \"???\"")
)
}
}
"decodePackages" should {
"successfully decode packages" in {
val List(archive1, archive2) = for {
differentiator <- List(1, 2)
archive = encodePackage(
p"""
metadata ( 'Package$differentiator' : '0.0.1' )
module Mod$differentiator {
record Record$differentiator = {};
}
"""
)
} yield archive
inside(
ArchiveConversions.decodePackages(
Iterable(
RawArchive(archive1.toByteString),
RawArchive(archive2.toByteString),
)
)
) { case Right(map) =>
map should have size 2
}
}
"fail on a broken package ID" in {
val archive = RawArchive(Archive.newBuilder().setHash("???").build().toByteString)
ArchiveConversions.decodePackages(Iterable(archive)) shouldBe Left(
ArchiveError.Parsing(
"Invalid hash: non expected character 0x3f in Daml-LF Package ID \"???\""
)
)
}
}
}
object ArchiveConversionsSpec {
def encodePackage[P](pkg: Ast.Package): Archive =
Encode.encodeArchive(
defaultParserParameters.defaultPackageId -> pkg,
defaultParserParameters.languageVersion,
)
}

View File

@ -1,88 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.contracts
import com.daml.lf.data.Ref
import com.daml.lf.kv.ConversionError
import com.daml.lf.transaction.{TransactionOuterClass, TransactionVersion, Versioned}
import com.daml.lf.value.{Value, ValueOuterClass}
import com.google.protobuf
import com.google.protobuf.ByteString
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class ContractConversionsSpec extends AnyWordSpec with Matchers {
import ContractConversionsSpec._
"encodeContractInstance" should {
"successfully encode a contract instance" in {
ContractConversions.encodeContractInstance(aContractInstance) shouldBe Right(
aRawContractInstance
)
}
}
"decodeContractInstance" should {
"successfully decode a contract instance" in {
ContractConversions.decodeContractInstance(aRawContractInstance) shouldBe Right(
aContractInstance
)
}
"fail on a broken contract instance" in {
ContractConversions.decodeContractInstance(
RawContractInstance(ByteString.copyFromUtf8("wrong"))
) shouldBe Left(ConversionError.ParseError("Protocol message tag had invalid wire type."))
}
}
}
object ContractConversionsSpec {
private val aDummyName = "dummyName"
private val aModuleName = "DummyModule"
private val aPackageId = "-dummyPkg-"
private val aAgreement = "agreement"
private val aContractInstance = Versioned(
version = TransactionVersion.VDev,
Value.ContractInstanceWithAgreement(
Value.ContractInstance(
template = Ref.Identifier(
Ref.PackageId.assertFromString(aPackageId),
Ref.QualifiedName.assertFromString(s"$aModuleName:$aDummyName"),
),
arg = Value.ValueUnit,
),
agreementText = aAgreement,
),
)
private val aRawContractInstance = RawContractInstance(
TransactionOuterClass.ContractInstance
.newBuilder()
.setTemplateId(
ValueOuterClass.Identifier
.newBuilder()
.setPackageId(aPackageId)
.addModuleName(aModuleName)
.addName(aDummyName)
)
.setAgreement(aAgreement)
.setArgVersioned(
ValueOuterClass.VersionedValue
.newBuilder()
.setValue(
ValueOuterClass.Value
.newBuilder()
.setUnit(protobuf.Empty.newBuilder())
.build()
.toByteString
)
.setVersion(TransactionVersion.VDev.protoValue)
.build()
)
.build()
.toByteString
)
}

View File

@ -1,574 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.daml.lf.transaction.test.TestNodeBuilder.CreateKey.{NoKey, SignatoryMaintainerKey}
import com.daml.lf.data.{ImmArray, Ref}
import com.daml.lf.kv.ConversionError
import com.daml.lf.kv.transactions.TransactionConversions.{
extractNodeId,
extractTransactionVersion,
reconstructTransaction,
}
import com.daml.lf.transaction.test.TreeTransactionBuilder
import com.daml.lf.transaction._
import com.daml.lf.value.{Value, ValueCoder, ValueOuterClass}
import com.daml.lf.transaction.test.TestNodeBuilder
import com.daml.lf.crypto
import com.google.protobuf
import com.google.protobuf.ByteString
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.jdk.CollectionConverters._
class TransactionConversionsSpec extends AnyWordSpec with Matchers {
import TransactionConversionsSpec._
import TreeTransactionBuilder._
private val cid = Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id"))
"TransactionUtilsSpec" should {
"encodeTransaction" in {
TransactionConversions.encodeTransaction(aVersionedTransaction) shouldBe Right(
aRawTransaction
)
}
"extractTransactionVersion" in {
val actualVersion = extractTransactionVersion(aRawTransaction)
actualVersion shouldBe TransactionVersion.VDev.protoValue
}
"extractNodeId" in {
val actualNodeId = extractNodeId(aRawRootNode)
actualNodeId shouldBe RawTransaction.NodeId("1")
}
}
"decodeTransaction" should {
"successfully decode a transaction" in {
TransactionConversions.decodeTransaction(aRawTransaction) shouldBe Right(
aVersionedTransaction
)
}
"fail to decode a non-parsable transaction" in {
TransactionConversions.decodeTransaction(
RawTransaction(ByteString.copyFromUtf8("wrong"))
) shouldBe Left(
ConversionError.ParseError("Protocol message tag had invalid wire type.")
)
}
}
"reconstructTransaction" should {
"successfully reconstruct a transaction" in {
val reconstructedTransaction = reconstructTransaction(
TransactionVersion.VDev.protoValue,
Seq(
TransactionNodeIdWithNode(aRawRootNodeId, aRawRootNode),
TransactionNodeIdWithNode(aRawChildNodeId, aRawChildNode),
),
)
reconstructedTransaction shouldBe Right(aRawTransaction)
}
"fail to reconstruct a non-parsable transaction" in {
val reconstructedTransaction = reconstructTransaction(
TransactionVersion.VDev.protoValue,
Seq(
TransactionNodeIdWithNode(
aRawRootNodeId,
RawTransaction.Node(ByteString.copyFromUtf8("wrong")),
)
),
)
reconstructedTransaction shouldBe Left(
ConversionError.ParseError("Protocol message tag had invalid wire type.")
)
}
}
"extractTransactionOutputs" should {
"return a single output for a create without a key" in {
val builder = TestNodeBuilder
val createNode = create(builder, cid)
val transaction = toVersionedTransaction(createNode)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(Set(ContractIdOrKey.Id(createNode.coid)))
}
"return two outputs for a create with a key" in {
val builder = TestNodeBuilder
val createNode = create(builder, cid, hasKey = true)
val transaction = toVersionedTransaction(createNode)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(
Set(
ContractIdOrKey.Id(createNode.coid),
ContractIdOrKey.Key(aGlobalKey),
)
)
}
"return a single output for a transient contract" in {
val builder = TestNodeBuilder
val createNode = create(builder, cid, hasKey = true)
val transaction = toVersionedTransaction(
createNode,
exercise(builder, cid),
)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(
Set(
ContractIdOrKey.Id(createNode.coid),
ContractIdOrKey.Key(aGlobalKey),
)
)
}
"return a single output for an exercise without a key" in {
val builder = TestNodeBuilder
val exerciseNode = exercise(builder, cid)
val transaction = toVersionedTransaction(exerciseNode)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(Set(ContractIdOrKey.Id(exerciseNode.targetCoid)))
}
"return two outputs for a consuming exercise with a key" in {
val builder = TestNodeBuilder
val exerciseNode = exercise(builder, cid, hasKey = true)
val transaction = toVersionedTransaction(exerciseNode)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(
Set(
ContractIdOrKey.Id(exerciseNode.targetCoid),
ContractIdOrKey.Key(aGlobalKey),
)
)
}
"return two outputs for a non-consuming exercise with a key" in {
val builder = TestNodeBuilder
val exerciseNode = exercise(builder, cid, hasKey = true, consuming = false)
val transaction = toVersionedTransaction(exerciseNode)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(
Set(
ContractIdOrKey.Id(exerciseNode.targetCoid),
ContractIdOrKey.Key(aGlobalKey),
)
)
}
"return one output per fetch and fetch-by-key" in {
val builder = TestNodeBuilder
val fetchNode1 =
fetch(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id1")), byKey = true)
val fetchNode2 =
fetch(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id2")), byKey = false)
val transaction = toVersionedTransaction(fetchNode1, fetchNode2)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(
Set(
ContractIdOrKey.Id(fetchNode1.coid),
ContractIdOrKey.Id(fetchNode2.coid),
)
)
}
"return no output for a failing lookup-by-key" in {
val builder = TestNodeBuilder
val transaction = toVersionedTransaction(lookup(builder, cid, found = false))
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(Set.empty)
}
"return no output for a successful lookup-by-key" in {
val builder = TestNodeBuilder
val transaction = toVersionedTransaction(lookup(builder, cid, found = true))
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(transaction)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(Set.empty)
}
"return outputs for nodes under a rollback node" in {
val builder = TestNodeBuilder
val createNode =
create(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id1")), hasKey = true)
val exerciseNode =
exercise(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id2")), hasKey = true)
val fetchNode =
fetch(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id3")), byKey = true)
val tx = toVersionedTransaction(
builder
.rollback()
.withChildren(
create(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id1")), hasKey = true),
exercise(
builder,
Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id2")),
hasKey = true,
),
fetch(builder, Value.ContractId.V1(crypto.Hash.hashPrivateKey("#id3")), byKey = true),
)
)
val result = TransactionConversions.extractTransactionOutputs(
TransactionConversions
.encodeTransaction(tx)
.fold(error => fail(error.errorMessage), identity)
)
result shouldBe Right(
Set(
ContractIdOrKey.Id(createNode.coid),
ContractIdOrKey.Key(aGlobalKey),
ContractIdOrKey.Id(exerciseNode.targetCoid),
ContractIdOrKey.Id(fetchNode.coid),
)
)
}
"fail on a non-parsable transaction" in {
val result = TransactionConversions.extractTransactionOutputs(
RawTransaction(ByteString.copyFromUtf8("wrong"))
)
result shouldBe Left(
ConversionError.ParseError("Protocol message tag had invalid wire type.")
)
}
"fail on a broken transaction version" in {
val result = TransactionConversions.extractTransactionOutputs(
RawTransaction(
TransactionOuterClass.Transaction.newBuilder().setVersion("???").build().toByteString
)
)
result shouldBe Left(
ConversionError.DecodeError(ValueCoder.DecodeError("Unsupported transaction version '???'"))
)
}
"fail on a broken transaction node version" in {
val result = TransactionConversions.extractTransactionOutputs(
RawTransaction(
TransactionOuterClass.Transaction
.newBuilder()
.setVersion(TransactionVersion.VDev.protoValue)
.addRoots(aRawRootNodeId.value)
.addNodes(buildProtoNode(aRawRootNodeId.value)(_.setVersion("???")))
.build()
.toByteString
)
)
result shouldBe Left(
ConversionError.DecodeError(ValueCoder.DecodeError("Unsupported transaction version '???'"))
)
}
"fail on a broken contract ID" in {
val result = TransactionConversions.extractTransactionOutputs(
RawTransaction(
TransactionOuterClass.Transaction
.newBuilder()
.setVersion(TransactionVersion.VDev.protoValue)
.addRoots(aRawRootNodeId.value)
.addNodes(
buildProtoNode(aRawRootNodeId.value) { builder =>
builder.setVersion(TransactionVersion.VDev.protoValue)
builder.setExercise(
exerciseBuilder()
.setContractIdStruct(
ValueOuterClass.ContractId.newBuilder().setContractId("???")
)
)
}
)
.build()
.toByteString
)
)
result shouldBe Left(
ConversionError.DecodeError(ValueCoder.DecodeError("cannot parse contractId \"???\""))
)
}
}
"keepCreateAndExerciseNodes" should {
"remove `Fetch`, `LookupByKey`, and `Rollback` nodes from the transaction tree" in {
val actual = TransactionConversions.keepCreateAndExerciseNodes(
RawTransaction(aRichNodeTreeTransaction.toByteString)
)
actual match {
case Right(rawTransaction) =>
val transaction = TransactionOuterClass.Transaction.parseFrom(rawTransaction.byteString)
transaction.getRootsList.asScala should contain theSameElementsInOrderAs Seq(
"Exercise-1",
"Create-1",
)
val nodes = transaction.getNodesList.asScala
nodes.map(_.getNodeId) should contain theSameElementsInOrderAs Seq(
"Create-1",
"Create-2",
"Create-3",
"Exercise-2",
"Exercise-1",
)
nodes(3).getExercise.getChildrenList.asScala should contain theSameElementsInOrderAs Seq(
"Create-3"
)
nodes(4).getExercise.getChildrenList.asScala should contain theSameElementsInOrderAs Seq(
"Create-2",
"Exercise-2",
)
case Left(_) => fail("should be Right")
}
}
"fail on a non-parsable transaction" in {
val result = TransactionConversions.keepCreateAndExerciseNodes(
RawTransaction(ByteString.copyFromUtf8("wrong"))
)
result shouldBe Left(
ConversionError.ParseError("Protocol message tag had invalid wire type.")
)
}
"fail on a transaction with invalid roots" in {
val result = TransactionConversions.keepCreateAndExerciseNodes(
RawTransaction(
aRichNodeTreeTransaction.toBuilder.addRoots("non-existent").build().toByteString
)
)
result shouldBe Left(
ConversionError.InternalError("Invalid transaction node id non-existent")
)
}
}
}
object TransactionConversionsSpec {
private val aChildNodeId = NodeId(2)
private val aRootNodeId = NodeId(1)
private val aRawChildNodeId = TransactionConversions.encodeTransactionNodeId(aChildNodeId)
private val aRawRootNodeId = TransactionConversions.encodeTransactionNodeId(aRootNodeId)
private val aChildObserver = Ref.Party.assertFromString("childObserver")
private val aChoiceId = Ref.Name.assertFromString("dummyChoice")
private val aDummyName = "dummyName"
private val aModuleName = "DummyModule"
private val aPackageId = "-dummyPkg-"
private val aProtoTemplateId = ValueOuterClass.Identifier
.newBuilder()
.setPackageId(aPackageId)
.addModuleName(aModuleName)
.addName(aDummyName)
private val aRootObserver = Ref.Party.assertFromString("rootObserver")
private val aTargetContractId = Value.ContractId.V1(crypto.Hash.hashPrivateKey("dummyCoid"))
private val aTemplateId = Ref.Identifier(
Ref.PackageId.assertFromString(aPackageId),
Ref.QualifiedName.assertFromString(s"$aModuleName:$aDummyName"),
)
private val aUnitArg =
ValueOuterClass.Value.newBuilder().setUnit(protobuf.Empty.newBuilder()).build().toByteString
private val aUnitValue = Value.ValueUnit
private val aChildNode = buildProtoNode(aRawChildNodeId.value) { builder =>
builder.setVersion(TransactionVersion.VDev.protoValue)
builder.setExercise(
exerciseBuilder()
.addObservers(aChildObserver)
.setArgUnversioned(aUnitArg)
.setChoice(aChoiceId)
.setConsuming(true)
.setContractIdStruct(
ValueOuterClass.ContractId.newBuilder().setContractId(aTargetContractId.coid)
)
.setTemplateId(aProtoTemplateId)
)
}
private val aChildExerciseNode = exercise(Set(aChildObserver), ImmArray.empty)
private val aGlobalKey = GlobalKey.assertBuild(aTemplateId, aUnitValue)
private val aRawChildNode = RawTransaction.Node(aChildNode.toByteString)
private val aRootExerciseNode = exercise(Set(aRootObserver), ImmArray(aChildNodeId))
private val aRootNode = buildProtoNode(aRawRootNodeId.value) { builder =>
builder.setVersion(TransactionVersion.VDev.protoValue)
builder.setExercise(
exerciseBuilder()
.addObservers(aRootObserver)
.setArgUnversioned(aUnitArg)
.addChildren(aRawChildNodeId.value)
.setChoice(aChoiceId)
.setConsuming(true)
.setContractIdStruct(
ValueOuterClass.ContractId.newBuilder().setContractId(aTargetContractId.coid)
)
.setTemplateId(aProtoTemplateId)
)
}
private val aRawRootNode = RawTransaction.Node(aRootNode.toByteString)
private val aRawTransaction = RawTransaction(
TransactionOuterClass.Transaction
.newBuilder()
.addRoots(aRawRootNodeId.value)
.addNodes(aRootNode)
.addNodes(aChildNode)
.setVersion(TransactionVersion.VDev.protoValue)
.build()
.toByteString
)
private val aRichNodeTreeTransaction = {
val roots = Seq("Exercise-1", "Fetch-1", "LookupByKey-1", "Create-1")
val nodes: Seq[TransactionOuterClass.Node] = Seq(
buildProtoNode("Fetch-1")(_.setFetch(fetchBuilder())),
buildProtoNode("LookupByKey-1")(_.setLookupByKey(lookupByKeyBuilder())),
buildProtoNode("Create-1")(_.setCreate(createBuilder())),
buildProtoNode("LookupByKey-2")(_.setLookupByKey(lookupByKeyBuilder())),
buildProtoNode("Fetch-2")(_.setFetch(fetchBuilder())),
buildProtoNode("Create-2")(_.setCreate(createBuilder())),
buildProtoNode("Fetch-3")(_.setFetch(fetchBuilder())),
buildProtoNode("Create-3")(_.setCreate(createBuilder())),
buildProtoNode("LookupByKey-3")(_.setLookupByKey(lookupByKeyBuilder())),
buildProtoNode("Exercise-2")(
_.setExercise(
exerciseBuilder().addAllChildren(
Seq("Fetch-3", "Create-3", "LookupByKey-3").asJava
)
)
),
buildProtoNode("Exercise-1")(
_.setExercise(
exerciseBuilder().addAllChildren(
Seq("LookupByKey-2", "Fetch-2", "Create-2", "Exercise-2").asJava
)
)
),
buildProtoNode("Rollback-1")(
_.setRollback(
rollbackBuilder().addAllChildren(Seq("RollbackChild-1", "RollbackChild-2").asJava)
)
),
buildProtoNode("RollbackChild-1")(_.setCreate(createBuilder())),
buildProtoNode("RollbackChild-2")(_.setFetch(fetchBuilder())),
)
TransactionOuterClass.Transaction
.newBuilder()
.addAllRoots(roots.asJava)
.addAllNodes(nodes.asJava)
.build()
}
private val aVersionedTransaction = VersionedTransaction(
TransactionVersion.VDev,
Map(aRootNodeId -> aRootExerciseNode, aChildNodeId -> aChildExerciseNode),
ImmArray(aRootNodeId),
)
private def create(builder: TestNodeBuilder, id: Value.ContractId, hasKey: Boolean = false) =
builder.create(
id = id,
templateId = aTemplateId,
argument = Value.ValueRecord(None, ImmArray.Empty),
signatories = Set.empty,
key = if (hasKey) SignatoryMaintainerKey(aUnitValue) else NoKey,
)
private def exercise(
builder: TestNodeBuilder,
id: Value.ContractId,
hasKey: Boolean = false,
consuming: Boolean = true,
) =
builder.exercise(
contract = create(builder, id, hasKey),
choice = aChoiceId,
argument = Value.ValueRecord(None, ImmArray.Empty),
actingParties = Set.empty,
consuming = consuming,
result = Some(aUnitValue),
byKey = false,
)
private def exercise(choiceObservers: Set[Ref.Party], children: ImmArray[NodeId]) =
Node.Exercise(
targetCoid = aTargetContractId,
templateId = aTemplateId,
interfaceId = None,
choiceId = aChoiceId,
consuming = true,
actingParties = Set.empty,
chosenValue = aUnitValue,
stakeholders = Set.empty,
signatories = Set.empty,
choiceObservers = choiceObservers,
choiceAuthorizers = None,
children = children,
exerciseResult = None,
keyOpt = None,
byKey = false,
version = TransactionVersion.VDev,
)
private def fetch(builder: TestNodeBuilder, id: Value.ContractId, byKey: Boolean) =
builder.fetch(contract = create(builder, id, hasKey = true), byKey = byKey)
private def lookup(builder: TestNodeBuilder, id: Value.ContractId, found: Boolean) =
builder.lookupByKey(contract = create(builder, id, hasKey = true), found = found)
private def buildProtoNode(nodeId: String)(
nodeImpl: TransactionOuterClass.Node.Builder => TransactionOuterClass.Node.Builder
) =
nodeImpl(TransactionOuterClass.Node.newBuilder().setNodeId(nodeId)).build()
private def fetchBuilder() = TransactionOuterClass.NodeFetch.newBuilder()
private def exerciseBuilder() = TransactionOuterClass.NodeExercise.newBuilder()
private def rollbackBuilder() = TransactionOuterClass.NodeRollback.newBuilder()
private def createBuilder() = TransactionOuterClass.NodeCreate.newBuilder()
private def lookupByKeyBuilder() = TransactionOuterClass.NodeLookupByKey.newBuilder()
}

View File

@ -1,106 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.daml.lf.data.ImmArray
import com.daml.lf.transaction._
import com.daml.lf.value.test.ValueGenerators._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
class TransactionNormalizerSpec
extends AnyWordSpec
with Matchers
with ScalaCheckDrivenPropertyChecks {
"normalizerTransaction" should {
"only keeps Create and Exercise nodes" in {
forAll(noDanglingRefGenVersionedTransaction) { tx =>
val normalized = TransactionNormalizer.normalize(CommittedTransaction(tx))
val nidsBefore: Set[NodeId] = tx.nodes.keySet
val nidsAfter: Set[NodeId] = normalized.nodes.keySet
// Every kept nid existed before
assert(nidsAfter.forall(nid => nidsBefore.contains(nid)))
// The normalized nodes mapping does not contain anything unreachanble
nidsAfter shouldBe normalized.reachableNodeIds
// Only create/exercise nodes are kept
assert(
nidsAfter
.map(normalized.nodes(_))
.forall(isCreateOrExercise)
)
// Everything kept is unchanged (except children may be dropped)
def unchangedByNormalization(nid: NodeId): Boolean = {
val before = tx.nodes(nid)
val after = normalized.nodes(nid)
// the node is unchanged when disregarding the children
nodeSansChildren(after) == nodeSansChildren(before)
// children can be lost, but nothing else
val beforeChildren = nodeChildren(before)
val afterChildren = nodeChildren(after)
afterChildren.forall(beforeChildren.contains(_))
}
assert(
nidsAfter.forall(unchangedByNormalization)
)
// Does a Nid reference a create/exercise node (in the before tx) ?
def isNidCE(nid: NodeId): Boolean = {
isCreateOrExercise(tx.nodes(nid))
}
// Is a Nid kept in the after transaction ?
def isKept(nid: NodeId): Boolean = {
nidsAfter.contains(nid)
}
// Create/exercise root nodes are kept
assert(
tx.roots.toList
.filter(isNidCE)
.forall(isKept)
)
// Create/exercise children of kept nodes should also be kept
assert(
nidsAfter
.flatMap(nid => nodeChildren(tx.nodes(nid)))
.filter(isNidCE)
.forall(isKept)
)
}
}
}
def isCreateOrExercise(node: Node): Boolean = {
node match {
case _: Node.Exercise => true
case _: Node.Create => true
case _ => false
}
}
def nodeSansChildren(node: Node): Node = {
node match {
case exe: Node.Exercise => exe.copy(children = ImmArray.Empty)
case _ => node
}
}
def nodeChildren(node: Node): List[NodeId] = {
node match {
case exe: Node.Exercise => exe.children.toList
case _ => List()
}
}
}

View File

@ -1,289 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.kv.transactions
import com.daml.lf.kv.ConversionError
import com.daml.lf.transaction.TransactionOuterClass.{
KeyWithMaintainers,
Node,
NodeLookupByKey,
NodeRollback,
Transaction,
}
import com.daml.lf.transaction.TransactionVersion
import com.daml.lf.value.{ValueCoder, ValueOuterClass}
import com.google.protobuf.ByteString
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import scala.jdk.CollectionConverters._
class TransactionTraversalSpec extends AnyFunSuite with Matchers {
test("traverseTransactionWithWitnesses - consuming nested exercises") {
val testTransaction = RawTransactionForTest()
TransactionTraversal.traverseTransactionWithWitnesses(testTransaction.rawTx) {
case (RawTransaction.NodeId(testTransaction.`createNid`), _, witnesses) =>
witnesses should contain.only("Alice", "Bob", "Charlie")
()
case (RawTransaction.NodeId(testTransaction.`exeNid`), _, witnesses) =>
witnesses should contain.only("Alice", "Charlie")
()
case (RawTransaction.NodeId(testTransaction.`nonConsumingExeNid`), _, witnesses) =>
// Non-consuming exercises are only witnessed by signatories.
witnesses should contain only "Alice"
()
case (RawTransaction.NodeId(testTransaction.`rootNid`), _, witnesses) =>
witnesses should contain only "Alice"
()
case (RawTransaction.NodeId(testTransaction.`fetchNid`), _, witnesses) =>
// This is of course ill-authorized, but we check that parent witnesses are included.
witnesses should contain.only("Alice", "Bob")
()
case what =>
fail(s"Traversed to unknown node: $what")
} shouldBe Right(())
}
test("extractPerPackageWitnesses - extract package witness mapping as expected") {
val testTransaction = RawTransactionForTest()
val result = TransactionTraversal.extractPerPackageWitnesses(testTransaction.rawTx)
result shouldBe
Right(
Map(
"template_exercise" -> Set("Alice", "Charlie"),
"interface_exercise" -> Set("Alice", "Charlie"),
"template_create" -> Set("Alice", "Charlie", "Bob"),
"template_fetch" -> Set("Alice", "Bob"),
)
)
}
test(
"extractPerPackageWitnesses - extract package witness mapping as expected including rollback node"
) {
val testTransaction = RawTransactionForTest()
def lookupByKeyNode(index: Int) = withNodeBuilder { builder =>
builder.setLookupByKey(
NodeLookupByKey.newBuilder
.setTemplateId(identifierForTemplateId(s"template_lookup_by_key$index"))
.setKeyWithMaintainers(
KeyWithMaintainers.newBuilder().addMaintainers(s"LookupByKey$index Party")
)
)
}
val transactionBuilder = testTransaction.builder
transactionBuilder.addRoot(
exerciseNode(
signatories = Seq("Exercise with Rollback Node Party"),
stakeholders = Seq.empty,
consuming = false,
children = transactionBuilder.addNode(
buildRollbackNodeWithChild(
transactionBuilder.addNode(lookupByKeyNode(0)),
transactionBuilder.addNode(
buildRollbackNodeWithChild(transactionBuilder.addNode(lookupByKeyNode(1)))
),
)
),
)
)
val result = TransactionTraversal.extractPerPackageWitnesses(testTransaction.rawTx)
result shouldBe
Right(
Map(
"template_exercise" -> Set("Alice", "Charlie", "Exercise with Rollback Node Party"),
"interface_exercise" -> Set("Alice", "Charlie", "Exercise with Rollback Node Party"),
"template_create" -> Set("Alice", "Charlie", "Bob"),
"template_fetch" -> Set("Alice", "Bob"),
"template_lookup_by_key0" -> Set(
"LookupByKey0 Party",
"Exercise with Rollback Node Party",
),
"template_lookup_by_key1" -> Set(
"LookupByKey1 Party",
"Exercise with Rollback Node Party",
),
)
)
}
private def buildRollbackNodeWithChild(children: String*) = {
withNodeBuilder { builder =>
builder.setRollback(
NodeRollback.newBuilder.addAllChildren(children.asJava)
)
}
}
test("traversal - transaction parsing error") {
val rawTx = RawTransaction(ByteString.copyFromUtf8("wrong"))
TransactionTraversal.traverseTransactionWithWitnesses(rawTx)((_, _, _) => ()) shouldBe Left(
ConversionError.ParseError("Protocol message tag had invalid wire type.")
)
TransactionTraversal.extractPerPackageWitnesses(rawTx) shouldBe Left(
ConversionError.ParseError("Protocol message tag had invalid wire type.")
)
}
test("traversal - transaction version parsing error") {
val rawTx = RawTransaction(Transaction.newBuilder().setVersion("wrong").build.toByteString)
val actual = TransactionTraversal.traverseTransactionWithWitnesses(rawTx)((_, _, _) => ())
actual shouldBe Left(ConversionError.ParseError("Unsupported transaction version 'wrong'"))
}
test("traversal - decode error on rollback nodes") {
val rootNodeId = "1"
val rawTx = RawTransaction(
Transaction
.newBuilder()
.setVersion(TransactionVersion.VDev.protoValue)
.addNodes(
Node.newBuilder().setNodeId(rootNodeId).setRollback(NodeRollback.getDefaultInstance)
)
.addRoots(rootNodeId)
.build
.toByteString
)
TransactionTraversal.traverseTransactionWithWitnesses(rawTx)((_, _, _) => ()) shouldBe Left(
ConversionError.DecodeError(
ValueCoder.DecodeError(
"protoActionNodeInfo only supports action nodes but was applied to a rollback node"
)
)
)
}
// --------------------------------------------------------
// Helpers for constructing transactions.
case class RawTransactionForTest() {
val builder: TransactionBuilder = TransactionBuilder()
// Creation of a contract with Alice as signatory, and Bob is controller of one of the choices.
val createNid: String = builder.addNode(
createNode(List("Alice"), List("Alice", "Bob"))
)
// Exercise of a contract where Alice as signatory, Charlie has a choice or is an observer.
val exeNid: String = builder.addNode(
exerciseNode(
signatories = List("Alice"),
stakeholders = List("Alice", "Charlie"),
consuming = true,
createNid,
)
)
// Non-consuming exercise of a contract where Alice as signatory, Charlie has a choice or is an observer.
val nonConsumingExeNid: String = builder.addNode(
exerciseNode(
signatories = List("Alice"),
stakeholders = List("Alice", "Charlie"),
consuming = false,
)
)
// A fetch of some contract created by Bob.
val fetchNid: String = builder.addNode(
fetchNode(
signatories = List("Bob"),
stakeholders = List("Bob"),
)
)
// Root node exercising a contract only known to Alice.
val rootNid: String = builder.addRoot(
exerciseNode(
signatories = List("Alice"),
stakeholders = List("Alice"),
consuming = true,
fetchNid,
nonConsumingExeNid,
exeNid,
)
)
def rawTx: RawTransaction = RawTransaction(builder.build.toByteString)
}
case class TransactionBuilder() {
private var roots = List.empty[String]
private var nodes = Map.empty[String, Node]
private var nextNodeId = -1
def addNode(node: Node.Builder): String = {
nextNodeId += 1
val nodeId = nextNodeId.toString
nodes += nodeId -> node.setNodeId(nodeId).build
nodeId
}
def addRoot(node: Node.Builder): String = {
// Check that children actually exist.
assert(
!node.hasExercise ||
node.getExercise.getChildrenList.asScala.map(nodes.contains).forall(identity)
)
val nodeId = addNode(node)
roots ::= nodeId
nodeId
}
def build: Transaction =
Transaction.newBuilder
.setVersion(TransactionVersion.minVersion.protoValue)
.addAllNodes(nodes.values.asJava)
.addAllRoots(roots.reverse.asJava)
.build
}
private def withNodeBuilder[A](build: Node.Builder => A): Node.Builder = {
val b = Node.newBuilder
build(b)
b
}
/** Construct a create node. Signatories are the parties whose signature is required to create the
* contract. Stakeholders of a contract are the signatories and observers.
*/
private def createNode(signatories: Iterable[String], stakeholders: Iterable[String]) =
withNodeBuilder {
_.getCreateBuilder
.setTemplateId(identifierForTemplateId("template_create"))
.addAllSignatories(signatories.asJava)
.addAllStakeholders(stakeholders.asJava)
}
/** Construct a fetch node. Signatories are the signatories of the contract we're fetching.
* Stakeholders of a contract are the signatories and observers.
*/
private def fetchNode(signatories: Iterable[String], stakeholders: Iterable[String]) =
withNodeBuilder {
_.getFetchBuilder
.setTemplateId(identifierForTemplateId("template_fetch"))
.addAllSignatories(signatories.asJava)
.addAllStakeholders(stakeholders.asJava)
}
/** Construct an exercise node. Signatories are the signatories of the contract we're exercising
* on.
*/
private def exerciseNode(
signatories: Seq[String],
stakeholders: Seq[String],
consuming: Boolean,
children: String*
) =
withNodeBuilder {
_.getExerciseBuilder
.setConsuming(consuming)
.setTemplateId(identifierForTemplateId("template_exercise"))
.setInterfaceId(identifierForTemplateId("interface_exercise"))
.addAllSignatories(signatories.asJava)
.addAllStakeholders(stakeholders.asJava)
/* NOTE(JM): Actors are no longer included in exercises by the compiler, hence we don't set them */
.addAllChildren(children.asJava)
}
private def identifierForTemplateId = ValueOuterClass.Identifier.newBuilder().setPackageId _
}

View File

@ -25,7 +25,7 @@ sealed abstract class Node extends Product with Serializable with CidContainer[N
object Node {
/** action nodes parametrized over identifier type */
sealed abstract class Action extends Node with ActionNodeInfo with CidContainer[Action] {
sealed abstract class Action extends Node with CidContainer[Action] {
def version: TransactionVersion
@ -46,6 +46,12 @@ object Node {
final override protected def self: this.type = this
/** Compute the informees of a node based on the ledger model definition.
*
* Refer to https://docs.daml.com/concepts/ledger-model/ledger-privacy.html#projections
*/
def informeesOfNode: Set[Party]
/** Required authorizers (see ledger model); UNSAFE TO USE on fetch nodes of transaction with versions < 5
*
* The ledger model defines the fetch node actors as the nodes' required authorizers.
@ -78,8 +84,7 @@ object Node {
keyOpt: Option[GlobalKeyWithMaintainers],
// For the sake of consistency between types with a version field, keep this field the last.
override val version: TransactionVersion,
) extends LeafOnlyAction
with ActionNodeInfo.Create {
) extends LeafOnlyAction {
@deprecated("use keyOpt", since = "2.6.0")
def key: Option[GlobalKeyWithMaintainers] = keyOpt
@ -102,6 +107,9 @@ object Node {
def versionedCoinst: Value.VersionedContractInstance = versioned(coinst)
def versionedKey: Option[Versioned[GlobalKeyWithMaintainers]] = keyOpt.map(versioned(_))
override def informeesOfNode: Set[Party] = stakeholders
override def requiredAuthorizers: Set[Party] = signatories
}
/** Denotes that the contract identifier `coid` needs to be active for the transaction to be valid. */
@ -115,9 +123,7 @@ object Node {
override val byKey: Boolean,
// For the sake of consistency between types with a version field, keep this field the last.
override val version: TransactionVersion,
) extends LeafOnlyAction
with ActionNodeInfo.Fetch {
) extends LeafOnlyAction {
@deprecated("use keyOpt", since = "2.6.0")
def key: Option[GlobalKeyWithMaintainers] = keyOpt
@ -128,6 +134,9 @@ object Node {
copy(coid = f(coid))
override def packageIds: Iterable[PackageId] = Iterable(templateId.packageId)
override def informeesOfNode: Set[Party] = signatories | actingParties
override def requiredAuthorizers: Set[Party] = actingParties
}
/** Denotes a transaction node for an exercise.
@ -146,15 +155,14 @@ object Node {
stakeholders: Set[Party],
signatories: Set[Party],
choiceObservers: Set[Party],
override val choiceAuthorizers: Option[Set[Party]],
choiceAuthorizers: Option[Set[Party]],
children: ImmArray[NodeId],
exerciseResult: Option[Value],
keyOpt: Option[GlobalKeyWithMaintainers],
override val byKey: Boolean,
// For the sake of consistency between types with a version field, keep this field the last.
override val version: TransactionVersion,
) extends Action
with ActionNodeInfo.Exercise {
) extends Action {
def qualifiedChoiceName = QualifiedChoiceName(interfaceId, choiceId)
@ -184,6 +192,12 @@ object Node {
def versionedKey: Option[Versioned[GlobalKeyWithMaintainers]] = keyOpt.map(versioned)
override def informeesOfNode: Set[Party] =
if (consuming)
stakeholders | actingParties | choiceObservers | choiceAuthorizers.getOrElse(Set.empty)
else
signatories | actingParties | choiceObservers | choiceAuthorizers.getOrElse(Set.empty)
override def requiredAuthorizers: Set[Party] = actingParties
}
final case class LookupByKey(
@ -192,8 +206,7 @@ object Node {
result: Option[ContractId],
// For the sake of consistency between types with a version field, keep this field the last.
override val version: TransactionVersion,
) extends LeafOnlyAction
with ActionNodeInfo.LookupByKey {
) extends LeafOnlyAction {
override def keyOpt: Some[GlobalKeyWithMaintainers] = Some(key)
@ -202,14 +215,22 @@ object Node {
override def mapCid(f: ContractId => ContractId): Node.LookupByKey =
copy(result = result.map(f))
override def keyMaintainers: Set[Party] = key.maintainers
override def hasResult: Boolean = result.isDefined
def keyMaintainers: Set[Party] = key.maintainers
override def byKey: Boolean = true
override private[lf] def updateVersion(version: TransactionVersion): Node.LookupByKey =
copy(version = version)
override def packageIds: Iterable[PackageId] = Iterable(templateId.packageId)
final def informeesOfNode: Set[Party] =
// TODO(JM): In the successful case the informees should be the
// signatories of the fetch contract. The signatories should be
// added to the LookupByKey node, or a successful lookup should
// become a Fetch.
keyMaintainers
def requiredAuthorizers: Set[Party] = keyMaintainers
}
@deprecated("use GlobalKey", since = "2.6.0")

View File

@ -1,89 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.transaction
import com.daml.lf.data.Ref.Party
/** Trait for extracting information from an abstract action node.
* Used for sharing the implementation of common computations
* over nodes and transactions.
*
* External codebases use these utilities on transaction and
* node implementations that are not the one defined by [[ActionNode]]
* and hence the need for the indirection.
*/
trait ActionNodeInfo {
/** Compute the informees of a node based on the ledger model definition.
*
* Refer to https://docs.daml.com/concepts/ledger-model/ledger-privacy.html#projections
*/
def informeesOfNode: Set[Party]
/** Required authorizers (see ledger model); UNSAFE TO USE on fetch nodes of transaction with versions < 5
*
* The ledger model defines the fetch node actingParties as the nodes' required authorizers.
* However, the our transaction data structure did not include the actingParties in versions < 5.
* The usage of this method must thus be restricted to:
* 1. settings where no fetch nodes appear (for example, the `validate` method of DAMLe, which uses it on root
* nodes, which are guaranteed never to contain a fetch node)
* 2. Daml ledger implementations that do not store or process any transactions with version < 5
*/
def requiredAuthorizers: Set[Party]
}
object ActionNodeInfo {
trait Create extends ActionNodeInfo {
def signatories: Set[Party]
def stakeholders: Set[Party]
final def requiredAuthorizers: Set[Party] = signatories
final def informeesOfNode: Set[Party] = stakeholders
}
trait Fetch extends ActionNodeInfo {
def signatories: Set[Party]
def stakeholders: Set[Party]
def actingParties: Set[Party]
final def requiredAuthorizers: Set[Party] =
actingParties
final def informeesOfNode: Set[Party] =
signatories | actingParties
}
trait Exercise extends ActionNodeInfo {
def consuming: Boolean
def signatories: Set[Party]
def stakeholders: Set[Party]
def actingParties: Set[Party]
def choiceObservers: Set[Party]
def choiceAuthorizers: Option[Set[Party]]
final def requiredAuthorizers: Set[Party] = actingParties
final def informeesOfNode: Set[Party] =
if (consuming)
stakeholders | actingParties | choiceObservers | choiceAuthorizers.getOrElse(Set.empty)
else
signatories | actingParties | choiceObservers | choiceAuthorizers.getOrElse(Set.empty)
}
trait LookupByKey extends ActionNodeInfo {
def keyMaintainers: Set[Party]
def hasResult: Boolean
final def requiredAuthorizers: Set[Party] = keyMaintainers
final def informeesOfNode: Set[Party] =
// TODO(JM): In the successful case the informees should be the
// signatories of the fetch contract. The signatories should be
// added to the LookupByKey node, or a successful lookup should
// become a Fetch.
keyMaintainers
}
}

View File

@ -255,13 +255,6 @@ sealed abstract class HasTxNodes {
def roots: ImmArray[NodeId]
/** 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
case (acc, _: Node.Rollback) => acc
}
// We assume that rollback node cannot be a root of a transaction.
// This is correct for an unprojected transaction. For a project transaction,
// Canton handles rollback nodes itself so this is assumption still holds

View File

@ -837,94 +837,6 @@ object TransactionCoder {
else
decodeVersion(node.getVersion)
/** Action node information for a serialized transaction node. Used to compute
* informees when deserialization is too costly.
* This method is not supported for transaction version <5 (as NodeInfo does not support it).
* We're not using e.g. "implicit class" in order to keep the decoding errors explicit.
* NOTE(JM): Currently used only externally, but kept here to keep in sync
* with the implementation.
* Note that this can only be applied to action nodes and will return Left on
* rollback nodes.
*/
def protoActionNodeInfo(
txVersion: TransactionVersion,
protoNode: TransactionOuterClass.Node,
): Either[DecodeError, ActionNodeInfo] =
protoNode.getNodeTypeCase match {
case NodeTypeCase.ROLLBACK =>
Left(
DecodeError(
"protoActionNodeInfo only supports action nodes but was applied to a rollback node"
)
)
case NodeTypeCase.CREATE =>
val protoCreate = protoNode.getCreate
for {
signatories_ <- toPartySet(protoCreate.getSignatoriesList)
stakeholders_ <- toPartySet(protoCreate.getStakeholdersList)
} yield {
new ActionNodeInfo.Create {
def signatories = signatories_
def stakeholders = stakeholders_
}
}
case NodeTypeCase.FETCH =>
val protoFetch = protoNode.getFetch
for {
actingParties_ <- toPartySet(protoFetch.getActorsList)
stakeholders_ <- toPartySet(protoFetch.getStakeholdersList)
signatories_ <- toPartySet(protoFetch.getSignatoriesList)
} yield {
new ActionNodeInfo.Fetch {
def signatories = signatories_
def stakeholders = stakeholders_
def actingParties = actingParties_
}
}
case NodeTypeCase.EXERCISE =>
val protoExe = protoNode.getExercise
for {
actingParties_ <- toPartySet(protoExe.getActorsList)
signatories_ <- toPartySet(protoExe.getSignatoriesList)
stakeholders_ <- toPartySet(protoExe.getStakeholdersList)
choiceObservers_ <-
if (txVersion < TransactionVersion.minChoiceObservers)
Right(Set.empty[Party])
else
toPartySet(protoExe.getObserversList)
choiceAuthorizers_ <-
if (txVersion < TransactionVersion.minChoiceAuthorizers) {
Right(None)
} else
toPartySet(protoExe.getAuthorizersList).map(choiceAuthorizersList =>
Option.when(choiceAuthorizersList.nonEmpty)(choiceAuthorizersList)
)
} yield {
new ActionNodeInfo.Exercise {
def signatories = signatories_
def stakeholders = stakeholders_
def actingParties = actingParties_
def choiceObservers = choiceObservers_
def choiceAuthorizers = choiceAuthorizers_
def consuming = protoExe.getConsuming
}
}
case NodeTypeCase.LOOKUP_BY_KEY =>
val protoLookupByKey = protoNode.getLookupByKey
for {
maintainers <- toPartySet(protoLookupByKey.getKeyWithMaintainers.getMaintainersList)
} yield {
new ActionNodeInfo.LookupByKey {
def hasResult = protoLookupByKey.hasContractIdStruct
def keyMaintainers = maintainers
}
}
case NodeTypeCase.NODETYPE_NOT_SET => Left(DecodeError("Unset Node type"))
}
private[this] def keyHash(
nodeVersion: TransactionVersion,
rawTmplId: ValueOuterClass.Identifier,

View File

@ -71,11 +71,6 @@ class TransactionCoderSpec
txVersion,
encodedNode,
) shouldBe Right((NodeId(0), normalizeCreate(versionedNode)))
Right(createNode.informeesOfNode) shouldEqual
TransactionCoder
.protoActionNodeInfo(txVersion, encodedNode)
.map(_.informeesOfNode)
}
}
@ -101,10 +96,6 @@ class TransactionCoderSpec
txVersion,
encodedNode,
) shouldBe Right((NodeId(0), normalizeFetch(versionedNode)))
Right(fetchNode.informeesOfNode) shouldEqual
TransactionCoder
.protoActionNodeInfo(txVersion, encodedNode)
.map(_.informeesOfNode)
}
}
@ -128,11 +119,6 @@ class TransactionCoderSpec
txVersion,
encodedNode,
) shouldBe Right((NodeId(0), normalizedNode))
Right(normalizedNode.informeesOfNode) shouldEqual
TransactionCoder
.protoActionNodeInfo(txVersion, encodedNode)
.map(_.informeesOfNode)
}
}

View File

@ -29,8 +29,6 @@
type: jar-scala
- target: //daml-lf/interpreter:interpreter
type: jar-scala
- target: //daml-lf/kv-support:kv-support
type: jar-scala
- target: //daml-lf/language:language
type: jar-scala
- target: //daml-lf/parser:parser