dynamically check type of contract ids, fixes #1005 (#1037)

Up to now, the engine blindly assumed that contract ids pointed to
contracts of the right type. However, this assumption is faulty:
contract ids coming from the Ledger API cannot be type checked
in command translation since we need access to the contract itself
to do so.

This caused some seriously surprising / broken behavior: one could
send an exercise command with the wrong template id and still go
through, or break internal invariants about the type of choices.

This commit fixes this by checking that the type of the contract
instances we fetch is correct at runtime.

cc @hurryabit @dajmaki @remyhaemmerle-da @S11001001 @meiersi-da
This commit is contained in:
Francesco Mazzoli 2019-05-09 17:11:05 +02:00 committed by GitHub
parent 66934f8a1e
commit de54e8f60f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 3 deletions

View File

@ -122,6 +122,9 @@ case class Conversions(homePackageId: Ref.PackageId) {
case SError.ScenarioErrorInvalidPartyName(party, _) =>
builder.setScenarioInvalidPartyName(party)
case wtc: SError.DamlEWronglyTypedContract =>
sys.error(
s"Got unexpected DamlEWronglyTypedContract error in scenario service: $wtc. Note that in the scenario service this error should never surface since contract fetches are all type checked.")
}
builder.build
}

View File

@ -65,6 +65,11 @@ object Pretty {
(line + text("Recursive exercise of ") + prettyTypeConName(tid)).nested(4)
case Some(nid) => (line + prettyTransactionNode(nid)).nested(4)
})
case DamlEWronglyTypedContract(coid, expected, actual) =>
text("Update failed due to wrongly typed contract id") & prettyContractId(coid) /
text("Expected contract of type") & prettyTypeConName(expected) & text("but got") & prettyTypeConName(
actual)
}
// A minimal pretty-print of an update transaction node, without recursing into child nodes..

View File

@ -11,7 +11,12 @@ import com.digitalasset.daml.lf.data._
import com.digitalasset.daml.lf.lfpackage.Ast._
import com.digitalasset.daml.lf.speedy.SError._
import com.digitalasset.daml.lf.speedy.SExpr._
import com.digitalasset.daml.lf.speedy.Speedy.{CtrlValue, Machine, SpeedyHungry}
import com.digitalasset.daml.lf.speedy.Speedy.{
CtrlValue,
CtrlWronglyTypeContractId,
Machine,
SpeedyHungry
}
import com.digitalasset.daml.lf.speedy.SResult._
import com.digitalasset.daml.lf.speedy.SValue._
import com.digitalasset.daml.lf.transaction.Transaction._
@ -774,6 +779,17 @@ object SBuiltin {
case Some((_, Some(consumedBy))) =>
throw DamlELocalContractNotActive(coid, templateId, consumedBy)
case Some((coinst, None)) =>
// Here we crash hard rather than throwing a "nice" error
// ([[DamlEWronglyTypedContract]]) since if _relative_ contract
// id to be of the wrong template it means that the DAML-LF
// program that generated it is ill-typed.
//
// On the other hand absolute contract ids can come from outside
// (e.g. Ledger API) and thus we need to fail more gracefully
// (see below).
if (coinst.template != templateId) {
crash(s"Relative contract $rcoid ($templateId) not found from partial transaction")
}
coinst.arg
}
case acoid: V.AbsoluteContractId =>
@ -784,7 +800,14 @@ object SBuiltin {
machine.committer,
cbMissing = _ => machine.tryHandleException(),
cbPresent = { coinst =>
machine.ctrl = CtrlValue(SValue.fromValue(coinst.arg.value))
// Note that we cannot throw in this continuation -- instead
// set the control appropriately which will crash the machine
// correctly later.
if (coinst.template != templateId) {
machine.ctrl = CtrlWronglyTypeContractId(acoid, templateId, coinst.template)
} else {
machine.ctrl = CtrlValue(SValue.fromValue(coinst.arg.value))
}
}
))
}

View File

