mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
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:
parent
ca012c3b54
commit
55c3e1cf45
@ -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 = [
|
||||
|
@ -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(_)
|
||||
|
@ -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 wasn’t 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 Canton’s 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,
|
||||
|
@ -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",
|
||||
|
235
daml-lf/tests/MultiKeys.daml
Normal file
235
daml-lf/tests/MultiKeys.daml
Normal 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'
|
Loading…
Reference in New Issue
Block a user