Prohibit contract IDs in contract keys and add key maintainers to exercises (#4048)

Prohibit contract IDs in contract keys and add key maintainers to exercises

CHANGELOG_BEGIN

- [DAML-LF] Prohibit contract IDs in contract keys completely. Previously, creating keys containing absolute (but not relative) contract IDs was allowed, but `lookupByKey` on such a key would crash. 

CHANGELOG_END

Co-authored-by: Remy <remy.haemmerle@daml.com>
Co-authored-by: Stephen Compall <scompall@nocandysw.com>
This commit is contained in:
Ognjen Maric 2020-01-20 16:36:38 +01:00 committed by GitHub
parent 8811006617
commit 589f710313
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 252 additions and 201 deletions

View File

@ -9,7 +9,7 @@ class EngineInfoTest extends WordSpec with Matchers {
EngineInfo.getClass.getSimpleName should {
"show supported LF, Transaction and Value versions" in {
EngineInfo.show shouldBe
"DAML LF Engine supports LF versions: 0, 0.dev, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.dev; Transaction versions: 1, 2, 3, 4, 5, 6, 7, 8; Value versions: 1, 2, 3, 4, 5, 6, 7"
"DAML LF Engine supports LF versions: 0, 0.dev, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.dev; Transaction versions: 1, 2, 3, 4, 5, 6, 7, 8, 9; Value versions: 1, 2, 3, 4, 5, 6, 7"
}
"toString returns the same value as show" in {

View File

@ -507,35 +507,29 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
case UpdateLookupByKey(retrieveByKey) =>
// Translates 'lookupByKey Foo <key>' into:
// let key = <key>
// let maintainers = keyMaintainers key
// let keyWithMaintainers = {key: <key>, maintainers: <key maintainers> <key>}
// in \token ->
// let mbContractId = $lookupKey key
// _ = $insertLookup Foo key
// let mbContractId = $lookupKey keyWithMaintainers
// _ = $insertLookup Foo keyWithMaintainers
// in mbContractId
val template = lookupTemplate(retrieveByKey.templateId)
withEnv { _ =>
val key = translate(retrieveByKey.key)
val keyMaintainers = template.key match {
case None =>
throw CompileError(
s"Expecting to find key for template ${retrieveByKey.templateId}, but couldn't")
case Some(tplKey) => translate(tplKey.maintainers)
}
SELet(key, SEApp(keyMaintainers, Array(SEVar(1)))) in {
env = env.incrPos // key
env = env.incrPos // keyMaintainers
val templateKey = template.key.getOrElse(
throw CompileError(
s"Expecting to find key for template ${retrieveByKey.templateId}, but couldn't")
)
SELet(encodeKeyWithMaintainers(key, templateKey)) in {
env = env.incrPos // keyWithM
SEAbs(1) {
env = env.incrPos // token
SELet(
SBULookupKey(retrieveByKey.templateId)(
SEVar(3), // key
SEVar(2), // maintainers
SEVar(2), // key with maintainers
SEVar(1) // token
),
SBUInsertLookupNode(retrieveByKey.templateId)(
SEVar(4), // key
SEVar(3), // maintainers
SEVar(3), // key with maintainers
SEVar(1), // mb contract id
SEVar(2) // token
)
@ -546,25 +540,21 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
case UpdateFetchByKey(retrieveByKey) =>
// Translates 'fetchByKey Foo <key>' into:
// let key = <key>
// let maintainers = keyMaintainers key
// let keyWithMaintainers = {key: <key>, maintainers: <key maintainers> <key>}
// in \token ->
// let coid = $fetchKey key maintainers token
// let coid = $fetchKey keyWithMaintainers token
// contract = $fetch coid token
// _ = $insertFetch coid <signatories> <observers>
// in { contractId: ContractId Foo, contract: Foo }
val template = lookupTemplate(retrieveByKey.templateId)
withEnv { _ =>
val key = translate(retrieveByKey.key)
val keyMaintainers = template.key match {
case None =>
throw CompileError(
s"Expecting to find key for template ${retrieveByKey.templateId}, but couldn't")
case Some(tplKey) => translate(tplKey.maintainers)
}
SELet(key, SEApp(keyMaintainers, Array(SEVar(1)))) in {
env = env.incrPos // key
.incrPos // keyMaintainers
val keyTemplate = template.key.getOrElse(
throw CompileError(
s"Expecting to find key for template ${retrieveByKey.templateId}, but couldn't")
)
SELet(encodeKeyWithMaintainers(key, keyTemplate)) in {
env = env.incrPos // key with maintainers
SEAbs(1) {
env = env.incrPos // token
env = env.addExprVar(template.param)
@ -574,8 +564,7 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
val observers = translate(template.observers)
SELet(
SBUFetchKey(retrieveByKey.templateId)(
SEVar(3), // key
SEVar(2), // maintainers
SEVar(2), // key with maintainers
SEVar(1) // token
),
SBUFetch(retrieveByKey.templateId)(
@ -788,12 +777,21 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
}
}
private def encodeKeyWithMaintainers(key: SExpr, tmplKey: TemplateKey): SExpr =
SELet(key) in
SBStructCon(Name.Array(keyFieldName, maintainersFieldName))(
SEVar(1), // key
SEApp(translate(tmplKey.maintainers), Array(SEVar(1) /* key */ )))
private def translateKeyWithMaintainers(tmplKey: TemplateKey): SExpr =
encodeKeyWithMaintainers(translate(tmplKey.body), tmplKey)
/** Compile a choice into a top-level function for exercising that choice */
private def compileChoice(tmplId: TypeConName, tmpl: Template, choice: TemplateChoice): SExpr =
// Compiles a choice into:
// SomeTemplate$SomeChoice = \actors cid arg token ->
// let targ = fetch cid
// _ = $beginExercise[tmplId, chId] arg cid actors <sigs> <obs> <ctrls> token
// _ = $beginExercise[tmplId, chId] arg cid actors <sigs> <obs> <ctrls> <mbKey> token
// result = <updateE>
// _ = $endExercise[tmplId]
// in result
@ -819,11 +817,7 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
val controllers = translate(choice.controllers)
val mbKey: SExpr = tmpl.key match {
case None => SEValue.None
case Some(k) =>
SEApp(
SEBuiltin(SBSome),
Array(translate(k.body)),
)
case Some(k) => SEApp(SEBuiltin(SBSome), Array(translateKeyWithMaintainers(k)))
}
env = env.incrPos // beginExercise's ()
@ -1164,12 +1158,7 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
val key = tmpl.key match {
case None => SEValue.None
case Some(tmplKey) =>
SELet(translate(tmplKey.body)) in
SBSome(
SBStructCon(Name.Array(keyFieldName, maintainersFieldName))(
SEVar(1), // key
SEApp(translate(tmplKey.maintainers), Array(SEVar(1) /* key */ ))))
case Some(k) => SEApp(SEBuiltin(SBSome), Array(translateKeyWithMaintainers(k)))
}
env = env.incrPos // key
@ -1237,18 +1226,15 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
// in exerciseResult
val template = lookupTemplate(tmplId)
withEnv { _ =>
val keyMaintainers = template.key match {
case None =>
throw CompileError(s"Expecting to find key for template ${tmplId}, but couldn't")
case Some(tplKey) => translate(tplKey.maintainers)
}
SELet(key, SEApp(keyMaintainers, Array(SEVar(1)))) in {
env = env.incrPos // key
env = env.incrPos // keyMaintainers
val tmplKey = template.key.getOrElse(
throw CompileError(s"Expecting to find key for template ${tmplId}, but couldn't")
)
SELet(encodeKeyWithMaintainers(key, tmplKey)) in {
env = env.incrPos // key with maintainers
SEAbs(1) {
env = env.incrPos // token
SELet(
SBUFetchKey(tmplId)(SEVar(3), SEVar(2), SEVar(1)),
SBUFetchKey(tmplId)(SEVar(2), SEVar(1)),
SEApp(compileExercise(tmplId, SEVar(1), choiceId, optActors, argument), Array(SEVar(2)))
) in SEVar(1)
}

View File

@ -24,7 +24,6 @@ import com.digitalasset.daml.lf.transaction.Transaction._
import com.digitalasset.daml.lf.value.{Value => V}
import com.digitalasset.daml.lf.value.ValueVersions.asVersionedValue
import com.digitalasset.daml.lf.transaction.Node.{GlobalKey, KeyWithMaintainers}
import com.digitalasset.daml.lf.value.Value.{AbsoluteContractId, RelativeContractId}
import scala.collection.JavaConverters._
@ -818,16 +817,10 @@ object SBuiltin {
val sigs = extractParties(args.get(2))
val obs = extractParties(args.get(3))
val key = args.get(4) match {
case SOptional(None) => None
case SOptional(Some(SStruct(flds, vals)))
if flds.length == 2 && flds(0) == "key" && flds(1) == "maintainers" =>
asVersionedValue(vals.get(0).toValue) match {
case Left(err) => crash(err)
case Right(keyVal) =>
Some(KeyWithMaintainers(key = keyVal, maintainers = extractParties(vals.get(1))))
}
case _ => crash("Bad key")
case SOptional(mbKey) => mbKey.map(extractKeyWithMaintainers)
case v => crash(s"Expected optional key with maintainers, got: $v")
}
val (coid, newPtx) = machine.ptx
.insertCreate(
coinst = V.ContractInst(template = templateId, arg = createArg, agreementText = agreement),
@ -845,13 +838,13 @@ object SBuiltin {
}
/** $beginExercise
* :: arg (choice argument)
* -> ContractId arg (contract to exercise)
* -> List Party (actors)
* -> List Party (signatories)
* -> List Party (observers)
* -> List Party (choice controllers)
* -> Optional key (template key)
* :: arg (choice argument)
* -> ContractId arg (contract to exercise)
* -> List Party (actors)
* -> List Party (signatories)
* -> List Party (observers)
* -> List Party (choice controllers)
* -> Optional {key: key, maintainers: List Party} (template key, if present)
* -> Token
* -> ()
*/
@ -875,9 +868,10 @@ object SBuiltin {
val sigs = extractParties(args.get(3))
val obs = extractParties(args.get(4))
val ctrls = extractParties(args.get(5))
val mbKey = args.get(6) match {
case SOptional(mbKey) => mbKey.map(_.toValue)
case _ => crash("Bad key, expected optional")
case SOptional(mbKey) => mbKey.map(extractKeyWithMaintainers)
case v => crash(s"Expected optional key with maintainers, got: $v")
}
machine.ptx = machine.ptx
@ -891,16 +885,7 @@ object SBuiltin {
signatories = sigs,
stakeholders = sigs union obs,
controllers = ctrls,
mbKey = mbKey.map { k =>
asVersionedValue(k) match {
case Left(err) => crash(err)
case Right(x) =>
x.mapContractId {
case RelativeContractId(rcoid) => crash(s"got relative contract id $rcoid in key")
case coid: AbsoluteContractId => coid
}
}
},
mbKey = mbKey,
chosenValue = asVersionedValue(arg) match {
case Left(err) => crash(err)
case Right(x) => x
@ -1023,23 +1008,16 @@ object SBuiltin {
}
/** $lookupKey[T]
* :: key
* -> List Party (maintainers)
* :: { key: key, maintainers: List Party }
* -> Token
* -> Maybe (ContractId T)
*/
final case class SBULookupKey(templateId: TypeConName) extends SBuiltin(3) {
final case class SBULookupKey(templateId: TypeConName) extends SBuiltin(2) {
def execute(args: util.ArrayList[SValue], machine: Machine): Unit = {
checkToken(args.get(2))
val key = asVersionedValue(args.get(0).toValue.mapContractId[Nothing] { cid =>
crash(s"Unexpected contract id in key: $cid")
}) match {
case Left(err) => crash(err)
case Right(x) => x
}
val maintainers = extractParties(args.get(1))
checkLookupMaintainers(templateId, machine, maintainers)
val gkey = GlobalKey(templateId, key)
checkToken(args.get(1))
val keyWithMaintainers = extractKeyWithMaintainers(args.get(0))
checkLookupMaintainers(templateId, machine, keyWithMaintainers.maintainers)
val gkey = GlobalKey(templateId, keyWithMaintainers.key)
// check if we find it locally
machine.ptx.keys.get(gkey) match {
case Some(mbCoid) =>
@ -1068,26 +1046,16 @@ object SBuiltin {
}
/** $insertLookup[T]
* :: key
* -> List Party (maintainers)
* :: { key : key, maintainers: List Party}
* -> Maybe (ContractId T)
* -> Token
* -> ()
*/
final case class SBUInsertLookupNode(templateId: TypeConName) extends SBuiltin(4) {
final case class SBUInsertLookupNode(templateId: TypeConName) extends SBuiltin(3) {
def execute(args: util.ArrayList[SValue], machine: Machine): Unit = {
checkToken(args.get(3))
val key =
asVersionedValue(
args
.get(0)
.toValue
.mapContractId(coid => crash(s"Unexpected contract id in key: $coid"))) match {
case Left(err) => crash(err)
case Right(v) => v
}
val maintainers = extractParties(args.get(1))
val mbCoid = args.get(2) match {
checkToken(args.get(2))
val keyWithMaintainers = extractKeyWithMaintainers(args.get(0))
val mbCoid = args.get(1) match {
case SOptional(mb) =>
mb.map {
case SContractId(coid) => coid
@ -1098,7 +1066,9 @@ object SBuiltin {
machine.ptx = machine.ptx.insertLookup(
templateId,
machine.lastLocation,
KeyWithMaintainers(key = key, maintainers = maintainers),
KeyWithMaintainers(
key = keyWithMaintainers.key,
maintainers = keyWithMaintainers.maintainers),
mbCoid)
machine.ctrl = CtrlValue.Unit
checkAborted(machine.ptx)
@ -1106,23 +1076,16 @@ object SBuiltin {
}
/** $fetchKey[T]
* :: key
* -> List Party (maintainers)
* :: { key: key, maintainers: List Party }
* -> Token
* -> ContractId T
*/
final case class SBUFetchKey(templateId: TypeConName) extends SBuiltin(3) {
final case class SBUFetchKey(templateId: TypeConName) extends SBuiltin(2) {
def execute(args: util.ArrayList[SValue], machine: Machine): Unit = {
checkToken(args.get(2))
val key = asVersionedValue(args.get(0).toValue.mapContractId[Nothing] { cid =>
crash(s"Unexpected contract id in key: $cid")
}) match {
case Left(err) => crash(err)
case Right(x) => x
}
val maintainers = extractParties(args.get(1))
checkLookupMaintainers(templateId, machine, maintainers)
val gkey = GlobalKey(templateId, key)
checkToken(args.get(1))
val keyWithMaintainers = extractKeyWithMaintainers(args.get(0))
checkLookupMaintainers(templateId, machine, keyWithMaintainers.maintainers)
val gkey = GlobalKey(templateId, keyWithMaintainers.key)
// check if we find it locally
machine.ptx.keys.get(gkey) match {
case Some(None) =>
@ -1524,6 +1487,19 @@ object SBuiltin {
crash(s"value not a list of parties or party: $v")
}
private def extractKeyWithMaintainers(v: SValue): KeyWithMaintainers[Value[Nothing]] = v match {
case SStruct(flds, vals)
if flds.length == 2 && flds(0) == keyFieldName && flds(1) == maintainersFieldName =>
asVersionedValue(vals.get(0).toValue) match {
case Left(err) => crash(err)
case Right(keyVal) =>
val keyWithoutContractIds =
keyVal.mapContractId(coid => crash(s"Unexpected contract id in key: $coid"))
KeyWithMaintainers(key = keyWithoutContractIds, maintainers = extractParties(vals.get(1)))
}
case _ => crash(s"Invalid key with maintainers: $v")
}
private def checkLookupMaintainers(
templateId: Identifier,
machine: Machine,

View File

@ -59,12 +59,8 @@ object Ledger {
}
@inline
def assertAbsoluteContractId(cid: ContractId): AbsoluteContractId =
cid match {
case acoid: AbsoluteContractId => acoid
case _: RelativeContractId =>
crash("Unexpected relative contract id '$cid'")
}
def assertNoContractId(cid: ContractId): Nothing =
crash(s"Not expecting to find a contract id here, but found '$cid'")
private val `:` = LedgerString.assertFromString(":")
@ -220,7 +216,7 @@ object Ledger {
controllers = nex.controllers,
children = nex.children.map(ScenarioNodeId(commitPrefix, _)),
exerciseResult = nex.exerciseResult.map(makeAbsolute(commitPrefix, _)),
key = nex.key.map(_.mapContractId(assertAbsoluteContractId))
key = nex.key.map(_.mapValue(_.mapContractId(assertNoContractId)))
)
case nlbk: NodeLookupByKey.WithTxValue[ContractId] =>
NodeLookupByKey(
@ -1136,7 +1132,9 @@ object Ledger {
val mbNewCache2 = nc.key match {
case None => Right(newCache1)
case Some(keyWithMaintainers) =>
val gk = GlobalKey(nc.coinst.template, keyWithMaintainers.key)
val gk = GlobalKey(
nc.coinst.template,
keyWithMaintainers.key.mapContractId(assertNoContractId))
newCache1.activeKeys.get(gk) match {
case None => Right(newCache1.addKey(gk, nc.coid))
case Some(_) => Left(UniqueKeyViolation(gk))
@ -1171,7 +1169,8 @@ object Ledger {
nc.key match {
case None => newCache0_1
case Some(key) =>
newCache0_1.removeKey(GlobalKey(ex.templateId, key.key))
newCache0_1.removeKey(
GlobalKey(ex.templateId, key.key.mapContractId(assertNoContractId)))
}
} else newCache0

View File

@ -4,7 +4,7 @@
DAML-LF Transaction Specification
=================================
**version 8, 26 June 2019**
**version 9, 13 January 2020**
This specification, in concert with the ``transaction.proto``
machine-readable definition, defines a format for _transactions_, to be
@ -161,6 +161,8 @@ This table lists every version of this specification in ascending order
+--------------------+-----------------+
| 8 | 2019-06-26 |
+--------------------+-----------------+
| 9 | 2020-01-13 |
+--------------------+-----------------+
message Transaction
^^^^^^^^^^^^^^^^^^^
@ -282,6 +284,9 @@ In this version, these fields are included:
``maintainers`` must be non-empty.
The key may not contain contract IDs.
message NodeCreate
^^^^^^^^^^^^^^^^^^
@ -501,7 +506,14 @@ Containing the result of the exercised choice.
*since version 8*
New optional field `contract_key` is now set when the exercised
contract has a contract key defined.
contract has a contract key defined. The key may not contain contract IDs.
*since version 9*
New optional field `key_with_maintainers` is now set when the exercised
contract has a contract key defined. The `contract_key` field is
not used any more.
message NodeLookupByKey
^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1 @@
Error: CRASH: Unexpected contract id in key: AbsoluteContractId(0:0)

View File

@ -0,0 +1,28 @@
-- Copyright (c) 2020 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
daml 1.2
module Test where
-- Test that values of contract keys may not contain contract IDs
template Simple
with
p: Party
where
signatory p
template KeyWithContractId
with
p: Party
k: ContractId Simple
where
signatory p
key (p, k): (Party, ContractId Simple)
maintainer key._1
run = scenario do
alice <- getParty "alice"
cid <- submit alice $ create Simple with p = alice
-- This should fail
submit alice $ create KeyWithContractId with p = alice, k = cid

View File

@ -338,6 +338,7 @@ object ValueGenerators {
.map(ImmArray(_))
exerciseResultValue <- versionedValueGen
key <- versionedValueGen
maintainers <- genNonEmptyParties
} yield
NodeExercises(
targetCoid,
@ -352,7 +353,7 @@ object ValueGenerators {
actingParties,
children,
Some(exerciseResultValue),
Some(key)
Some(KeyWithMaintainers(key, maintainers))
)
}

View File

@ -13,6 +13,7 @@
// * 6: removal of controllers in exercise nodes
// * 7: new field return_value in NodeExercise
// * 8: new field contract_key in NodeExercise
// * 9: new field key_maintainers in NodeExercise
syntax = "proto3";
package com.digitalasset.daml.lf.transaction;
@ -90,6 +91,7 @@ message NodeExercise {
com.digitalasset.daml.lf.value.ContractId contract_id_struct = 11;
com.digitalasset.daml.lf.value.VersionedValue return_value = 12;
com.digitalasset.daml.lf.value.VersionedValue contract_key = 13; // optional
KeyWithMaintainers key_with_maintainers = 14; // optional
}
message NodeLookupByKey {

View File

@ -4,7 +4,7 @@
package com.digitalasset.daml.lf.transaction
import com.digitalasset.daml.lf.data.{ImmArray, ScalazEqual}
import com.digitalasset.daml.lf.data.Ref._
import com.digitalasset.daml.lf.value.Value.{AbsoluteContractId, ContractInst, VersionedValue}
import com.digitalasset.daml.lf.value.Value.{ContractInst, VersionedValue}
import scala.language.higherKinds
import scalaz.Equal
@ -109,7 +109,7 @@ object Node {
controllers: Set[Party],
children: ImmArray[Nid],
exerciseResult: Option[Val],
key: Option[Val])
key: Option[KeyWithMaintainers[Val]])
extends GenNode[Nid, Cid, Val] {
override def mapContractIdAndValue[Cid2, Val2](
f: Cid => Cid2,
@ -118,7 +118,7 @@ object Node {
targetCoid = f(targetCoid),
chosenValue = g(chosenValue),
exerciseResult = exerciseResult.map(g),
key = key.map(g))
key = key.map(_.mapValue(g)))
override def mapNodeId[Nid2](f: Nid => Nid2): NodeExercises[Nid2, Cid, Val] =
copy(
@ -147,7 +147,7 @@ object Node {
signatories: Set[Party],
children: ImmArray[Nid],
exerciseResult: Option[Val],
key: Option[Val]): NodeExercises[Nid, Cid, Val] =
key: Option[KeyWithMaintainers[Val]]): NodeExercises[Nid, Cid, Val] =
NodeExercises(
targetCoid,
templateId,
@ -255,7 +255,7 @@ object Node {
/** Useful in various circumstances -- basically this is what a ledger implementation must use as
* a key.
*/
case class GlobalKey(templateId: Identifier, key: VersionedValue[AbsoluteContractId])
case class GlobalKey(templateId: Identifier, key: VersionedValue[Nothing])
sealed trait WithTxValue2[F[+ _, + _]] {
type WithTxValue[+Cid] = F[Cid, Transaction.Value[Cid]]

View File

@ -16,7 +16,6 @@ import scala.annotation.tailrec
import scala.annotation.unchecked.uncheckedVariance
import scala.collection.breakOut
import scala.collection.immutable.{SortedMap, TreeMap}
import scala.util.Try
case class VersionedTransaction[Nid, Cid](
version: TransactionVersion,
@ -455,7 +454,7 @@ object Transaction {
case class ExercisesContext(
targetId: TContractId,
templateId: TypeConName,
contractKey: Option[Value[TContractId]],
contractKey: Option[KeyWithMaintainers[Value[Nothing]]],
choiceId: ChoiceName,
optLocation: Option[Location],
consuming: Boolean,
@ -587,7 +586,7 @@ object Transaction {
optLocation: Option[Location],
signatories: Set[Party],
stakeholders: Set[Party],
key: Option[KeyWithMaintainers[Value[TContractId]]]
key: Option[KeyWithMaintainers[Value[Nothing]]]
): Either[String, (TContractId, PartialTransaction)] = {
val serializableErrs = serializable(coinst.arg)
if (serializableErrs.nonEmpty) {
@ -610,19 +609,9 @@ object Transaction {
// active keys
key match {
case None => Right((cid, ptx))
case Some(k) =>
// TODO is there a nicer way of doing this?
val mbNoRels =
Try(k.key.mapContractId {
case abs: AbsoluteContractId => abs
case rel: RelativeContractId =>
throw new RuntimeException(
s"Trying to create contract key with relative contract id $rel")
}).toEither.left.map(_.getMessage)
mbNoRels.map { noRels =>
val gk = GlobalKey(coinst.template, noRels)
(cid, ptx.copy(keys = ptx.keys.updated(gk, Some(cid))))
}
case Some(kWithM) =>
val ck = GlobalKey(coinst.template, kWithM.key)
Right((cid, ptx.copy(keys = ptx.keys.updated(ck, Some(cid)))))
}
}
}
@ -654,7 +643,7 @@ object Transaction {
def insertLookup(
templateId: TypeConName,
optLocation: Option[Location],
key: KeyWithMaintainers[Value.VersionedValue[Nothing]],
key: KeyWithMaintainers[Value[Nothing]],
result: Option[TContractId]
): PartialTransaction =
insertLeafNode(_ => NodeLookupByKey(templateId, optLocation, key, result))._2
@ -669,7 +658,7 @@ object Transaction {
signatories: Set[Party],
stakeholders: Set[Party],
controllers: Set[Party],
mbKey: Option[Value[AbsoluteContractId]],
mbKey: Option[KeyWithMaintainers[Value[Nothing]]],
chosenValue: Value[TContractId]
): Either[String, PartialTransaction] = {
val serializableErrs = serializable(chosenValue)
@ -705,7 +694,8 @@ object Transaction {
// inactive as soon as you exercise it. therefore, mark it as consumed now.
consumedBy = if (consuming) consumedBy.updated(targetId, nextNodeId) else consumedBy,
keys = mbKey match {
case Some(key) if consuming => keys.updated(GlobalKey(templateId, key), None)
case Some(kWithM) if consuming =>
keys.updated(GlobalKey(templateId, kWithM.key), None)
case _ => keys
},
)

View File

@ -106,7 +106,8 @@ object TransactionCoder {
minKeyOrLookupByKey,
minNoControllers,
minExerciseResult,
minContractKeyInExercise
minContractKeyInExercise,
minMaintainersInExercise
}
node match {
case c: NodeCreate[Cid, Val] =>
@ -193,16 +194,22 @@ object TransactionCoder {
s"Trying to encode transaction of version $transactionVersion, which requires the exercise return value, but did not get exercise return value in node."))
case (_, true) => Right(())
}
_ <- (e.key, transactionVersion precedes minContractKeyInExercise) match {
case (Some(k), false) =>
encodeVal(k).map { encodedKey =>
exBuilder.setContractKey(encodedKey._2)
_ <- Right(
e.key
.map { kWithM =>
if (transactionVersion precedes minContractKeyInExercise) ()
else if (transactionVersion precedes minMaintainersInExercise) {
encodeVal(kWithM.key).map { encodedKey =>
exBuilder.setContractKey(encodedKey._2)
}
} else
encodeKeyWithMaintainers(encodeVal, kWithM).map {
case (_, encodedKey) =>
exBuilder.setKeyWithMaintainers(encodedKey)
}
()
}
case (None, _) | (Some(_), true) =>
Right(())
}
.getOrElse(()))
} yield nodeBuilder.setExercise(exBuilder).build()
case nlbk: NodeLookupByKey[Cid, Val] =>
@ -232,9 +239,9 @@ object TransactionCoder {
keyWithMaintainers: TransactionOuterClass.KeyWithMaintainers)
: Either[DecodeError, KeyWithMaintainers[Val]] =
for {
mainteners <- toPartySet(keyWithMaintainers.getMaintainersList)
maintainers <- toPartySet(keyWithMaintainers.getMaintainersList)
key <- decodeVal(keyWithMaintainers.getKey())
} yield KeyWithMaintainers(key, mainteners)
} yield KeyWithMaintainers(key, maintainers)
/**
* read a [[GenNode[Nid, Cid]] from protobuf
@ -258,7 +265,8 @@ object TransactionCoder {
minKeyOrLookupByKey,
minNoControllers,
minExerciseResult,
minContractKeyInExercise
minContractKeyInExercise,
minMaintainersInExercise
}
protoNode.getNodeTypeCase match {
case NodeTypeCase.CREATE =>
@ -310,11 +318,23 @@ object TransactionCoder {
Left(DecodeError(txVersion, isTooOldFor = "exercise result"))
else Right(None)
} else decodeVal(protoExe.getReturnValue).map(Some(_))
contractKey <- if (protoExe.hasContractKey) {
hasKeyWithMaintainersField = (protoExe.getKeyWithMaintainers != TransactionOuterClass.KeyWithMaintainers.getDefaultInstance)
keyWithMaintainers <- if (protoExe.hasContractKey) {
if (txVersion precedes minContractKeyInExercise)
Left(DecodeError(txVersion, isTooOldFor = "contract key in exercise"))
else if (!(txVersion precedes minMaintainersInExercise))
Left(DecodeError(
s"contract key field in exercise must not be present for transactions of version $txVersion"))
else if (hasKeyWithMaintainersField)
Left(DecodeError(
"an exercise may not contain both contract key and contract key with maintainers"))
else
decodeVal(protoExe.getContractKey).map(Some(_))
decodeVal(protoExe.getContractKey).map(k => Some(KeyWithMaintainers(k, Set.empty)))
} else if (hasKeyWithMaintainersField) {
if (txVersion precedes minMaintainersInExercise)
Left(DecodeError(txVersion, isTooOldFor = "NodeExercises maintainers"))
else
decodeKeyWithMaintainers(decodeVal, protoExe.getKeyWithMaintainers).map(k => Some(k))
} else Right(None)
ni <- nodeId
@ -354,7 +374,7 @@ object TransactionCoder {
controllers = controllers,
children = children,
exerciseResult = rv,
key = contractKey
key = keyWithMaintainers
))
case NodeTypeCase.LOOKUP_BY_KEY =>
val protoLookupByKey = protoNode.getLookupByKey

View File

@ -4,6 +4,7 @@
package com.digitalasset.daml.lf
package transaction
import com.digitalasset.daml.lf.transaction.Node.KeyWithMaintainers
import com.digitalasset.daml.lf.value.Value.VersionedValue
import com.digitalasset.daml.lf.value.ValueVersion
@ -22,6 +23,7 @@ object TransactionVersions
private[transaction] val minNoControllers = TransactionVersion("6")
private[transaction] val minExerciseResult = TransactionVersion("7")
private[transaction] val minContractKeyInExercise = TransactionVersion("8")
private[transaction] val minMaintainersInExercise = TransactionVersion("9")
def assignVersion(a: GenTransaction[_, _, _ <: VersionedValue[_]]): TransactionVersion = {
require(a != null)
@ -60,6 +62,17 @@ object TransactionVersions
})
minContractKeyInExercise
else minVersion,
if (a.nodes.values
.exists {
case ne: Node.NodeExercises[_, _, _] =>
ne.key match {
case Some(KeyWithMaintainers(key @ _, maintainers)) => maintainers.nonEmpty
case _ => false
}
case _ => false
})
minMaintainersInExercise
else minVersion,
)
}
}

View File

@ -55,6 +55,7 @@ private[digitalasset] object VersionTimeline {
This(That(TransactionVersion("8"))),
Both(This(ValueVersion("5")), LanguageVersion(LMV.V1, "6")),
Both(This(ValueVersion("6")), LanguageVersion(LMV.V1, "7")),
This(That(TransactionVersion("9"))),
// FIXME https://github.com/digital-asset/daml/issues/2256
// * change the following line when LF 1.8 is frozen.
// * do not insert line after this once until 1.8 is frozen.

View File

@ -301,6 +301,13 @@ class TransactionCoderSpec
case _ => gn
}
def withoutMaintainersInExercise[Nid, Cid, Val](
gn: GenNode[Nid, Cid, Val]): GenNode[Nid, Cid, Val] =
gn match {
case ne: NodeExercises[Nid, Cid, Val] =>
ne copy (key = ne.key.map(_.copy(maintainers = Set.empty)))
case _ => gn
}
def transactionWithout[Nid: Ordering, Cid, Val](
t: GenTransaction[Nid, Cid, Val],
f: GenNode[Nid, Cid, Val] => GenNode[Nid, Cid, Val]): GenTransaction[Nid, Cid, Val] =
@ -308,13 +315,17 @@ class TransactionCoderSpec
def minimalistTx[Nid: Ordering, Cid, Val](
txvMin: TransactionVersion,
tx: GenTransaction[Nid, Cid, Val]): GenTransaction[Nid, Cid, Val] =
if (txvMin precedes minExerciseResult)
transactionWithout(
tx,
(x: GenNode[Nid, Cid, Val]) => withoutExerciseResult(withoutContractKeyInExercise(x)))
else if (txvMin precedes minContractKeyInExercise)
transactionWithout(tx, (x: GenNode[Nid, Cid, Val]) => withoutContractKeyInExercise(x))
else tx
tx: GenTransaction[Nid, Cid, Val]): GenTransaction[Nid, Cid, Val] = {
def condApply(before: TransactionVersion, f: GenNode[Nid, Cid, Val] => GenNode[Nid, Cid, Val])
: GenNode[Nid, Cid, Val] => GenNode[Nid, Cid, Val] =
if (txvMin precedes before) f else identity
transactionWithout(
tx,
condApply(minMaintainersInExercise, withoutMaintainersInExercise)
.compose(condApply(minContractKeyInExercise, withoutContractKeyInExercise))
.compose(condApply(minExerciseResult, withoutExerciseResult))
)
}
}

View File

@ -19,7 +19,7 @@ Here's an example of setting up a contract key for a bank account, to act as a b
What can be a contract key
**************************
The key can be an arbitrary expression but it **must** include every party that you want to use as a ``maintainer`` (see `Specifying maintainers`_ below).
The key can be an arbitrary expression that does **not** contain contract IDs. However, it **must** include every party that you want to use as a ``maintainer`` (see `Specifying maintainers`_ below).
It's best to use simple types for your keys like ``Text`` or ``Int``, rather than a list or more complex type.

View File

@ -121,7 +121,7 @@ private[state] object Conversions {
throw Err
.DecodeError("ContractKey", s"Cannot decode template id: ${key.getTemplateId}")
),
forceAbsoluteContractIds(
forceNoContractIds(
valDecoder(key.getKey)
.fold(
err =>
@ -264,6 +264,10 @@ private[state] object Conversions {
case acoid: AbsoluteContractId => acoid
}
def forceNoContractIds(v: VersionedValue[ContractId]): VersionedValue[Nothing] =
v.mapContractId(coid =>
throw Err.InternalError(s"Contract identifier encountered in contract key! $coid"))
def contractIdStructOrStringToStateKey(
entryId: DamlLogEntryId,
coidString: String,

View File

@ -84,7 +84,7 @@ private[kvutils] object InputsAndEffects {
case create: NodeCreate[ContractId, VersionedValue[ContractId]] =>
create.key.fold(inputs) { keyWithM =>
inputs + contractKeyToStateKey(
GlobalKey(create.coinst.template, forceAbsoluteContractIds(keyWithM.key)))
GlobalKey(create.coinst.template, forceNoContractIds(keyWithM.key)))
} ++ partyInputs(create.signatories) ++ partyInputs(create.stakeholders)
case exe: NodeExercises[_, ContractId, _] =>
inputs ++ contractInputs(exe.targetCoid) ++ partyInputs(exe.stakeholders) ++ partyInputs(
@ -93,7 +93,7 @@ private[kvutils] object InputsAndEffects {
// We need both the contract key state and the contract state. The latter is used to verify
// that the submitter can access the contract.
l.result.fold(inputs)(inputs ++ contractInputs(_)) +
contractKeyToStateKey(GlobalKey(l.templateId, forceAbsoluteContractIds(l.key.key)))
contractKeyToStateKey(GlobalKey(l.templateId, forceNoContractIds(l.key.key)))
}
}
.toList
@ -120,7 +120,7 @@ private[kvutils] object InputsAndEffects {
(contractKeyToStateKey(
GlobalKey(
create.coinst.template,
forceAbsoluteContractIds(keyWithMaintainers.key))) ->
forceNoContractIds(keyWithMaintainers.key))) ->
DamlContractKeyState.newBuilder
.setContractId(encodeRelativeContractId(
entryId,
@ -143,7 +143,7 @@ private[kvutils] object InputsAndEffects {
key =>
effects.updatedContractKeys +
(contractKeyToStateKey(
GlobalKey(exe.templateId, forceAbsoluteContractIds(key))) ->
GlobalKey(exe.templateId, forceNoContractIds(key.key))) ->
DamlContractKeyState.newBuilder.build)
)
)

View File

@ -202,13 +202,13 @@ object KeyValueCommitting {
case TransactionOuterClass.Node.NodeTypeCase.EXERCISE =>
val exe = node.getExercise
val ckeyOrEmpty =
if (exe.getConsuming && exe.hasContractKey)
if (exe.getConsuming && exe.hasKeyWithMaintainers)
List(
DamlStateKey.newBuilder
.setContractKey(
DamlContractKey.newBuilder
.setTemplateId(exe.getTemplateId)
.setKey(exe.getContractKey))
.setKey(exe.getKeyWithMaintainers.getKey))
.build)
else
List.empty

View File

@ -185,7 +185,7 @@ private[kvutils] case class ProcessTransactionSubmission(
(_nodeId, exe: NodeExercises[_, _, VersionedValue[ContractId]]))
if exe.key.isDefined && exe.consuming =>
val stateKey = Conversions.contractKeyToStateKey(
GlobalKey(exe.templateId, Conversions.forceAbsoluteContractIds(exe.key.get)))
GlobalKey(exe.templateId, Conversions.forceNoContractIds(exe.key.get.key)))
(allUnique, existingKeys - stateKey)
case (
@ -193,9 +193,7 @@ private[kvutils] case class ProcessTransactionSubmission(
(_nodeId, create: NodeCreate[_, VersionedValue[ContractId]]))
if create.key.isDefined =>
val stateKey = Conversions.contractKeyToStateKey(
GlobalKey(
create.coinst.template,
Conversions.forceAbsoluteContractIds(create.key.get.key)))
GlobalKey(create.coinst.template, Conversions.forceNoContractIds(create.key.get.key)))
(allUnique && !existingKeys.contains(stateKey), existingKeys + stateKey)
@ -242,7 +240,7 @@ private[kvutils] case class ProcessTransactionSubmission(
Conversions.encodeContractKey(
GlobalKey(
createNode.coinst.template,
Conversions.forceAbsoluteContractIds(keyWithMaintainers.key)
Conversions.forceNoContractIds(keyWithMaintainers.key)
)
))
}

View File

@ -116,7 +116,7 @@ object ActiveLedgerState {
contract: ContractInst[VersionedValue[AbsoluteContractId]],
witnesses: Set[Party],
divulgences: Map[Party, TransactionIdString], // for each party, the transaction id at which the contract was divulged
key: Option[KeyWithMaintainers[VersionedValue[AbsoluteContractId]]],
key: Option[KeyWithMaintainers[VersionedValue[Nothing]]],
signatories: Set[Party],
observers: Set[Party],
agreementText: String)

View File

@ -160,7 +160,8 @@ class ActiveLedgerStateManager[ALS](initialState: => ALS)(
.getOrElse(nodeId, Set.empty) diff nc.stakeholders).toList
.map(p => p -> transactionId)
.toMap,
key = nc.key,
key = nc.key.map(_.mapValue(_.mapContractId(coid =>
throw new IllegalStateException(s"Contract ID $coid found in contract key")))),
signatories = nc.signatories,
observers = nc.stakeholders.diff(nc.signatories),
agreementText = nc.coinst.agreementText
@ -202,7 +203,9 @@ class ActiveLedgerStateManager[ALS](initialState: => ALS)(
)
case nlkup: N.NodeLookupByKey.WithTxValue[AbsoluteContractId] =>
// Check that the stored lookup result matches the current result
val gk = GlobalKey(nlkup.templateId, nlkup.key.key)
val key = nlkup.key.key.mapContractId(coid =>
throw new IllegalStateException(s"Contract ID $coid found in contract key"))
val gk = GlobalKey(nlkup.templateId, key)
val nodeParties = nlkup.key.maintainers
submitter match {

View File

@ -1356,6 +1356,7 @@ private class JdbcLedgerDao(
val keyValue = valueSerializer
.deserializeValue(ByteStreams.toByteArray(keyStream))
.getOrElse(sys.error(s"failed to deserialize key value! cid:$coid"))
.mapContractId(coid => sys.error(s"Found contract ID $coid in a contract key"))
KeyWithMaintainers(keyValue, keyMaintainers)
}),
signatories,

View File

@ -61,6 +61,8 @@ class V3__Recompute_Key_Hash extends BaseJavaMigration {
val key = ValueSerializer
.deserializeValue(rows.getBytes("contract_key"))
.fold(err => throw new IllegalArgumentException(err.errorMessage), identity)
.mapContractId(coid =>
throw new IllegalArgumentException(s"Found contract ID $coid in contract key"))
hasNext = rows.next()

View File

@ -830,7 +830,10 @@ class JdbcLedgerDaoSpec
children = ImmArray.empty,
exerciseResult =
Some(VersionedValue(ValueVersions.acceptedVersions.head, ValueUnit)),
key = Some(VersionedValue(ValueVersions.acceptedVersions.head, ValueText(key)))
key = Some(
KeyWithMaintainers(
VersionedValue(ValueVersions.acceptedVersions.head, ValueText(key)),
Set(party)))
)),
ImmArray[EventId](s"event$id"),
None

View File

@ -25,7 +25,7 @@ class KeyHasherSpec extends WordSpec with Matchers {
)
private[this] def complexValue = {
val builder = ImmArray.newBuilder[(Option[Name], Value[AbsoluteContractId])]
val builder = ImmArray.newBuilder[(Option[Name], Value[Nothing])]
builder += None -> ValueInt64(0)
builder += None -> ValueInt64(123456)
builder += None -> ValueInt64(-1)