Skip to content

Commit

Permalink
Add support for party management in the sandbox (digital-asset#1452)
Browse files Browse the repository at this point in the history
Fixes digital-asset#1312

This PR adds support for party management in the sandbox:

Both the in-memory and the SQL backend track a list of known
parties, and implicitly add any party mentioned in a transaction.
New calls were added to the IndexService and the WriteService
for managing parties. These calls are wired to the above mentioned
persistence backends, and to a new API service.
A corresponding client interface was added to the scala API client.
An integration test was added for checking that a call to allocate a
party succeeds.
An integration test for the sandbox was added for checking that the
sandbox implicitly adds all parties mentioned in a transaction.
  • Loading branch information
rautenrieth-da authored Jun 11, 2019
1 parent 3d5bd2e commit 80e8ac1
Show file tree
Hide file tree
Showing 29 changed files with 965 additions and 35 deletions.
7 changes: 6 additions & 1 deletion docs/source/support/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ DAML Integration Kit

- Added new CLI flag ``--all-tests`` to the :doc:`Ledger API Test Tool </tools/ledger-api-test-tool/index>` to run all default and optional tests.

Sandbox
~~~~~~~

- Introduced a new API for party management.
See `#1312 <https://github.com/digital-asset/daml/issues/1312>`__.

.. _release-0-12-24:

0.12.24 - 2019-06-06
Expand Down Expand Up @@ -79,7 +85,6 @@ Sandbox

- Added recovery around failing ledger entry persistence queries using Postgres. See `#1505 <https://github.com/digital-asset/daml/pull/1505>`__.


DAML Integration Kit
~~~~~~~~~~~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,16 @@ service PartyManagementService {
// Adds a new party to the set managed by the backing participant.
// Caller specifies a party identifier suggestion, the actual identifier
// allocated might be different and is implementation specific.
// This call will either succeed or respond with UNIMPLEMENTED if synchronous
// party allocation is not supported by the backing participant.
// This call may:
// - Succeed, in which case the actual allocated identifier is visible in
// the response.
// - Respond with UNIMPLEMENTED if synchronous party allocation is not
// supported by the backing participant.
// - Respond with INVALID_ARGUMENT if the provided hint and/or display name
// is invalid on the given ledger (see below).
// daml-on-sql: suggestion's uniqueness is checked and call rejected if the
// identifier is already present
// daml-on-kv-ledger: suggestion's uniqueness is checked bby the validators in
// daml-on-kv-ledger: suggestion's uniqueness is checked by the validators in
// the consensus layer and call rejected if the identifier is already present.
// canton: completely different globally unique identifier is allocated.
// Behind the scenes calls to an internal protocol are made. As that protocol
Expand Down
1 change: 1 addition & 0 deletions ledger/ledger-api-client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ da_scala_library(
"//3rdparty/jvm/io/grpc:grpc_netty",
"//3rdparty/jvm/io/netty:netty_handler",
"//3rdparty/jvm/org/slf4j:slf4j_api",
"//daml-lf/data",
"//language-support/scala/bindings",
"//ledger-api/rs-grpc-akka",
"//ledger-api/rs-grpc-bridge",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.digitalasset.ledger.client.services.admin

import com.digitalasset.daml.lf.data.Ref.Party
import com.digitalasset.ledger.api.domain.{ParticipantId, PartyDetails}
import com.digitalasset.ledger.api.v1.admin.party_management_service.{
AllocatePartyRequest,
GetParticipantIdRequest,
ListKnownPartiesRequest,
PartyDetails => ApiPartyDetails
}
import com.digitalasset.ledger.api.v1.admin.party_management_service.PartyManagementServiceGrpc.PartyManagementService

import scala.concurrent.{ExecutionContext, Future}

final class PartyManagementClient(partyManagementService: PartyManagementService)(
implicit ec: ExecutionContext) {

private[this] def mapPartyDetails(details: ApiPartyDetails): PartyDetails = {
PartyDetails(
Party.assertFromString(details.party),
if (details.displayName.isEmpty) None else Some(details.displayName),
details.isLocal)
}

def getParticipantId(): Future[ParticipantId] =
partyManagementService
.getParticipantId(new GetParticipantIdRequest())
.map(r => ParticipantId(r.participantId))

def listKnownParties(): Future[List[PartyDetails]] =
partyManagementService
.listKnownParties(new ListKnownPartiesRequest())
.map(_.partyDetails.map(mapPartyDetails).toList)

def allocateParty(hint: Option[String], displayName: Option[String]): Future[PartyDetails] =
partyManagementService
.allocateParty(new AllocatePartyRequest(hint.getOrElse(""), displayName.getOrElse("")))
.map(_.partyDetails.getOrElse(sys.error("No PartyDetails in response.")))
.map(mapPartyDetails)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ trait ErrorFactories {
def aborted(description: String): StatusRuntimeException =
grpcError(Status.INTERNAL.withDescription(description))

def unimplemented(description: String): StatusRuntimeException =
grpcError(Status.UNIMPLEMENTED.withDescription(description))

def grpcError(status: Status) = new ApiException(status)

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// 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 java.util.UUID

import com.digitalasset.daml.lf.data.Ref
import com.digitalasset.ledger.api.domain.PartyDetails
import com.digitalasset.ledger.api.testing.utils.{
AkkaBeforeAndAfterAll,
SuiteResourceManagementAroundAll
}
import com.digitalasset.ledger.api.v1.admin.party_management_service.PartyManagementServiceGrpc.PartyManagementService
import com.digitalasset.ledger.client.services.admin.PartyManagementClient
import com.digitalasset.platform.apitesting.MultiLedgerFixture
import com.digitalasset.platform.esf.TestExecutionSequencerFactory
import io.grpc.{Status, StatusException, StatusRuntimeException}
import org.scalatest._
import org.scalatest.concurrent.AsyncTimeLimitedTests
import org.scalatest.time.Span
import org.scalatest.time.SpanSugar._
import scalaz.syntax.tag._

import scala.concurrent.Future

@SuppressWarnings(
Array(
"org.wartremover.warts.Any",
"org.wartremover.warts.Option2Iterable",
"org.wartremover.warts.StringPlusAny"
))
class PartyManagementServiceIT
extends AsyncWordSpec
with AkkaBeforeAndAfterAll
with MultiLedgerFixture
with SuiteResourceManagementAroundAll
with AsyncTimeLimitedTests
with TestExecutionSequencerFactory
with Matchers
with Inside
with OptionValues {

override def timeLimit: Span = 15.seconds

override protected def config: Config = Config.default

private def partyManagementClient(stub: PartyManagementService): PartyManagementClient = {
new PartyManagementClient(stub)
}

private type GrpcResult[T] = Either[Status.Code, T]

/**
* Takes the future produced by a GRPC call.
* If the call was successful, returns Right(result).
* If the call failed, returns Left(statusCode).
*/
private def withGrpcError[T](future: Future[T]): Future[GrpcResult[T]] = {
future
.map(Right(_))
.recoverWith {
case s: StatusRuntimeException => Future.successful(Left(s.getStatus.getCode))
case s: StatusException => Future.successful(Left(s.getStatus.getCode))
case other => fail(s"$other is not a gRPC Status exception.")
}
}

/**
* Performs checks on the result of a party allocation call.
* Since the result of the call depends on the implementation,
* we need to handle all error cases.
*/
private def whenSuccessful[T, R](resultE: GrpcResult[T])(check: T => Assertion): Assertion =
resultE match {
case Right(result) =>
// The call succeeded, check the result.
check(result)
case Left(Status.Code.UNIMPLEMENTED) =>
// Ledgers are allowed to not implement this call.
succeed
case Left(Status.Code.INVALID_ARGUMENT) =>
// Ledgers are allowed to reject the request if they don't like the hint and/or display name.
// However, we try really hard to use globally unique hints
// that are also valid party names, so this outcome is unexpected.
fail(s"Party allocation failed with INVALID_ARGUMENT.")
case other =>
fail(s"Unexpected GRPC error code $other while allocating a party")
}

"Party Management Service" when {

"returning the participant ID" should {
"succeed" in allFixtures { c =>
partyManagementClient(c.partyManagementService)
.getParticipantId()
.map(id => id.unwrap.isEmpty shouldBe false)

}
}

// Note: The ledger has some freedom in how to implement the allocateParty() call.
//
// The ledger may require that the given hint is a valid party name and may reject
// the call if such a party already exists. This test therefore generates a unique hints.
//
// The ledger is allowed to disregard the hint, we can not make any assertions
// on the relation between the given hint and the allocated party name.
"allocating parties" should {

"work when allocating a party with a hint" in allFixtures { c =>
val client = partyManagementClient(c.partyManagementService)

val hint = Ref.Party.assertFromString(s"party-${UUID.randomUUID().toString}")
val displayName = s"Test party '$hint' for PartyManagementServiceIT"

for {
initialParties <- client.listKnownParties()
resultE <- withGrpcError(client.allocateParty(Some(hint), Some(displayName)))
finalParties <- client.listKnownParties()
} yield {
whenSuccessful(resultE)(result => {
result.displayName shouldBe Some(displayName)
initialParties.exists(p => p.party == result.party) shouldBe false
finalParties.contains(result) shouldBe true
})
}
}

"work when allocating a party without a hint" in allFixtures { c =>
val client = partyManagementClient(c.partyManagementService)

val displayName = s"Test party for PartyManagementServiceIT"

for {
initialParties <- client.listKnownParties()
resultE <- withGrpcError(client.allocateParty(None, Some(displayName)))
finalParties <- client.listKnownParties()
} yield {
whenSuccessful(resultE)(result => {
result.displayName shouldBe Some(displayName)
initialParties.exists(p => p.party == result.party) shouldBe false
finalParties.contains(result) shouldBe true
})
}
}

"work when allocating a party without a hint or display name" in allFixtures { c =>
val client = partyManagementClient(c.partyManagementService)

for {
initialParties <- client.listKnownParties()
resultE <- withGrpcError(client.allocateParty(None, None))
finalParties <- client.listKnownParties()
} yield {
whenSuccessful(resultE)(result => {
// Note: the ledger may or may not assign a display name
initialParties.exists(p => p.party == result.party) shouldBe false
finalParties.contains(result) shouldBe true
})
}
}

"create unique party names when allocating many parties" in allFixtures { c =>
val client = partyManagementClient(c.partyManagementService)
val N = 100

for {
initialParties <- client.listKnownParties()
// Note: The following call concurrently creates N parties
resultEs <- Future.traverse(1 to N)(i =>
withGrpcError(client.allocateParty(None, Some(s"Test party $i"))))
finalParties <- client.listKnownParties()
} yield {

// Collect outcomes
val results = resultEs.foldRight((0, 0, List.empty[PartyDetails])) { (elem, acc) =>
elem match {
case Right(result) =>
acc.copy(_3 = result :: acc._3)
case Left(Status.Code.UNIMPLEMENTED) =>
acc.copy(_1 = acc._1 + 1)
case Left(Status.Code.INVALID_ARGUMENT) =>
acc.copy(_2 = acc._2 + 1)
case other =>
fail(s"Unexpected GRPC error code $other while allocating a party")
}
}

results match {
case (0, 0, result) =>
// All calls succeeded. None of the new parties must be present initially,
// all of them must be present at the end, and all new party names must be unique.
result.foreach(r => initialParties.exists(p => p.party == r.party) shouldBe false)
result.foreach(r => finalParties.contains(r) shouldBe true)
result.map(_.party).toSet.size shouldBe N
case (_, 0, Nil) =>
// All calls returned UNIMPLEMENTED
succeed
case (0, _, Nil) =>
// All calls returned INVALID_ARGUMENT
fail(s"Party allocation failed with INVALID_ARGUMENT.")
case (u, i, result) =>
// A party allocation call may fail, but they all should fail with the same error
fail(
s"Not all party allocation calls had the same outcome. $u UNIMPLEMENTED, $i INVALID_ARGUMENT, ${result.size} succeeded")
}
}
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ReflectionIT
for {
response <- execRequest(ledger, listServices)
} yield {
response.getListServicesResponse.getServiceCount shouldEqual 11
response.getListServicesResponse.getServiceCount shouldEqual 12
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.digitalasset.ledger.api.domain
import com.digitalasset.ledger.api.testing.utils.{MockMessages, Resource}
import com.digitalasset.ledger.api.v1.active_contracts_service.ActiveContractsServiceGrpc
import com.digitalasset.ledger.api.v1.active_contracts_service.ActiveContractsServiceGrpc.ActiveContractsService
import com.digitalasset.ledger.api.v1.admin.party_management_service.PartyManagementServiceGrpc
import com.digitalasset.ledger.api.v1.admin.party_management_service.PartyManagementServiceGrpc.PartyManagementService
import com.digitalasset.ledger.api.v1.command_completion_service.CommandCompletionServiceGrpc
import com.digitalasset.ledger.api.v1.command_completion_service.CommandCompletionServiceGrpc.CommandCompletionService
import com.digitalasset.ledger.api.v1.command_service.CommandServiceGrpc
Expand Down Expand Up @@ -88,6 +90,7 @@ trait LedgerContext {
def packageClient: PackageClient
def acsClient: ActiveContractSetClient
def reflectionService: ServerReflectionGrpc.ServerReflectionStub
def partyManagementService: PartyManagementService

/**
* resetService is protected on purpose, to disallow moving an instance of LedgerContext into an invalid state,
Expand Down Expand Up @@ -220,6 +223,9 @@ object LedgerContext {

override def reflectionService: ServerReflectionGrpc.ServerReflectionStub =
ServerReflectionGrpc.newStub(channel)

override def partyManagementService: PartyManagementService =
PartyManagementServiceGrpc.stub(channel)
}

object SingleChannelContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import scala.concurrent.Future
* Serves as a backend to implement
* [[com.digitalasset.ledger.api.v1.admin.party_management_service.PartyManagementServiceGrpc]]
*/
trait PartyManagementService {
trait IndexPartyManagementService {
def getParticipantId(): Future[ParticipantId]

def listParties(): Future[List[PartyDetails]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ trait IndexService
with ContractStore
with IdentityProvider
//with IndexTimeService //TODO: this needs some further discussion as the TimeService is actually optional
with PartyManagementService
with IndexPartyManagementService
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5311430af384b090e773afcb546c91eea8d8123316ceeaf7520e5161ebbeabb0
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

---------------------------------------------------------------------------------------------------
-- V4: List of parties
--
-- This schema version adds a table for tracking known parties.
-- In the sandbox, parties are added implicitly when they are first mentioned in a transaction,
-- or explicitly through an API call.
---------------------------------------------------------------------------------------------------



CREATE TABLE parties (
-- The unique identifier of the party
party varchar primary key not null,
-- A human readable name of the party, might not be unique
display_name varchar,
-- True iff the party was added explicitly through an API call
explicit bool not null,
-- For implicitly added parties: the offset of the transaction that introduced the party
-- For explicitly added parties: the ledger end at the time when the party was added
ledger_offset bigint
);

Loading

0 comments on commit 80e8ac1

Please sign in to comment.