Test expected behaviour when superfluous (i.e. unused) contracts are disclosed in commands (#14617)

* Resolves #14350

CHANGELOG_BEGIN

* Engine/speedy-level tests for explicit disclosure (#14227): Test expected behaviour when superfluous (i.e. unused) contracts are disclosed in commands.

CHANGELOG_END
This commit is contained in:
Carl Pulley 2022-08-16 14:15:47 +01:00 committed by GitHub
parent 3ed252a9c1
commit 26b48133b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 464 additions and 201 deletions

View File

@ -111,7 +111,11 @@ class Engine(val config: EngineConfig = Engine.StableConfig) {
// TODO (drsk) remove this assertion once disclosed contracts feature becomes stable.
// https://github.com/digital-asset/daml/issues/13952.
assert(disclosures.isEmpty || config.allowedLanguageVersions.contains(LanguageVersion.v1_dev))
assert(
disclosures.isEmpty || config.allowedLanguageVersions.contains(
LanguageVersion.Features.explicitDisclosure
)
)
val submissionTime = cmds.ledgerEffectiveTime
for {
@ -462,6 +466,7 @@ class Engine(val config: EngineConfig = Engine.StableConfig) {
}
ResultDone((tx, meta))
}
case SResultFinal(_, None) =>
ResultError(
Error.Interpretation.Internal(

View File

@ -6,9 +6,8 @@ package engine
import java.io.File
import com.daml.lf.archive.UniversalArchiveDecoder
import com.daml.bazeltools.BazelRunfiles
import com.daml.lf.data.Ref._
import com.daml.lf.data._
import com.daml.lf.data.{Ref, _}
import com.daml.lf.language.Ast._
import com.daml.lf.language.Util._
import com.daml.lf.transaction.{
@ -16,27 +15,34 @@ import com.daml.lf.transaction.{
GlobalKeyWithMaintainers,
Node,
NodeId,
Normalization,
ReplayMismatch,
SubmittedTransaction,
Validation,
VersionedTransaction,
Transaction => Tx,
TransactionVersion => TxVersions,
}
import com.daml.lf.transaction.{Normalization, Validation, ReplayMismatch}
import com.daml.lf.value.Value
import Value._
import com.daml.lf.speedy.{ArrayList, InitialSeeding, SValue, svalue}
import com.daml.bazeltools.BazelRunfiles.rlocation
import com.daml.lf
import com.daml.lf.speedy.{ArrayList, DisclosedContract, InitialSeeding, SValue, svalue}
import com.daml.lf.speedy.SValue._
import com.daml.lf.command._
import com.daml.lf.crypto.Hash
import com.daml.lf.engine.Error.Interpretation
import com.daml.lf.engine.Error.Interpretation.DamlException
import com.daml.lf.language.PackageInterface
import com.daml.lf.transaction.test.TransactionBuilder.assertAsVersionedContract
import com.daml.logging.LoggingContext
import org.scalactic.Equality
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.EitherValues
import org.scalatest.{Assertion, EitherValues}
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside._
import org.scalatest.matchers.{MatchResult, Matcher}
import scala.collection.immutable.HashMap
import scala.language.implicitConversions
@ -52,104 +58,10 @@ class EngineTest
extends AnyWordSpec
with Matchers
with TableDrivenPropertyChecks
with EitherValues
with BazelRunfiles {
with EitherValues {
import EngineTest._
private def loadPackage(resource: String): (PackageId, Package, Map[PackageId, Package]) = {
val packages = UniversalArchiveDecoder.assertReadFile(new File(rlocation(resource)))
val (mainPkgId, mainPkg) = packages.main
(mainPkgId, mainPkg, packages.all.toMap)
}
private val (basicTestsPkgId, basicTestsPkg, allPackages) = loadPackage(
"daml-lf/tests/BasicTests.dar"
)
val basicTestsSignatures = language.PackageInterface(Map(basicTestsPkgId -> basicTestsPkg))
val withKeyTemplate = "BasicTests:WithKey"
val BasicTests_WithKey = Identifier(basicTestsPkgId, withKeyTemplate)
val withKeyContractInst: VersionedContractInstance =
assertAsVersionedContract(
ContractInstance(
TypeConName(basicTestsPkgId, withKeyTemplate),
ValueRecord(
Some(BasicTests_WithKey),
ImmArray(
(Some[Ref.Name]("p"), ValueParty(alice)),
(Some[Ref.Name]("k"), ValueInt64(42)),
),
),
"",
)
)
val defaultContracts: Map[ContractId, VersionedContractInstance] =
Map(
toContractId("BasicTests:Simple:1") ->
assertAsVersionedContract(
ContractInstance(
TypeConName(basicTestsPkgId, "BasicTests:Simple"),
ValueRecord(
Some(Identifier(basicTestsPkgId, "BasicTests:Simple")),
ImmArray((Some[Name]("p"), ValueParty(party))),
),
"",
)
),
toContractId("BasicTests:CallablePayout:1") ->
assertAsVersionedContract(
ContractInstance(
TypeConName(basicTestsPkgId, "BasicTests:CallablePayout"),
ValueRecord(
Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")),
ImmArray(
(Some[Ref.Name]("giver"), ValueParty(alice)),
(Some[Ref.Name]("receiver"), ValueParty(bob)),
),
),
"",
)
),
toContractId("BasicTests:WithKey:1") ->
withKeyContractInst,
)
val defaultKey = Map(
GlobalKey.assertBuild(
TypeConName(basicTestsPkgId, withKeyTemplate),
ValueRecord(None, ImmArray((None, ValueParty(alice)), (None, ValueInt64(42)))),
)
->
toContractId("BasicTests:WithKey:1")
)
private[this] val lookupContract = defaultContracts.get(_)
private[this] def lookupPackage(pkgId: PackageId): Option[Package] = {
allPackages.get(pkgId)
}
private[this] def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] =
(key.globalKey.templateId, key.globalKey.key) match {
case (
BasicTests_WithKey,
ValueRecord(_, ImmArray((_, ValueParty(`alice`)), (_, ValueInt64(42)))),
) =>
Some(toContractId("BasicTests:WithKey:1"))
case _ =>
None
}
private[this] val suffixLenientEngine = newEngine()
private[this] val suffixStrictEngine = newEngine(requireCidSuffixes = true)
private[this] val preprocessor =
new preprocessing.Preprocessor(
ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig)
)
"minimal create command" should {
val id = Identifier(basicTestsPkgId, "BasicTests:Simple")
val let = Time.Timestamp.now()
@ -666,7 +578,7 @@ class EngineTest
}
}
"fecth-by-key" should {
"fetch-by-key" should {
val seed = hash("fetch-by-key")
val now = Time.Timestamp.now()
@ -749,6 +661,7 @@ class EngineTest
)
}
}
"error if Speedy fails to find the key" in {
val templateId = Identifier(basicTestsPkgId, "BasicTests:FailedFetchByKey")
@ -795,6 +708,61 @@ class EngineTest
)
}
}
"unused disclosed contracts not saved to ledger" in {
val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey")
val usedContractSKey = SValue.SRecord(
templateId,
ImmArray("_1", "_2").map(Ref.Name.assertFromString),
values = ArrayList(SValue.SParty(alice), SValue.SInt64(42)),
)
val usedContractKey = usedContractSKey.toNormalizedValue(TxVersions.minExplicitDisclosure)
val unusedContractKey = Value.ValueRecord(
None,
ImmArray(
None -> Value.ValueParty(alice),
None -> Value.ValueInt64(69),
),
)
val usedDisclosedContract = DisclosedContract(
templateId,
SValue.SContractId(toContractId("BasicTests:WithKey:1")),
SValue.SRecord(
templateId,
ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")),
ArrayList(SValue.SParty(alice), SValue.SInt64(42)),
),
ContractMetadata(
now,
Some(crypto.Hash.assertHashContractKey(templateId, usedContractKey)),
ImmArray.empty,
),
)
val unusedDisclosedContract = DisclosedContract(
templateId,
SValue.SContractId(toContractId("BasicTests:WithKey:2")),
SValue.SRecord(
templateId,
ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")),
ArrayList(SValue.SParty(alice), SValue.SInt64(42)),
),
ContractMetadata(
now,
Some(crypto.Hash.assertHashContractKey(templateId, unusedContractKey)),
ImmArray.empty,
),
)
val fetchByKeyCommand = speedy.Command.FetchByKey(
templateId = templateId,
key = usedContractSKey,
)
ExplicitDisclosureTesting.unusedDisclosedContractsNotSavedToLedger(
fetchByKeyCommand,
unusedDisclosedContract,
usedDisclosedContract,
)
}
}
"create-and-exercise command" should {
@ -1379,13 +1347,10 @@ class EngineTest
reinterpreted shouldBe a[Right[_, _]]
}
}
"lookup by key" should {
val seed = hash("interpreting lookup by key nodes")
val lookedUpCid = toContractId("1")
val lookerUpTemplate = "BasicTests:LookerUpByKey"
val lookerUpTemplateId = Identifier(basicTestsPkgId, lookerUpTemplate)
@ -1570,6 +1535,104 @@ class EngineTest
)
}
}
"unused disclosed contracts not saved to ledger" in {
val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey")
val usedContractSKey = SValue.SRecord(
templateId,
ImmArray("_1", "_2").map(Ref.Name.assertFromString),
values = ArrayList(SValue.SParty(alice), SValue.SInt64(42)),
)
val usedContractKey = Value.ValueRecord(
None,
ImmArray(
None -> Value.ValueParty(alice),
None -> Value.ValueInt64(42),
),
)
val unusedContractKey = Value.ValueRecord(
None,
ImmArray(
None -> Value.ValueParty(alice),
None -> Value.ValueInt64(69),
),
)
val usedDisclosedContract = DisclosedContract(
templateId,
SValue.SContractId(toContractId("BasicTests:WithKey:1")),
SValue.SRecord(
templateId,
ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")),
ArrayList(SValue.SParty(alice), SValue.SInt64(42)),
),
ContractMetadata(
now,
Some(crypto.Hash.assertHashContractKey(templateId, usedContractKey)),
ImmArray.empty,
),
)
val unusedDisclosedContract = DisclosedContract(
templateId,
SValue.SContractId(toContractId("BasicTests:WithKey:2")),
SValue.SRecord(
templateId,
ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")),
ArrayList(SValue.SParty(alice), SValue.SInt64(42)),
),
ContractMetadata(
now,
Some(crypto.Hash.assertHashContractKey(templateId, unusedContractKey)),
ImmArray.empty,
),
)
val lookupByKeyCommand = speedy.Command.LookupByKey(
templateId = templateId,
contractKey = usedContractSKey,
)
ExplicitDisclosureTesting.unusedDisclosedContractsNotSavedToLedger(
lookupByKeyCommand,
unusedDisclosedContract,
usedDisclosedContract,
)
}
}
"fetch template" should {
val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple")
val usedDisclosedContract = DisclosedContract(
templateId,
SValue.SContractId(toContractId("BasicTests:Simple:1")),
SValue.SRecord(
templateId,
ImmArray(Ref.Name.assertFromString("p")),
ArrayList(SValue.SParty(alice)),
),
ContractMetadata(Time.Timestamp.now(), None, ImmArray.empty),
)
val unusedDisclosedContract = DisclosedContract(
templateId,
SValue.SContractId(toContractId("BasicTests:Simple:2")),
SValue.SRecord(
templateId,
ImmArray(Ref.Name.assertFromString("p")),
ArrayList(SValue.SParty(alice)),
),
ContractMetadata(Time.Timestamp.now(), None, ImmArray.empty),
)
"unused disclosed contracts not saved to ledger" in {
val fetchTemplateCommand = speedy.Command.FetchTemplate(
templateId = templateId,
coid = usedDisclosedContract.contractId,
)
ExplicitDisclosureTesting.unusedDisclosedContractsNotSavedToLedger(
fetchTemplateCommand,
unusedDisclosedContract,
usedDisclosedContract,
)
}
}
"getTime set dependsOnTime flag" in {
@ -1743,7 +1806,7 @@ class EngineTest
)
)
val contracts = defaultContracts + (fetcherCid -> fetcherInst)
val lookupContract = contracts.get(_)
val lookupContract = contracts.get _
val correctCommand =
ApiCommand.Exercise(
withKeyId,
@ -1892,7 +1955,7 @@ class EngineTest
"exceptions" should {
val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar")
val lookupPackage = allExceptionsPkgs.get(_)
val lookupPackage = allExceptionsPkgs.get _
val kId = Identifier(exceptionsPkgId, "Exceptions:K")
val tId = Identifier(exceptionsPkgId, "Exceptions:T")
val let = Time.Timestamp.now()
@ -1908,7 +1971,7 @@ class EngineTest
)
)
)
val lookupContract = contracts.get(_)
val lookupContract = contracts.get _
def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] =
(key.globalKey.templateId, key.globalKey.key) match {
case (
@ -2041,7 +2104,7 @@ class EngineTest
"action node seeds" should {
val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar")
val lookupPackage = allExceptionsPkgs.get(_)
val lookupPackage = allExceptionsPkgs.get _
val kId = Identifier(exceptionsPkgId, "Exceptions:K")
val seedId = Identifier(exceptionsPkgId, "Exceptions:NodeSeeds")
val let = Time.Timestamp.now()
@ -2057,7 +2120,7 @@ class EngineTest
)
)
)
val lookupContract = contracts.get(_)
val lookupContract = contracts.get _
def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] =
(key.globalKey.templateId, key.globalKey.key) match {
case (
@ -2093,6 +2156,7 @@ class EngineTest
lookupKey,
)
}
"Only create and exercise nodes end up in actionNodeSeeds" in {
val command = ApiCommand.CreateAndExercise(
seedId,
@ -2118,7 +2182,7 @@ class EngineTest
"global key lookups" should {
val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar")
val lookupPackage = allExceptionsPkgs.get(_)
val lookupPackage = allExceptionsPkgs.get _
val kId = Identifier(exceptionsPkgId, "Exceptions:K")
val tId = Identifier(exceptionsPkgId, "Exceptions:GlobalLookups")
val let = Time.Timestamp.now()
@ -2134,7 +2198,7 @@ class EngineTest
)
)
)
val lookupContract = contracts.get(_)
val lookupContract = contracts.get _
def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] =
(key.globalKey.templateId, key.globalKey.key) match {
case (
@ -2249,24 +2313,134 @@ class EngineTest
}
}
}
object EngineTest {
private implicit def logContext: LoggingContext = LoggingContext.ForTesting
import Matchers._
private def hash(s: String) = crypto.Hash.hashPrivateKey(s)
private def participant = Ref.ParticipantId.assertFromString("participant")
private def byKeyNodes(tx: VersionedTransaction) =
implicit val logContext: LoggingContext = LoggingContext.ForTesting
@SuppressWarnings(Array("org.wartremover.warts.Any"))
implicit val resultEq: Equality[Either[Error, SValue]] = {
case (Right(v1: SValue), Right(v2: SValue)) => svalue.Equality.areEqual(v1, v2)
case (Left(e1), Left(e2)) => e1 == e2
case _ => false
}
implicit def qualifiedNameStr(s: String): QualifiedName =
QualifiedName.assertFromString(s)
implicit def toName(s: String): Name =
Name.assertFromString(s)
val (basicTestsPkgId, basicTestsPkg, allPackages) = loadPackage(
"daml-lf/tests/BasicTests.dar"
)
val basicTestsSignatures: PackageInterface =
language.PackageInterface(Map(basicTestsPkgId -> basicTestsPkg))
val party: Ref.IdString.Party = Party.assertFromString("Party")
val alice: Ref.IdString.Party = Party.assertFromString("Alice")
val bob: Ref.IdString.Party = Party.assertFromString("Bob")
val clara: Ref.IdString.Party = Party.assertFromString("Clara")
val dummySuffix: Bytes = Bytes.assertFromString("00")
val withKeyTemplate = "BasicTests:WithKey"
val BasicTests_WithKey: lf.data.Ref.ValueRef = Identifier(basicTestsPkgId, withKeyTemplate)
val withKeyContractInst: VersionedContractInstance =
assertAsVersionedContract(
ContractInstance(
TypeConName(basicTestsPkgId, withKeyTemplate),
ValueRecord(
Some(BasicTests_WithKey),
ImmArray(
(Some[Ref.Name]("p"), ValueParty(alice)),
(Some[Ref.Name]("k"), ValueInt64(42)),
),
),
"",
)
)
val defaultContracts: Map[ContractId, VersionedContractInstance] =
Map(
toContractId("BasicTests:Simple:1") ->
assertAsVersionedContract(
ContractInstance(
TypeConName(basicTestsPkgId, "BasicTests:Simple"),
ValueRecord(
Some(Identifier(basicTestsPkgId, "BasicTests:Simple")),
ImmArray((Some[Name]("p"), ValueParty(party))),
),
"",
)
),
toContractId("BasicTests:CallablePayout:1") ->
assertAsVersionedContract(
ContractInstance(
TypeConName(basicTestsPkgId, "BasicTests:CallablePayout"),
ValueRecord(
Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")),
ImmArray(
(Some[Ref.Name]("giver"), ValueParty(alice)),
(Some[Ref.Name]("receiver"), ValueParty(bob)),
),
),
"",
)
),
toContractId("BasicTests:WithKey:1") ->
withKeyContractInst,
)
val defaultKey = Map(
GlobalKey.assertBuild(
TypeConName(basicTestsPkgId, withKeyTemplate),
ValueRecord(None, ImmArray((None, ValueParty(alice)), (None, ValueInt64(42)))),
)
->
toContractId("BasicTests:WithKey:1")
)
val lookupContract: ContractId => Option[VersionedContractInstance] = defaultContracts.get
val suffixLenientEngine: Engine = newEngine()
val suffixStrictEngine: Engine = newEngine(requireCidSuffixes = true)
val preprocessor =
new preprocessing.Preprocessor(
ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig)
)
def loadPackage(resource: String): (PackageId, Package, Map[PackageId, Package]) = {
val packages = UniversalArchiveDecoder.assertReadFile(new File(rlocation(resource)))
val (mainPkgId, mainPkg) = packages.main
(mainPkgId, mainPkg, packages.all.toMap)
}
def lookupPackage(pkgId: PackageId): Option[Package] = {
allPackages.get(pkgId)
}
def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] =
(key.globalKey.templateId, key.globalKey.key) match {
case (
BasicTests_WithKey,
ValueRecord(_, ImmArray((_, ValueParty(`alice`)), (_, ValueInt64(42)))),
) =>
Some(toContractId("BasicTests:WithKey:1"))
case _ =>
None
}
def hash(s: String): Hash = crypto.Hash.hashPrivateKey(s)
def participant: Ref.IdString.ParticipantId = Ref.ParticipantId.assertFromString("participant")
def byKeyNodes(tx: VersionedTransaction): Set[NodeId] =
tx.nodes.collect { case (nodeId, node: Node.Action) if node.byKey => nodeId }.toSet
private val party = Party.assertFromString("Party")
private val alice = Party.assertFromString("Alice")
private val bob = Party.assertFromString("Bob")
private val clara = Party.assertFromString("Clara")
private def newEngine(requireCidSuffixes: Boolean = false) =
def newEngine(requireCidSuffixes: Boolean = false) =
new Engine(
EngineConfig(
allowedLanguageVersions = language.LanguageVersion.DevVersions,
@ -2275,28 +2449,13 @@ object EngineTest {
)
)
private implicit def qualifiedNameStr(s: String): QualifiedName =
QualifiedName.assertFromString(s)
private implicit def toName(s: String): Name =
Name.assertFromString(s)
private val dummySuffix = Bytes.assertFromString("00")
private def toContractId(s: String): ContractId =
def toContractId(s: String): ContractId =
ContractId.V1.assertBuild(crypto.Hash.hashPrivateKey(s), dummySuffix)
private def findNodeByIdx[Cid](nodes: Map[NodeId, Node], idx: Int) =
def findNodeByIdx[Cid](nodes: Map[NodeId, Node], idx: Int): Option[Node] =
nodes.collectFirst { case (nodeId, node) if nodeId.index == idx => node }
@SuppressWarnings(Array("org.wartremover.warts.Any"))
private implicit def resultEq: Equality[Either[Error, SValue]] = {
case (Right(v1: SValue), Right(v2: SValue)) => svalue.Equality.areEqual(v1, v2)
case (Left(e1), Left(e2)) => e1 == e2
case _ => false
}
private def isReplayedBy(
def isReplayedBy(
recorded: VersionedTransaction,
replayed: VersionedTransaction,
): Either[ReplayMismatch, Unit] = {
@ -2304,47 +2463,12 @@ object EngineTest {
Validation.isReplayedBy(Normalization.normalizeTx(recorded), replayed)
}
private def suffix(tx: VersionedTransaction) =
def suffix(tx: VersionedTransaction): VersionedTransaction =
data.assertRight(tx.suffixCid(_ => dummySuffix))
private[this] case class ReinterpretState(
contracts: Map[ContractId, VersionedContractInstance],
keys: Map[GlobalKey, ContractId],
nodes: HashMap[NodeId, Node] = HashMap.empty,
roots: BackStack[NodeId] = BackStack.empty,
dependsOnTime: Boolean = false,
nodeSeeds: BackStack[(NodeId, crypto.Hash)] = BackStack.empty,
) {
def commit(tr: Tx, meta: Tx.Metadata) = {
val (newContracts, newKeys) = tr.fold((contracts, keys)) {
case ((contracts, keys), (_, exe: Node.Exercise)) =>
(contracts - exe.targetCoid, keys)
case ((contracts, keys), (_, create: Node.Create)) =>
(
contracts.updated(
create.coid,
create.versionedCoinst,
),
create.key.fold(keys)(k =>
keys.updated(GlobalKey.assertBuild(create.templateId, k.key), create.coid)
),
)
case (acc, _) => acc
}
ReinterpretState(
newContracts,
newKeys,
nodes ++ tr.nodes,
roots :++ tr.roots,
dependsOnTime || meta.dependsOnTime,
nodeSeeds :++ meta.nodeSeeds,
)
}
}
// Mimics Canton reinterpreation
// Mimics Canton reinterpretation
// requires a suffixed transaction.
private def reinterpret(
def reinterpret(
engine: Engine,
submitters: Set[Party],
nodes: ImmArray[NodeId],
@ -2434,4 +2558,109 @@ object EngineTest {
)
}
object ExplicitDisclosureTesting {
def unusedDisclosedContractsNotSavedToLedger(
cmd: speedy.Command,
unusedDisclosedContract: DisclosedContract,
usedDisclosedContract: DisclosedContract,
): Assertion = {
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = Set(alice),
readAs = Set.empty,
commands = ImmArray(cmd),
ledgerTime = Time.Timestamp.now(),
submissionTime = Time.Timestamp.now(),
seeding = InitialSeeding.TransactionSeed(hash(s"$cmd")),
disclosures = ImmArray(unusedDisclosedContract, usedDisclosedContract),
)
.consume(_ => None, lookupPackage, lookupKey)
inside(result) { case Right((transaction, metadata)) =>
transaction should haveDisclosedInputContracts(usedDisclosedContract)
metadata should haveDisclosedContracts(usedDisclosedContract)(preprocessor)
}
}
@SuppressWarnings(
Array(
"org.wartremover.warts.JavaSerializable",
"org.wartremover.warts.Product",
"org.wartremover.warts.Serializable",
)
)
def haveDisclosedContracts(
disclosedContracts: DisclosedContract*
)(preprocessor: preprocessing.Preprocessor): Matcher[Tx.Metadata] =
Matcher { metadata =>
val expectedResult = ImmArray(disclosedContracts: _*)
val actualResult = metadata.disclosures
.map(_.unversioned)
.map(preprocessor.commandPreprocessor.unsafePreprocessDisclosedContract)
val debugMessage = Seq(
s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_)).map(_.contractId.value)}",
s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_)).map(_.contractId)}",
).mkString("\n ", "\n ", "")
MatchResult(
expectedResult == actualResult,
s"Failed with unexpected disclosed contracts: $expectedResult != $actualResult $debugMessage",
s"Failed with unexpected disclosed contracts: $expectedResult == $actualResult",
)
}
def haveDisclosedInputContracts(
disclosedContracts: DisclosedContract*
): Matcher[VersionedTransaction] =
Matcher { transaction =>
val expectedResult = Set(disclosedContracts: _*).map(_.contractId.value)
val actualResult = transaction.inputContracts
val debugMessage = Seq(
s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_))}",
s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_))}",
).mkString("\n ", "\n ", "")
MatchResult(
expectedResult == actualResult,
s"Failed with unexpected disclosed contracts: $expectedResult != $actualResult $debugMessage",
s"Failed with unexpected disclosed contracts: $expectedResult == $actualResult",
)
}
}
case class ReinterpretState(
contracts: Map[ContractId, VersionedContractInstance],
keys: Map[GlobalKey, ContractId],
nodes: HashMap[NodeId, Node] = HashMap.empty,
roots: BackStack[NodeId] = BackStack.empty,
dependsOnTime: Boolean = false,
nodeSeeds: BackStack[(NodeId, crypto.Hash)] = BackStack.empty,
) {
def commit(tr: Tx, meta: Tx.Metadata): ReinterpretState = {
val (newContracts, newKeys) = tr.fold((contracts, keys)) {
case ((contracts, keys), (_, exe: Node.Exercise)) =>
(contracts - exe.targetCoid, keys)
case ((contracts, keys), (_, create: Node.Create)) =>
(
contracts.updated(
create.coid,
create.versionedCoinst,
),
create.key.fold(keys)(k =>
keys.updated(GlobalKey.assertBuild(create.templateId, k.key), create.coid)
),
)
case (acc, _) => acc
}
ReinterpretState(
newContracts,
newKeys,
nodes ++ tr.nodes,
roots :++ tr.roots,
dependsOnTime || meta.dependsOnTime,
nodeSeeds :++ meta.nodeSeeds,
)
}
}
}

