mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 16:57:40 +03:00
[LF] Drop kv-support (#16927)
This commit is contained in:
parent
4845ddf48d
commit
198a81f9c3
@ -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",
|
||||
],
|
||||
)
|
@ -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.
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
@ -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
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 _
|
||||
}
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user