Skip to content

Commit

Permalink
[JSON-API] Make key_hash idx non unique (#11102)
Browse files Browse the repository at this point in the history
* Add failing test that covers the bug

* Fix on conflict error for inserts into the contracts table

changelog_begin

- [JSON-API] make key_hash indexes non-unique, this fixes a bug where a duplicate key conflict was raised on the query store when the same contract was being witnessed twice by two separate parties

changelog_end

* move test to parent so as to test oracle query store

* make key_hash indexes non-unique

* use recordFromFields

Co-authored-by: Akshay <[email protected]>
  • Loading branch information
realvictorprm and akshayshirahatti-da authored Oct 5, 2021
1 parent 429f437 commit 018e908
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ private final class PostgresQueries(tablePrefix: String, tpIdCacheMaxEntries: Lo

private[this] val contractKeyHashIndexName = Fragment.const0(s"${tablePrefix}ckey_hash_idx")
private[this] val contractKeyHashIndex = CreateIndex(
sql"""CREATE UNIQUE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)"""
sql"""CREATE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)"""
)

protected[this] override def extraDatabaseDdls =
Expand Down Expand Up @@ -845,7 +845,7 @@ private final class OracleQueries(

private[this] val contractKeyHashIndexName = Fragment.const0(s"${tablePrefix}ckey_hash_idx")
private[this] val contractKeyHashIndex = CreateIndex(
sql"""CREATE UNIQUE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)"""
sql"""CREATE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)"""
)

private[this] val indexPayload = CreateIndex(sql"""
Expand Down
1 change: 1 addition & 0 deletions ledger-service/http-json-oracle/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ da_scala_test(
data = [
"//docs:quickstart-model.dar",
"//ledger-service/http-json:Account.dar",
"//ledger-service/http-json:User.dar",
"//ledger/test-common:dar-files",
"//ledger/test-common/test-certificates",
],
Expand Down
7 changes: 7 additions & 0 deletions ledger-service/http-json/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ daml_compile(
visibility = ["//ledger-service:__subpackages__"],
)

daml_compile(
name = "User",
srcs = ["src/it/daml/User.daml"],
visibility = ["//ledger-service:__subpackages__"],
)

[
da_scala_test(
name = "tests-{}".format(edition),
Expand Down Expand Up @@ -337,6 +343,7 @@ alias(
] if scala_major_version == "2.12" else [],
data = [
":Account.dar",
":User.dar",
"//docs:quickstart-model.dar",
"//ledger/test-common:dar-files",
"//ledger/test-common/test-certificates",
Expand Down
27 changes: 27 additions & 0 deletions ledger-service/http-json/src/it/daml/User.daml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

module User where

-- MAIN_TEMPLATE_BEGIN
template User with
username: Party
following: [Party]
where
signatory username
observer following
-- MAIN_TEMPLATE_END

key username: Party
maintainer key

-- FOLLOW_BEGIN
nonconsuming choice Follow: ContractId User with
userToFollow: Party
controller username
do
assertMsg "You cannot follow yourself" (userToFollow /= username)
assertMsg "You cannot follow the same user twice" (notElem userToFollow following)
archive self
create this with following = userToFollow :: following
-- FOLLOW_END
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ object AbstractHttpServiceIntegrationTestFuns {

private[http] val dar3 = requiredResource(SemanticTestDar.path)

private[http] val userDar = requiredResource("ledger-service/http-json/User.dar")

def sha256(source: Source[ByteString, Any])(implicit mat: Materializer): Try[String] = Try {
import java.security.MessageDigest
import javax.xml.bind.DatatypeConverter
Expand Down Expand Up @@ -103,6 +105,9 @@ trait AbstractHttpServiceIntegrationTestFuns
protected val metadata2: MetadataReader.LfMetadata =
MetadataReader.readFromDar(dar2).valueOr(e => fail(s"Cannot read dar2 metadata: $e"))

protected val metadataUser: MetadataReader.LfMetadata =
MetadataReader.readFromDar(userDar).valueOr(e => fail(s"Cannot read userDar metadata: $e"))

protected val jwt: Jwt = jwtForParties(List("Alice"), List(), testId)

protected val jwtAdminNoParty: Jwt = {
Expand All @@ -128,7 +133,7 @@ trait AbstractHttpServiceIntegrationTestFuns
import tag.@@ // used for subtyping to make `AHS ec` beat executionContext
implicit val `AHS ec`: ExecutionContext @@ this.type = tag[this.type](`AHS asys`.dispatcher)

override def packageFiles = List(dar1, dar2)
override def packageFiles = List(dar1, dar2, userDar)

protected def getUniqueParty(name: String) = getUniquePartyAndAuthHeaders(name)._1
protected def getUniquePartyAndAuthHeaders(name: String): (domain.Party, List[HttpHeader]) = {
Expand Down Expand Up @@ -274,7 +279,7 @@ trait AbstractHttpServiceIntegrationTestFuns
at[K :->>: V]((fn.value.name, _))
}

private[this] def recordFromFields[L <: HList, I <: HList](hlist: L)(implicit
protected[this] def recordFromFields[L <: HList, I <: HList](hlist: L)(implicit
mapper: shapeless.ops.hlist.Mapper.Aux[RecordFromFields.type, L, I],
lister: shapeless.ops.hlist.ToTraversable.Aux[I, Seq, (String, v.Value.Sum)],
): v.Record = v.Record(fields = hlist.map(RecordFromFields).to[Seq].map { case (n, vs) =>
Expand Down Expand Up @@ -1822,4 +1827,106 @@ abstract class AbstractHttpServiceIntegrationTest
_ <- queryN(0)
} yield succeed
}

"Should ignore conflicts on contract key hash constraint violation" in withHttpServiceAndClient {
(uri, encoder, _, _, _) =>
import scalaz.std.vector._
import scalaz.syntax.tag._
import scalaz.syntax.traverse._
import scalaz.std.scalaFuture._
import shapeless.record.{Record => ShRecord}
import com.daml.ledger.api.refinements.{ApiTypes => lar}

val partyIds = Vector("Alice", "Bob").map(getUniqueParty)
val packageId: Ref.PackageId = MetadataReader
.templateByName(metadataUser)(Ref.QualifiedName.assertFromString("User:User"))
.collectFirst { case (pkgid, _) => pkgid }
.getOrElse(fail(s"Cannot retrieve packageId"))

def userCreateCommand(
username: domain.Party,
following: Seq[domain.Party] = Seq.empty,
): domain.CreateCommand[v.Record, domain.TemplateId.OptionalPkg] = {
val templateId = domain.TemplateId(None, "User", "User")
val followingList = following.map(party => v.Value(v.Value.Sum.Party(party.unwrap)))
val arg = recordFromFields(
ShRecord(
username = v.Value.Sum.Party(username.unwrap),
following = v.Value.Sum.List(v.List.of(followingList)),
)
)

domain.CreateCommand(templateId, arg, None)
}
def userExerciseFollowCommand(
contractId: lar.ContractId,
toFollow: domain.Party,
): domain.ExerciseCommand[v.Value, domain.EnrichedContractId] = {
val templateId = domain.TemplateId(None, "User", "User")
val reference = domain.EnrichedContractId(Some(templateId), contractId)
val arg = recordFromFields(ShRecord(userToFollow = v.Value.Sum.Party(toFollow.unwrap)))
val choice = lar.Choice("Follow")

domain.ExerciseCommand(reference, choice, boxedRecord(arg), None)
}

def followUser(contractId: lar.ContractId, actAs: domain.Party, toFollow: domain.Party) = {
val exercise: domain.ExerciseCommand[v.Value, domain.EnrichedContractId] =
userExerciseFollowCommand(contractId, toFollow)
val exerciseJson: JsValue = encodeExercise(encoder)(exercise)

postJsonRequest(
uri.withPath(Uri.Path("/v1/exercise")),
exerciseJson,
headers = headersWithPartyAuth(actAs = List(actAs.unwrap)),
)
.map { case (exerciseStatus, exerciseOutput) =>
exerciseStatus shouldBe StatusCodes.OK
assertStatus(exerciseOutput, StatusCodes.OK)
()
}

}

def queryUsers(fromPerspectiveOfParty: domain.Party) = {
val query = jsObject(s"""{
"templateIds": ["$packageId:User:User"],
"query": {}
}""")

postJsonRequest(
uri.withPath(Uri.Path("/v1/query")),
query,
headers = headersWithPartyAuth(actAs = List(fromPerspectiveOfParty.unwrap)),
).map { case (searchStatus, searchOutput) =>
searchStatus shouldBe StatusCodes.OK
assertStatus(searchOutput, StatusCodes.OK)
}
}
val commands = partyIds.map { p =>
(p, userCreateCommand(p))
}

for {
users <- commands.traverse { case (party, command) =>
val fut = postCreateCommand(
command,
encoder,
uri,
headers = headersWithPartyAuth(actAs = List(party.unwrap)),
).map { case (status, output) =>
status shouldBe StatusCodes.OK
assertStatus(output, StatusCodes.OK)
getContractId(getResult(output))
}: Future[ContractId]
fut.map(cid => (party, cid))
}
(alice, aliceUserId) = users(0)
(bob, bobUserId) = users(1)
_ <- followUser(aliceUserId, alice, bob)
_ <- queryUsers(bob)
_ <- followUser(bobUserId, bob, alice)
_ <- queryUsers(alice)
} yield succeed
}
}

0 comments on commit 018e908

Please sign in to comment.