[LF] Define FatContractInstance and its (de)serialization (#17598)

This commit is contained in:
Remy 2023-10-23 17:27:18 +02:00 committed by GitHub
parent 25ae8d2281
commit 8b0daf03b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 814 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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