Test multi-keys behavior (#9472)

* Document and test multi-key semantics

Canton relies on the Engine working correctly even in a setting where
we do not have multiple keys. So far this worked by accident but the
semantics of this are rather unclear. To make things worse, Canton
upgrades rely on those semantics being stable so we really do care
about the choices we make here.

This PR adds a bunch of tests as an executable documentation of the
current behavior. However, we do not provide stability guarantees for
the current behavior and therefore these tests can be changed as
needed. But at least we are aware of those changes rather than doing
them by accident.

changelog_begin
changelog_end

* Update daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala

Co-authored-by: Sofia Faro <sofia.faro@digitalasset.com>

* Update daml-lf/tests/MultiKeys.daml

Co-authored-by: Sofia Faro <sofia.faro@digitalasset.com>

* Update daml-lf/tests/MultiKeys.daml

Co-authored-by: Sofia Faro <sofia.faro@digitalasset.com>

Co-authored-by: Sofia Faro <sofia.faro@digitalasset.com>
This commit is contained in:
Moritz Kiefer 2021-05-03 12:59:57 +02:00 committed by GitHub
parent ca012c3b54
commit 55c3e1cf45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 372 additions and 10 deletions

View File

@ -45,6 +45,7 @@ da_scala_test_suite(
data = [
"//daml-lf/tests:BasicTests.dar",
"//daml-lf/tests:Exceptions.dar",
"//daml-lf/tests:MultiKeys.dar",
"//daml-lf/tests:Optional.dar",
],
scala_deps = [

View File

@ -2177,6 +2177,112 @@ class EngineTest
}
}
// Note that we provide no stability for multi key semantics so
// these tests serve only as an indication of the current behavior
// but can be changed freely.
"multi keys" should {
val (multiKeysPkgId, _, allMultiKeysPkgs) = loadPackage("daml-lf/tests/MultiKeys.dar")
val lookupPackage = allMultiKeysPkgs.get(_)
val keyedId = Identifier(multiKeysPkgId, "MultiKeys:Keyed")
val opsId = Identifier(multiKeysPkgId, "MultiKeys:KeyOperations")
val let = Time.Timestamp.now()
val submissionSeed = hash("multikeys")
val seeding = Engine.initialSeeding(submissionSeed, participant, let)
val cid1 = toContractId("#1")
val cid2 = toContractId("#2")
val keyedInst = ContractInst(
TypeConName(multiKeysPkgId, "MultiKeys:Keyed"),
assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueParty(party))))),
"",
)
val contracts = Map(cid1 -> keyedInst, cid2 -> keyedInst)
val lookupContract = contracts.get(_)
def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] =
(key.globalKey.templateId, key.globalKey.key) match {
case (
`keyedId`,
ValueParty(`party`),
) =>
Some(cid1)
case _ =>
None
}
def run(choice: String, argument: Value[Value.ContractId]) = {
val cmd = CreateAndExerciseCommand(
opsId,
ValueRecord(None, ImmArray((None, ValueParty(party)))),
choice,
argument,
)
val Right((cmds, globalCids)) = preprocessor
.preprocessCommands(ImmArray(cmd))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
engine
.interpretCommands(
validating = false,
submitters = Set(party),
commands = cmds,
ledgerTime = let,
submissionTime = let,
seeding = seeding,
globalCids = globalCids,
)
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
}
val emptyRecord = ValueRecord(None, ImmArray.empty)
// The cid returned by a fetchByKey at the beginning
val keyResultCid = ValueRecord(None, ImmArray((None, ValueContractId(cid1))))
// The cid not returned by a fetchByKey at the beginning
val nonKeyResultCid = ValueRecord(None, ImmArray((None, ValueContractId(cid2))))
val twoCids =
ValueRecord(None, ImmArray((None, ValueContractId(cid1)), (None, ValueContractId(cid2))))
val createOverwritesLocal = ("CreateOverwritesLocal", emptyRecord)
val createOverwritesUnknownGlobal = ("CreateOverwritesUnknownGlobal", emptyRecord)
val createOverwritesKnownGlobal = ("CreateOverwritesKnownGlobal", emptyRecord)
val fetchDoesNotOverwriteGlobal = ("FetchDoesNotOverwriteGlobal", nonKeyResultCid)
val fetchDoesNotOverwriteLocal = ("FetchDoesNotOverwriteLocal", keyResultCid)
val localArchiveOverwritesUnknownGlobal = ("LocalArchiveOverwritesUnknownGlobal", emptyRecord)
val localArchiveOverwritesKnownGlobal = ("LocalArchiveOverwritesKnownGlobal", emptyRecord)
val globalArchiveOverwritesUnknownGlobal = ("GlobalArchiveOverwritesUnknownGlobal", twoCids)
val globalArchiveOverwritesKnownGlobal1 = ("GlobalArchiveOverwritesKnownGlobal1", twoCids)
val globalArchiveOverwritesKnownGlobal2 = ("GlobalArchiveOverwritesKnownGlobal2", twoCids)
val rollbackCreateNonRollbackFetchByKey = ("RollbackCreateNonRollbackFetchByKey", emptyRecord)
val rollbackFetchByKeyNonRollbackCreate = ("RollbackFetchByKeyNonRollbackCreate", emptyRecord)
val rollbackFetchNonRollbackCreate = ("RollbackFetchNonRollbackCreate", keyResultCid)
val rollbackGlobalArchiveNonRollbackCreate =
("RollbackGlobalArchiveNonRollbackCreate", keyResultCid)
val rollbackCreateNonRollbackGlobalArchive =
("RollbackCreateNonRollbackGlobalArchive", keyResultCid)
val rollbackGlobalArchiveUpdates =
("RollbackGlobalArchiveUpdates", twoCids)
"non-uck mode" in {
val allCases = Table(
("choice", "argument"),
createOverwritesLocal,
createOverwritesUnknownGlobal,
createOverwritesKnownGlobal,
fetchDoesNotOverwriteGlobal,
fetchDoesNotOverwriteLocal,
localArchiveOverwritesUnknownGlobal,
localArchiveOverwritesKnownGlobal,
globalArchiveOverwritesUnknownGlobal,
globalArchiveOverwritesKnownGlobal1,
globalArchiveOverwritesKnownGlobal2,
rollbackCreateNonRollbackFetchByKey,
rollbackFetchByKeyNonRollbackCreate,
rollbackFetchNonRollbackCreate,
rollbackGlobalArchiveNonRollbackCreate,
rollbackCreateNonRollbackGlobalArchive,
rollbackGlobalArchiveUpdates,
)
forEvery(allCases) { case (name, arg) =>
run(name, arg) shouldBe a[Right[_, _]]
}
}
}
"exceptions" should {
val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar")
val lookupPackage = allExceptionsPkgs.get(_)

View File

@ -180,16 +180,27 @@ private[lf] object PartialTransaction {
* the transaction was in when aborted. It is up to
* the caller to check for 'isAborted' after every
* change to a transaction.
* @param keys A local store of the contract keys. Note that this contains
* info both about relative and contract ids. We must
* do this because contract ids can be archived as
* part of execution, and we must record these archivals locally.
* Note: it is important for keys that we know to not be present
* to be present as [[None]]. The reason for this is that we must
* record the "no key" information for contract ids that
* we archive. This is not an optimization and is required for
* correct semantics, since otherwise lookups for keys for
* locally archived contract ids will succeed wrongly.
* @param keys A local store of the contract keys used for lookups and fetches by keys
* (including exercise by key). Each of those operations will be resolved
* against this map first. Only if there is no entry in here
* (but not if there is an entry mapped to None), will we ask the ledger.
*
* This map is mutated by the following operations:
* 1. fetch-by-key/lookup-by-key/exercise-by-key will insert an
* an entry in the map if there wasnt already one (i.e., if they queried the ledger).
* 2. ACS mutating operations if the corresponding contract has a key. Specifically,
* 2.1. A create will set the corresponding map entry to Some(cid) if the contract has a key.
* 2.2. A consuming choice will set the corresponding map entry to None if the contract has a key.
*
* On a rollback, we restore the state at the beginning of the rollback. Note that
* This means that at the moment, we will query (by-key) for a global contract again even if
* we queried it in a rollback node already.
* TODO (MK) This should be fixed for performance and to ensure that the engine
* never queries a key more than once thereby giving us consistency within a transaction.
*
* Note that the engine is also used in Cantons non-uck (unique contract key) mode.
* In that mode, duplicate keys should not be an error. We provide no stability
* guarantees for this mode at this point so tests can be changed freely.
*/
private[lf] case class PartialTransaction(
packageToTransactionVersion: Ref.PackageId => TxVersion,

View File

@ -40,6 +40,15 @@ daml_compile(
visibility = ["//daml-lf:__subpackages__"],
)
daml_compile(
name = "MultiKeys",
srcs = ["MultiKeys.daml"],
# TODO https://github.com/digital-asset/daml/issues/8020
# Switch to a stable LF version.
target = "1.dev",
visibility = ["//daml-lf:__subpackages__"],
)
[
sh_test(
name = name + "-test",

View File

@ -0,0 +1,235 @@
-- Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module MultiKeys where
import DA.Assert
import DA.Exception (throw)
import DA.Optional
exception E
where
message "E"
template Keyed
with
p : Party
where
signatory p
key p : Party
maintainer key
-- All these tests operate under the assumption that there
-- exists two global contracts with the same key `p` and one
-- of those will always be returned by fetch/lookup-by-key at
-- the beginning of the choice.
-- All choices here are accepted in non-uck (unique contract key) mode.
-- Behavior in uck-mode is documented in comments.
-- Note that uck-mode here refers to the uck mode in the engine
-- (not yet implemented, TODO(MK) remove this comment once it is) which only detects
-- a subset of unique key errors.
-- Note: At the moment, we do not provide any stability guarantees for
-- non-uck at this point so these tests serve only as an indication of the
-- current behavior and can be changed freely.
template KeyOperations
with
p : Party
where
signatory p
-- should be rejected in uck
nonconsuming choice CreateOverwritesLocal : ()
controller p
do cid1 <- create (Keyed p)
cid2 <- create (Keyed p)
(cid, _) <- fetchByKey @Keyed p
cid2 === cid
-- should be rejected in uck
nonconsuming choice CreateOverwritesUnknownGlobal : ()
controller p
do -- assume the global exists but do not fetch it
cid1 <- create (Keyed p)
(cid, _) <- fetchByKey @Keyed p
cid1 === cid
-- should be rejected in uck
nonconsuming choice CreateOverwritesKnownGlobal : ()
controller p
do _ <- fetchByKey @Keyed p
cid1 <- create (Keyed p)
(cid, _) <- fetchByKey @Keyed p
cid1 === cid
-- should be accepted in uck, cid not fetched by key
nonconsuming choice FetchDoesNotOverwriteGlobal : ()
with
cid : ContractId Keyed
controller p
do (cidByKey, _) <- fetchByKey @Keyed p
cid =/= cidByKey -- sanity-check the choice argument
c <- fetch cid
key c === p
(cidByKey', _) <- fetchByKey @Keyed p
cidByKey' === cidByKey
-- should be accepted in uck, cid not fetched by key
nonconsuming choice FetchDoesNotOverwriteLocal : ()
with
cid : ContractId Keyed
controller p
do local <- create (Keyed p)
c <- fetch cid
key c === p
(cid', _) <- fetchByKey @Keyed p
local === cid'
-- should be accepted in uck
nonconsuming choice LocalArchiveOverwritesUnknownGlobal : ()
controller p
do local <- create (Keyed p)
archive local
None <- lookupByKey @Keyed p
pure ()
-- should be rejected in uck
nonconsuming choice LocalArchiveOverwritesKnownGlobal : ()
controller p
do _ <- fetchByKey @Keyed p
local <- create (Keyed p)
archive local
None <- lookupByKey @Keyed p
pure ()
-- Should be accepted in uck
nonconsuming choice GlobalArchiveOverwritesUnknownGlobal : ()
with
cid1 : ContractId Keyed
cid2 : ContractId Keyed
controller p
do cid1 =/= cid2
c1 <- fetch cid1
key c1 === p
c2 <- fetch cid2
key c2 === p
archive cid1
None <- lookupByKey @Keyed p
pure ()
-- should be accepted in uck
nonconsuming choice GlobalArchiveOverwritesKnownGlobal1 : ()
with
cid1 : ContractId Keyed
cid2 : ContractId Keyed
controller p
do cid1 =/= cid2
c1 <- fetch cid1
key c1 === p
c2 <- fetch cid2
key c2 === p
(keyCid, _) <- fetchByKey @Keyed p
assert (keyCid `elem` [cid1, cid2])
archive (fromSome (find (/= keyCid) [cid1, cid2]))
None <- lookupByKey @Keyed p
pure ()
-- should be accepted in uck
nonconsuming choice GlobalArchiveOverwritesKnownGlobal2 : ()
with
cid1 : ContractId Keyed
cid2 : ContractId Keyed
controller p
do cid1 =/= cid2
c1 <- fetch cid1
key c1 === p
c2 <- fetch cid2
key c2 === p
(keyCid, _) <- fetchByKey @Keyed p
assert (keyCid `elem` [cid1, cid2])
archive keyCid
None <- lookupByKey @Keyed p
pure ()
-- should be rejected in uck
nonconsuming choice RollbackCreateNonRollbackFetchByKey : ()
controller p
do cid1 <- fetchByKey @Keyed p
try do
cid <- create (Keyed p)
(cid', _) <- fetchByKey @Keyed p
cid === cid'
throw E
catch E -> pure ()
cid2 <- fetchByKey @Keyed p
cid1 === cid2
-- should be rejected in uck
nonconsuming choice RollbackFetchByKeyNonRollbackCreate : ()
controller p
do try do
_ <- fetchByKey @Keyed p
throw E
catch E -> pure ()
cid <- create (Keyed p)
(cid', _) <- fetchByKey @Keyed p
cid === cid'
-- should be accepted in uck
nonconsuming choice RollbackFetchNonRollbackCreate : ()
with
cid : ContractId Keyed
controller p
do try do
c <- fetch cid
key c === p
throw E
catch E -> pure ()
cid <- create (Keyed p)
(cid', _) <- fetchByKey @Keyed p
cid === cid'
-- should be rejected in uck
nonconsuming choice RollbackGlobalArchiveNonRollbackCreate : ()
with
cid : ContractId Keyed
controller p
do try do
c <- fetch cid
key c === p
archive cid
throw E
catch E -> pure ()
cid <- create (Keyed p)
(cid', _) <- fetchByKey @Keyed p
cid === cid'
-- should be rejected in uck
nonconsuming choice RollbackCreateNonRollbackGlobalArchive : ()
with
cid : ContractId Keyed
controller p
do try do
create (Keyed p)
throw E
catch E -> pure ()
c <- fetch cid
key c === p
archive cid
None <- lookupByKey @Keyed p
pure ()
-- should be rejected in uck
nonconsuming choice RollbackGlobalArchiveUpdates : ()
with
cid1 : ContractId Keyed
cid2 : ContractId Keyed
controller p
do (cid', _) <- fetchByKey @Keyed p
cid1 === cid'
try do
archive cid2
throw E
catch
E -> pure ()
(cid', _) <- fetchByKey @Keyed p
cid1 === cid'