@ -76,6 +76,15 @@ object SError {
consumedBy: Ledger.NodeId)
extends SErrorScenario
/** We tried to fetch / exercise a contract of the wrong type --
* see <https://github.com/digital-asset/daml/issues/1005>.
*/
final case class DamlEWronglyTypedContract(
coid: ContractId,
expected: TypeConName,
actual: TypeConName)
extends SErrorDamlException
/** A fetch or exercise was being made against a contract that has not
* been disclosed to 'committer'. */
final case class ScenarioErrorContractNotVisible(

View File

@ -16,6 +16,7 @@ import scala.collection.JavaConverters._
import java.util.ArrayList
import com.digitalasset.daml.lf.CompiledPackages
import com.digitalasset.daml.lf.value.Value.AbsoluteContractId
object Speedy {
@ -224,7 +225,10 @@ object Speedy {
def execute(machine: Machine): Unit
}
/** A special control object to guard against misbehaving operations */
/** A special control object to guard against misbehaving operations.
* It is set by default, so for example if an action forgets to set the
* control we won't loop but rather we'll crash.
*/
final case class CtrlCrash(before: Ctrl) extends Ctrl {
def execute(machine: Machine) =
crash(s"CtrlCrash: control set to crash after evaluting: $before")
@ -268,6 +272,20 @@ object Speedy {
}
}
/** When we fetch a contract id from upstream we cannot crash in the
* that upstream calls. Rather, we set the control to this and then crash
* when executing.
*/
final case class CtrlWronglyTypeContractId(
acoid: AbsoluteContractId,
expected: TypeConName,
actual: TypeConName)
extends Ctrl {
override def execute(machine: Machine): Unit = {
throw DamlEWronglyTypedContract(acoid, expected, actual)
}
}
object Ctrl {
def fromPrim(prim: Prim, arity: Int): Ctrl =
CtrlValue(SPAP(prim, new ArrayList[SValue](), arity))

View File

@ -0,0 +1,76 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.platform.tests.integration.ledger.api
import org.scalatest.{AsyncFreeSpec, Matchers}
import com.digitalasset.ledger.api.testing.utils.{
AkkaBeforeAndAfterAll,
SuiteResourceManagementAroundEach
}
import org.scalatest.concurrent.{AsyncTimeLimitedTests, ScalaFutures}
import com.digitalasset.platform.apitesting.{LedgerContext, MultiLedgerFixture, TestTemplateIds}
import com.digitalasset.platform.apitesting.LedgerContextExtensions._
import com.digitalasset.ledger.api.v1.value.{Record, RecordField, Value}
import com.digitalasset.platform.participant.util.ValueConversions._
import com.google.rpc.code.Code
class WronglyTypedContractIdIT
extends AsyncFreeSpec
with AkkaBeforeAndAfterAll
with MultiLedgerFixture
with SuiteResourceManagementAroundEach
with ScalaFutures
with AsyncTimeLimitedTests
with Matchers
with TestTemplateIds {
override protected def config: Config = Config.default
def createDummy(ctx: LedgerContext) = ctx.testingHelpers.simpleCreate(
"create-dummy",
"alice",
templateIds.dummy,
Record(fields = List(RecordField(value = "alice".asParty)))
)
"exercising something of the wrong type fails" in allFixtures { ctx =>
for {
ce <- createDummy(ctx)
_ <- ctx.testingHelpers.failingExercise(
"exercise-wrong",
"alice",
templateIds.dummyWithParam,
ce.contractId,
"DummyChoice2",
Value(Value.Sum.Record(Record(fields = List(RecordField(value = "txt".asText))))),
Code.INVALID_ARGUMENT,
"wrongly typed contract id"
)
} yield succeed
}
"fetching something of the wrong type fails" in allFixtures { ctx =>
for {
dummyCreateEvt <- createDummy(ctx)
delegationCreateEvt <- ctx.testingHelpers.simpleCreate(
"create-delegation",
"alice",
templateIds.delegation,
Record(
fields = List(RecordField(value = "alice".asParty), RecordField(value = "bob".asParty)))
)
_ <- ctx.testingHelpers.failingExercise(
"fetch-wrong",
"alice",
templateIds.delegation,
delegationCreateEvt.contractId,
"FetchDelegated",
Value(
Value.Sum.Record(
Record(fields = List(RecordField(value = dummyCreateEvt.contractId.asContractId))))),
Code.INVALID_ARGUMENT,
"wrongly typed contract id"
)
} yield succeed
}
}