View File

@ -1570,12 +1570,12 @@ private[lf] object SBuiltin {
NameOf.qualifiedNameOfCurrentFunc,
skey,
)
if (keyWithMaintainers.maintainers.isEmpty) {
Control.Error(
IE.FetchEmptyContractKeyMaintainers(operation.templateId, keyWithMaintainers.key)
)
} else {
val gkey = GlobalKey(operation.templateId, keyWithMaintainers.key)
onLedger.ptx.contractState.resolveKey(gkey) match {

View File

@ -241,7 +241,7 @@ private[lf] object Speedy {
)
case None =>
val m1_prime = table.contractById + (coid -> (d.templateId, arg))
val contractByIdUpdates = table.contractById + (coid -> (d.templateId, arg))
d.metadata.keyHash match {
case Some(hash) =>
// check for duplicate contract key hashes
@ -256,14 +256,18 @@ private[lf] object Speedy {
)
)
case None => DisclosureTable(table.contractIdByKey + (hash -> coid), m1_prime)
case None =>
DisclosureTable(
table.contractIdByKey + (hash -> coid),
contractByIdUpdates,
)
}
case None =>
packageInterface.lookupTemplate(d.templateId) match {
case Right(template) if template.key.isEmpty =>
// Success - template exists, but has no key defined
table.copy(contractById = m1_prime)
table.copy(contractById = contractByIdUpdates)
case Right(_) =>
// Error - disclosed contract lacks a key hash, but the template requires a key

View File

@ -216,7 +216,7 @@ private[lf] object PartialTransaction {
* @param contractState summarizes the changes to the contract states caused by nodes up to now
* @param actionNodeLocations The optional locations of create/exercise/fetch/lookup nodes in pre-order.
* Used by 'locationInfo()', called by 'finish()' and 'finishIncomplete()'
* @param disclosedContracts contracts that have been explicitly disclosed
* @param disclosedContracts contracts that have been explicitly disclosed to Speedy (usage will be determined by 'finish()')
*/
private[speedy] case class PartialTransaction(
nextNodeIdx: Int,
@ -308,13 +308,17 @@ private[speedy] case class PartialTransaction(
val roots = context.children.toImmArray
val tx0 = Tx(nodes, roots)
val (tx, seeds) = NormalizeRollbacks.normalizeTx(tx0)
val txResult = SubmittedTransaction(TxVersion.asVersionedTransaction(tx))
Result(
SubmittedTransaction(TxVersion.asVersionedTransaction(tx)),
txResult,
locationInfo(),
seeds.zip(actionNodeSeeds.toImmArray),
contractState.globalKeyInputs.transform((_, v) => v.toKeyMapping),
disclosedContracts,
disclosedContracts.filter(disclosedContract =>
txResult.inputContracts.contains(disclosedContract.contractId.value)
),
)
case _ =>
InternalError.runtimeException(
NameOf.qualifiedNameOfCurrentFunc,

View File

@ -75,7 +75,6 @@ class BuildDisclosureTableTest extends AnyFreeSpec with Inside with Matchers {
DisclosurePreprocessing.DuplicateContractKeys(houseTemplateId, collidingKeyHash)
)
)
}
}
}

View File

@ -298,28 +298,50 @@ object ExplicitDisclosureLib {
def haveInactiveContractIds(contractIds: ContractId*): Matcher[Speedy.OnLedger] = Matcher {
ledger =>
val expectedResult = contractIds.toSet
val actualResult = ledger.ptx.contractState.activeState.consumedBy.keySet
val debugMessage = Seq(
s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_))}",
s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_))}",
).mkString("\n ", "\n ", "")
MatchResult(
ledger.ptx.contractState.activeState.consumedBy.keySet == contractIds.toSet,
s"Failed with unexpected inactive contracts: ${ledger.ptx.contractState.activeState.consumedBy.keySet} != $contractIds",
s"Failed with unexpected inactive contracts: ${ledger.ptx.contractState.activeState.consumedBy.keySet} == $contractIds",
expectedResult == actualResult,
s"Failed with unexpected inactive contracts: $expectedResult != $actualResult $debugMessage",
s"Failed with unexpected inactive contracts: $expectedResult == $actualResult",
)
}
def haveCachedContractIds(contractIds: ContractId*): Matcher[Speedy.OnLedger] = Matcher {
ledger =>
val expectedResult = contractIds.toSet
val actualResult = ledger.cachedContracts.keySet
val debugMessage = Seq(
s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_))}",
s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_))}",
).mkString("\n ", "\n ", "")
MatchResult(
ledger.cachedContracts.keySet == contractIds.toSet,
s"Failed with unexpected cached contracts: ${ledger.cachedContracts.keySet} != $contractIds",
s"Failed with unexpected cached contracts: ${ledger.cachedContracts.keySet} == $contractIds",
expectedResult == actualResult,
s"Failed with unexpected cached contracts: $expectedResult != $actualResult $debugMessage",
s"Failed with unexpected cached contracts: $expectedResult == $actualResult",
)
}
def haveDisclosedContracts(contractIds: DisclosedContract*): Matcher[Speedy.OnLedger] = Matcher {
ledger =>
def haveDisclosedContracts(disclosedContracts: DisclosedContract*): Matcher[Speedy.OnLedger] =
Matcher { ledger =>
val expectedResult = ImmArray(disclosedContracts: _*)
val actualResult = ledger.ptx.disclosedContracts
val debugMessage = Seq(
s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_)).map(_.contractId)}",
s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_)).map(_.contractId)}",
).mkString("\n ", "\n ", "")
MatchResult(
ledger.ptx.disclosedContracts == ImmArray(contractIds: _*),
s"Failed with unexpected disclosed contracts: ${ledger.ptx.disclosedContracts} != $contractIds",
s"Failed with unexpected disclosed contracts: ${ledger.ptx.disclosedContracts} == $contractIds",
expectedResult == actualResult,
s"Failed with unexpected disclosed contracts: $expectedResult != $actualResult $debugMessage",
s"Failed with unexpected disclosed contracts: $expectedResult == $actualResult",
)
}
}
}

View File

@ -701,7 +701,7 @@ object Transaction {
* @param nodeSeeds : An association list that maps to each ID of create and exercise
* nodes its seeds.
* @param globalKeyMapping : input key mapping inferred by interpretation
* @param disclosures : contracts explicitly disclosed to this transaction
* @param disclosures : contracts passed via explicit disclosure that have been used in this transaction
*/
final case class Metadata(
submissionSeed: Option[crypto.Hash],

View File

@ -175,7 +175,7 @@
- Evaluation order of successful lookup_by_key of a local contract: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L2913)
- Evaluation order of successful lookup_by_key of a non-cached global contract: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L2761)
- Exceptions, throw/catch.: [ExceptionTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala#L25)
- Rollback creates cannot be exercise: [EngineTest.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala#L2001)
- Rollback creates cannot be exercise: [EngineTest.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala#L2064)
- This checks that type checking in exercise_interface is done after checking activeness.: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L1851)
- This checks that type checking is done after checking activeness.: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L1743)
- This checks that type checking is done after checking activeness.: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L2705)