mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
[LF] Define FatContractInstance and its (de)serialization (#17598)
This commit is contained in:
parent
25ae8d2281
commit
8b0daf03b7
@ -185,6 +185,8 @@ later.
|
||||
+--------------------+-----------------+
|
||||
| 14 | 2021-06-03 |
|
||||
+--------------------+-----------------+
|
||||
| 15 | 2022-07-29 |
|
||||
+--------------------+-----------------+
|
||||
| dev | |
|
||||
+--------------------+-----------------+
|
||||
|
||||
@ -508,6 +510,17 @@ As of version 14, this field is required:
|
||||
|
||||
``bool`` byKey
|
||||
|
||||
(* since version 15*)
|
||||
|
||||
As of version 15, this field is included.
|
||||
|
||||
* `message Identifier`_ interface_id
|
||||
|
||||
``interface_id``'s structure is defined by `the value specification`_
|
||||
|
||||
|
||||
(*since version dev*)
|
||||
|
||||
.. TODO: https://github.com/digital-asset/daml/issues/15882
|
||||
.. -- update for choice authorizers
|
||||
|
||||
@ -547,3 +560,102 @@ The rollback of a sub-transaction.
|
||||
As of version 14, these fields are included:
|
||||
|
||||
* repeated ``string`` children
|
||||
|
||||
message Versioned
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Generic wrapper to version encode versioned object
|
||||
|
||||
(*since version 14*)
|
||||
|
||||
As of version 14 the following fields are included:
|
||||
|
||||
* ``string`` version
|
||||
* ``bytes`` versioned
|
||||
|
||||
|
||||
``version`` is required, and must be a version of this
|
||||
specification newer than 14.
|
||||
|
||||
``versionned`` is the serialization of the versioned object
|
||||
as of version ``version``.
|
||||
|
||||
Consumers can expect this field to be present and to have the
|
||||
semantics defined here without knowing the version of this versioned
|
||||
object.
|
||||
|
||||
Known versions are listed in ascending order in `Version history`_; any
|
||||
``version`` not in this list should be considered newer than any version
|
||||
in same list, and consumers must reject values with such unknown
|
||||
versions.
|
||||
|
||||
|
||||
(*since version 14*)
|
||||
|
||||
message FatContractInstance
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A self contained representation of a committed contract.
|
||||
|
||||
The message is assumed ty be wrapped in a `message Versioned`_, which
|
||||
dictated the version used for decoding the message.
|
||||
|
||||
(* since version 14*)
|
||||
|
||||
As of version 14 the following fileds are included.
|
||||
|
||||
* ``bytes`` contract_id
|
||||
* `message Identifier`_ template_id
|
||||
* ``bytes`` create_arg
|
||||
* `message KeyWithMaintainers`_ contract_key_with_maintainers
|
||||
* repeated ``string`` non_maintainer_signatories
|
||||
* repetaed ``string`` non_signatory_stakeholders
|
||||
* ``int64`` created_at
|
||||
* ``bytes`` canton_data
|
||||
|
||||
|
||||
``contract_id`, ``template_id``, ``create_arg``, ``create_at`` are
|
||||
required.
|
||||
|
||||
``contract_id`` must be a valid Contract Identifiers as described in
|
||||
`the contract ID specification`_
|
||||
|
||||
``create_arg`` must be the serialization of the `message Value`_
|
||||
|
||||
If the ``contract_key_with_maintainers`` field is present, the
|
||||
elements of ``contract_key_with_maintainers.maintainers`` must be
|
||||
ordered without duplicate.
|
||||
|
||||
Elements of ``non_maintainer_signatories`` must be ordered party
|
||||
identifiers without duplicate.
|
||||
|
||||
Elements ``non_signatory_stakeholders`` must be ordered party
|
||||
identifiers without duplicate.
|
||||
|
||||
``sfixed64`` `created_at` is the number of microseconds since
|
||||
1970-01-01T00:00:00Z. It must be in the range from
|
||||
0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999Z, inclusive; while
|
||||
``sfixed64`` supports numbers outside that range, such created_at are
|
||||
not allowed and must be rejected with error by conforming consumers.
|
||||
|
||||
The message ``canton_data`` is considered as opaque blob by this
|
||||
specification. A conforming consumer must accept the message whatever
|
||||
the contain of this field is.
|
||||
|
||||
Additionally a conforming consumer must reject any message such there
|
||||
exists some party identifiers repeated in the concatenation of
|
||||
``non_maintainer_signatories``, ``non_signatory_stakeholders``, and
|
||||
``contract_key_with_maintainers.maintainers`` if
|
||||
``contract_key_with_maintainers`` is present.
|
||||
|
||||
|
||||
.. _`message Identifier`: value.rst#message-identifier
|
||||
.. _`message Value`: value.rst#message-value
|
||||
.. _`the contract ID specification`: contract-id.rst#contract-identifiers
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -164,6 +164,8 @@ later.
|
||||
+--------------------+-----------------+
|
||||
| 14 | 2021-22-06 |
|
||||
+--------------------+-----------------+
|
||||
| 15 | 2022-07-29 |
|
||||
+--------------------+-----------------+
|
||||
| dev | |
|
||||
+--------------------+-----------------+
|
||||
|
||||
@ -600,3 +602,4 @@ In this version, these fields are included:
|
||||
must conform to the same type. If two ore more entries have the
|
||||
same keys, the last one overrides the former entry. Entries with
|
||||
different key may occur in arbitrary order.
|
||||
|
||||
|
@ -290,9 +290,11 @@ object ValueGenerators {
|
||||
* 1. stakeholders may not be a superset of signatories
|
||||
* 2. key's maintainers may not be a subset of signatories
|
||||
*/
|
||||
val malformedCreateNodeGen: Gen[Node.Create] = {
|
||||
def malformedCreateNodeGen(
|
||||
minVersion: TransactionVersion = TransactionVersion.V14
|
||||
): Gen[Node.Create] = {
|
||||
for {
|
||||
version <- transactionVersionGen()
|
||||
version <- transactionVersionGen(minVersion)
|
||||
node <- malformedCreateNodeGenWithVersion(version)
|
||||
} yield node
|
||||
}
|
||||
@ -523,7 +525,7 @@ object ValueGenerators {
|
||||
node <- Gen.frequency(
|
||||
exerciseFreq -> danglingRefExerciseNodeGen,
|
||||
rollbackFreq -> danglingRefRollbackNodeGen,
|
||||
1 -> malformedCreateNodeGen,
|
||||
1 -> malformedCreateNodeGen(),
|
||||
2 -> fetchNodeGen,
|
||||
)
|
||||
nodeWithChildren <- node match {
|
||||
|
@ -127,3 +127,28 @@ message NodeRollback { // *since version 14*
|
||||
}
|
||||
|
||||
// architecture-handbook-entry-end: Nodes
|
||||
|
||||
message Versioned {
|
||||
string version = 1;
|
||||
bytes payload = 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// A self contains representation of a committed contract.
|
||||
message FatContractInstance {
|
||||
bytes contract_id = 1;
|
||||
com.daml.lf.value.Identifier template_id = 2;
|
||||
|
||||
bytes create_arg = 3;
|
||||
|
||||
KeyWithMaintainers contract_key_with_maintainers = 4;
|
||||
|
||||
repeated string non_maintainer_signatories = 5;
|
||||
|
||||
repeated string non_signatory_stakeholders = 6;
|
||||
|
||||
sfixed64 created_at = 7;
|
||||
|
||||
bytes canton_data = 8;
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.lf
|
||||
package transaction
|
||||
|
||||
import data.{Bytes, Ref, Time}
|
||||
import value.{CidContainer, Value}
|
||||
|
||||
import scala.collection.immutable.TreeSet
|
||||
|
||||
// This should replace value.ContractInstance in the whole daml/canton codespace
|
||||
// TODO: Rename to ContractInstance once value.ContractInstance is properly deprecated
|
||||
sealed abstract class FatContractInstance extends CidContainer[FatContractInstance] {
|
||||
val version: TransactionVersion
|
||||
val contractId: Value.ContractId
|
||||
val templateId: Ref.TypeConName
|
||||
val createArg: Value
|
||||
val signatories: TreeSet[Ref.Party]
|
||||
val stakeholders: TreeSet[Ref.Party]
|
||||
val contractKeyWithMaintainers: Option[GlobalKeyWithMaintainers]
|
||||
val createdAt: Time.Timestamp
|
||||
val cantonData: Bytes
|
||||
private[lf] def toImplementation: FatContractInstanceImpl =
|
||||
this.asInstanceOf[FatContractInstanceImpl]
|
||||
final lazy val maintainers: TreeSet[Ref.Party] =
|
||||
contractKeyWithMaintainers.fold(TreeSet.empty[Ref.Party])(k => TreeSet.from(k.maintainers))
|
||||
final lazy val nonMaintainerSignatories: TreeSet[Ref.Party] = signatories -- maintainers
|
||||
final lazy val nonSignatoryStakeholders: TreeSet[Ref.Party] = stakeholders -- signatories
|
||||
def updateCreateAt(updatedTime: Time.Timestamp): FatContractInstance
|
||||
def setSalt(cantonData: Bytes): FatContractInstance
|
||||
}
|
||||
|
||||
private[lf] final case class FatContractInstanceImpl(
|
||||
version: TransactionVersion,
|
||||
contractId: Value.ContractId,
|
||||
templateId: Ref.TypeConName,
|
||||
createArg: Value,
|
||||
signatories: TreeSet[Ref.Party],
|
||||
stakeholders: TreeSet[Ref.Party],
|
||||
contractKeyWithMaintainers: Option[GlobalKeyWithMaintainers],
|
||||
createdAt: Time.Timestamp,
|
||||
cantonData: Bytes,
|
||||
) extends FatContractInstance
|
||||
with CidContainer[FatContractInstanceImpl] {
|
||||
|
||||
// TODO (change implementation of KeyWithMaintainers.maintainer to TreeSet)
|
||||
require(maintainers.isInstanceOf[TreeSet[Ref.Party]])
|
||||
require(maintainers.subsetOf(signatories))
|
||||
require(signatories.nonEmpty)
|
||||
require(signatories.subsetOf(stakeholders))
|
||||
|
||||
override protected def self: FatContractInstanceImpl = this
|
||||
|
||||
override def mapCid(f: Value.ContractId => Value.ContractId): FatContractInstanceImpl = {
|
||||
copy(
|
||||
contractId = f(contractId),
|
||||
createArg = createArg.mapCid(f),
|
||||
)
|
||||
}
|
||||
|
||||
override def updateCreateAt(updatedTime: Time.Timestamp): FatContractInstanceImpl =
|
||||
copy(createdAt = updatedTime)
|
||||
|
||||
override def setSalt(cantonData: Bytes): FatContractInstanceImpl = {
|
||||
assert(cantonData.nonEmpty)
|
||||
copy(cantonData = cantonData)
|
||||
}
|
||||
}
|
||||
|
||||
object FatContractInstance {
|
||||
|
||||
def fromCreateNode(
|
||||
create: Node.Create,
|
||||
createTime: Time.Timestamp,
|
||||
cantonData: Bytes,
|
||||
): FatContractInstance =
|
||||
FatContractInstanceImpl(
|
||||
version = create.version,
|
||||
contractId = create.coid,
|
||||
templateId = create.templateId,
|
||||
createArg = create.arg,
|
||||
signatories = TreeSet.from(create.signatories),
|
||||
stakeholders = TreeSet.from(create.stakeholders),
|
||||
contractKeyWithMaintainers =
|
||||
create.keyOpt.map(k => k.copy(maintainers = TreeSet.from(k.maintainers))),
|
||||
createdAt = createTime,
|
||||
cantonData = cantonData,
|
||||
)
|
||||
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
package com.daml.lf
|
||||
package transaction
|
||||
|
||||
import com.daml.lf.crypto.Hash
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.lf.data.Ref.TypeConName
|
||||
import com.daml.lf.value.Value
|
||||
@ -58,7 +59,14 @@ object GlobalKeyWithMaintainers {
|
||||
value: Value,
|
||||
maintainers: Set[Ref.Party],
|
||||
): GlobalKeyWithMaintainers =
|
||||
GlobalKeyWithMaintainers(GlobalKey.assertBuild(templateId, value), maintainers)
|
||||
data.assertRight(build(templateId, value, maintainers).left.map(_.msg))
|
||||
|
||||
def build(
|
||||
templateId: Ref.TypeConName,
|
||||
value: Value,
|
||||
maintainers: Set[Ref.Party],
|
||||
): Either[Hash.HashingError, GlobalKeyWithMaintainers] =
|
||||
GlobalKey.build(templateId, value).map(GlobalKeyWithMaintainers(_, maintainers))
|
||||
}
|
||||
|
||||
/** Controls whether the engine should error out when it encounters duplicate keys.
|
||||
|
@ -14,7 +14,7 @@ import com.daml.scalautil.Statement.discard
|
||||
import com.google.protobuf.{ByteString, GeneratedMessageV3, ProtocolStringList}
|
||||
|
||||
import scala.Ordering.Implicits.infixOrderingOps
|
||||
import scala.collection.immutable.HashMap
|
||||
import scala.collection.immutable.{HashMap, TreeSet}
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
object TransactionCoder {
|
||||
@ -180,14 +180,12 @@ object TransactionCoder {
|
||||
Value.ContractInstanceWithAgreement(Value.ContractInstance(id, arg), protoCoinst.getAgreement)
|
||||
)
|
||||
|
||||
private[this] def encodeKeyWithMaintainers(
|
||||
private[transaction] def encodeKeyWithMaintainers(
|
||||
version: TransactionVersion,
|
||||
key: GlobalKeyWithMaintainers,
|
||||
): Either[EncodeError, TransactionOuterClass.KeyWithMaintainers] = {
|
||||
val builder =
|
||||
TransactionOuterClass.KeyWithMaintainers
|
||||
.newBuilder()
|
||||
.addAllMaintainers(key.maintainers.toSet[String].asJava)
|
||||
val builder = TransactionOuterClass.KeyWithMaintainers.newBuilder()
|
||||
key.maintainers.foreach(builder.addMaintainers(_))
|
||||
if (version < TransactionVersion.minNoVersionValue) {
|
||||
ValueCoder
|
||||
.encodeVersionedValue(ValueCoder.UnsafeNoCidEncoder, version, key.value)
|
||||
@ -429,6 +427,23 @@ object TransactionCoder {
|
||||
} yield GlobalKeyWithMaintainers(gkey, maintainers)
|
||||
}
|
||||
|
||||
private[transaction] def strictDecodeKeyWithMaintainers(
|
||||
version: TransactionVersion,
|
||||
templateId: Ref.TypeConName,
|
||||
keyWithMaintainers: TransactionOuterClass.KeyWithMaintainers,
|
||||
): Either[DecodeError, GlobalKeyWithMaintainers] =
|
||||
for {
|
||||
maintainers <- toOrderedPartySet(keyWithMaintainers.getMaintainersList)
|
||||
_ <- Either.cond(maintainers.nonEmpty, (), DecodeError("key without maintainers"))
|
||||
value <- decodeValue(
|
||||
ValueCoder.NoCidDecoder,
|
||||
version,
|
||||
keyWithMaintainers.getKeyVersioned,
|
||||
keyWithMaintainers.getKeyUnversioned,
|
||||
)
|
||||
gkey <- GlobalKey.build(templateId, value).left.map(hashErr => DecodeError(hashErr.msg))
|
||||
} yield GlobalKeyWithMaintainers(gkey, maintainers)
|
||||
|
||||
private val RightNone = Right(None)
|
||||
|
||||
private[this] def decodeOptionalKeyWithMaintainers(
|
||||
@ -828,6 +843,29 @@ object TransactionCoder {
|
||||
}
|
||||
}
|
||||
|
||||
// like toParty but requires entries to be strictly ordered
|
||||
def toOrderedPartySet(strList: ProtocolStringList): Either[DecodeError, TreeSet[Party]] =
|
||||
if (strList.isEmpty)
|
||||
Right(TreeSet.empty)
|
||||
else {
|
||||
val parties = strList
|
||||
.asByteStringList()
|
||||
.asScala
|
||||
.map(bs => Party.fromString(bs.toStringUtf8))
|
||||
|
||||
sequence(parties) match {
|
||||
case Left(err) => Left(DecodeError(s"Cannot decode party: $err"))
|
||||
case Right(ps) =>
|
||||
// TODO. Write a linear function that convert ordered sequences of elements into
|
||||
// a TreeSet, similarly to what is done for TreeMap with com.data.TreeMap
|
||||
(ps zip ps.tail)
|
||||
.collectFirst {
|
||||
case (p1, p2) if p1 >= p2 => DecodeError("the parties are not strictly ordered ")
|
||||
}
|
||||
.toLeft(TreeSet.from(ps))
|
||||
}
|
||||
}
|
||||
|
||||
private def toIdentifier(s: String): Either[DecodeError, Name] =
|
||||
Name.fromString(s).left.map(DecodeError)
|
||||
|
||||
@ -889,4 +927,130 @@ object TransactionCoder {
|
||||
Right(None)
|
||||
}
|
||||
|
||||
private[this] def ensureNoUnknownFields(
|
||||
proto: com.google.protobuf.Message
|
||||
): Either[DecodeError, Unit] = {
|
||||
val unknownFields = proto.getUnknownFields.asMap()
|
||||
Either.cond(
|
||||
unknownFields.isEmpty,
|
||||
(),
|
||||
DecodeError(
|
||||
s"unexpected field(s) ${unknownFields.keySet().asScala.mkString(", ")} in ${proto.getClass.getSimpleName} message"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private[transaction] def encodeVersioned(
|
||||
version: TransactionVersion,
|
||||
payload: ByteString,
|
||||
): ByteString = {
|
||||
val builder = TransactionOuterClass.Versioned.newBuilder()
|
||||
discard(builder.setVersion(version.protoValue))
|
||||
discard(builder.setPayload(payload))
|
||||
builder.build().toByteString
|
||||
}
|
||||
|
||||
private[transaction] def decodeVersioned(
|
||||
bytes: ByteString
|
||||
): Either[DecodeError, Versioned[ByteString]] =
|
||||
for {
|
||||
proto <- scala.util
|
||||
.Try(TransactionOuterClass.Versioned.parseFrom(bytes))
|
||||
.toEither
|
||||
.left
|
||||
.map(e => DecodeError(s"exception $e while decoding the versioned object"))
|
||||
_ <- ensureNoUnknownFields(proto)
|
||||
version <- TransactionVersion.fromString(proto.getVersion).left.map(DecodeError)
|
||||
payload = proto.getPayload
|
||||
} yield Versioned(version, payload)
|
||||
|
||||
def encodeFatContractInstance(
|
||||
contractInstance: FatContractInstance
|
||||
): Either[EncodeError, ByteString] = {
|
||||
import contractInstance._
|
||||
for {
|
||||
encodedArg <- ValueCoder.encodeValue(ValueCoder.CidEncoder, version, createArg)
|
||||
encodedKeyOpt <- contractKeyWithMaintainers match {
|
||||
case None =>
|
||||
Right(None)
|
||||
case Some(key) =>
|
||||
encodeKeyWithMaintainers(version, key).map(Some(_))
|
||||
}
|
||||
} yield {
|
||||
val builder = TransactionOuterClass.FatContractInstance.newBuilder()
|
||||
contractId match {
|
||||
case cid: Value.ContractId.V1 =>
|
||||
discard(builder.setContractId(cid.toBytes.toByteString))
|
||||
}
|
||||
discard(builder.setTemplateId(ValueCoder.encodeIdentifier(templateId)))
|
||||
discard(builder.setCreateArg(encodedArg))
|
||||
encodedKeyOpt.foreach(builder.setContractKeyWithMaintainers)
|
||||
nonMaintainerSignatories.foreach(builder.addNonMaintainerSignatories)
|
||||
nonSignatoryStakeholders.foreach(builder.addNonSignatoryStakeholders)
|
||||
discard(builder.setCreatedAt(createdAt.micros))
|
||||
discard(builder.setCreatedAt(createdAt.micros))
|
||||
discard(builder.setCantonData(cantonData.toByteString))
|
||||
encodeVersioned(version, builder.build().toByteString)
|
||||
}
|
||||
}
|
||||
|
||||
def decodeFatContractInstance(bytes: ByteString): Either[DecodeError, FatContractInstance] =
|
||||
for {
|
||||
versionedBlob <- decodeVersioned(bytes)
|
||||
Versioned(version, unversioned) = versionedBlob
|
||||
_ <- Either.cond(
|
||||
version >= TransactionVersion.V14,
|
||||
(),
|
||||
DecodeError(s"version $version does not support FatContractInstance"),
|
||||
)
|
||||
proto <- scala.util
|
||||
.Try(TransactionOuterClass.FatContractInstance.parseFrom(unversioned))
|
||||
.toEither
|
||||
.left
|
||||
.map(e => DecodeError(s"exception $e while decoding the object"))
|
||||
_ <- ensureNoUnknownFields(proto)
|
||||
contractId <- Value.ContractId.V1
|
||||
.fromBytes(data.Bytes.fromByteString(proto.getContractId))
|
||||
.left
|
||||
.map(DecodeError)
|
||||
templateId <- ValueCoder.decodeIdentifier(proto.getTemplateId)
|
||||
createArg <- ValueCoder.decodeValue(ValueCoder.CidDecoder, version, proto.getCreateArg)
|
||||
keyWithMaintainers <-
|
||||
if (proto.hasContractKeyWithMaintainers)
|
||||
strictDecodeKeyWithMaintainers(version, templateId, proto.getContractKeyWithMaintainers)
|
||||
.map(Some(_))
|
||||
else
|
||||
RightNone
|
||||
maintainers = keyWithMaintainers.fold(TreeSet.empty[Party])(k => TreeSet.from(k.maintainers))
|
||||
nonMaintainerSignatories <- toOrderedPartySet(proto.getNonMaintainerSignatoriesList)
|
||||
_ <- Either.cond(
|
||||
maintainers.nonEmpty || nonMaintainerSignatories.nonEmpty,
|
||||
(),
|
||||
DecodeError("maintainers or non_maintainer_signatories should be non empty"),
|
||||
)
|
||||
nonSignatoryStakeholders <- toOrderedPartySet(proto.getNonSignatoryStakeholdersList)
|
||||
signatories <- maintainers.find(nonMaintainerSignatories) match {
|
||||
case Some(p) =>
|
||||
Left(DecodeError(s"party $p is declared as maintainer and nonMaintainerSignatory"))
|
||||
case None => Right(maintainers | nonMaintainerSignatories)
|
||||
}
|
||||
stakeholders <- nonSignatoryStakeholders.find(signatories) match {
|
||||
case Some(p) =>
|
||||
Left(DecodeError(s"party $p is declared as signatory and nonSignatoryStakeholder"))
|
||||
case None => Right(signatories | nonSignatoryStakeholders)
|
||||
}
|
||||
createdAt <- data.Time.Timestamp.fromLong(proto.getCreatedAt).left.map(DecodeError)
|
||||
cantonData = proto.getCantonData
|
||||
} yield FatContractInstanceImpl(
|
||||
version = versionedBlob.version,
|
||||
contractId = contractId,
|
||||
templateId = templateId,
|
||||
createArg = createArg,
|
||||
signatories = signatories,
|
||||
stakeholders = stakeholders,
|
||||
createdAt = createdAt,
|
||||
contractKeyWithMaintainers = keyWithMaintainers,
|
||||
cantonData = data.Bytes.fromByteString(cantonData),
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ package transaction
|
||||
|
||||
import com.daml.lf.language.{LanguageMajorVersion, LanguageVersion}
|
||||
|
||||
sealed abstract class TransactionVersion private (val protoValue: String, private val index: Int)
|
||||
extends Product
|
||||
sealed abstract class TransactionVersion private (
|
||||
val protoValue: String,
|
||||
private[transaction] val index: Int,
|
||||
) extends Product
|
||||
with Serializable
|
||||
|
||||
/** Currently supported versions of the Daml-LF transaction specification.
|
||||
@ -27,18 +29,22 @@ object TransactionVersion {
|
||||
implicit val Ordering: scala.Ordering[TransactionVersion] =
|
||||
scala.Ordering.by(_.index)
|
||||
|
||||
private[this] val stringMapping = All.iterator.map(v => v.protoValue -> v).toMap
|
||||
private[this] val stringMapping = All.view.map(v => v.protoValue -> v).toMap
|
||||
|
||||
private[this] val intMapping = All.view.map(v => v.index -> v).toMap
|
||||
|
||||
def fromString(vs: String): Either[String, TransactionVersion] =
|
||||
stringMapping.get(vs) match {
|
||||
case Some(value) => Right(value)
|
||||
case None =>
|
||||
Left(s"Unsupported transaction version '$vs'")
|
||||
}
|
||||
stringMapping.get(vs).toRight(s"Unsupported transaction version '$vs'")
|
||||
|
||||
def assertFromString(vs: String): TransactionVersion =
|
||||
data.assertRight(fromString(vs))
|
||||
|
||||
def fromInt(i: Int): Either[String, TransactionVersion] =
|
||||
intMapping.get(i).toRight(s"Unsupported transaction version '$i'")
|
||||
|
||||
def assertFromInt(i: Int): TransactionVersion =
|
||||
data.assertRight(fromInt(i))
|
||||
|
||||
val minVersion: TransactionVersion = All.min
|
||||
def maxVersion: TransactionVersion = VDev
|
||||
|
||||
|
@ -164,6 +164,7 @@ object Value {
|
||||
new `Value Equal instance`
|
||||
|
||||
/** A contract instance is a value plus the template that originated it. */
|
||||
// Prefer to use transaction.FatContractInstance
|
||||
final case class ContractInstance(template: Identifier, arg: Value)
|
||||
extends CidContainer[ContractInstance] {
|
||||
|
||||
@ -241,19 +242,18 @@ object Value {
|
||||
|
||||
private val suffixStart: Int = crypto.Hash.underlyingHashLength + prefix.length
|
||||
|
||||
def fromBytes(bytes: Bytes): Either[String, V1] =
|
||||
if (bytes.startsWith(prefix) && bytes.length >= suffixStart)
|
||||
crypto.Hash
|
||||
.fromBytes(bytes.slice(prefix.length, suffixStart))
|
||||
.flatMap(
|
||||
V1.build(_, bytes.slice(suffixStart, bytes.length))
|
||||
)
|
||||
else
|
||||
Left(s"""cannot parse V1 ContractId "${bytes.toHexString}"""")
|
||||
|
||||
def fromString(s: String): Either[String, V1] =
|
||||
Bytes
|
||||
.fromString(s)
|
||||
.flatMap(bytes =>
|
||||
if (bytes.startsWith(prefix) && bytes.length >= suffixStart)
|
||||
crypto.Hash
|
||||
.fromBytes(bytes.slice(prefix.length, suffixStart))
|
||||
.flatMap(
|
||||
V1.build(_, bytes.slice(suffixStart, bytes.length))
|
||||
)
|
||||
else
|
||||
Left(s"""cannot parse V1 ContractId "$s"""")
|
||||
)
|
||||
Bytes.fromString(s).flatMap(fromBytes)
|
||||
|
||||
def assertFromString(s: String): V1 = assertRight(fromString(s))
|
||||
|
||||
|
@ -7,17 +7,20 @@ package transaction
|
||||
|
||||
import com.daml.lf.crypto.Hash
|
||||
import com.daml.lf.data.ImmArray
|
||||
import com.daml.lf.data.Ref.{Identifier, Party}
|
||||
import com.daml.lf.data.Ref.{Party, Identifier}
|
||||
import com.daml.lf.transaction.{TransactionOuterClass => proto}
|
||||
import com.daml.lf.value.Value.ContractId
|
||||
import com.daml.lf.value.ValueCoder.{DecodeError, EncodeError}
|
||||
import com.daml.lf.value.ValueCoder.{EncodeError, DecodeError}
|
||||
import com.daml.lf.value.{Value, ValueCoder}
|
||||
import org.scalacheck.{Arbitrary, Gen}
|
||||
import com.google.protobuf
|
||||
import com.google.protobuf.{ByteString, Message}
|
||||
import org.scalacheck.{Gen, Arbitrary}
|
||||
import org.scalatest.Inside
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
|
||||
|
||||
import collection.immutable.TreeSet
|
||||
import scala.Ordering.Implicits.infixOrderingOps
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
@ -53,7 +56,7 @@ class TransactionCoderSpec
|
||||
}
|
||||
|
||||
"do Node.Create" in {
|
||||
forAll(malformedCreateNodeGen, versionInIncreasingOrder()) {
|
||||
forAll(malformedCreateNodeGen(), versionInIncreasingOrder()) {
|
||||
case (createNode, (nodeVersion, txVersion)) =>
|
||||
val versionedNode = createNode.updateVersion(nodeVersion)
|
||||
val Right(encodedNode) = TransactionCoder
|
||||
@ -180,8 +183,8 @@ class TransactionCoderSpec
|
||||
}
|
||||
|
||||
"transactions decoding should fail when unsupported transaction version received" in
|
||||
forAll(noDanglingRefGenTransaction, minSuccessful(50)) { tx =>
|
||||
forAll(stringVersionGen, minSuccessful(20)) { badTxVer =>
|
||||
forAll(noDanglingRefGenTransaction, minSuccessful(30)) { tx =>
|
||||
forAll(stringVersionGen, minSuccessful(10)) { badTxVer =>
|
||||
whenever(TransactionVersion.fromString(badTxVer).isLeft) {
|
||||
val encodedTxWithBadTxVer: proto.Transaction = assertRight(
|
||||
TransactionCoder
|
||||
@ -605,6 +608,297 @@ class TransactionCoderSpec
|
||||
}
|
||||
}
|
||||
|
||||
"do Versioned" in {
|
||||
forAll(Gen.oneOf(TransactionVersion.All), bytesGen, minSuccessful(5)) { (version, bytes) =>
|
||||
val encoded = TransactionCoder.encodeVersioned(version, bytes)
|
||||
val Right(decoded) = TransactionCoder.decodeVersioned(encoded)
|
||||
decoded shouldBe Versioned(version, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
"reject versioned message with trailing data" in {
|
||||
forAll(
|
||||
Gen.oneOf(TransactionVersion.All),
|
||||
bytesGen,
|
||||
bytesGen.filterNot(_.isEmpty),
|
||||
minSuccessful(5),
|
||||
) { (version, bytes1, bytes2) =>
|
||||
val encoded = TransactionCoder.encodeVersioned(version, bytes1)
|
||||
TransactionCoder.decodeVersioned(encoded concat bytes2) shouldBe a[Left[_, _]]
|
||||
}
|
||||
}
|
||||
|
||||
"reject Versioned message with unknown fields" in {
|
||||
forAll(
|
||||
Gen.oneOf(TransactionVersion.All),
|
||||
bytesGen,
|
||||
Arbitrary.arbInt.arbitrary,
|
||||
bytesGen.filterNot(_.isEmpty),
|
||||
minSuccessful(5),
|
||||
) { (version, payload, i, extraData) =>
|
||||
val encoded = TransactionCoder.encodeVersioned(version, payload)
|
||||
val proto = TransactionOuterClass.Versioned.parseFrom(encoded)
|
||||
val reencoded = addUnknownField(proto.toBuilder, i, extraData).toByteString
|
||||
assert(reencoded != encoded)
|
||||
inside(TransactionCoder.decodeVersioned(reencoded)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include("unexpected field(s)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"do FatContractInstance" in {
|
||||
forAll(
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
minSuccessful(5),
|
||||
) { (create, time, salt) =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val Right(encoded) = TransactionCoder.encodeFatContractInstance(instance)
|
||||
val Right(decoded) = TransactionCoder.decodeFatContractInstance(encoded)
|
||||
|
||||
decoded shouldBe instance
|
||||
}
|
||||
}
|
||||
|
||||
def hackProto(
|
||||
instance: FatContractInstance,
|
||||
f: TransactionOuterClass.FatContractInstance.Builder => Message,
|
||||
): ByteString = {
|
||||
val Right(encoded) = TransactionCoder.encodeFatContractInstance(instance)
|
||||
val Right(Versioned(v, bytes)) = TransactionCoder.decodeVersioned(encoded)
|
||||
val builder = TransactionOuterClass.FatContractInstance.parseFrom(bytes).toBuilder
|
||||
TransactionCoder.encodeVersioned(v, f(builder).toByteString)
|
||||
}
|
||||
|
||||
"reject FatContractInstance with unknown fields" in {
|
||||
forAll(
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
Arbitrary.arbInt.arbitrary,
|
||||
bytesGen.filterNot(_.isEmpty),
|
||||
minSuccessful(5),
|
||||
) { (create, time, salt, i, extraBytes) =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val bytes = hackProto(instance, addUnknownField(_, i, extraBytes))
|
||||
inside(TransactionCoder.decodeFatContractInstance(bytes)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include("unexpected field(s)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"reject FatContractInstance with key but empty maintainers" in {
|
||||
forAll(
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
minSuccessful(2),
|
||||
) { (create, time, salt) =>
|
||||
forAll(
|
||||
keyWithMaintainersGen(create.templateId),
|
||||
minSuccessful(2),
|
||||
) { key =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val Right(protoKey) = TransactionCoder.encodeKeyWithMaintainers(create.version, key)
|
||||
val bytes = hackProto(
|
||||
instance,
|
||||
_.setContractKeyWithMaintainers(protoKey.toBuilder.clearMaintainers()).build(),
|
||||
)
|
||||
inside(TransactionCoder.decodeFatContractInstance(bytes)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include("key without maintainers")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"reject FatContractInstance with empty signatories" in {
|
||||
forAll(
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
minSuccessful(3),
|
||||
) { (create, time, salt) =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val bytes =
|
||||
hackProto(
|
||||
instance,
|
||||
_.clearContractKeyWithMaintainers().clearNonMaintainerSignatories().build(),
|
||||
)
|
||||
inside(TransactionCoder.decodeFatContractInstance(bytes)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include(
|
||||
"maintainers or non_maintainer_signatories should be non empty"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def hackKeyProto(
|
||||
version: TransactionVersion,
|
||||
key: GlobalKeyWithMaintainers,
|
||||
f: TransactionOuterClass.KeyWithMaintainers.Builder => TransactionOuterClass.KeyWithMaintainers.Builder,
|
||||
): TransactionOuterClass.KeyWithMaintainers = {
|
||||
val Right(encoded) = TransactionCoder.encodeKeyWithMaintainers(version, key)
|
||||
f(encoded.toBuilder).build()
|
||||
}
|
||||
|
||||
"reject FatContractInstance with nonMaintainerSignatories containing maintainers" in {
|
||||
forAll(
|
||||
party,
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
minSuccessful(2),
|
||||
) { (party, create, time, salt) =>
|
||||
forAll(
|
||||
keyWithMaintainersGen(create.templateId),
|
||||
minSuccessful(2),
|
||||
) { key =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val nonMaintainerSignatories = (instance.nonMaintainerSignatories + party)
|
||||
val maintainers = TreeSet.from(key.maintainers + party)
|
||||
val protoKey = hackKeyProto(
|
||||
create.version,
|
||||
key,
|
||||
{ builder =>
|
||||
builder.clearMaintainers()
|
||||
maintainers.foreach(builder.addMaintainers)
|
||||
builder
|
||||
},
|
||||
)
|
||||
|
||||
val bytes = hackProto(
|
||||
instance,
|
||||
{ builder =>
|
||||
builder.clearNonMaintainerSignatories()
|
||||
nonMaintainerSignatories.foreach(builder.addNonMaintainerSignatories)
|
||||
builder.setContractKeyWithMaintainers(protoKey)
|
||||
builder.build()
|
||||
},
|
||||
)
|
||||
inside(TransactionCoder.decodeFatContractInstance(bytes)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include("is declared as maintainer and nonMaintainerSignatory")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"reject FatContractInstance with nonSignatoryStakeholders containing maintainers" in {
|
||||
forAll(
|
||||
party,
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
minSuccessful(2),
|
||||
) { (party, create, time, salt) =>
|
||||
forAll(
|
||||
keyWithMaintainersGen(create.templateId),
|
||||
minSuccessful(2),
|
||||
) { key =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val maintainers = TreeSet.from(key.maintainers + party)
|
||||
val nonMaintainerSignatories =
|
||||
instance.nonMaintainerSignatories -- key.maintainers - party
|
||||
val nonSignatoryStakeholders = instance.nonSignatoryStakeholders + party
|
||||
val protoKey = hackKeyProto(
|
||||
create.version,
|
||||
key,
|
||||
{ builder =>
|
||||
builder.clearMaintainers()
|
||||
maintainers.foreach(builder.addMaintainers)
|
||||
builder
|
||||
},
|
||||
)
|
||||
|
||||
val bytes = hackProto(
|
||||
instance,
|
||||
{ builder =>
|
||||
builder.setContractKeyWithMaintainers(protoKey)
|
||||
builder.clearNonMaintainerSignatories()
|
||||
nonMaintainerSignatories.foreach(builder.addNonMaintainerSignatories)
|
||||
builder.clearNonSignatoryStakeholders()
|
||||
nonSignatoryStakeholders.foreach(builder.addNonSignatoryStakeholders)
|
||||
builder.build()
|
||||
},
|
||||
)
|
||||
inside(TransactionCoder.decodeFatContractInstance(bytes)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include("is declared as signatory and nonSignatoryStakeholder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"reject FatContractInstance with nonSignatoryStakeholders containing nonMaintainerSignatories" in {
|
||||
forAll(
|
||||
party,
|
||||
malformedCreateNodeGen(minVersion = V14),
|
||||
timestampGen,
|
||||
bytesGen,
|
||||
minSuccessful(4),
|
||||
) { (party, create, time, salt) =>
|
||||
val normalizedCreate = adjustStakeholders(normalizeCreate(create))
|
||||
val instance = FatContractInstance.fromCreateNode(
|
||||
normalizedCreate,
|
||||
time,
|
||||
data.Bytes.fromByteString(salt),
|
||||
)
|
||||
val nonMaintainerSignatories = instance.nonMaintainerSignatories + party
|
||||
val nonSignatoryStakeholders = instance.nonSignatoryStakeholders + party
|
||||
|
||||
val bytes = hackProto(
|
||||
instance,
|
||||
{ builder =>
|
||||
builder.clearNonMaintainerSignatories()
|
||||
nonMaintainerSignatories.foreach(builder.addNonMaintainerSignatories)
|
||||
builder.clearNonSignatoryStakeholders()
|
||||
nonSignatoryStakeholders.foreach(builder.addNonSignatoryStakeholders)
|
||||
builder.build()
|
||||
},
|
||||
)
|
||||
inside(TransactionCoder.decodeFatContractInstance(bytes)) {
|
||||
case Left(DecodeError(errorMessage)) =>
|
||||
errorMessage should include("is declared as signatory and nonSignatoryStakeholder")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"decodeVersionedNode" should {
|
||||
@ -831,6 +1125,50 @@ class TransactionCoderSpec
|
||||
}
|
||||
}
|
||||
|
||||
"toOrderPartySet" should {
|
||||
import com.google.protobuf.LazyStringArrayList
|
||||
import scala.util.Random.shuffle
|
||||
|
||||
def toProto(strings: Seq[String]) = {
|
||||
val l = new LazyStringArrayList()
|
||||
strings.foreach(s => l.add(ByteString.copyFromUtf8(s)))
|
||||
l
|
||||
}
|
||||
|
||||
"accept strictly order list of parties" in {
|
||||
forAll(Gen.listOf(party)) { parties =>
|
||||
val sortedParties = parties.sorted.distinct
|
||||
val proto = toProto(sortedParties)
|
||||
inside(TransactionCoder.toOrderedPartySet(proto)) { case Right(decoded: TreeSet[Party]) =>
|
||||
decoded shouldBe TreeSet.from(sortedParties)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"reject non sorted list of parties" in {
|
||||
forAll(party, Gen.nonEmptyListOf(party)) { (party0, parties0) =>
|
||||
val party = Iterator
|
||||
.iterate(party0)(p => Party.assertFromString("_" + p))
|
||||
.filterNot(parties0.contains)
|
||||
.next()
|
||||
val parties = party :: parties0
|
||||
val sortedParties = parties.sorted
|
||||
val nonSortedParties =
|
||||
Iterator.iterate(parties)(shuffle(_)).filterNot(_ == sortedParties).next()
|
||||
val proto = toProto(nonSortedParties)
|
||||
TransactionCoder.toOrderedPartySet(proto) shouldBe a[Left[_, _]]
|
||||
}
|
||||
}
|
||||
|
||||
"reject non list with duplicate" in {
|
||||
forAll(party, Gen.listOf(party)) { (party, parties) =>
|
||||
val partiesWithDuplicate = (party :: party :: parties).sorted
|
||||
val proto = toProto(partiesWithDuplicate)
|
||||
TransactionCoder.toOrderedPartySet(proto) shouldBe a[Left[_, _]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def withoutExerciseResult(gn: Node): Node =
|
||||
gn match {
|
||||
case ne: Node.Exercise => ne copy (exerciseResult = None)
|
||||
@ -886,6 +1224,11 @@ class TransactionCoderSpec
|
||||
v2 <- Gen.oneOf(versions.filter(_ > v1))
|
||||
} yield (v1, v2)
|
||||
|
||||
private val bytesGen: Gen[ByteString] =
|
||||
Gen
|
||||
.listOf(Arbitrary.arbByte.arbitrary)
|
||||
.map(x => ByteString.copyFrom(x.toArray))
|
||||
|
||||
private[this] def normalizeNode(node: Node) =
|
||||
node match {
|
||||
case rb: Node.Rollback => rb // nothing to normalize
|
||||
@ -895,6 +1238,16 @@ class TransactionCoderSpec
|
||||
case lookup: Node.LookupByKey => lookup
|
||||
}
|
||||
|
||||
private[this] def adjustStakeholders(create: Node.Create) = {
|
||||
val maintainers = create.keyOpt.fold(Set.empty[Party])(_.maintainers)
|
||||
val signatories = create.signatories | maintainers
|
||||
val stakeholders = create.stakeholders | signatories
|
||||
create.copy(
|
||||
signatories = signatories,
|
||||
stakeholders = stakeholders,
|
||||
)
|
||||
}
|
||||
|
||||
private[this] def normalizeCreate(
|
||||
create: Node.Create
|
||||
): Node.Create = {
|
||||
@ -971,4 +1324,18 @@ class TransactionCoderSpec
|
||||
case node: Node.Rollback => node
|
||||
}
|
||||
|
||||
def addUnknownField(
|
||||
builder: Message.Builder,
|
||||
i: Int,
|
||||
content: ByteString,
|
||||
): Message = {
|
||||
require(!content.isEmpty)
|
||||
def norm(i: Int) = (i % 536870911).abs + 1 // valid proto field index are 1 to 536870911
|
||||
val knownFieldIndex = builder.getDescriptorForType.getFields.asScala.map(_.getNumber).toSet
|
||||
val j = Iterator.iterate(norm(i))(i => norm(i + 1)).filterNot(knownFieldIndex).next()
|
||||
val field = protobuf.UnknownFieldSet.Field.newBuilder().addLengthDelimited(content).build()
|
||||
val extraFields = protobuf.UnknownFieldSet.newBuilder().addField(j, field).build()
|
||||
builder.setUnknownFields(extraFields).build()
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user