From 9ef3377864371e53bf49490a3e61914125f20675 Mon Sep 17 00:00:00 2001 From: Remy Date: Tue, 31 Aug 2021 13:01:25 +0200 Subject: [PATCH] LF: Update specification with Contract ID Comparability check (#10703) This is part of #10504. CHANGELOG_BEGIN CHANGELOG_END --- .../daml/lf/scenario/Conversions.scala | 4 +- .../digitalasset/daml/lf/speedy/Pretty.scala | 2 +- .../daml/lf/speedy/svalue/Equality.scala | 8 ++- .../daml/lf/speedy/svalue/Ordering.scala | 8 ++- .../daml/lf/speedy/svalue/EqualitySpec.scala | 6 +- .../daml/lf/speedy/svalue/OrderingSpec.scala | 6 +- daml-lf/spec/contract-id.rst | 64 +++++++++++-------- daml-lf/spec/daml-lf-1.rst | 26 ++++++-- .../src/main/scala/com/daml/lf/Error.scala | 6 +- 9 files changed, 84 insertions(+), 46 deletions(-) diff --git a/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala b/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala index aeb1e6be2c7b..2bd9760e474d 100644 --- a/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala +++ b/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala @@ -174,10 +174,10 @@ final class Conversions( builder.setContractIdInContractKey( proto.ScenarioError.ContractIdInContractKey.newBuilder.setKey(convertValue(key)) ) - case ContractIdFreshness(_) => + case ContractIdComparability(_) => // We crash here because you cannot construct a cid yourself in scenarios // or daml script. - builder.setCrash(s"Contract Id freshness Error") + builder.setCrash(s"Contract Id comparability Error") case NonComparableValues => builder.setComparableValueError(proto.Empty.newBuilder) case ValueExceedsMaxNesting => diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala index 98a2544a148a..330afb828af2 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala @@ -96,7 +96,7 @@ private[lf] object Pretty { text("Update failed due to a unknown contract") & prettyContractId(cid) case NonComparableValues => text("functions are not comparable") - case ContractIdFreshness(globalCid) => + case ContractIdComparability(globalCid) => text(s"The global contract ID $globalCid conflicts with a local contract ID") case ContractIdInContractKey(key) => text( diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Equality.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Equality.scala index 9b55838ca1c4..b076aafd26dd 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Equality.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Equality.scala @@ -49,9 +49,13 @@ private[lf] object Equality { SContractId(cid2 @ ContractId.V1(discriminator2, suffix2)), ) if discriminator1 == discriminator2 => if (suffix1.isEmpty) - throw SError.SErrorDamlException(interpretation.Error.ContractIdFreshness(cid2)) + throw SError.SErrorDamlException( + interpretation.Error.ContractIdComparability(cid2) + ) else if (suffix2.isEmpty) - throw SError.SErrorDamlException(interpretation.Error.ContractIdFreshness(cid1)) + throw SError.SErrorDamlException( + interpretation.Error.ContractIdComparability(cid1) + ) else false case _ => diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Ordering.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Ordering.scala index bb52c84bb910..35ecc9aac6d8 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Ordering.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/svalue/Ordering.scala @@ -107,6 +107,10 @@ object Ordering extends scala.math.Ordering[SValue] { } @inline private[this] def compareCid(cid1: ContractId, cid2: ContractId): Int = { + // Note that the engine never produce V0 contract IDs directly + // (V0 contract ID scheme can only be "emulated" at commit using + // `com.daml.lf.transaction.LegacyTransactionCommitter`). + // So we can assume here that all V0 Contract IDs are global. (cid1, cid2) match { case (ContractId.V0(s1), ContractId.V0(s2)) => s1 compareTo s2 @@ -125,11 +129,11 @@ object Ordering extends scala.math.Ordering[SValue] { if (suffix2.isEmpty) { 0 } else { - throw SError.SErrorDamlException(interpretation.Error.ContractIdFreshness(cid2)) + throw SError.SErrorDamlException(interpretation.Error.ContractIdComparability(cid2)) } } else { if (suffix2.isEmpty) { - throw SError.SErrorDamlException(interpretation.Error.ContractIdFreshness(cid1)) + throw SError.SErrorDamlException(interpretation.Error.ContractIdComparability(cid1)) } else { Bytes.ordering.compare(suffix1, suffix2) } diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/EqualitySpec.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/EqualitySpec.scala index 8a392c678e32..d5ff8bbe1444 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/EqualitySpec.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/EqualitySpec.scala @@ -6,7 +6,7 @@ package svalue import com.daml.lf.crypto import com.daml.lf.data.Bytes -import com.daml.lf.interpretation.Error.ContractIdFreshness +import com.daml.lf.interpretation.Error.ContractIdComparability import com.daml.lf.speedy.SValue._ import com.daml.lf.value.Value import org.scalatest.Inside @@ -56,9 +56,9 @@ class EqualitySpec extends AnyWordSpec with Inside with Matchers with ScalaCheck forEvery(positiveTestCases) { globalCid => Try(Equality.areEqual(vCid10, SContractId(globalCid))) shouldBe - Failure(SError.SErrorDamlException(ContractIdFreshness(globalCid))) + Failure(SError.SErrorDamlException(ContractIdComparability(globalCid))) Try(Equality.areEqual(SContractId(globalCid), vCid10)) shouldBe - Failure(SError.SErrorDamlException(ContractIdFreshness(globalCid))) + Failure(SError.SErrorDamlException(ContractIdComparability(globalCid))) } } diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/OrderingSpec.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/OrderingSpec.scala index 0ae72450a1da..222f00471c76 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/OrderingSpec.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/svalue/OrderingSpec.scala @@ -14,7 +14,7 @@ import com.daml.lf.value.test.TypedValueGenerators.genAddend import com.daml.lf.value.test.ValueGenerators.{cidV0Gen, comparableCoidsGen} import com.daml.lf.PureCompiledPackages import com.daml.lf.iface -import com.daml.lf.interpretation.Error.ContractIdFreshness +import com.daml.lf.interpretation.Error.ContractIdComparability import com.daml.lf.language.{Ast, Util => AstUtil} import org.scalacheck.{Arbitrary, Gen} import org.scalatest.Inside @@ -173,9 +173,9 @@ class OrderingSpec forEvery(positiveTestCases) { globalCid => Try(Ordering.compare(vCid10, SContractId(globalCid))) shouldBe - Failure(SError.SErrorDamlException(ContractIdFreshness(globalCid))) + Failure(SError.SErrorDamlException(ContractIdComparability(globalCid))) Try(Ordering.compare(SContractId(globalCid), vCid10)) shouldBe - Failure(SError.SErrorDamlException(ContractIdFreshness(globalCid))) + Failure(SError.SErrorDamlException(ContractIdComparability(globalCid))) } } diff --git a/daml-lf/spec/contract-id.rst b/daml-lf/spec/contract-id.rst index e8f1acb952de..20b8a0d2483f 100644 --- a/daml-lf/spec/contract-id.rst +++ b/daml-lf/spec/contract-id.rst @@ -26,17 +26,18 @@ projection. That is, the contract IDs for contracts created in the projection of a transaction to a set of parties can be computed solely from the projection and input seeds. -**Unlinkability**: It is computationally infeasible to link the contract -contents to the contract ID unless the create node is witnessed or the input seeds are known. The -contract contents include the contract instance, the template ID, the -stakeholders / signatories / maintainers, and the contract key. +**Unlinkability**: It is computationally infeasible to link the +contract contents to the contract ID unless the create node is +witnessed or the input seeds are known. The contract contents include +the contract instance, the template ID, the stakeholders / signatories +/ maintainers, and the contract key. -**Freshness**: It is computationally infeasible to find contracts C1 and -C2 with the same contract ID such that one of the following holds: +**Freshness**: It is computationally infeasible to find contracts C1 +and C2 with the same contract ID such that one of the following holds: * The contracts have different stakeholders. * The contracts are created by different transactions. -* The contracts are created by different subactions of the same +* The contracts are created by different sub-actions of the same transaction forest where the seeds are pairwise different. **Distinctness**: The ledger can enforce that the seeds are pairwise @@ -64,8 +65,8 @@ where uniqueness of the contract ID in a distributed ledger. -Discriminator freshness ------------------------ +Local/Global Contract IDs +------------------------- In a transaction we distinguish two kinds of contract IDs: @@ -73,7 +74,7 @@ In a transaction we distinguish two kinds of contract IDs: transaction. * The *global* contract IDs are the contract IDs that: - + * appear in the commands that produced the transaction. This includes the IDs of the exercised contract, together with all the IDs referenced in the arguments of the create and exercise @@ -82,19 +83,11 @@ In a transaction we distinguish two kinds of contract IDs: * are referenced in the input contracts. Note that local contract IDs correspond to the ID of output contracts -together with those contracts that have been created and archived in -the transaction. On the other hand, global contract IDs do not -only reference IDs of some contracts that have been previously -persisted on the ledger, but also any arbitrary contract IDs that the -submitter has referenced in its submission. - -The so-called *discriminator freshness condition* holds if the -discriminators from local contract IDs are distinct from the -discriminators from global contract IDs. - -This ensures that only the discriminators, not the suffix are needed -to order the contract IDs of created contracts w.r.t. other contract -IDs. +together with those contracts that have been both created and archived +in the transaction. On the other hand, global contract IDs do not only +reference IDs of some contracts that have been previously persisted on +the ledger, but also any arbitrary contract IDs that the submitter has +referenced in its submission. Contract ID uniqueness @@ -116,6 +109,23 @@ discriminator allocation scheme ensures in this case the uniqueness of allocated discriminators. +Contract ID Comparability +------------------------- + +The so-called *contract ID comparability restriction*, states that the +comparison of a local contract ID with a global contract ID with the +same discriminator is forbidden. Any attempt to compare such IDs +during interpretation, either explicitly (using Generic equality and +order builtins) or implicitly (though ordering of key in generic +maps), must be rejected. Ledger implementations that suffix contract +IDs should furthermore enforce that all global Contract IDs are +suffixed. + +This ensures that only the discriminators, not the suffix are needed +to compare the contract IDs of local contract IDs w.r.t. global +contract IDs. + + Submission time --------------- @@ -232,9 +242,11 @@ The submission performs the following steps: Depending on the ledger implementation, the local contract IDs are suffixed with a suffix in a later step. This yields the *committed -transaction*. For ledgers that do not require suffixing, committed and submitted -transactions coincide. Committed transactions are the source of truth to -derive the state of the ledger. +transaction*. + +For ledgers that do not require suffixing, committed and +submitted transactions coincide. Committed transactions are the source +of truth to derive the state of the ledger. Validation diff --git a/daml-lf/spec/daml-lf-1.rst b/daml-lf/spec/daml-lf-1.rst index bd342ca36b3e..0700185ed0c0 100644 --- a/daml-lf/spec/daml-lf-1.rst +++ b/daml-lf/spec/daml-lf-1.rst @@ -470,11 +470,11 @@ V0 contract identifiers, a Daml-LF compliant engine must refuse to load any Daml-LF >= 1.11 archives. On the contrary, when configured to produce V1 contract IDs, a Daml-LF compliant engine must accept to load any non-deprecated Daml-LF version. V1 Contract IDs allocation -scheme is described in the `V1 Contract ID allocation -scheme specification <./contract-id.rst>`_. - -Also note that package identifiers are typically `cryptographic hash -`_ of the content of the package itself. +scheme is described in the `V1 Contract ID allocation scheme +specification <./contract-id.rst>`_. In the following we will say that +a V1 contract identifier is *non-suffixed* if it is built from exactly +66 charters. Otherwise (meaning it has between 68 and 254 charters) we +will say it is *suffixed*. Literals ~~~~~~~~ @@ -3554,6 +3554,15 @@ The following builtin functions defines an order on the so-called abstractions, functions, partially applied builtin functions, and updates. +Note that as described in the `V1 Contract ID allocation scheme +specification <./contract-id.rst>`_ the comparison of two V1 contract +identifiers may fail at run time. For the purpose of this +specification, we will say that two contract identifiers are *not +comparable* if (i) both of them are V1 contract identifiers, (ii) one +of them is ``non-suffixed``, and (iii) is a strict prefixed of the +other one. + + * ``LESS_EQ : ∀ (α:*). α → α → 'Bool'`` The builtin function ``LESS_EQ`` returns ``'True'`` if the first @@ -3602,7 +3611,12 @@ updates. 𝕆('LESS_EQ' @σ LitRoundingMode₁ LitRoundingMode₂) = Ok (LitRoundingMode₁ ≤ₗ LitRoundingMode₂) - —————————————————————————————————————————————————————————————————————— EvLessEqContractId + cid₁ and cid₂ are not comparable + —————————————————————————————————————————————————————————————————————— EvLessEqNonComparableContractId + 𝕆('LESS_EQ' @σ cid₁ cid₂) = Err 'ContractIdComparability' + + cid₁ and cid₂ are comparable + —————————————————————————————————————————————————————————————————————— EvLessEqComparableContractId 𝕆('LESS_EQ' @σ cid₁ cid₂) = Ok (cid₁ ≤ₗ cid₂) —————————————————————————————————————————————————————————————————————— EvLessEqStructEmpty diff --git a/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala b/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala index 2542a0912c53..1fac1e3d297b 100644 --- a/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala +++ b/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala @@ -100,7 +100,11 @@ object Error { // as are not serializable. final case object NonComparableValues extends Error - final case class ContractIdFreshness(globalCid: ContractId.V1) extends Error + // Attempt to compare the global contract ID `globalCid` and a local + // contract ID with same discriminator. See the "Contract ID + // Comparability" section in the contract ID specification + // (//daml-lf/spec/contract-id.rst) for more details. + final case class ContractIdComparability(globalCid: ContractId.V1) extends Error final case class ContractIdInContractKey(key: Value[ContractId]) extends Error