diff --git a/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/TestUtil.scala b/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/TestUtil.scala
index 13f76c2ccf..77b7668c60 100644
--- a/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/TestUtil.scala
+++ b/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/TestUtil.scala
@@ -22,7 +22,8 @@ import com.digitalasset.ledger.api.v1.TransactionServiceOuterClass.{
import com.digitalasset.platform.common.LedgerIdMode
import com.digitalasset.platform.sandbox.config.SandboxConfig
import com.digitalasset.platform.sandbox.services.SandboxServerResource
-import com.digitalasset.platform.services.time.{TimeModel, TimeProviderType}
+import com.digitalasset.platform.services.time.TimeProviderType
+import com.daml.ledger.participant.state.v1.TimeModel
import io.grpc.Channel
import org.scalatest.Assertion
diff --git a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/server/services/command/time/TimeModelValidator.scala b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/server/services/command/time/TimeModelValidator.scala
index 87127958b0..48b122fecb 100644
--- a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/server/services/command/time/TimeModelValidator.scala
+++ b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/server/services/command/time/TimeModelValidator.scala
@@ -7,7 +7,6 @@ import java.time.{Duration, Instant}
import com.daml.ledger.participant.state.v1.TimeModel
import com.digitalasset.platform.server.api.validation.ErrorFactories
-import com.digitalasset.platform.services.time.TimeModelChecker
import io.grpc.Status
import scala.util.{Failure, Success, Try}
@@ -17,13 +16,11 @@ import scala.util.{Failure, Success, Try}
*/
final case class TimeModelValidator(model: TimeModel) extends ErrorFactories {
- private val timeModelChecker = TimeModelChecker(model)
-
/**
* Wraps [[model.checkTtl]] with a StatusRuntimeException wrapper.
*/
def checkTtl(givenLedgerEffectiveTime: Instant, givenMaximumRecordTime: Instant): Try[Unit] = {
- if (timeModelChecker.checkTtl(givenLedgerEffectiveTime, givenMaximumRecordTime))
+ if (model.checkTtl(givenLedgerEffectiveTime, givenMaximumRecordTime))
Success(())
else {
val givenTtl = Duration.between(givenLedgerEffectiveTime, givenMaximumRecordTime)
@@ -46,10 +43,7 @@ final case class TimeModelValidator(model: TimeModel) extends ErrorFactories {
applicationId: String): Try[Unit] =
for {
_ <- checkTtl(givenLedgerEffectiveTime, givenMaximumRecordTime)
- _ <- if (timeModelChecker.checkLet(
- currentTime,
- givenLedgerEffectiveTime,
- givenMaximumRecordTime))
+ _ <- if (model.checkLet(currentTime, givenLedgerEffectiveTime, givenMaximumRecordTime))
Success(())
else
Failure(
diff --git a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/services/time/TimeModel.scala b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/services/time/TimeModel.scala
deleted file mode 100644
index 2d4aee15e4..0000000000
--- a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/services/time/TimeModel.scala
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) 2019 The DAML Authors. All rights reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package com.digitalasset.platform.services.time
-
-import com.daml.ledger.participant.state.v1.{TimeModel => ITimeModel}
-import com.daml.ledger.participant.state.v1.{TimeModelChecker => ITimeModelChecker}
-
-import java.time.{Duration, Instant}
-
-import scala.util.Try
-
-/**
- * The ledger time model and associated validations. Some values are given by constructor args; others are derived.
- *
- * @param minTransactionLatency The expected minimum latency of a transaction.
- * @param maxClockSkew The maximum allowed clock skew between the ledger and clients.
- * @param maxTtl The maximum allowed time to live for a transaction.
- * Must be greater than the derived minimum time to live.
- * @throws IllegalArgumentException if the parameters aren't valid
- */
-case class TimeModel private (
- val minTransactionLatency: Duration,
- val maxClockSkew: Duration,
- val maxTtl: Duration)
- extends ITimeModel {
-
- /**
- * The minimum time to live for a transaction. Equal to the minimum transaction latency plus the maximum clock skew.
- */
- val minTtl: Duration = minTransactionLatency.plus(maxClockSkew)
-
- /**
- * The maximum window after the current time when transaction ledger effective times will be accepted.
- * Currently equal to the max clock skew.
- *
- * The corresponding past acceptance window is given by the command's TTL, and thus bounded inclusive by [[maxTtl]].
- */
- val futureAcceptanceWindow: Duration = maxClockSkew
-
-}
-
-object TimeModel {
-
- /**
- * A default TimeModel that's reasonable for a test or sandbox ledger application.
- * Serious applications (viz. ledger) should probably specify their own TimeModel.
- */
- val reasonableDefault: TimeModel =
- TimeModel(Duration.ofSeconds(1L), Duration.ofSeconds(1L), Duration.ofSeconds(30L)).get
-
- def apply(
- minTransactionLatency: Duration,
- maxClockSkew: Duration,
- maxTtl: Duration): Try[TimeModel] = Try {
- require(!minTransactionLatency.isNegative, "Negative min transaction latency")
- require(!maxTtl.isNegative, "Negative max TTL")
- require(!maxClockSkew.isNegative, "Negative max clock skew")
- require(!maxTtl.minus(maxClockSkew).isNegative, "Max TTL must be greater than max clock skew")
- new TimeModel(minTransactionLatency, maxClockSkew, maxTtl)
- }
-}
-
-case class TimeModelChecker(timeModel: ITimeModel) extends ITimeModelChecker {
-
- import timeModel._
-
- override def checkTtl(
- givenLedgerEffectiveTime: Instant,
- givenMaximumRecordTime: Instant): Boolean = {
- val givenTtl = Duration.between(givenLedgerEffectiveTime, givenMaximumRecordTime)
- !givenTtl.minus(minTtl).isNegative && !maxTtl.minus(givenTtl).isNegative
- }
-
- override def checkLet(
- currentTime: Instant,
- givenLedgerEffectiveTime: Instant,
- givenMaximumRecordTime: Instant): Boolean = {
- // Note that, contrary to the documented spec, the record time of a transaction is when it's sequenced.
- // It turns out this isn't a problem for the participant or the sandbox,
- // and MRT seems to be going away in Sirius anyway, so I've left it as is.
- val lowerBound = givenLedgerEffectiveTime.minus(futureAcceptanceWindow)
- !currentTime.isBefore(lowerBound) && !currentTime.isAfter(givenMaximumRecordTime)
- }
-}
diff --git a/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/PlatformApplications.scala b/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/PlatformApplications.scala
index f64e2374ea..d9af5dc6a0 100644
--- a/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/PlatformApplications.scala
+++ b/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/PlatformApplications.scala
@@ -6,15 +6,14 @@ package com.digitalasset.platform
import java.io.File
import java.nio.file.Path
import java.time.Duration
-
import ch.qos.logback.classic.Level
import com.digitalasset.daml.bazeltools.BazelRunfiles._
import com.digitalasset.daml.lf.data.Ref
import com.digitalasset.ledger.api.auth.AuthService
import com.digitalasset.platform.common.LedgerIdMode
import com.digitalasset.platform.sandbox.config.{CommandConfiguration, SandboxConfig}
-import com.digitalasset.platform.services.time.{TimeModel, TimeProviderType}
-
+import com.digitalasset.platform.services.time.TimeProviderType
+import com.daml.ledger.participant.state.v1.TimeModel
import scala.concurrent.duration.{FiniteDuration, _}
import com.digitalasset.ledger.api.domain.LedgerId
import com.digitalasset.platform.apitesting.TestParties
diff --git a/ledger/participant-state/BUILD.bazel b/ledger/participant-state/BUILD.bazel
index f4b72ee483..e3953f4cfe 100644
--- a/ledger/participant-state/BUILD.bazel
+++ b/ledger/participant-state/BUILD.bazel
@@ -4,7 +4,7 @@
load(
"//bazel_tools:scala.bzl",
"da_scala_library",
- "da_scala_test_suite",
+ "da_scala_test",
)
da_scala_library(
@@ -15,6 +15,9 @@ da_scala_library(
visibility = [
"//visibility:public",
],
+ exports = [
+ "//ledger/participant-state/protobuf:ledger_configuration_java_proto",
+ ],
runtime_deps = [],
deps = [
"//daml-lf/archive:daml_lf_dev_archive_java_proto",
@@ -23,6 +26,7 @@ da_scala_library(
"//daml-lf/transaction:transaction_java_proto",
"//daml-lf/transaction:value_java_proto",
"//ledger/ledger-api-domain",
+ "//ledger/participant-state/protobuf:ledger_configuration_java_proto",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
"@maven//:com_typesafe_akka_akka_actor_2_12",
@@ -32,3 +36,15 @@ da_scala_library(
"@maven//:io_grpc_grpc_services",
],
)
+
+da_scala_test(
+ name = "participant-state-tests",
+ size = "small",
+ srcs = glob(["src/test/suite/**/*.scala"]),
+ resources = glob(["src/test/resources/*"]),
+ deps = [
+ ":participant-state",
+ "//bazel_tools/runfiles:scala_runfiles",
+ "@maven//:org_scalatest_scalatest_2_12",
+ ],
+)
diff --git a/ledger/participant-state/kvutils/BUILD.bazel b/ledger/participant-state/kvutils/BUILD.bazel
index 329fa16e86..2251d532a2 100644
--- a/ledger/participant-state/kvutils/BUILD.bazel
+++ b/ledger/participant-state/kvutils/BUILD.bazel
@@ -31,6 +31,7 @@ da_scala_library(
"//ledger/ledger-api-common",
"//ledger/ledger-api-domain",
"//ledger/participant-state",
+ "//ledger/participant-state/protobuf:ledger_configuration_java_proto",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
"@maven//:com_typesafe_akka_akka_actor_2_12",
@@ -67,6 +68,7 @@ da_scala_test(
"//ledger/ledger-api-common",
"//ledger/ledger-api-domain",
"//ledger/participant-state",
+ "//ledger/participant-state/protobuf:ledger_configuration_java_proto",
"@maven//:ch_qos_logback_logback_classic",
"@maven//:ch_qos_logback_logback_core",
"@maven//:com_google_protobuf_protobuf_java",
@@ -83,10 +85,12 @@ da_scala_test(
proto_library(
name = "daml_kvutils_proto",
srcs = ["src/main/protobuf/daml_kvutils.proto"],
+ strip_import_prefix = "src/main/protobuf/",
deps = [
"//daml-lf/archive:daml_lf_dev_archive_proto",
"//daml-lf/transaction:transaction_proto",
"//daml-lf/transaction:value_proto",
+ "//ledger/participant-state/protobuf:ledger_configuration_proto",
"@com_google_protobuf//:duration_proto",
"@com_google_protobuf//:empty_proto",
"@com_google_protobuf//:timestamp_proto",
diff --git a/ledger/participant-state/kvutils/src/main/protobuf/daml_kvutils.proto b/ledger/participant-state/kvutils/src/main/protobuf/daml_kvutils.proto
index 0aeba74478..ced510ce6b 100644
--- a/ledger/participant-state/kvutils/src/main/protobuf/daml_kvutils.proto
+++ b/ledger/participant-state/kvutils/src/main/protobuf/daml_kvutils.proto
@@ -20,6 +20,7 @@ import "google/protobuf/duration.proto";
import "com/digitalasset/daml_lf_dev/daml_lf.proto";
import "com/digitalasset/daml/lf/transaction.proto";
import "com/digitalasset/daml/lf/value.proto";
+import "com/daml/ledger/participant/state/ledger_configuration.proto";
// Envelope with which we wrap all kvutils messages that are sent over the network
@@ -249,12 +250,15 @@ message DamlConfigurationSubmission {
// Implementers are free to select adequate mechanism e.g. UUID or similar.
string submission_id = 1;
+ // Submitting participant's id.
+ string participant_id = 2;
+
// The maximum record time after which the submission will be rejected.
// Allows submitter to control when the request times out and to retry.
- google.protobuf.Timestamp maximum_record_time = 2;
+ google.protobuf.Timestamp maximum_record_time = 3;
// The new configuration that replaces the current configuration.
- DamlConfiguration configuration = 3;
+ com.daml.ledger.participant.state.LedgerConfiguration configuration = 4;
}
// A log entry describing a rejected configuration change.
@@ -263,8 +267,11 @@ message DamlConfigurationRejectionEntry {
// request with the result.
string submission_id = 1;
+ // Submitting participant's id.
+ string participant_id = 2;
+
// The new proposed configuration that was rejected.
- DamlConfiguration configuration = 2;
+ com.daml.ledger.participant.state.LedgerConfiguration configuration = 3;
// A mismatch in the configuration generation, that is, the
// new configuration did not carry a generation that was one
@@ -291,53 +298,26 @@ message DamlConfigurationRejectionEntry {
}
oneof reason {
- ParticipantNotAuthorized participant_not_authorized = 3;
- GenerationMismatch generation_mismatch = 4;
- InvalidConfiguration invalid_configuration = 5;
- TimedOut timed_out = 6;
+ ParticipantNotAuthorized participant_not_authorized = 4;
+ GenerationMismatch generation_mismatch = 5;
+ InvalidConfiguration invalid_configuration = 6;
+ TimedOut timed_out = 7;
}
}
+// Configuration entry that records a configuration change.
+// Also used in state to look up latest configuration.
+// When a configuration exists, only the participant that
+// submitted previously can change it.
message DamlConfigurationEntry {
// The submission from which this configuration originated.
string submission_id = 1;
+ // Submitting participant's id.
+ string participant_id = 2;
+
// The ledger configuration.
- DamlConfiguration configuration = 2;
-}
-
-message DamlConfiguration {
- // The configuration generation. If submitting a configuration the new generation
- // must be one larger than previous configuration. This safe-guards against
- // configuration changes that are based upon stale data.
- int64 generation = 1;
-
- // The ledger time model, specifying the bounds for
- // ledger effective time and maximum record time of transactions.
- DamlTimeModel time_model = 2;
-
- // The identity of the participant that is allowed to change
- // the ledger configuration.
- // If unset the configuration can be changed by anyone.
- string authorized_participant_id = 3;
-
- // If the "open world" flag is true, then party allocations are not
- // explicitly required. That is, a submission from a party that is
- // not allocated is allowed. This setting is useful when testing.
- bool open_world = 4;
-}
-
-
-message DamlTimeModel {
- // The expected minimum latency of a transaction.
- google.protobuf.Duration min_transaction_latency = 1;
-
- // The maximum allowed clock skew between the ledger and clients.
- google.protobuf.Duration max_clock_skew = 2;
-
- // The maximum allowed time to live for a transaction.
- // Must be greater than the derived minimum time to live.
- google.protobuf.Duration max_ttl = 3;
+ com.daml.ledger.participant.state.LedgerConfiguration configuration = 3;
}
// An allocation of party name and assignment of a party to a given
@@ -414,7 +394,7 @@ message DamlStateValue {
DamlCommandDedupValue command_dedup = 3;
DamlPartyAllocation party = 4;
DamlContractKeyState contract_key_state = 5;
- DamlConfiguration configuration = 6;
+ DamlConfigurationEntry configuration_entry = 6;
}
}
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/backport/TimeModel.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/backport/TimeModel.scala
deleted file mode 100644
index d9ca2166a6..0000000000
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/backport/TimeModel.scala
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) 2019 The DAML Authors. All rights reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package com.daml.ledger.participant.state.backport
-
-import java.time.{Duration, Instant}
-
-import com.daml.ledger.participant.state.v1.{
- TimeModel => ITimeModel,
- TimeModelChecker => ITimeModelChecker
-}
-
-import scala.util.Try
-
-/**
- * The ledger time model and associated validations. Some values are given by constructor args; others are derived.
- *
- * @param minTransactionLatency The expected minimum latency of a transaction.
- * @param maxClockSkew The maximum allowed clock skew between the ledger and clients.
- * @param maxTtl The maximum allowed time to live for a transaction.
- * Must be greater than the derived minimum time to live.
- * @throws IllegalArgumentException if the parameters aren't valid
- */
-case class TimeModel private (
- minTransactionLatency: Duration,
- maxClockSkew: Duration,
- maxTtl: Duration)
- extends ITimeModel {
-
- /**
- * The minimum time to live for a transaction. Equal to the minimum transaction latency plus the maximum clock skew.
- */
- val minTtl: Duration = minTransactionLatency.plus(maxClockSkew)
-
- /**
- * The maximum window after the current time when transaction ledger effective times will be accepted.
- * Currently equal to the max clock skew.
- *
- * The corresponding past acceptance window is given by the command's TTL, and thus bounded inclusive by [[maxTtl]].
- */
- val futureAcceptanceWindow: Duration = maxClockSkew
-
-}
-
-object TimeModel {
-
- /**
- * A default TimeModel that's reasonable for a test or sandbox ledger application.
- * Serious applications (viz. ledger) should probably specify their own TimeModel.
- */
- val reasonableDefault: TimeModel =
- TimeModel(Duration.ofSeconds(1L), Duration.ofSeconds(1L), Duration.ofSeconds(30L)).get
-
- def apply(
- minTransactionLatency: Duration,
- maxClockSkew: Duration,
- maxTtl: Duration): Try[TimeModel] = Try {
- require(!minTransactionLatency.isNegative, "Negative min transaction latency")
- require(!maxTtl.isNegative, "Negative max TTL")
- require(!maxClockSkew.isNegative, "Negative max clock skew")
- require(!maxTtl.minus(maxClockSkew).isNegative, "Max TTL must be greater than max clock skew")
- new TimeModel(minTransactionLatency, maxClockSkew, maxTtl)
- }
-}
-
-case class TimeModelChecker(timeModel: ITimeModel) extends ITimeModelChecker {
-
- import timeModel._
-
- override def checkTtl(
- givenLedgerEffectiveTime: Instant,
- givenMaximumRecordTime: Instant): Boolean = {
- val givenTtl = Duration.between(givenLedgerEffectiveTime, givenMaximumRecordTime)
- !givenTtl.minus(minTtl).isNegative && !maxTtl.minus(givenTtl).isNegative
- }
-
- override def checkLet(
- currentTime: Instant,
- givenLedgerEffectiveTime: Instant,
- givenMaximumRecordTime: Instant): Boolean = {
- // Note that, contrary to the documented spec, the record time of a transaction is when it's sequenced.
- // It turns out this isn't a problem for the participant or the sandbox,
- // and MRT seems to be going away in Sirius anyway, so I've left it as is.
- val lowerBound = givenLedgerEffectiveTime.minus(futureAcceptanceWindow)
- !currentTime.isBefore(lowerBound) && !currentTime.isAfter(givenMaximumRecordTime)
- }
-}
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala
index f8bccfa5ef..f11c296b3a 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala
@@ -5,14 +5,8 @@ package com.daml.ledger.participant.state.kvutils
import java.time.{Duration, Instant}
-import com.daml.ledger.participant.state.backport.TimeModel
import com.daml.ledger.participant.state.kvutils.DamlKvutils._
-import com.daml.ledger.participant.state.v1.{
- Configuration,
- PackageId,
- SubmittedTransaction,
- SubmitterInfo
-}
+import com.daml.ledger.participant.state.v1.{PackageId, SubmittedTransaction, SubmitterInfo}
import com.digitalasset.daml.lf.data.Ref.{ContractIdString, LedgerString, Party}
import com.digitalasset.daml.lf.data.Time
import com.digitalasset.daml.lf.transaction.Node.GlobalKey
@@ -29,7 +23,7 @@ import com.digitalasset.daml.lf.value.{Value, ValueCoder, ValueOuterClass}
import com.google.common.io.BaseEncoding
import com.google.protobuf.{ByteString, Empty}
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
/** Internal utilities for converting between protobuf messages and our scala
* data structures.
@@ -172,51 +166,6 @@ private[kvutils] object Conversions {
maxRecordTime = parseTimestamp(subInfo.getMaximumRecordTime)
)
- def buildDamlConfiguration(config: Configuration): DamlConfiguration = {
- val tm = config.timeModel
- DamlConfiguration.newBuilder
- .setGeneration(config.generation)
- .setAuthorizedParticipantId(config.authorizedParticipantId.fold("")(identity))
- .setOpenWorld(config.openWorld)
- .setTimeModel(
- DamlTimeModel.newBuilder
- .setMaxClockSkew(buildDuration(tm.maxClockSkew))
- .setMinTransactionLatency(buildDuration(tm.minTransactionLatency))
- .setMaxTtl(buildDuration(tm.maxTtl))
- )
- .build
- }
-
- def parseDamlConfiguration(config: DamlConfiguration): Try[Configuration] =
- for {
- tm <- if (config.hasTimeModel)
- Success(config.getTimeModel)
- else
- Failure(Err.DecodeError("Configuration", "No time model"))
- parsedTM <- TimeModel(
- maxClockSkew = parseDuration(tm.getMaxClockSkew),
- minTransactionLatency = parseDuration(tm.getMinTransactionLatency),
- maxTtl = parseDuration(tm.getMaxTtl)
- )
- authPidString = config.getAuthorizedParticipantId
- authPid <- if (authPidString.isEmpty)
- Success(None)
- else
- LedgerString
- .fromString(config.getAuthorizedParticipantId)
- .fold(
- err => Failure(Err.DecodeError("Configuration", err)),
- ls => Success(Some(ls))
- )
-
- parsedConfig = Configuration(
- generation = config.getGeneration,
- timeModel = parsedTM,
- authorizedParticipantId = authPid,
- openWorld = config.getOpenWorld
- )
- } yield parsedConfig
-
def buildTimestamp(ts: Time.Timestamp): com.google.protobuf.Timestamp = {
val instant = ts.toInstant
com.google.protobuf.Timestamp.newBuilder
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantState.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantState.scala
index 372434c27b..4bb14a2869 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantState.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantState.scala
@@ -14,7 +14,6 @@ import akka.actor.{Actor, ActorSystem, PoisonPill, Props}
import akka.pattern.gracefulStop
import akka.stream.Materializer
import akka.stream.scaladsl.{Sink, Source}
-import com.daml.ledger.participant.state.backport.TimeModel
import com.daml.ledger.participant.state.kvutils.{DamlKvutils => Proto}
import com.daml.ledger.participant.state.v1.{UploadPackagesResult, _}
import com.digitalasset.daml.lf.data.Ref
@@ -96,8 +95,7 @@ object InMemoryKVParticipantState {
class InMemoryKVParticipantState(
val participantId: ParticipantId,
val ledgerId: LedgerString = Ref.LedgerString.assertFromString(UUID.randomUUID.toString),
- file: Option[File] = None,
- openWorld: Boolean = true)(implicit system: ActorSystem, mat: Materializer)
+ file: Option[File] = None)(implicit system: ActorSystem, mat: Materializer)
extends ReadService
with WriteService
with AutoCloseable {
@@ -111,9 +109,7 @@ class InMemoryKVParticipantState(
// The initial ledger configuration
private val initialLedgerConfig = Configuration(
generation = 0,
- timeModel = TimeModel.reasonableDefault,
- authorizedParticipantId = Some(participantId),
- openWorld = openWorld
+ timeModel = TimeModel.reasonableDefault
)
// DAML Engine for transaction validation.
@@ -252,7 +248,7 @@ class InMemoryKVParticipantState(
case Right(_) => sys.error("Unexpected message in envelope")
}
val state = stateRef
- val newRecordTime = getNewRecordTime()
+ val newRecordTime = getNewRecordTime
if (state.store.contains(entryId.getEntryId)) {
// The entry identifier already in use, drop the message and let the
@@ -320,7 +316,7 @@ class InMemoryKVParticipantState(
// This source stops when the actor dies.
val _ = Source
.tick(HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, ())
- .map(_ => CommitHeartbeat(getNewRecordTime()))
+ .map(_ => CommitHeartbeat(getNewRecordTime))
.to(Sink.actorRef(actorRef, onCompleteMessage = ()))
.run()
@@ -548,7 +544,7 @@ class InMemoryKVParticipantState(
* at which this class has been instantiated.
*/
private val initialConditions =
- LedgerInitialConditions(ledgerId, initialLedgerConfig, getNewRecordTime())
+ LedgerInitialConditions(ledgerId, initialLedgerConfig, getNewRecordTime)
/** Get a new record time for the ledger from the system clock.
* Public for use from integration tests.
@@ -563,7 +559,8 @@ class InMemoryKVParticipantState(
config: Configuration): CompletionStage[SubmissionResult] =
CompletableFuture.completedFuture({
val submission =
- KeyValueSubmission.configurationToSubmission(maxRecordTime, submissionId, config)
+ KeyValueSubmission
+ .configurationToSubmission(maxRecordTime, submissionId, participantId, config)
commitActorRef ! CommitSubmission(
allocateEntryId,
Envelope.enclose(submission)
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueCommitting.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueCommitting.scala
index 1fdd18363e..68e68c15bb 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueCommitting.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueCommitting.scala
@@ -55,6 +55,10 @@ object KeyValueCommitting {
* Resolved input state specified in submission. Optional to mark that input state was resolved
* but not present. Specifically we require the command de-duplication input to be resolved, but don't
* expect to be present.
+ * We also do not trust the submitter to provide the correct list of input keys and we need
+ * to verify that an input actually does not exist and was not just included in inputs.
+ * For example when committing a configuration we need the current configuration to authorize
+ * the submission.
* @return Log entry to be committed and the DAML state updates to be applied.
*/
@throws(classOf[Err])
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala
index f99e4be4c8..c803c44f43 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala
@@ -7,7 +7,6 @@ import com.daml.ledger.participant.state.kvutils.Conversions._
import com.daml.ledger.participant.state.kvutils.DamlKvutils._
import com.daml.ledger.participant.state.v1._
import com.digitalasset.daml.lf.data.Ref
-import com.digitalasset.daml.lf.data.Ref.{LedgerString, Party}
import com.digitalasset.daml.lf.data.Time.Timestamp
import com.digitalasset.ledger.api.domain.PartyDetails
import com.google.common.io.BaseEncoding
@@ -30,16 +29,17 @@ object KeyValueConsumption {
def packDamlLogEntry(entry: DamlStateKey): ByteString = entry.toByteString
def unpackDamlLogEntry(bytes: ByteString): DamlLogEntry = DamlLogEntry.parseFrom(bytes)
- /** Construct a participant-state [[Update]] from a [[DamlLogEntry]].
+ /** Construct participant-state [[Update]]s from a [[DamlLogEntry]].
+ * Throws [[Err]] exception on badly formed data.
*
* This method is expected to be used to implement [[com.daml.ledger.participant.state.v1.ReadService.stateUpdates]].
*
* @param entryId: The log entry identifier.
* @param entry: The log entry.
- * @return [[Update]] constructed from log entry.
+ * @return [[Update]]s constructed from log entry.
*/
+ @throws(classOf[Err])
def logEntryToUpdate(entryId: DamlLogEntryId, entry: DamlLogEntry): List[Update] = {
-
val recordTime = parseTimestamp(entry.getRecordTime)
entry.getPayloadCase match {
@@ -50,7 +50,7 @@ object KeyValueConsumption {
if (entry.getPackageUploadEntry.getSourceDescription.nonEmpty)
Some(entry.getPackageUploadEntry.getSourceDescription)
else None,
- Ref.LedgerString.assertFromString(entry.getPackageUploadEntry.getParticipantId),
+ parseLedgerString("ParticipantId")(entry.getPackageUploadEntry.getParticipantId),
recordTime
)
}(breakOut)
@@ -59,13 +59,11 @@ object KeyValueConsumption {
List.empty
case DamlLogEntry.PayloadCase.PARTY_ALLOCATION_ENTRY =>
+ val pae = entry.getPartyAllocationEntry
+ val party = parseParty(pae.getParty)
+ val participantId = parseLedgerString("ParticipantId")(pae.getParticipantId)
List(
- Update.PartyAddedToParticipant(
- Party.assertFromString(entry.getPartyAllocationEntry.getParty),
- entry.getPartyAllocationEntry.getDisplayName,
- Ref.LedgerString.assertFromString(entry.getPartyAllocationEntry.getParticipantId),
- recordTime
- )
+ Update.PartyAddedToParticipant(party, pae.getDisplayName, participantId, recordTime)
)
case DamlLogEntry.PayloadCase.PARTY_ALLOCATION_REJECTION_ENTRY =>
@@ -76,18 +74,36 @@ object KeyValueConsumption {
case DamlLogEntry.PayloadCase.CONFIGURATION_ENTRY =>
val configEntry = entry.getConfigurationEntry
- val newConfig = parseDamlConfiguration(configEntry.getConfiguration).get
- List(Update.ConfigurationChanged(configEntry.getSubmissionId, newConfig))
+ val newConfig = Configuration
+ .decode(configEntry.getConfiguration)
+ .fold(err => throw Err.DecodeError("Configuration", err), identity)
+ val participantId =
+ parseLedgerString("ParticipantId")(configEntry.getParticipantId)
+ List(
+ Update.ConfigurationChanged(
+ recordTime,
+ configEntry.getSubmissionId,
+ participantId,
+ newConfig
+ )
+ )
case DamlLogEntry.PayloadCase.CONFIGURATION_REJECTION_ENTRY =>
val rejection = entry.getConfigurationRejectionEntry
- val proposedConfig = rejection.getConfiguration
+ val proposedConfig = Configuration
+ .decode(rejection.getConfiguration)
+ .fold(err => throw Err.DecodeError("Configuration", err), identity)
+ val participantId =
+ parseLedgerString("ParticipantId")(rejection.getParticipantId)
List(
Update.ConfigurationChangeRejected(
+ recordTime = recordTime,
submissionId = rejection.getSubmissionId,
- reason = rejection.getReasonCase match {
+ participantId = participantId,
+ proposedConfiguration = proposedConfig,
+ rejectionReason = rejection.getReasonCase match {
case DamlConfigurationRejectionEntry.ReasonCase.GENERATION_MISMATCH =>
- s"Generation mismatch: ${proposedConfig.getGeneration} != ${rejection.getGenerationMismatch.getExpectedGeneration}"
+ s"Generation mismatch: ${proposedConfig.generation} != ${rejection.getGenerationMismatch.getExpectedGeneration}"
case DamlConfigurationRejectionEntry.ReasonCase.INVALID_CONFIGURATION =>
s"Invalid configuration: ${rejection.getInvalidConfiguration.getError}"
case DamlConfigurationRejectionEntry.ReasonCase.PARTICIPANT_NOT_AUTHORIZED =>
@@ -103,10 +119,12 @@ object KeyValueConsumption {
))
case DamlLogEntry.PayloadCase.TRANSACTION_REJECTION_ENTRY =>
- List(transactionRejectionEntryToUpdate(entry.getTransactionRejectionEntry))
+ List(
+ transactionRejectionEntryToUpdate(recordTime, entry.getTransactionRejectionEntry)
+ )
case DamlLogEntry.PayloadCase.PAYLOAD_NOT_SET =>
- sys.error("entryToUpdate: PAYLOAD_NOT_SET!")
+ throw Err.InternalError("logEntryToUpdate: PAYLOAD_NOT_SET!")
}
}
@@ -148,7 +166,7 @@ object KeyValueConsumption {
entry.getPartyAllocationEntry.getSubmissionId,
PartyAllocationResult.Ok(
PartyDetails(
- Party.assertFromString(entry.getPartyAllocationEntry.getParty),
+ parseParty(entry.getPartyAllocationEntry.getParty),
if (entry.getPartyAllocationEntry.getDisplayName.isEmpty)
None
else
@@ -178,14 +196,15 @@ object KeyValueConsumption {
None
case DamlLogEntry.PayloadCase.PAYLOAD_NOT_SET =>
- sys.error("logEntryToAsyncResponse: PAYLOAD_NOT_SET!")
+ throw Err.InternalError("logEntryToAsyncResponse: PAYLOAD_NOT_SET!")
}
}
private def transactionRejectionEntryToUpdate(
- rejEntry: DamlTransactionRejectionEntry): Update.CommandRejected = {
-
+ recordTime: Timestamp,
+ rejEntry: DamlTransactionRejectionEntry): Update.CommandRejected =
Update.CommandRejected(
+ recordTime = recordTime,
submitterInfo = parseSubmitterInfo(rejEntry.getSubmitterInfo),
reason = rejEntry.getReasonCase match {
case DamlTransactionRejectionEntry.ReasonCase.DISPUTED =>
@@ -205,10 +224,9 @@ object KeyValueConsumption {
rejEntry.getSubmitterCannotActViaParticipant.getDetails
)
case DamlTransactionRejectionEntry.ReasonCase.REASON_NOT_SET =>
- sys.error("transactionRejectionEntryToUpdate: REASON_NOT_SET!")
+ throw Err.InternalError("transactionRejectionEntryToUpdate: REASON_NOT_SET!")
}
)
- }
private def partyRejectionEntryToAsyncResponse(
rejEntry: DamlPartyAllocationRejectionEntry): PartyAllocationResponse = {
@@ -223,7 +241,7 @@ object KeyValueConsumption {
case DamlPartyAllocationRejectionEntry.ReasonCase.PARTICIPANT_NOT_AUTHORIZED =>
PartyAllocationResult.ParticipantNotAuthorized
case DamlPartyAllocationRejectionEntry.ReasonCase.REASON_NOT_SET =>
- sys.error("partyRejectionEntryToUpdate: REASON_NOT_SET!")
+ throw Err.InternalError("partyRejectionEntryToUpdate: REASON_NOT_SET!")
}
)
}
@@ -239,7 +257,7 @@ object KeyValueConsumption {
case DamlPackageUploadRejectionEntry.ReasonCase.PARTICIPANT_NOT_AUTHORIZED =>
UploadPackagesResult.ParticipantNotAuthorized
case DamlPackageUploadRejectionEntry.ReasonCase.REASON_NOT_SET =>
- sys.error("rejectionEntryToUpdate: REASON_NOT_SET!")
+ throw Err.InternalError("rejectionEntryToUpdate: REASON_NOT_SET!")
}
)
}
@@ -250,14 +268,16 @@ object KeyValueConsumption {
txEntry: DamlTransactionEntry,
recordTime: Timestamp): Update.TransactionAccepted = {
val relTx = Conversions.decodeTransaction(txEntry.getTransaction)
- val hexTxId = LedgerString.assertFromString(BaseEncoding.base16.encode(entryId.toByteArray))
-
+ val hexTxId = parseLedgerString("TransactionId")(
+ BaseEncoding.base16.encode(entryId.toByteArray)
+ )
Update.TransactionAccepted(
optSubmitterInfo = Some(parseSubmitterInfo(txEntry.getSubmitterInfo)),
transactionMeta = TransactionMeta(
ledgerEffectiveTime = parseTimestamp(txEntry.getLedgerEffectiveTime),
- workflowId =
- Some(txEntry.getWorkflowId).filter(_.nonEmpty).map(LedgerString.assertFromString),
+ workflowId = Some(txEntry.getWorkflowId)
+ .filter(_.nonEmpty)
+ .map(parseLedgerString("WorkflowId")),
),
transaction = makeCommittedTransaction(entryId, relTx),
transactionId = hexTxId,
@@ -277,4 +297,16 @@ object KeyValueConsumption {
)
}
+ @throws(classOf[Err])
+ private def parseLedgerString(what: String)(s: String): Ref.LedgerString =
+ Ref.LedgerString
+ .fromString(s)
+ .fold(err => throw Err.DecodeError(what, "Cannot parse '$s': $err"), identity)
+
+ @throws(classOf[Err])
+ private def parseParty(s: String): Ref.Party =
+ Ref.Party
+ .fromString(s)
+ .fold(err => throw Err.DecodeError("Party", "Cannot parse '$s': $err"), identity)
+
}
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueSubmission.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueSubmission.scala
index df3f802607..50c5cbe6d8 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueSubmission.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueSubmission.scala
@@ -112,6 +112,7 @@ object KeyValueSubmission {
def configurationToSubmission(
maxRecordTime: Timestamp,
submissionId: String,
+ participantId: String,
config: Configuration): DamlSubmission = {
val tm = config.timeModel
DamlSubmission.newBuilder
@@ -119,8 +120,9 @@ object KeyValueSubmission {
.setConfigurationSubmission(
DamlConfigurationSubmission.newBuilder
.setSubmissionId(submissionId)
+ .setParticipantId(participantId)
.setMaximumRecordTime(buildTimestamp(maxRecordTime))
- .setConfiguration(buildDamlConfiguration(config))
+ .setConfiguration(Configuration.encode(config))
)
.build
}
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Version.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Version.scala
index 064e39e222..3d3064449c 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Version.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Version.scala
@@ -7,16 +7,21 @@ package com.daml.ledger.participant.state.kvutils
* and the changelog of kvutils.
*
* Changes:
- * [since 100.13.29]:
+ * [after 100.13.37]:
+ * - Removed DamlConfiguration in favour of participant-state's LedgerConfiguration.
+ * - Authorization of configuration changes is now based on validating against the participant id
+ * of the previously submitted configuration.
+ *
+ * [after 100.13.29]:
* - Add support for ledger dumps via environment variable: "KVUTILS_LEDGER_DUMP=/tmp/ledger.dump".
* - Add integrity checker tool to verify ledger dumps for validating compatibility of new versions.
*
- * [since 100.13.26]:
+ * [after 100.13.26]:
* - Added metrics to track submission processing.
* - Use InsertOrdMap to store resulting state in kvutils for deterministic ordering of state key-values.
* - Fix bug with transient contract keys, e.g. keys created and archived in same transaction.
*
- * [since 100.13.21]:
+ * [after 100.13.21]:
* - Added 'Envelope' for compressing and versioning kvutils messages that are transmitted
* or stored on disk. [[Envelope.enclose]] and [[Envelope.open]] should be now used for
* submissions and for results from processing them.
@@ -24,7 +29,7 @@ package com.daml.ledger.participant.state.kvutils
* time model is being redesigned and the checks will be reimplemented once we have
* the new design.
*
- * [since 100.13.16]: *BACKWARDS INCOMPATIBLE*
+ * [after 100.13.16]: *BACKWARDS INCOMPATIBLE*
* - Log entries are no longer used as inputs to submission processing. The
* contract instance is now stored within DamlContractStatus.
* - Configuration extended with "Open World" flag that defines whether
@@ -36,5 +41,11 @@ package com.daml.ledger.participant.state.kvutils
* - Bug in command deduplication fixed: rejected commands are now deduplicated correctly.
*/
object Version {
+
+ /** The kvutils version number. Packing kvutils messages into envelopes carries the version number.
+ * Version should be incremented when semantics of fields change and migration of data is required or
+ * when the protobuf default value for a field is insufficient and must be filled in during decoding.
+ * Handling of older versions is handled by [[Envelope.open]] which performs the migration to latest version.
+ */
val version: Long = 0
}
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/PackageCommitter.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/PackageCommitter.scala
index d570c2baa7..4a514a39b0 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/PackageCommitter.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/PackageCommitter.scala
@@ -130,7 +130,7 @@ private[kvutils] case class PackageCommitter(engine: Engine)
*/
private def preload(submissionId: String, archives: Iterable[Archive]): Runnable = { () =>
val ctx = Metrics.preloadTimer.time()
- def trace(msg: String): Unit = logger.trace("[submissionId=$submissionId]: " + msg)
+ def trace(msg: String): Unit = logger.trace(s"[submissionId=$submissionId]: " + msg)
try {
val loadedPackages = engine.compiledPackages().packageIds
val packages: Map[Ref.PackageId, Ast.Package] = Metrics.decodeTimer.time { () =>
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/Common.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/Common.scala
index 81c1a3cd0d..2fa1aee017 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/Common.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/Common.scala
@@ -9,7 +9,8 @@ import com.codahale.metrics
import com.daml.ledger.participant.state.kvutils.DamlKvutils.{
DamlLogEntry,
DamlStateKey,
- DamlStateValue
+ DamlStateValue,
+ DamlConfigurationEntry
}
import com.daml.ledger.participant.state.kvutils.{Conversions, Err}
import com.daml.ledger.participant.state.v1.Configuration
@@ -173,17 +174,23 @@ private[kvutils] object Common {
def getCurrentConfiguration(
defaultConfig: Configuration,
inputState: Map[DamlStateKey, Option[DamlStateValue]],
- logger: Logger): Configuration =
+ logger: Logger): (Option[DamlConfigurationEntry], Configuration) =
inputState
- .get(Conversions.configurationStateKey)
- .flatten
+ .getOrElse(
+ Conversions.configurationStateKey,
+ /* If we're retrieving configuration, we require it to at least
+ * have been declared as an input by the submitter as it is used
+ * to authorize configuration changes. */
+ throw Err.MissingInputState(Conversions.configurationStateKey)
+ )
.flatMap { v =>
- Conversions
- .parseDamlConfiguration(v.getConfiguration)
+ val entry = v.getConfigurationEntry
+ Configuration
+ .decode(entry.getConfiguration)
.fold({ err =>
logger.error(s"Failed to parse configuration: $err, using default configuration.")
None
- }, Some(_))
+ }, conf => Some(Some(entry) -> conf))
}
- .getOrElse(defaultConfig)
+ .getOrElse(None -> defaultConfig)
}
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessConfigSubmission.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessConfigSubmission.scala
index 883a596524..a9ace8c655 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessConfigSubmission.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessConfigSubmission.scala
@@ -26,7 +26,7 @@ private[kvutils] case class ProcessConfigSubmission(
private implicit val logger =
LoggerFactory.getLogger(
s"ProcessConfigSubmission[entryId=${Pretty.prettyEntryId(entryId)}, submId=${configSubmission.getSubmissionId}]")
- private val currentConfig =
+ private val (currentConfigEntry, currentConfig) =
Common.getCurrentConfiguration(defaultConfig, inputState, logger)
private val newConfig = configSubmission.getConfiguration
@@ -41,7 +41,7 @@ private[kvutils] case class ProcessConfigSubmission(
)
}
- private def checkTtl(): Commit[Unit] = delay {
+ private val checkTtl: Commit[Unit] = delay {
// Check the maximum record time against the record time of the commit.
// This mechanism allows the submitter to detect lost submissions and retry
// with a submitter controlled rate.
@@ -49,128 +49,134 @@ private[kvutils] case class ProcessConfigSubmission(
if (recordTime > maxRecordTime) {
logger.warn(
s"Rejected configuration submission. The submission timed out ($recordTime > $maxRecordTime)")
- rejectTimedOut
+ reject(
+ _.setTimedOut(
+ DamlConfigurationRejectionEntry.TimedOut.newBuilder
+ .setMaximumRecordTime(configSubmission.getMaximumRecordTime)
+ ))
} else {
pass
}
}
- private def authorizeSubmission(): Commit[Unit] = delay {
+ private val authorizeSubmission: Commit[Unit] = delay {
// Submission is authorized when:
- // 1) The authorized participant is unset
- // 2) The authorized participant matches the submitting participant.
- val authorized =
- currentConfig.authorizedParticipantId
- .fold(true)(authPid => authPid == participantId)
+ // the provided participant id matches source participant id
+ // AND (
+ // there exists no current configuration
+ // OR the current configuration's participant matches the submitting participant.
+ // )
+ val submittingParticipantId = configSubmission.getParticipantId
+ val wellFormed = participantId == submittingParticipantId
- if (!authorized) {
+ val authorized =
+ currentConfigEntry.forall(_.getParticipantId == participantId)
+
+ if (!wellFormed) {
logger.warn(
- s"Rejected configuration submission. Authorized participant (${currentConfig.authorizedParticipantId}) does not match submitting participant $participantId.")
- rejectParticipantNotAuthorized
+ s"Rejected configuration submission. Submitting participant $submittingParticipantId does not match request participant $participantId")
+
+ reject(
+ _.setParticipantNotAuthorized(
+ DamlConfigurationRejectionEntry.ParticipantNotAuthorized.newBuilder
+ .setDetails(
+ s"Participant $participantId in request is not the submitting participant $submittingParticipantId"
+ )
+ ))
+ } else if (!authorized) {
+ logger.warn(s"Rejected configuration submission. $participantId is not authorized.")
+
+ reject(
+ _.setParticipantNotAuthorized(
+ DamlConfigurationRejectionEntry.ParticipantNotAuthorized.newBuilder
+ .setDetails(
+ "Participant $participantId is not an authorized participant"
+ )
+ ))
} else {
pass
}
}
- private def validateSubmission(): Commit[Unit] =
- parseDamlConfiguration(newConfig)
- .fold(exc => rejectInvalidConfiguration(exc.getMessage), pure)
+ private val validateSubmission: Commit[Unit] =
+ Configuration
+ .decode(newConfig)
+ .fold(
+ err =>
+ reject(
+ _.setInvalidConfiguration(
+ DamlConfigurationRejectionEntry.InvalidConfiguration.newBuilder
+ .setError(err))),
+ pure)
.flatMap { config =>
if (config.generation != (1 + currentConfig.generation))
- rejectGenerationMismatch(1 + currentConfig.generation)
+ reject(
+ _.setGenerationMismatch(DamlConfigurationRejectionEntry.GenerationMismatch.newBuilder
+ .setExpectedGeneration(1 + currentConfig.generation)))
else
pass
}
- private def buildLogEntry(): Commit[Unit] = sequence2(
- delay {
- Metrics.accepts.inc()
- logger.trace(s"New configuration with generation ${newConfig.getGeneration} accepted.")
- set(
- configurationStateKey ->
- DamlStateValue.newBuilder
- .setConfiguration(newConfig)
- .build)
- },
- done(
- DamlLogEntry.newBuilder
- .setRecordTime(buildTimestamp(recordTime))
- .setConfigurationEntry(DamlConfigurationEntry.newBuilder
- .setSubmissionId(configSubmission.getSubmissionId)
- .setConfiguration(configSubmission.getConfiguration))
- .build)
- )
+ private def buildLogEntry(): Commit[Unit] = {
+ val configEntry = DamlConfigurationEntry.newBuilder
+ .setSubmissionId(configSubmission.getSubmissionId)
+ .setParticipantId(participantId)
+ .setConfiguration(configSubmission.getConfiguration)
- private def rejectGenerationMismatch(expected: Long): Commit[Unit] = {
+ sequence2(
+ delay {
+ Metrics.accepts.inc()
+ logger.trace(s"New configuration with generation ${newConfig.getGeneration} accepted.")
+
+ set(
+ configurationStateKey ->
+ DamlStateValue.newBuilder
+ .setConfigurationEntry(configEntry)
+ .build)
+ },
+ done(
+ DamlLogEntry.newBuilder
+ .setRecordTime(buildTimestamp(recordTime))
+ .setConfigurationEntry(configEntry)
+ .build
+ )
+ )
+ }
+
+ private def rejectGenerationMismatch(expected: Long): Commit[Unit] =
+ reject {
+ _.setGenerationMismatch(
+ DamlConfigurationRejectionEntry.GenerationMismatch.newBuilder
+ .setExpectedGeneration(expected)
+ )
+ }
+
+ private def rejectTimedOut[A]: Commit[A] =
+ reject {
+ _.setTimedOut(
+ DamlConfigurationRejectionEntry.TimedOut.newBuilder
+ .setMaximumRecordTime(configSubmission.getMaximumRecordTime)
+ )
+ }
+
+ private def reject[A](
+ addReason: DamlConfigurationRejectionEntry.Builder => DamlConfigurationRejectionEntry.Builder)
+ : Commit[A] = {
Metrics.rejections.inc()
done(
DamlLogEntry.newBuilder
.setConfigurationRejectionEntry(
- DamlConfigurationRejectionEntry.newBuilder
- .setSubmissionId(configSubmission.getSubmissionId)
- .setConfiguration(configSubmission.getConfiguration)
- .setGenerationMismatch(
- DamlConfigurationRejectionEntry.GenerationMismatch.newBuilder
- .setExpectedGeneration(expected)
- )
+ addReason(
+ DamlConfigurationRejectionEntry.newBuilder
+ .setSubmissionId(configSubmission.getSubmissionId)
+ .setParticipantId(participantId)
+ .setConfiguration(configSubmission.getConfiguration)
+ )
)
.build
)
}
- private def rejectInvalidConfiguration(error: String): Commit[Configuration] = {
- Metrics.rejections.inc()
- done(
- DamlLogEntry.newBuilder
- .setConfigurationRejectionEntry(
- DamlConfigurationRejectionEntry.newBuilder
- .setSubmissionId(configSubmission.getSubmissionId)
- .setConfiguration(configSubmission.getConfiguration)
- .setInvalidConfiguration(
- DamlConfigurationRejectionEntry.InvalidConfiguration.newBuilder
- .setError(error)
- )
- )
- .build
- )
- }
-
- private def rejectParticipantNotAuthorized[A]: Commit[A] = {
- Metrics.rejections.inc()
- done(
- DamlLogEntry.newBuilder
- .setConfigurationRejectionEntry(
- DamlConfigurationRejectionEntry.newBuilder
- .setSubmissionId(configSubmission.getSubmissionId)
- .setConfiguration(configSubmission.getConfiguration)
- .setParticipantNotAuthorized(
- DamlConfigurationRejectionEntry.ParticipantNotAuthorized.newBuilder
- .setDetails(
- s"Participant $participantId is not the authorized participant " +
- currentConfig.authorizedParticipantId.getOrElse("")
- )
- )
- )
- .build
- )
- }
-
- private def rejectTimedOut[A]: Commit[A] = {
- Metrics.rejections.inc()
- done(
- DamlLogEntry.newBuilder
- .setConfigurationRejectionEntry(
- DamlConfigurationRejectionEntry.newBuilder
- .setSubmissionId(configSubmission.getSubmissionId)
- .setConfiguration(configSubmission.getConfiguration)
- .setTimedOut(
- DamlConfigurationRejectionEntry.TimedOut.newBuilder
- .setMaximumRecordTime(configSubmission.getMaximumRecordTime)
- )
- )
- .build
- )
- }
}
private[kvutils] object ProcessConfigSubmission {
diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessTransactionSubmission.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessTransactionSubmission.scala
index ee2b3ad018..db1f00f570 100644
--- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessTransactionSubmission.scala
+++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committing/ProcessTransactionSubmission.scala
@@ -5,7 +5,6 @@ package com.daml.ledger.participant.state.kvutils.committing
import com.codahale.metrics
import com.codahale.metrics.{Counter, Timer}
-import com.daml.ledger.participant.state.backport.TimeModelChecker
import com.daml.ledger.participant.state.kvutils.Conversions.{buildTimestamp, commandDedupKey, _}
import com.daml.ledger.participant.state.kvutils.DamlKvutils._
import com.daml.ledger.participant.state.kvutils.{Conversions, Err, InputsAndEffects, Pretty}
@@ -55,7 +54,7 @@ private[kvutils] case class ProcessTransactionSubmission(
// -------------------------------------------------------------------------------
- private val config: Configuration =
+ private val (_, config) =
Common.getCurrentConfiguration(defaultConfig, inputState, logger)
private val txLet = parseTimestamp(txEntry.getLedgerEffectiveTime)
@@ -120,19 +119,16 @@ private[kvutils] case class ProcessTransactionSubmission(
RejectionReason.SubmitterCannotActViaParticipant(
s"Party '$submitter' not hosted by participant $participantId"))
case None =>
- if (config.openWorld)
- pass
- else
- reject(RejectionReason.PartyNotKnownOnLedger)
+ reject(RejectionReason.PartyNotKnownOnLedger)
}
/** Validate ledger effective time and the command's time-to-live. */
private def validateLetAndTtl: Commit[Unit] = delay {
- val timeModelChecker = TimeModelChecker(config.timeModel)
+ val timeModel = config.timeModel
val givenLET = txLet.toInstant
val givenMRT = parseTimestamp(txEntry.getSubmitterInfo.getMaximumRecordTime).toInstant
- if (timeModelChecker.checkLet(
+ if (timeModel.checkLet(
currentTime = recordTime.toInstant,
givenLedgerEffectiveTime = givenLET,
givenMaximumRecordTime = givenMRT)
diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantStateIT.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantStateIT.scala
index cdaf098a7b..b8835d5c68 100644
--- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantStateIT.scala
+++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/InMemoryKVParticipantStateIT.scala
@@ -8,7 +8,6 @@ import java.time.Duration
import java.util.UUID
import akka.stream.scaladsl.Sink
-import com.daml.ledger.participant.state.backport.TimeModel
import com.daml.ledger.participant.state.kvutils.InMemoryKVParticipantStateIT._
import com.daml.ledger.participant.state.v1.Update.{PartyAddedToParticipant, PublicPackageUploaded}
import com.daml.ledger.participant.state.v1._
@@ -23,6 +22,7 @@ import com.digitalasset.ledger.api.testing.utils.AkkaBeforeAndAfterAll
import org.scalatest.Assertions._
import org.scalatest.{Assertion, AsyncWordSpec, BeforeAndAfterEach}
+import scala.concurrent.Future
import scala.compat.java8.FutureConverters._
import scala.util.Try
@@ -45,10 +45,14 @@ class InMemoryKVParticipantStateIT
super.afterEach()
}
- "In-memory implementation" should {
+ private def allocateParty(hint: String): Future[Party] = {
+ ps.allocateParty(Some(hint), None).toScala.flatMap {
+ case PartyAllocationResult.Ok(details) => Future.successful(details.party)
+ case err => Future.failed(new RuntimeException("failed to allocate party: $err"))
+ }
+ }
- // FIXME(JM): Setup fixture for the participant-state
- // creation & teardown!
+ "In-memory implementation" should {
"return initial conditions" in {
for {
@@ -204,52 +208,60 @@ class InMemoryKVParticipantStateIT
"provide update after transaction submission" in {
val rt = ps.getNewRecordTime()
- val updateResult = ps.stateUpdates(beginAfter = None).runWith(Sink.head)
-
- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction)
-
- updateResult.map {
- case (offset, _) =>
- assert(offset == Offset(Array(0L, 0L)))
+ for {
+ alice <- allocateParty("alice")
+ _ <- ps
+ .submitTransaction(submitterInfo(rt, alice), transactionMeta(rt), emptyTransaction)
+ .toScala
+ update <- ps.stateUpdates(beginAfter = None).drop(1).runWith(Sink.head)
+ } yield {
+ assert(update._1 == Offset(Array(1L, 0L)))
}
}
"reject duplicate commands" in {
val rt = ps.getNewRecordTime()
- val updatesResult = ps.stateUpdates(beginAfter = None).take(2).runWith(Sink.seq)
+ for {
+ alice <- allocateParty("alice")
+ _ <- ps
+ .submitTransaction(submitterInfo(rt, alice), transactionMeta(rt), emptyTransaction)
+ .toScala
+ _ <- ps
+ .submitTransaction(submitterInfo(rt, alice), transactionMeta(rt), emptyTransaction)
+ .toScala
+ updates <- ps.stateUpdates(beginAfter = None).take(3).runWith(Sink.seq)
+ } yield {
+ val (offset0, update0) = updates(0)
+ assert(offset0 == Offset(Array(0L, 0L)))
+ assert(update0.isInstanceOf[Update.PartyAddedToParticipant])
- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction)
- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction)
-
- updatesResult.map { updates =>
- val (offset1, update1) = updates.head
- val (offset2, update2) = updates(1)
- assert(offset1 == Offset(Array(0L, 0L)))
+ val (offset1, update1) = updates(1)
+ assert(offset1 == Offset(Array(1L, 0L)))
assert(update1.isInstanceOf[Update.TransactionAccepted])
- assert(offset2 == Offset(Array(1L, 0L)))
- assert(update2.isInstanceOf[Update.CommandRejected])
- assert(
- update2
- .asInstanceOf[Update.CommandRejected]
- .reason == RejectionReason.DuplicateCommand)
+ val (offset2, update2) = updates(2)
+ assert(offset2 == Offset(Array(2L, 0L)))
}
}
"return second update with beginAfter=0" in {
val rt = ps.getNewRecordTime()
-
- val updateResult =
- ps.stateUpdates(beginAfter = Some(Offset(Array(0L, 0L)))).runWith(Sink.head)
-
- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction)
- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction)
-
- updateResult.map {
- case (offset, update) =>
- assert(offset == Offset(Array(1L, 0L)))
- assert(update.isInstanceOf[Update.CommandRejected])
+ for {
+ alice <- allocateParty("alice") // offset now at [1,0]
+ _ <- ps
+ .submitTransaction(submitterInfo(rt, alice), transactionMeta(rt), emptyTransaction)
+ .toScala
+ _ <- ps
+ .submitTransaction(submitterInfo(rt, alice), transactionMeta(rt), emptyTransaction)
+ .toScala
+ offsetAndUpdate <- ps
+ .stateUpdates(beginAfter = Some(Offset(Array(1L, 0L))))
+ .runWith(Sink.head)
+ } yield {
+ val (offset, update) = offsetAndUpdate
+ assert(offset == Offset(Array(2L, 0L)))
+ assert(update.isInstanceOf[Update.CommandRejected])
}
}
@@ -266,47 +278,11 @@ class InMemoryKVParticipantStateIT
}
}
- "correctly implements open world tx submission authorization" in {
- val rt = ps.getNewRecordTime()
-
- val updatesResult = ps.stateUpdates(beginAfter = None).take(3).runWith(Sink.seq)
-
- for {
- // Submit without allocation in open world setting, expecting this to succeed.
- _ <- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction).toScala
-
- // Allocate a party and try the submission again with an allocated party.
- allocResult <- ps
- .allocateParty(
- None /* no name hint, implementation decides party name */,
- Some("Somebody"))
- .toScala
- _ <- assert(allocResult.isInstanceOf[PartyAllocationResult.Ok])
- _ <- ps
- .submitTransaction(
- submitterInfo(
- rt,
- party = allocResult.asInstanceOf[PartyAllocationResult.Ok].result.party),
- transactionMeta(rt),
- emptyTransaction)
- .toScala
- Seq((offset1, update1), (offset2, update2), (offset3, update3)) <- updatesResult
- } yield {
- assert(offset1 == Offset(Array(0, 0)))
- assert(update1.isInstanceOf[Update.TransactionAccepted])
-
- assert(offset2 == Offset(Array(1, 0)))
- assert(update2.isInstanceOf[Update.PartyAddedToParticipant])
-
- assert(offset3 == Offset(Array(2, 0)))
- assert(update3.isInstanceOf[Update.TransactionAccepted])
- }
- }
-
- "correctly implements closed world tx submission authorization" in {
+ "correctly implements tx submission authorization" in {
val rt = ps.getNewRecordTime()
val updatesResult = ps.stateUpdates(beginAfter = None).take(4).runWith(Sink.seq)
+ val unallocatedParty = Ref.Party.assertFromString("nobody")
for {
lic <- ps.getLedgerInitialConditions().runWith(Sink.head)
@@ -317,13 +293,17 @@ class InMemoryKVParticipantStateIT
submissionId = "test1",
config = lic.config.copy(
generation = lic.config.generation + 1,
- openWorld = false,
)
)
.toScala
- // Submit without allocation in closed world setting.
- _ <- ps.submitTransaction(submitterInfo(rt), transactionMeta(rt), emptyTransaction).toScala
+ // Submit without allocation
+ _ <- ps
+ .submitTransaction(
+ submitterInfo(rt, unallocatedParty),
+ transactionMeta(rt),
+ emptyTransaction)
+ .toScala
// Allocate a party and try the submission again with an allocated party.
allocResult <- ps
@@ -340,23 +320,24 @@ class InMemoryKVParticipantStateIT
transactionMeta(rt),
emptyTransaction)
.toScala
- updates <- updatesResult
+
+ Seq((offset1, update1), (offset2, update2), (offset3, update3), (offset4, update4)) <- ps
+ .stateUpdates(beginAfter = None)
+ .take(4)
+ .runWith(Sink.seq)
+
} yield {
- def takeUpdate(n: Int) = {
- val (offset, update) = updates(n)
- assert(offset == Offset(Array(n.toLong, 0L)))
- update
- }
-
- assert(takeUpdate(0).isInstanceOf[Update.ConfigurationChanged])
-
+ assert(update1.isInstanceOf[Update.ConfigurationChanged])
+ assert(offset1 == Offset(Array(0L, 0L)))
assert(
- takeUpdate(1)
+ update2
.asInstanceOf[Update.CommandRejected]
.reason == RejectionReason.PartyNotKnownOnLedger)
-
- assert(takeUpdate(2).isInstanceOf[Update.PartyAddedToParticipant])
- assert(takeUpdate(3).isInstanceOf[Update.TransactionAccepted])
+ assert(offset2 == Offset(Array(1L, 0L)))
+ assert(update3.isInstanceOf[Update.PartyAddedToParticipant])
+ assert(offset3 == Offset(Array(2L, 0L)))
+ assert(update4.isInstanceOf[Update.TransactionAccepted])
+ assert(offset4 == Offset(Array(3L, 0L)))
}
}
@@ -366,14 +347,13 @@ class InMemoryKVParticipantStateIT
for {
lic <- ps.getLedgerInitialConditions().runWith(Sink.head)
- // Submit a configuration change that flips the "open world" flag.
+ // Submit an initial configuration change
_ <- ps
.submitConfiguration(
maxRecordTime = rt.addMicros(1000000),
submissionId = "test1",
config = lic.config.copy(
generation = lic.config.generation + 1,
- openWorld = !lic.config.openWorld
))
.toScala
@@ -417,8 +397,8 @@ object InMemoryKVParticipantStateIT {
private val archives =
darReader.readArchiveFromFile(new File(rlocation("ledger/test-common/Test-stable.dar"))).get.all
- private def submitterInfo(rt: Timestamp, party: String = "Alice") = SubmitterInfo(
- submitter = Ref.Party.assertFromString(party),
+ private def submitterInfo(rt: Timestamp, party: Ref.Party) = SubmitterInfo(
+ submitter = party,
applicationId = Ref.LedgerString.assertFromString("tests"),
commandId = Ref.LedgerString.assertFromString("X"),
maxRecordTime = rt.addMicros(Duration.ofSeconds(10).toNanos / 1000)
diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVTest.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVTest.scala
index 26f8a820da..924dd5165e 100644
--- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVTest.scala
+++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVTest.scala
@@ -14,6 +14,8 @@ import com.digitalasset.daml.lf.data.Time.Timestamp
import com.digitalasset.daml.lf.engine.Engine
import com.digitalasset.daml_lf_dev.DamlLf
import scalaz.State
+import scalaz.syntax.traverse._
+import scalaz.std.list._
import scala.collection.JavaConverters._
@@ -44,9 +46,10 @@ object KVTest {
def runTest[A](test: KVTest[A]): A =
test.eval(initialTestState)
- def runTestWithSimplePackage[A](test: KVTest[A]): A =
+ def runTestWithSimplePackage[A](parties: Party*)(test: KVTest[A]): A =
(for {
_ <- uploadSimpleArchive
+ _ <- parties.toList.map(p => allocateParty(p, p)).sequenceU
r <- test
} yield r).eval(initialTestState)
@@ -83,7 +86,8 @@ object KVTest {
getDamlState(Conversions.configurationStateKey)
.flatMap {
case None => getDefaultConfiguration
- case Some(v) => State.state(Conversions.parseDamlConfiguration(v.getConfiguration).get)
+ case Some(v) =>
+ State.state(Configuration.decode(v.getConfigurationEntry.getConfiguration).right.get)
}
def setRecordTime(rt: Timestamp): KVTest[Unit] =
@@ -196,7 +200,7 @@ object KVTest {
def submitConfig(
configModify: Configuration => Configuration,
- submissionId: String = "",
+ submissionId: String = randomString,
mrtDelta: Duration = minMRTDelta
): KVTest[DamlLogEntry] =
for {
@@ -206,6 +210,7 @@ object KVTest {
KeyValueSubmission.configurationToSubmission(
maxRecordTime = testState.recordTime.addMicros(mrtDelta.toNanos / 1000),
submissionId = submissionId,
+ participantId = testState.participantId,
config = configModify(oldConf)
)
)
diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsConfigSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsConfigSpec.scala
index 2e134a3a74..1b5d252562 100644
--- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsConfigSpec.scala
+++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsConfigSpec.scala
@@ -6,10 +6,10 @@ package com.daml.ledger.participant.state.kvutils
import java.time.Duration
import com.daml.ledger.participant.state.kvutils.DamlKvutils._
+import com.daml.ledger.participant.state.v1.Configuration
+import com.digitalasset.daml.lf.data.Ref
import org.scalatest.{Matchers, WordSpec}
-import scala.util.Success
-
class KVUtilsConfigSpec extends WordSpec with Matchers {
import KVTest._
import TestHelpers._
@@ -22,27 +22,27 @@ class KVUtilsConfigSpec extends WordSpec with Matchers {
KeyValueSubmission.configurationToSubmission(
maxRecordTime = theRecordTime,
submissionId = "foobar",
+ participantId = Ref.LedgerString.assertFromString("participant"),
config = theDefaultConfig
)))
val configSubm = subm.getConfigurationSubmission
Conversions.parseTimestamp(configSubm.getMaximumRecordTime) shouldEqual theRecordTime
configSubm.getSubmissionId shouldEqual "foobar"
- Conversions.parseDamlConfiguration(configSubm.getConfiguration) shouldEqual Success(
- theDefaultConfig)
+ Configuration.decode(configSubm.getConfiguration) shouldEqual Right(theDefaultConfig)
}
"check generation" in KVTest.runTest {
for {
logEntry <- submitConfig(
- configModify = c => c.copy(generation = c.generation + 1, openWorld = false),
+ configModify = c => c.copy(generation = c.generation + 1),
submissionId = "submission0"
)
newConfig <- getConfiguration
// Change again, but without bumping generation.
logEntry2 <- submitConfig(
- configModify = c => c.copy(generation = c.generation, openWorld = true),
+ configModify = c => c.copy(generation = c.generation),
submissionId = "submission1"
)
newConfig2 <- getConfiguration
@@ -51,7 +51,6 @@ class KVUtilsConfigSpec extends WordSpec with Matchers {
logEntry.getPayloadCase shouldEqual DamlLogEntry.PayloadCase.CONFIGURATION_ENTRY
logEntry.getConfigurationEntry.getSubmissionId shouldEqual "submission0"
newConfig.generation shouldEqual 1
- newConfig.openWorld shouldEqual false
logEntry2.getPayloadCase shouldEqual DamlLogEntry.PayloadCase.CONFIGURATION_REJECTION_ENTRY
logEntry2.getConfigurationRejectionEntry.getSubmissionId shouldEqual "submission1"
@@ -79,8 +78,7 @@ class KVUtilsConfigSpec extends WordSpec with Matchers {
// Set a configuration with an authorized participant id
logEntry0 <- submitConfig { c =>
c.copy(
- generation = c.generation + 1,
- authorizedParticipantId = Some(p0)
+ generation = c.generation + 1
)
}
@@ -93,7 +91,6 @@ class KVUtilsConfigSpec extends WordSpec with Matchers {
c =>
c.copy(
generation = c.generation + 1,
- openWorld = false
)
)
}
@@ -107,7 +104,6 @@ class KVUtilsConfigSpec extends WordSpec with Matchers {
c =>
c.copy(
generation = c.generation + 1,
- openWorld = false
))
}
diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsTransactionSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsTransactionSpec.scala
index e713f75df9..348f080c46 100644
--- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsTransactionSpec.scala
+++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KVUtilsTransactionSpec.scala
@@ -45,7 +45,7 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
val p0 = mkParticipantId(0)
val p1 = mkParticipantId(1)
- "be able to submit transaction" in KVTest.runTestWithSimplePackage(
+ "be able to submit transaction" in KVTest.runTestWithSimplePackage(alice)(
for {
tx <- runCommand(alice, simpleCreateCmd)
logEntry <- submitTransaction(submitter = alice, tx = tx).map(_._2)
@@ -67,7 +67,7 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
)
*/
- "reject transaction with out of bounds LET" in KVTest.runTestWithSimplePackage(
+ "reject transaction with out of bounds LET" in KVTest.runTestWithSimplePackage(alice)(
for {
tx <- runCommand(alice, simpleCreateCmd)
conf <- getDefaultConfiguration
@@ -80,7 +80,7 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
}
)
- "be able to exercise and rejects double spends" in KVTest.runTestWithSimplePackage {
+ "be able to exercise and rejects double spends" in KVTest.runTestWithSimplePackage(alice) {
for {
createTx <- runCommand(alice, simpleCreateCmd)
result <- submitTransaction(submitter = alice, tx = createTx)
@@ -108,10 +108,10 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
}
}
- "reject transactions for unallocated parties" in KVTest.runTestWithSimplePackage {
+ "reject transactions for unallocated parties" in KVTest.runTestWithSimplePackage() {
for {
configEntry <- submitConfig { c =>
- c.copy(generation = c.generation + 1, openWorld = false)
+ c.copy(generation = c.generation + 1)
}
createTx <- runCommand(alice, simpleCreateCmd)
txEntry <- submitTransaction(submitter = alice, tx = createTx).map(_._2)
@@ -122,10 +122,10 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
}
}
- "reject transactions for unhosted parties" in KVTest.runTestWithSimplePackage {
+ "reject transactions for unhosted parties" in KVTest.runTestWithSimplePackage() {
for {
configEntry <- submitConfig { c =>
- c.copy(generation = c.generation + 1, openWorld = false)
+ c.copy(generation = c.generation + 1)
}
createTx <- runCommand(alice, simpleCreateCmd)
@@ -143,7 +143,8 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
}
}
- "reject unauthorized transactions " in KVTest.runTestWithSimplePackage {
+
+ "reject unauthorized transactions" in KVTest.runTestWithSimplePackage() {
for {
// Submit a creation of a contract with owner 'Alice', but submit it as 'Bob'.
createTx <- runCommand(alice, simpleCreateCmd)
@@ -164,7 +165,7 @@ class KVUtilsTransactionSpec extends WordSpec with Matchers {
}
}
- "transient contracts and keys are properly archived" in KVTest.runTestWithSimplePackage {
+ "transient contracts and keys are properly archived" in KVTest.runTestWithSimplePackage(alice) {
for {
tx1 <- runCommand(alice, simpleCreateAndExerciseCmd)
createAndExerciseTx1 <- submitTransaction(alice, tx1).map(_._2)
diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/TestHelpers.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/TestHelpers.scala
index a6f68de00d..590c0992da 100644
--- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/TestHelpers.scala
+++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/TestHelpers.scala
@@ -5,9 +5,8 @@ package com.daml.ledger.participant.state.kvutils
import java.util.UUID
-import com.daml.ledger.participant.state.backport.TimeModel
import com.daml.ledger.participant.state.kvutils.DamlKvutils.DamlLogEntryId
-import com.daml.ledger.participant.state.v1.{Configuration, ParticipantId}
+import com.daml.ledger.participant.state.v1.{Configuration, ParticipantId, TimeModel}
import com.digitalasset.daml.lf.archive.Decode
import com.digitalasset.daml.lf.archive.testing.Encode
import com.digitalasset.daml.lf.data.Time.Timestamp
@@ -72,9 +71,7 @@ object TestHelpers {
val theRecordTime: Timestamp = Timestamp.Epoch
val theDefaultConfig = Configuration(
generation = 0,
- timeModel = TimeModel.reasonableDefault,
- authorizedParticipantId = None,
- openWorld = true,
+ timeModel = TimeModel.reasonableDefault
)
def mkEntryId(n: Int): DamlLogEntryId = {
diff --git a/ledger/participant-state/kvutils/tools/src/main/scala/com/daml/ledger/participant/state/kvutils/tools/IntegrityCheck.scala b/ledger/participant-state/kvutils/tools/src/main/scala/com/daml/ledger/participant/state/kvutils/tools/IntegrityCheck.scala
index 23855783ec..54f7e2235d 100644
--- a/ledger/participant-state/kvutils/tools/src/main/scala/com/daml/ledger/participant/state/kvutils/tools/IntegrityCheck.scala
+++ b/ledger/participant-state/kvutils/tools/src/main/scala/com/daml/ledger/participant/state/kvutils/tools/IntegrityCheck.scala
@@ -7,9 +7,8 @@ import java.io.{DataInputStream, FileInputStream}
import java.util.concurrent.TimeUnit
import com.codahale.metrics
-import com.daml.ledger.participant.state.backport.TimeModel
import com.daml.ledger.participant.state.kvutils.{DamlKvutils => Proto, _}
-import com.daml.ledger.participant.state.v1.{Configuration, Update}
+import com.daml.ledger.participant.state.v1._
import com.digitalasset.daml.lf.data.Ref
import com.digitalasset.daml.lf.engine.Engine
@@ -51,9 +50,7 @@ object IntegrityCheck extends App {
val engine = Engine()
val defaultConfig = Configuration(
generation = 0,
- timeModel = TimeModel.reasonableDefault,
- authorizedParticipantId = None,
- openWorld = true
+ timeModel = TimeModel.reasonableDefault
)
var state = Map.empty[Proto.DamlStateKey, Proto.DamlStateValue]
diff --git a/ledger/participant-state/protobuf/BUILD.bazel b/ledger/participant-state/protobuf/BUILD.bazel
new file mode 100644
index 0000000000..4b96dcc784
--- /dev/null
+++ b/ledger/participant-state/protobuf/BUILD.bazel
@@ -0,0 +1,21 @@
+# Copyright (c) 2019 The DAML Authors. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+load("//bazel_tools:java.bzl", "da_java_proto_library")
+
+proto_library(
+ name = "ledger_configuration_proto",
+ srcs = ["com/daml/ledger/participant/state/ledger_configuration.proto"],
+ strip_import_prefix = "/ledger/participant-state/protobuf",
+ visibility = ["//ledger/participant-state:__subpackages__"],
+ deps = [
+ "@com_google_protobuf//:duration_proto",
+ ],
+)
+
+da_java_proto_library(
+ name = "ledger_configuration_java_proto",
+ tags = ["maven_coordinates=com.daml.ledger:participant-state-ledger-configuration-java-proto:__VERSION__"],
+ visibility = ["//ledger:__subpackages__"],
+ deps = [":ledger_configuration_proto"],
+)
diff --git a/ledger/participant-state/protobuf/README.md b/ledger/participant-state/protobuf/README.md
new file mode 100644
index 0000000000..0af3e35c5b
--- /dev/null
+++ b/ledger/participant-state/protobuf/README.md
@@ -0,0 +1,6 @@
+
+participant-state/protobuf
+--------------------------
+
+Common protocol buffer definitions used across different implementations.
+
diff --git a/ledger/participant-state/protobuf/com/daml/ledger/participant/state/ledger_configuration.proto b/ledger/participant-state/protobuf/com/daml/ledger/participant/state/ledger_configuration.proto
new file mode 100644
index 0000000000..bef942b41c
--- /dev/null
+++ b/ledger/participant-state/protobuf/com/daml/ledger/participant/state/ledger_configuration.proto
@@ -0,0 +1,45 @@
+// Copyright (c) 2019 The DAML Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+//
+// The DAML Ledger configuration. Please refer to the spec
+// (ledger/participant-state/protobuf/ledger_configuration.rst)
+// for detailed information on versions and semantics.
+//
+// version summary:
+// * 1: initial version
+//
+
+syntax = "proto3";
+package com.daml.ledger.participant.state;
+option java_package = "com.daml.ledger.participant.state.protobuf";
+option java_multiple_files = true;
+option csharp_namespace = "Com.Daml.Ledger.Participant.State.Protobuf";
+
+import "google/protobuf/duration.proto";
+
+message LedgerConfiguration {
+ // The version of the configuration message. Defines the semantics
+ // of how it is decoded and interpreted.
+ int64 version = 1;
+
+ // The configuration generation. If submitting a configuration the new generation
+ // must be one larger than previous configuration. This safe-guards against
+ // configuration changes that are based upon stale data.
+ int64 generation = 2;
+
+ // The ledger time model, specifying the bounds for
+ // ledger effective time and maximum record time of transactions.
+ LedgerTimeModel time_model = 3;
+}
+
+message LedgerTimeModel {
+ // The expected minimum latency of a transaction.
+ google.protobuf.Duration min_transaction_latency = 1;
+
+ // The maximum allowed clock skew between the ledger and clients.
+ google.protobuf.Duration max_clock_skew = 2;
+
+ // The maximum allowed time to live for a transaction.
+ // Must be greater than the derived minimum time to live.
+ google.protobuf.Duration max_ttl = 3;
+}
diff --git a/ledger/participant-state/protobuf/ledger_configuration.rst b/ledger/participant-state/protobuf/ledger_configuration.rst
new file mode 100644
index 0000000000..bbe10676c4
--- /dev/null
+++ b/ledger/participant-state/protobuf/ledger_configuration.rst
@@ -0,0 +1,86 @@
+.. Copyright (c) 2019 The DAML Authors. All rights reserved.
+.. SPDX-License-Identifier: Apache-2.0
+
+DAML Ledger Configuration Specification
+=======================================
+
+**version 1, 25th November 2019**
+
+This specification, along with ``ledger_configuration.proto``
+defines the data representation and the semantics of ledger
+configuration, including the time model.
+
+We follow the same rules as ``transaction.proto`` with regards
+to assignment of version numbers and maintaining backwards compatibility.
+Please read ``daml-lf/spec/transaction.rst`` to understand the rules
+we follow for evolving the ledger configuration.
+
+The canonical specification compliant implementation for encoding and
+decoding ledger configurations is part of the participant-state package
+in ``ledger/participant-state``.
+
+Version history
+^^^^^^^^^^^^^^^
+
+Please refer to ``ledger_configuration.proto`` for version history.
+
+message LedgerConfiguration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+*since version 1*
+
+An instance of the ledger configuration.
+
+As of version 1, these fields are included:
+* ``int64`` version
+* ``int64`` generation
+* ``LedgerTimeModel`` time_model
+* ``string`` authorized_participant_id
+
+``version`` is required, and should be set to the latest version as
+specified by this document. Consumers should reject configurations
+with an unknown version.
+
+``generation`` is required and must be set to a number one larger than
+the current configuration in order for the configuration change to be
+accepted. This safe-guards against configuration changes based on
+stale data.
+
+``time_model`` is required.
+
+``authorized_participant_ids`` is optional. If non-empty, then configuration
+change originating from a participant that does not match any of the authorized
+participants field will be rejected.
+If unset, then change from any participant is accepted and that participant can set this field.
+
+message LedgerTimeModel
+^^^^^^^^^^^^^^^^^^^^^^^
+
+*since version 1*
+
+Defines the ledger time model, which governs the rules for acceptable
+ledger effective time and maximum record time parameters that are part
+of transaction submission.
+
+As of version 1, these fields are included:
+
+* ``Duration`` min_transaction_latency
+* ``Duration`` max_clock_skew
+* ``Duration`` max_ttl
+
+``min_transaction_latency`` is required. It defines the minimum expected
+latency for a transaction to be committed.
+
+``max_clock_skew`` is required. It defines the maximum allowed clock skew
+between the ledger and clients.
+
+``max_ttl`` is required. It defines the maximum allowed time to live for a
+transaction.
+
+These three parameters are used in the following way.
+Given the record time ``RT``, a transaction submission with a ledger effective time ``LET``
+ and maximum record time ``MRT`` is accepted if:
+
+* ``LET - MRT >= min_transaction_latency + max_clock_skew``
+* ``LET - MRT <= max_ttl``.
+* ``LET - max_clock_skew <= RT <= MRT``.
diff --git a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Configuration.scala b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Configuration.scala
index 3e883674fd..4b82d61af1 100644
--- a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Configuration.scala
+++ b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Configuration.scala
@@ -3,6 +3,10 @@
package com.daml.ledger.participant.state.v1
+import java.time.Duration
+
+import scala.util.Try
+
/** Ledger configuration describing the ledger's time model.
* Emitted in [[com.daml.ledger.participant.state.v1.Update.ConfigurationChanged]].
*/
@@ -11,9 +15,71 @@ final case class Configuration(
generation: Long,
/** The time model of the ledger. Specifying the time-to-live bounds for Ledger API commands. */
timeModel: TimeModel,
- /** The identity of the participant allowed to change the configuration. If not set, any participant
- * can change the configuration. */
- authorizedParticipantId: Option[ParticipantId],
- /** Flag to enable "open world" mode in which submissions from unallocated parties are allowed through. Useful in testing. */
- openWorld: Boolean
)
+
+object Configuration {
+ import com.daml.ledger.participant.state.protobuf
+
+ val protobufVersion: Long = 1L
+
+ def decode(bytes: Array[Byte]): Either[String, Configuration] =
+ Try(protobuf.LedgerConfiguration.parseFrom(bytes)).toEither.left
+ .map(_.getMessage)
+ .right
+ .flatMap(decode)
+
+ def decode(config: protobuf.LedgerConfiguration): Either[String, Configuration] =
+ config.getVersion match {
+ case 1 => DecodeV1.decode(config)
+ case v => Left("Unknown version: $v")
+ }
+
+ private object DecodeV1 {
+
+ def decode(config: protobuf.LedgerConfiguration): Either[String, Configuration] =
+ for {
+ tm <- if (config.hasTimeModel)
+ decodeTimeModel(config.getTimeModel)
+ else
+ Left("Missing time model")
+ } yield {
+ Configuration(
+ generation = config.getGeneration,
+ timeModel = tm,
+ )
+ }
+
+ def decodeTimeModel(tm: protobuf.LedgerTimeModel): Either[String, TimeModel] =
+ TimeModel(
+ maxClockSkew = parseDuration(tm.getMaxClockSkew),
+ minTransactionLatency = parseDuration(tm.getMinTransactionLatency),
+ maxTtl = parseDuration(tm.getMaxTtl)
+ ).toEither.left.map(e => s"decodeTimeModel: ${e.getMessage}")
+ }
+
+ def encode(config: Configuration): protobuf.LedgerConfiguration = {
+ val tm = config.timeModel
+ protobuf.LedgerConfiguration.newBuilder
+ .setVersion(protobufVersion)
+ .setGeneration(config.generation)
+ .setTimeModel(
+ protobuf.LedgerTimeModel.newBuilder
+ .setMaxClockSkew(buildDuration(tm.maxClockSkew))
+ .setMinTransactionLatency(buildDuration(tm.minTransactionLatency))
+ .setMaxTtl(buildDuration(tm.maxTtl))
+ )
+ .build
+ }
+
+ private def parseDuration(dur: com.google.protobuf.Duration): Duration = {
+ Duration.ofSeconds(dur.getSeconds, dur.getNanos.toLong)
+ }
+
+ private def buildDuration(dur: Duration): com.google.protobuf.Duration = {
+ com.google.protobuf.Duration.newBuilder
+ .setSeconds(dur.getSeconds)
+ .setNanos(dur.getNano)
+ .build
+ }
+
+}
diff --git a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/TimeModel.scala b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/TimeModel.scala
index c5c308de32..41ba594d05 100644
--- a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/TimeModel.scala
+++ b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/TimeModel.scala
@@ -4,44 +4,70 @@
package com.daml.ledger.participant.state.v1
import java.time.{Duration, Instant}
+import scala.util.Try
-trait TimeModel {
-
- def minTransactionLatency: Duration
-
- def futureAcceptanceWindow: Duration
-
- def maxClockSkew: Duration
-
- def minTtl: Duration
-
- def maxTtl: Duration
-
-}
-
-trait TimeModelChecker {
+/**
+ * The ledger time model and associated validations. Some values are given by constructor args; others are derived.
+ *
+ * @param minTransactionLatency The expected minimum latency of a transaction.
+ * @param maxClockSkew The maximum allowed clock skew between the ledger and clients.
+ * @param maxTtl The maximum allowed time to live for a transaction.
+ * Must be greater than the derived minimum time to live.
+ * @throws IllegalArgumentException if the parameters aren't valid
+ */
+case class TimeModel private (
+ minTransactionLatency: Duration,
+ maxClockSkew: Duration,
+ maxTtl: Duration) {
/**
- * Validates that the given ledger effective time is within an acceptable time window of the current system time.
- *
- * @param currentTime the current time
- * @param givenLedgerEffectiveTime The ledger effective time to validate.
- * @param givenMaximumRecordTime The maximum record time to validate.
- * @return true if successful
+ * The minimum time to live for a transaction. Equal to the minimum transaction latency plus the maximum clock skew.
*/
+ val minTtl: Duration = minTransactionLatency.plus(maxClockSkew)
+
+ /**
+ * The maximum window after the current time when transaction ledger effective times will be accepted.
+ * Currently equal to the max clock skew.
+ *
+ * The corresponding past acceptance window is given by the command's TTL, and thus bounded inclusive by [[maxTtl]].
+ */
+ val futureAcceptanceWindow: Duration = maxClockSkew
+
+ def checkTtl(givenLedgerEffectiveTime: Instant, givenMaximumRecordTime: Instant): Boolean = {
+ val givenTtl = Duration.between(givenLedgerEffectiveTime, givenMaximumRecordTime)
+ !givenTtl.minus(minTtl).isNegative && !maxTtl.minus(givenTtl).isNegative
+ }
+
def checkLet(
currentTime: Instant,
givenLedgerEffectiveTime: Instant,
- givenMaximumRecordTime: Instant): Boolean
+ givenMaximumRecordTime: Instant): Boolean = {
+ // Note that, contrary to the documented spec, the record time of a transaction is when it's sequenced.
+ // It turns out this isn't a problem for the participant or the sandbox,
+ // and MRT seems to be going away in Sirius anyway, so I've left it as is.
+ val lowerBound = givenLedgerEffectiveTime.minus(futureAcceptanceWindow)
+ !currentTime.isBefore(lowerBound) && !currentTime.isAfter(givenMaximumRecordTime)
+ }
+
+}
+
+object TimeModel {
/**
- * Validates that the ttl of the given times is within bounds.
- * The ttl of a command is defined as the duration between
- * the ledger effective time and maximum record time.
- *
- * @param givenLedgerEffectiveTime The given ledger effective time.
- * @param givenMaximumRecordTime The given maximum record time.
- * @return true if successful
+ * A default TimeModel that's reasonable for a test or sandbox ledger application.
+ * Serious applications (viz. ledger) should probably specify their own TimeModel.
*/
- def checkTtl(givenLedgerEffectiveTime: Instant, givenMaximumRecordTime: Instant): Boolean
+ val reasonableDefault: TimeModel =
+ TimeModel(Duration.ofSeconds(1L), Duration.ofSeconds(1L), Duration.ofSeconds(30L)).get
+
+ def apply(
+ minTransactionLatency: Duration,
+ maxClockSkew: Duration,
+ maxTtl: Duration): Try[TimeModel] = Try {
+ require(!minTransactionLatency.isNegative, "Negative min transaction latency")
+ require(!maxTtl.isNegative, "Negative max TTL")
+ require(!maxClockSkew.isNegative, "Negative max clock skew")
+ require(!maxTtl.minus(maxClockSkew).isNegative, "Max TTL must be greater than max clock skew")
+ new TimeModel(minTransactionLatency, maxClockSkew, maxTtl)
+ }
}
diff --git a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Update.scala b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Update.scala
index 301dff34fa..0dbb375115 100644
--- a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Update.scala
+++ b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Update.scala
@@ -20,6 +20,9 @@ sealed trait Update extends Product with Serializable {
/** Short human-readable one-line description summarizing the state updates content. */
def description: String
+
+ /** The record time at which the state change was committed. */
+ def recordTime: Timestamp
}
object Update {
@@ -30,18 +33,27 @@ object Update {
}
/** Signal that the current [[Configuration]] has changed. */
- final case class ConfigurationChanged(submissionId: String, newConfiguration: Configuration)
+ final case class ConfigurationChanged(
+ recordTime: Timestamp,
+ submissionId: String,
+ participantId: ParticipantId,
+ newConfiguration: Configuration)
extends Update {
override def description: String =
- s"Configuration changed to: $newConfiguration"
+ s"Configuration change '$submissionId' from participant '$participantId' accepted with configuration: $newConfiguration"
}
/** Signal that a configuration change submitted by this participant was rejected.
*/
- final case class ConfigurationChangeRejected(submissionId: String, reason: String)
+ final case class ConfigurationChangeRejected(
+ recordTime: Timestamp,
+ submissionId: String,
+ participantId: ParticipantId,
+ proposedConfiguration: Configuration,
+ rejectionReason: String)
extends Update {
override def description: String = {
- s"Configuration change '$submissionId' was rejected: $reason"
+ s"Configuration change '$submissionId' from participant '$participantId' was rejected: $rejectionReason"
}
}
@@ -155,6 +167,7 @@ object Update {
* rejected.
*/
final case class CommandRejected(
+ recordTime: Timestamp,
submitterInfo: SubmitterInfo,
reason: RejectionReason,
) extends Update {
diff --git a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Version.scala b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Version.scala
index b9c39d7374..df2ed84380 100644
--- a/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Version.scala
+++ b/ledger/participant-state/src/main/scala/com/daml/ledger/participant/state/v1/Version.scala
@@ -7,7 +7,13 @@ package com.daml.ledger.participant.state.v1
* and version constants (currently none).
*
* Changes:
- * [since 100.13.21]:
+ * [after 100.13.37]:
+ * - Moved configuration serialization from kvutils to participant-state. This is used both by
+ * kvutils and the index to encode and decode configurations.
+ * - Authorized participant identifier and "open-world" flag removed from configuration.
+ * - Record time added to all [[Update]]s.
+ *
+ * [after 100.13.21]:
* - Rename referencedContracts to divulgedContracts in [[Update.TransactionAccepted]].
*/
object Version {}
diff --git a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/services/time/TimeModelTest.scala b/ledger/participant-state/src/test/suite/scala/com/daml/ledger/participant/state/TimeModelTest.scala
similarity index 53%
rename from ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/services/time/TimeModelTest.scala
rename to ledger/participant-state/src/test/suite/scala/com/daml/ledger/participant/state/TimeModelTest.scala
index 180c2128a5..d65b065d65 100644
--- a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/services/time/TimeModelTest.scala
+++ b/ledger/participant-state/src/test/suite/scala/com/daml/ledger/participant/state/TimeModelTest.scala
@@ -1,36 +1,33 @@
// Copyright (c) 2019 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-package com.digitalasset.platform.server.services.time
+package com.daml.ledger.participant.state.v1
import java.time._
-import com.digitalasset.platform.services.time.{TimeModel, TimeModelChecker}
import org.scalatest.{Matchers, WordSpec}
class TimeModelTest extends WordSpec with Matchers {
private val referenceTime = Instant.EPOCH
private val epsilon = Duration.ofMillis(10L)
- private val sut =
+ private val timeModel =
TimeModel(Duration.ofSeconds(1L), Duration.ofSeconds(1L), Duration.ofSeconds(30L)).get
- private val checker = TimeModelChecker(sut)
-
- private val referenceMrt = referenceTime.plus(sut.maxTtl)
+ private val referenceMrt = referenceTime.plus(timeModel.maxTtl)
private def acceptLet(let: Instant): Boolean =
- checker.checkLet(referenceTime, let, let.plus(sut.maxTtl))
+ timeModel.checkLet(referenceTime, let, let.plus(timeModel.maxTtl))
private def acceptMrt(mrt: Instant): Boolean =
- checker.checkLet(referenceTime, mrt.minus(sut.maxTtl), mrt)
+ timeModel.checkLet(referenceTime, mrt.minus(timeModel.maxTtl), mrt)
- private def acceptTtl(mrt: Instant): Boolean = checker.checkTtl(referenceTime, mrt)
+ private def acceptTtl(mrt: Instant): Boolean = timeModel.checkTtl(referenceTime, mrt)
"Ledger effective time model checker" when {
"calculating derived values" should {
"calculate minTtl correctly" in {
- sut.minTtl shouldEqual sut.minTransactionLatency.plus(sut.maxClockSkew)
+ timeModel.minTtl shouldEqual timeModel.minTransactionLatency.plus(timeModel.maxClockSkew)
}
}
@@ -40,55 +37,55 @@ class TimeModelTest extends WordSpec with Matchers {
}
"succeed if the time is higher than the current time and is within tolerance limit" in {
- acceptLet(referenceTime.plus(sut.futureAcceptanceWindow).minus(epsilon)) shouldEqual true
+ acceptLet(referenceTime.plus(timeModel.futureAcceptanceWindow).minus(epsilon)) shouldEqual true
}
"succeed if the time is equal to the high boundary" in {
- acceptLet(referenceTime.plus(sut.futureAcceptanceWindow)) shouldEqual true
+ acceptLet(referenceTime.plus(timeModel.futureAcceptanceWindow)) shouldEqual true
}
"fail if the time is higher than the high boundary" in {
- acceptLet(referenceTime.plus(sut.futureAcceptanceWindow).plus(epsilon)) shouldEqual false
+ acceptLet(referenceTime.plus(timeModel.futureAcceptanceWindow).plus(epsilon)) shouldEqual false
}
"fail if the MRT is less than the low boundary" in {
- acceptMrt(referenceMrt.minus(sut.maxTtl).minus(epsilon)) shouldEqual false
+ acceptMrt(referenceMrt.minus(timeModel.maxTtl).minus(epsilon)) shouldEqual false
}
"succeed if the MRT is equal to the low boundary" in {
- acceptMrt(referenceMrt.minus(sut.maxTtl)) shouldEqual true
+ acceptMrt(referenceMrt.minus(timeModel.maxTtl)) shouldEqual true
}
"succeed if the MRT is greater than than the low boundary" in {
- acceptMrt(referenceMrt.minus(sut.maxTtl).plus(epsilon)) shouldEqual true
+ acceptMrt(referenceMrt.minus(timeModel.maxTtl).plus(epsilon)) shouldEqual true
}
}
}
- "TTL time model checker" when {
+ "TTL time model" when {
"checking if TTL is within accepted boundaries" should {
"fail if the TTL is less than than the low boundary" in {
- acceptTtl(referenceTime.plus(sut.minTtl).minus(epsilon)) shouldEqual false
+ acceptTtl(referenceTime.plus(timeModel.minTtl).minus(epsilon)) shouldEqual false
}
"succeed if the TTL is equal to the low boundary" in {
- acceptTtl(referenceTime.plus(sut.minTtl)) shouldEqual true
+ acceptTtl(referenceTime.plus(timeModel.minTtl)) shouldEqual true
}
"succeed if the TTL is greater than the low boundary" in {
- acceptTtl(referenceTime.plus(sut.minTtl).plus(epsilon)) shouldEqual true
+ acceptTtl(referenceTime.plus(timeModel.minTtl).plus(epsilon)) shouldEqual true
}
"succeed if the TTL is less than the high boundary" in {
- acceptTtl(referenceTime.plus(sut.maxTtl).minus(epsilon)) shouldEqual true
+ acceptTtl(referenceTime.plus(timeModel.maxTtl).minus(epsilon)) shouldEqual true
}
"succeed if the TTL is equal to the high boundary" in {
- acceptTtl(referenceTime.plus(sut.maxTtl)) shouldEqual true
+ acceptTtl(referenceTime.plus(timeModel.maxTtl)) shouldEqual true
}
"fail if the TTL is greater than than the high boundary" in {
- acceptTtl(referenceTime.plus(sut.maxTtl).plus(epsilon)) shouldEqual false
+ acceptTtl(referenceTime.plus(timeModel.maxTtl).plus(epsilon)) shouldEqual false
}
}
}
diff --git a/ledger/sandbox/src/main/resources/db/migration/h2database/V7__Add_configuration.sha256 b/ledger/sandbox/src/main/resources/db/migration/h2database/V7__Add_configuration.sha256
new file mode 100644
index 0000000000..6f1794b033
--- /dev/null
+++ b/ledger/sandbox/src/main/resources/db/migration/h2database/V7__Add_configuration.sha256
@@ -0,0 +1 @@
+55762555d92c53b57217b32a0feb4a278dde5d0a7be23d227ac816f2b899b7fc
diff --git a/ledger/sandbox/src/main/resources/db/migration/h2database/V7__Add_configuration.sql b/ledger/sandbox/src/main/resources/db/migration/h2database/V7__Add_configuration.sql
new file mode 100644
index 0000000000..9fd42ddba2
--- /dev/null
+++ b/ledger/sandbox/src/main/resources/db/migration/h2database/V7__Add_configuration.sql
@@ -0,0 +1,42 @@
+-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
+-- SPDX-License-Identifier: Apache-2.0
+
+---------------------------------------------------------------------------------------------------
+-- V7: Add table for ledger configuration changes
+--
+-- This schema version adds a table for ledger configuration changes and adds the latest
+-- configuration to the parameters table.
+---------------------------------------------------------------------------------------------------
+
+-- Table for storing a log of ledger configuration changes and rejections.
+CREATE TABLE configuration_entries (
+ ledger_offset bigint primary key not null,
+ recorded_at timestamp not null, -- with time zone
+
+ submission_id varchar not null,
+ participant_id varchar not null,
+ -- The type of entry, one of 'accept' or 'reject'.
+ typ varchar not null,
+ -- The configuration that was proposed and either accepted or rejected depending on the type.
+ -- Encoded according to participant-state/protobuf/ledger_configuration.proto.
+ configuration bytea not null,
+
+ -- If the type is 'rejection', then the rejection reason is set.
+ -- Rejection reason is a human-readable description why the change was rejected.
+ rejection_reason varchar,
+
+ -- Check that fields are correctly set based on the type.
+ constraint configuration_entries_check_reason
+ check (
+ (typ = 'accept' and rejection_reason is null) or
+ (typ = 'reject' and rejection_reason is not null))
+);
+
+-- Index for retrieving the configuration entry by submission identifier.
+-- To be used for completing configuration submissions.
+CREATE UNIQUE INDEX idx_configuration_submission
+ ON configuration_entries (submission_id, participant_id);
+
+-- Add the current configuration column to parameters.
+ALTER TABLE parameters
+ADD configuration bytea;
diff --git a/ledger/sandbox/src/main/resources/db/migration/postgres/V12__Add_configuration.sha256 b/ledger/sandbox/src/main/resources/db/migration/postgres/V12__Add_configuration.sha256
new file mode 100644
index 0000000000..6c5898ff8d
--- /dev/null
+++ b/ledger/sandbox/src/main/resources/db/migration/postgres/V12__Add_configuration.sha256
@@ -0,0 +1 @@
+2371c0f1dfa9a21f3536dd129cc6c9b517a02a6604b58912ef665f0318e542a9
diff --git a/ledger/sandbox/src/main/resources/db/migration/postgres/V12__Add_configuration.sql b/ledger/sandbox/src/main/resources/db/migration/postgres/V12__Add_configuration.sql
new file mode 100644
index 0000000000..bd940cea05
--- /dev/null
+++ b/ledger/sandbox/src/main/resources/db/migration/postgres/V12__Add_configuration.sql
@@ -0,0 +1,42 @@
+-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
+-- SPDX-License-Identifier: Apache-2.0
+
+---------------------------------------------------------------------------------------------------
+-- V12: Add table for ledger configuration changes
+--
+-- This schema version adds a table for ledger configuration changes and adds the latest
+-- configuration to the parameters table.
+---------------------------------------------------------------------------------------------------
+
+-- Table for storing a log of ledger configuration changes and rejections.
+CREATE TABLE configuration_entries (
+ ledger_offset bigint primary key not null,
+ recorded_at timestamp not null, -- with time zone
+
+ submission_id varchar not null,
+ participant_id varchar not null,
+ -- The type of entry, one of 'accept' or 'reject'.
+ typ varchar not null,
+ -- The configuration that was proposed and either accepted or rejected depending on the type.
+ -- Encoded according to participant-state/protobuf/ledger_configuration.proto.
+ configuration bytea not null,
+
+ -- If the type is 'rejection', then the rejection reason is set.
+ -- Rejection reason is a human-readable description why the change was rejected.
+ rejection_reason varchar,
+
+ -- Check that fields are correctly set based on the type.
+ constraint check_entry
+ check (
+ (typ = 'accept' and rejection_reason is null) or
+ (typ = 'reject' and rejection_reason is not null))
+);
+
+-- Index for retrieving the configuration entry by submission identifier.
+-- To be used for completing configuration submissions.
+CREATE UNIQUE INDEX idx_configuration_submission
+ ON configuration_entries (submission_id, participant_id);
+
+-- Add the current configuration column to parameters.
+ALTER TABLE parameters
+ADD configuration bytea;
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/index/JdbcIndexer.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/index/JdbcIndexer.scala
index 52c48a68f8..ae650c3a7c 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/index/JdbcIndexer.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/index/JdbcIndexer.scala
@@ -245,7 +245,7 @@ class JdbcIndexer private[index] (
private def handleStateUpdate(offset: Offset, update: Update): Future[Unit] = {
lastReceivedOffset = offset.toLedgerString
- stateUpdateRecordTime(update).foreach(lastReceivedRecordTime = _)
+ lastReceivedRecordTime = update.recordTime.toInstant
val externalOffset = Some(offset.toLedgerString)
update match {
@@ -319,21 +319,38 @@ class JdbcIndexer private[index] (
.storeLedgerEntry(headRef, headRef + 1, externalOffset, pt)
.map(_ => headRef = headRef + 1)(DEC)
- case _: ConfigurationChanged =>
- // TODO (GS) implement configuration changes
- Future.successful(())
+ case config: ConfigurationChanged =>
+ ledgerDao
+ .storeConfigurationEntry(
+ headRef,
+ headRef + 1,
+ externalOffset,
+ config.recordTime.toInstant,
+ config.submissionId,
+ config.participantId,
+ config.newConfiguration,
+ None
+ )
+ .map(_ => headRef = headRef + 1)(DEC)
- case _: ConfigurationChangeRejected =>
- // TODO(JM) implement configuration rejections
- Future.successful(())
+ case configRejection: ConfigurationChangeRejected =>
+ ledgerDao
+ .storeConfigurationEntry(
+ headRef,
+ headRef + 1,
+ externalOffset,
+ configRejection.recordTime.toInstant,
+ configRejection.submissionId,
+ configRejection.participantId,
+ configRejection.proposedConfiguration,
+ Some(configRejection.rejectionReason)
+ )
+ .map(_ => headRef = headRef + 1)(DEC)
- case CommandRejected(submitterInfo, RejectionReason.DuplicateCommand) =>
- Future.successful(())
-
- case CommandRejected(submitterInfo, reason) =>
+ case CommandRejected(recordTime, submitterInfo, reason) =>
val rejection = PersistenceEntry.Rejection(
LedgerEntry.Rejection(
- Instant.now(), // TODO should we get this from the backend?
+ recordTime.toInstant,
submitterInfo.commandId,
submitterInfo.applicationId,
submitterInfo.submitter,
@@ -347,17 +364,6 @@ class JdbcIndexer private[index] (
}
}
- private def stateUpdateRecordTime(update: Update): Option[Instant] =
- (update match {
- case Heartbeat(recordTime) => Some(recordTime)
- case PartyAddedToParticipant(_, _, _, recordTime) => Some(recordTime)
- case PublicPackageUploaded(_, _, _, recordTime) => Some(recordTime)
- case TransactionAccepted(_, _, _, _, recordTime, _) => Some(recordTime)
- case ConfigurationChanged(_, _) => None
- case ConfigurationChangeRejected(_, _) => None
- case CommandRejected(_, _) => None
- }) map (_.toInstant)
-
private def toDomainRejection(
submitterInfo: SubmitterInfo,
state: RejectionReason): domain.RejectionReason = state match {
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/config/SandboxConfig.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/config/SandboxConfig.scala
index ba8484065b..d112fb564f 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/config/SandboxConfig.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/config/SandboxConfig.scala
@@ -9,7 +9,8 @@ import ch.qos.logback.classic.Level
import com.digitalasset.ledger.api.auth.AuthService
import com.digitalasset.ledger.api.tls.TlsConfiguration
import com.digitalasset.platform.common.LedgerIdMode
-import com.digitalasset.platform.services.time.{TimeModel, TimeProviderType}
+import com.digitalasset.platform.services.time.TimeProviderType
+import com.daml.ledger.participant.state.v1.TimeModel
import scala.concurrent.duration._
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/SandboxIndexAndWriteService.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/SandboxIndexAndWriteService.scala
index 9b37b815a8..29b5aa25d9 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/SandboxIndexAndWriteService.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/SandboxIndexAndWriteService.scala
@@ -40,7 +40,6 @@ import com.digitalasset.platform.sandbox.stores.ledger.ScenarioLoader.LedgerEntr
import com.digitalasset.platform.sandbox.stores.ledger._
import com.digitalasset.platform.sandbox.stores.ledger.sql.SqlStartMode
import com.digitalasset.platform.server.api.validation.ErrorFactories
-import com.digitalasset.platform.services.time.TimeModel
import org.slf4j.LoggerFactory
import scalaz.Tag
import scalaz.syntax.tag._
@@ -65,7 +64,7 @@ object SandboxIndexAndWriteService {
ledgerId: LedgerId,
participantId: ParticipantId,
jdbcUrl: String,
- timeModel: TimeModel,
+ timeModel: ParticipantState.TimeModel,
timeProvider: TimeProvider,
acs: InMemoryActiveLedgerState,
ledgerEntries: ImmArray[LedgerEntryOrBump],
@@ -78,6 +77,7 @@ object SandboxIndexAndWriteService {
.jdbcBacked(
jdbcUrl,
ledgerId,
+ participantId,
timeProvider,
acs,
templateStore,
@@ -94,7 +94,7 @@ object SandboxIndexAndWriteService {
def inMemory(
ledgerId: LedgerId,
participantId: ParticipantId,
- timeModel: TimeModel,
+ timeModel: ParticipantState.TimeModel,
timeProvider: TimeProvider,
acs: InMemoryActiveLedgerState,
ledgerEntries: ImmArray[LedgerEntryOrBump],
@@ -102,7 +102,7 @@ object SandboxIndexAndWriteService {
metrics: MetricRegistry)(implicit mat: Materializer): IndexAndWriteService = {
val ledger =
Ledger.metered(
- Ledger.inMemory(ledgerId, timeProvider, acs, templateStore, ledgerEntries),
+ Ledger.inMemory(ledgerId, participantId, timeProvider, acs, templateStore, ledgerEntries),
metrics)
createInstance(ledger, participantId, timeModel, timeProvider)
}
@@ -110,7 +110,7 @@ object SandboxIndexAndWriteService {
private def createInstance(
ledger: Ledger,
participantId: ParticipantId,
- timeModel: TimeModel,
+ timeModel: ParticipantState.TimeModel,
timeProvider: TimeProvider)(implicit mat: Materializer) = {
val contractStore = new SandboxContractStore(ledger)
val indexSvc = new LedgerBackedIndexService(ledger, contractStore, participantId) {
@@ -439,6 +439,5 @@ class LedgerBackedWriteService(ledger: Ledger, timeProvider: TimeProvider) exten
maxRecordTime: Time.Timestamp,
submissionId: String,
config: Configuration): CompletionStage[SubmissionResult] =
- // FIXME(JM): Implement configuration changes in sandbox.
- CompletableFuture.completedFuture(SubmissionResult.NotSupported)
+ FutureConverters.toJava(ledger.publishConfiguration(maxRecordTime, submissionId, config))
}
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/ConfigurationEntry.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/ConfigurationEntry.scala
new file mode 100644
index 0000000000..7048a74f40
--- /dev/null
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/ConfigurationEntry.scala
@@ -0,0 +1,25 @@
+// Copyright (c) 2019 The DAML Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.digitalasset.platform.sandbox.stores.ledger
+
+import com.daml.ledger.participant.state.v1.{Configuration, ParticipantId}
+
+sealed abstract class ConfigurationEntry extends Product with Serializable
+
+object ConfigurationEntry {
+
+ final case class Accepted(
+ submissionId: String,
+ participantId: ParticipantId,
+ configuration: Configuration,
+ ) extends ConfigurationEntry
+
+ final case class Rejected(
+ submissionId: String,
+ participantId: ParticipantId,
+ rejectionReason: String,
+ proposedConfiguration: Configuration
+ ) extends ConfigurationEntry
+
+}
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/Ledger.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/Ledger.scala
index 204ffd09e5..6781a85b38 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/Ledger.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/Ledger.scala
@@ -14,6 +14,7 @@ import com.daml.ledger.participant.state.v1._
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.data.ImmArray
import com.digitalasset.daml.lf.data.Ref.{PackageId, Party, TransactionIdString}
+import com.digitalasset.daml.lf.data.Time.Timestamp
import com.digitalasset.daml.lf.language.Ast
import com.digitalasset.daml.lf.transaction.Node.GlobalKey
import com.digitalasset.daml.lf.value.Value
@@ -53,6 +54,14 @@ trait WriteLedger extends AutoCloseable {
knownSince: Instant,
sourceDescription: Option[String],
payload: List[Archive]): Future[UploadPackagesResult]
+
+ // Configuration management
+ def publishConfiguration(
+ maxRecordTime: Timestamp,
+ submissionId: String,
+ config: Configuration
+ ): Future[SubmissionResult]
+
}
/** Defines all the functionalities a Ledger needs to provide */
@@ -85,6 +94,9 @@ trait ReadOnlyLedger extends AutoCloseable {
def getLfPackage(packageId: PackageId): Future[Option[Ast.Package]]
+ // Configuration management
+ def lookupLedgerConfiguration(): Future[Option[Configuration]]
+ def configurationEntries(offset: Option[Long]): Source[(Long, ConfigurationEntry), NotUsed]
}
object Ledger {
@@ -95,6 +107,7 @@ object Ledger {
* Creates an in-memory ledger
*
* @param ledgerId the id to be used for the ledger
+ * @param participantId the id of the participant
* @param timeProvider the provider of time
* @param acs the starting ACS store
* @param ledgerEntries the starting entries
@@ -102,17 +115,19 @@ object Ledger {
*/
def inMemory(
ledgerId: LedgerId,
+ participantId: ParticipantId,
timeProvider: TimeProvider,
acs: InMemoryActiveLedgerState,
packages: InMemoryPackageStore,
ledgerEntries: ImmArray[LedgerEntryOrBump]): Ledger =
- new InMemoryLedger(ledgerId, timeProvider, acs, packages, ledgerEntries)
+ new InMemoryLedger(ledgerId, participantId, timeProvider, acs, packages, ledgerEntries)
/**
* Creates a JDBC backed ledger
*
* @param jdbcUrl the jdbc url string containing the username and password as well
* @param ledgerId the id to be used for the ledger
+ * @param participantId the participant identifier
* @param timeProvider the provider of time
* @param acs the starting ACS store
* @param ledgerEntries the starting entries
@@ -123,6 +138,7 @@ object Ledger {
def jdbcBacked(
jdbcUrl: String,
ledgerId: LedgerId,
+ participantId: ParticipantId,
timeProvider: TimeProvider,
acs: InMemoryActiveLedgerState,
packages: InMemoryPackageStore,
@@ -135,6 +151,7 @@ object Ledger {
SqlLedger(
jdbcUrl,
Some(ledgerId),
+ participantId,
timeProvider,
acs,
packages,
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/MeteredLedger.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/MeteredLedger.scala
index 6ad4cba641..6103340939 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/MeteredLedger.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/MeteredLedger.scala
@@ -11,6 +11,7 @@ import com.codahale.metrics.MetricRegistry
import com.daml.ledger.participant.state.index.v2.PackageDetails
import com.daml.ledger.participant.state.v1._
import com.digitalasset.daml.lf.data.Ref.{PackageId, Party, TransactionIdString}
+import com.digitalasset.daml.lf.data.Time
import com.digitalasset.daml.lf.language.Ast
import com.digitalasset.daml.lf.transaction.Node.GlobalKey
import com.digitalasset.daml.lf.value.Value
@@ -30,6 +31,7 @@ private class MeteredReadOnlyLedger(ledger: ReadOnlyLedger, metrics: MetricRegis
val lookupContract = metrics.timer("Ledger.lookupContract")
val lookupKey = metrics.timer("Ledger.lookupKey")
val lookupTransaction = metrics.timer("Ledger.lookupTransaction")
+ val lookupLedgerConfiguration = metrics.timer("Ledger.lookupLedgerConfiguration ")
val parties = metrics.timer("Ledger.parties")
val listLfPackages = metrics.timer("Ledger.listLfPackages")
val getLfArchive = metrics.timer("Ledger.getLfArchive")
@@ -73,6 +75,13 @@ private class MeteredReadOnlyLedger(ledger: ReadOnlyLedger, metrics: MetricRegis
override def close(): Unit = {
ledger.close()
}
+
+ override def lookupLedgerConfiguration(): Future[Option[Configuration]] =
+ timedFuture(Metrics.lookupLedgerConfiguration, ledger.lookupLedgerConfiguration())
+
+ override def configurationEntries(
+ offset: Option[Long]): Source[(Long, ConfigurationEntry), NotUsed] =
+ ledger.configurationEntries(offset)
}
object MeteredReadOnlyLedger {
@@ -89,6 +98,7 @@ private class MeteredLedger(ledger: Ledger, metrics: MetricRegistry)
val publishTransaction = metrics.timer("Ledger.publishTransaction")
val addParty = metrics.timer("Ledger.addParty")
val uploadPackages = metrics.timer("Ledger.uploadPackages")
+ val publishConfiguration = metrics.timer("Ledger.publishConfiguration ")
}
override def publishHeartbeat(time: Instant): Future[Unit] =
@@ -115,9 +125,18 @@ private class MeteredLedger(ledger: Ledger, metrics: MetricRegistry)
Metrics.uploadPackages,
ledger.uploadPackages(knownSince, sourceDescription, payload))
+ override def publishConfiguration(
+ maxRecordTime: Time.Timestamp,
+ submissionId: String,
+ config: Configuration): Future[SubmissionResult] =
+ timedFuture(
+ Metrics.publishConfiguration,
+ ledger.publishConfiguration(maxRecordTime, submissionId, config))
+
override def close(): Unit = {
ledger.close()
}
+
}
object MeteredLedger {
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/inmemory/InMemoryLedger.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/inmemory/InMemoryLedger.scala
index 713381f254..e825bdd95b 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/inmemory/InMemoryLedger.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/inmemory/InMemoryLedger.scala
@@ -10,6 +10,8 @@ import akka.NotUsed
import akka.stream.scaladsl.Source
import com.daml.ledger.participant.state.index.v2.PackageDetails
import com.daml.ledger.participant.state.v1.{
+ Configuration,
+ ParticipantId,
PartyAllocationResult,
SubmissionResult,
SubmittedTransaction,
@@ -18,7 +20,7 @@ import com.daml.ledger.participant.state.v1.{
UploadPackagesResult
}
import com.digitalasset.api.util.TimeProvider
-import com.digitalasset.daml.lf.data.ImmArray
+import com.digitalasset.daml.lf.data.{ImmArray, Time}
import com.digitalasset.daml.lf.data.Ref.{PackageId, Party, TransactionIdString}
import com.digitalasset.daml.lf.data.Ref.LedgerString.ordering
import com.digitalasset.daml.lf.engine.Blinding
@@ -39,7 +41,12 @@ import com.digitalasset.platform.sandbox.stores.ActiveLedgerState.ActiveContract
import com.digitalasset.platform.sandbox.stores.deduplicator.Deduplicator
import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry.{Checkpoint, Rejection}
import com.digitalasset.platform.sandbox.stores.ledger.ScenarioLoader.LedgerEntryOrBump
-import com.digitalasset.platform.sandbox.stores.ledger.{Ledger, LedgerEntry, LedgerSnapshot}
+import com.digitalasset.platform.sandbox.stores.ledger.{
+ Ledger,
+ LedgerEntry,
+ LedgerSnapshot,
+ ConfigurationEntry
+}
import com.digitalasset.platform.sandbox.stores.{
ActiveLedgerState,
InMemoryActiveLedgerState,
@@ -50,11 +57,16 @@ import org.slf4j.LoggerFactory
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
+sealed trait InMemoryEntry extends Product with Serializable
+final case class InMemoryLedgerEntry(entry: LedgerEntry) extends InMemoryEntry
+final case class InMemoryConfigEntry(entry: ConfigurationEntry) extends InMemoryEntry
+
/** This stores all the mutable data that we need to run a ledger: the PCS, the ACS, and the deduplicator.
*
*/
class InMemoryLedger(
val ledgerId: LedgerId,
+ participantId: ParticipantId,
timeProvider: TimeProvider,
acs0: InMemoryActiveLedgerState,
packageStoreInit: InMemoryPackageStore,
@@ -64,13 +76,13 @@ class InMemoryLedger(
private val logger = LoggerFactory.getLogger(this.getClass)
private val entries = {
- val l = new LedgerEntries[LedgerEntry](_.toString)
+ val l = new LedgerEntries[InMemoryEntry](_.toString)
ledgerEntries.foreach {
case LedgerEntryOrBump.Bump(increment) =>
l.incrementOffset(increment)
()
case LedgerEntryOrBump.Entry(entry) =>
- l.publish(entry)
+ l.publish(InMemoryLedgerEntry(entry))
()
}
l
@@ -79,11 +91,14 @@ class InMemoryLedger(
private val packageStoreRef = new AtomicReference[InMemoryPackageStore](packageStoreInit)
override def ledgerEntries(offset: Option[Long]): Source[(Long, LedgerEntry), NotUsed] =
- entries.getSource(offset)
+ entries
+ .getSource(offset)
+ .collect { case (offset, InMemoryLedgerEntry(entry)) => offset -> entry }
// mutable state
private var acs = acs0
private var deduplicator = Deduplicator()
+ private var ledgerConfiguration: Option[Configuration] = None
override def ledgerEnd: Long = entries.ledgerEnd
@@ -114,7 +129,7 @@ class InMemoryLedger(
override def publishHeartbeat(time: Instant): Future[Unit] =
Future.successful(this.synchronized[Unit] {
- entries.publish(Checkpoint(time))
+ entries.publish(InMemoryLedgerEntry(Checkpoint(time)))
()
})
@@ -208,7 +223,7 @@ class InMemoryLedger(
mappedTx,
recordBlinding
)
- entries.publish(entry)
+ entries.publish(InMemoryLedgerEntry(entry))
()
}
}
@@ -218,12 +233,14 @@ class InMemoryLedger(
private def handleError(submitterInfo: SubmitterInfo, reason: RejectionReason): Unit = {
logger.warn(s"Publishing error to ledger: ${reason.description}")
entries.publish(
- Rejection(
- timeProvider.getCurrentTime,
- submitterInfo.commandId,
- submitterInfo.applicationId,
- submitterInfo.submitter,
- reason)
+ InMemoryLedgerEntry(
+ Rejection(
+ timeProvider.getCurrentTime,
+ submitterInfo.commandId,
+ submitterInfo.applicationId,
+ submitterInfo.submitter,
+ reason)
+ )
)
()
}
@@ -240,8 +257,8 @@ class InMemoryLedger(
Future.successful(
entries
.getEntryAt(n)
- .collect[(Long, LedgerEntry.Transaction)] {
- case t: LedgerEntry.Transaction =>
+ .collect {
+ case InMemoryLedgerEntry(t: LedgerEntry.Transaction) =>
(n, t) // the transaction id is also the offset
})
}
@@ -292,4 +309,37 @@ class InMemoryLedger(
}
)
}
+
+ override def publishConfiguration(
+ maxRecordTime: Time.Timestamp,
+ submissionId: String,
+ config: Configuration): Future[SubmissionResult] =
+ Future.successful {
+ this.synchronized {
+ ledgerConfiguration match {
+ case Some(currentConfig) if config.generation != currentConfig.generation =>
+ entries.publish(InMemoryConfigEntry(ConfigurationEntry.Rejected(
+ submissionId,
+ participantId,
+ "Generation mismatch, expected ${currentConfig.generation}, got ${config.generation}",
+ config)))
+
+ case _ =>
+ entries.publish(
+ InMemoryConfigEntry(ConfigurationEntry.Accepted(submissionId, participantId, config)))
+ ledgerConfiguration = Some(config)
+ }
+ SubmissionResult.Acknowledged
+ }
+ }
+
+ override def lookupLedgerConfiguration(): Future[Option[Configuration]] =
+ Future.successful(this.synchronized { ledgerConfiguration })
+
+ override def configurationEntries(
+ offset: Option[Long]): Source[(Long, ConfigurationEntry), NotUsed] =
+ entries
+ .getSource(offset)
+ .collect { case (offset, InMemoryConfigEntry(entry)) => offset -> entry }
+
}
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/BaseLedger.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/BaseLedger.scala
index 165fae71dc..f9fe432ed6 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/BaseLedger.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/BaseLedger.scala
@@ -7,6 +7,7 @@ import akka.NotUsed
import akka.stream.Materializer
import akka.stream.scaladsl.Source
import com.daml.ledger.participant.state.index.v2
+import com.daml.ledger.participant.state.v1.Configuration
import com.digitalasset.daml.lf.archive.Decode
import com.digitalasset.daml.lf.data.Ref.{PackageId, Party, TransactionIdString}
import com.digitalasset.daml.lf.language.Ast
@@ -21,7 +22,12 @@ import com.digitalasset.platform.common.util.DirectExecutionContext
import com.digitalasset.platform.participant.util.EventFilter.TemplateAwareFilter
import com.digitalasset.platform.sandbox.stores.ActiveLedgerState
import com.digitalasset.platform.sandbox.stores.ledger.sql.dao.LedgerReadDao
-import com.digitalasset.platform.sandbox.stores.ledger.{LedgerEntry, LedgerSnapshot, ReadOnlyLedger}
+import com.digitalasset.platform.sandbox.stores.ledger.{
+ LedgerEntry,
+ LedgerSnapshot,
+ ReadOnlyLedger,
+ ConfigurationEntry
+}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
@@ -85,8 +91,16 @@ class BaseLedger(val ledgerId: LedgerId, headAtInitialization: Long, ledgerDao:
.flatMap(archiveO =>
Future.fromTry(Try(archiveO.map(archive => Decode.decodeArchive(archive)._2))))(DEC)
+ override def lookupLedgerConfiguration(): Future[Option[Configuration]] =
+ ledgerDao.lookupLedgerConfiguration()
+
+ override def configurationEntries(
+ offset: Option[Long]): Source[(Long, ConfigurationEntry), NotUsed] =
+ dispatcher.startingAt(offset.getOrElse(0), RangeSource(ledgerDao.getConfigurationEntries(_, _)))
+
override def close(): Unit = {
dispatcher.close()
ledgerDao.close()
}
+
}
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedger.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedger.scala
index 4d274e05b6..00d60b7604 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedger.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedger.scala
@@ -14,9 +14,9 @@ import com.codahale.metrics.MetricRegistry
import com.daml.ledger.participant.state.index.v2.PackageDetails
import com.daml.ledger.participant.state.v1._
import com.digitalasset.api.util.TimeProvider
-import com.digitalasset.daml.lf.data.Ref.Party
import com.digitalasset.daml.lf.data.Ref.LedgerString.ordering
-import com.digitalasset.daml.lf.data.{ImmArray, Ref}
+import com.digitalasset.daml.lf.data.Ref.Party
+import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time}
import com.digitalasset.daml.lf.engine.Blinding
import com.digitalasset.daml.lf.value.Value.{AbsoluteContractId, ContractId}
import com.digitalasset.daml_lf_dev.DamlLf.Archive
@@ -69,6 +69,7 @@ object SqlLedger {
def apply(
jdbcUrl: String,
ledgerId: Option[LedgerId],
+ participantId: ParticipantId,
timeProvider: TimeProvider,
acs: InMemoryActiveLedgerState,
packages: InMemoryPackageStore,
@@ -108,6 +109,7 @@ object SqlLedger {
sqlLedgerFactory.createSqlLedger(
ledgerId,
+ participantId,
timeProvider,
startMode,
acs,
@@ -123,6 +125,7 @@ object SqlLedger {
private class SqlLedger(
ledgerId: LedgerId,
+ participantId: ParticipantId,
headAtInitialization: Long,
ledgerDao: LedgerDao,
timeProvider: TimeProvider,
@@ -135,11 +138,13 @@ private class SqlLedger(
private val logger = loggerFactory.getLogger(getClass)
+ private case class Offsets(offset: Long, nextOffset: Long)
+
// the reason for modelling persistence as a reactive pipeline is to avoid having race-conditions between the
// moving ledger-end, the async persistence operation and the dispatcher head notification
private val (checkpointQueue, persistenceQueue): (
- SourceQueueWithComplete[Long => PersistenceEntry],
- SourceQueueWithComplete[Long => PersistenceEntry]) = createQueues()
+ SourceQueueWithComplete[Offsets => Future[Unit]],
+ SourceQueueWithComplete[Offsets => Future[Unit]]) = createQueues()
watchForFailures(checkpointQueue, "checkpoint")
watchForFailures(persistenceQueue, "persistence")
@@ -153,12 +158,12 @@ private class SqlLedger(
}(DEC)
private def createQueues(): (
- SourceQueueWithComplete[Long => PersistenceEntry],
- SourceQueueWithComplete[Long => PersistenceEntry]) = {
+ SourceQueueWithComplete[Offsets => Future[Unit]],
+ SourceQueueWithComplete[Offsets => Future[Unit]]) = {
- val checkpointQueue = Source.queue[Long => PersistenceEntry](1, OverflowStrategy.dropHead)
+ val checkpointQueue = Source.queue[Offsets => Future[Unit]](1, OverflowStrategy.dropHead)
val persistenceQueue =
- Source.queue[Long => PersistenceEntry](queueDepth, OverflowStrategy.dropNew)
+ Source.queue[Offsets => Future[Unit]](queueDepth, OverflowStrategy.dropNew)
implicit val ec: ExecutionContext = DEC
@@ -167,7 +172,7 @@ private class SqlLedger(
q1Mat -> q2Mat
} { implicit b => (s1, s2) =>
import akka.stream.scaladsl.GraphDSL.Implicits._
- val merge = b.add(MergePreferred[Long => PersistenceEntry](1))
+ val merge = b.add(MergePreferred[Offsets => Future[Unit]](1))
s1 ~> merge.preferred
s2 ~> merge.in(0)
@@ -187,28 +192,21 @@ private class SqlLedger(
//shooting the SQL queries in parallel
Future
.sequence(queue.toIterator.zipWithIndex.map {
- case (ledgerEntryGen, i) =>
+ case (persist, i) =>
val offset = startOffset + i
- ledgerDao
- .storeLedgerEntry(offset, offset + 1, None, ledgerEntryGen(offset))
- .map(_ => ())(DEC)
- .recover {
- case t =>
- //recovering from the failure so the persistence stream doesn't die
- logger.error(s"Failed to persist entry with offset: $offset", t)
- ()
- }(DEC)
+ persist(Offsets(offset, offset + 1L))
})
.map { _ =>
//note that we can have holes in offsets in case of the storing of an entry failed for some reason
dispatcher.signalNewHead(startOffset + queue.length) //signalling downstream subscriptions
}(DEC)
}
- .toMat(Sink.ignore)(Keep.left[
- (
- SourceQueueWithComplete[Long => PersistenceEntry],
- SourceQueueWithComplete[Long => PersistenceEntry]),
- Future[Done]])
+ .toMat(Sink.ignore)(
+ Keep.left[
+ (
+ SourceQueueWithComplete[Offsets => Future[Unit]],
+ SourceQueueWithComplete[Offsets => Future[Unit]]),
+ Future[Done]])
.run()
}
@@ -218,17 +216,29 @@ private class SqlLedger(
checkpointQueue.complete()
}
+ private def storeLedgerEntry(offsets: Offsets, entry: PersistenceEntry): Future[Unit] =
+ ledgerDao
+ .storeLedgerEntry(offsets.offset, offsets.nextOffset, None, entry)
+ .map(_ => ())(DEC)
+ .recover {
+ case t =>
+ //recovering from the failure so the persistence stream doesn't die
+ logger.error(s"Failed to persist entry with offsets: $offsets", t)
+ ()
+ }(DEC)
+
override def publishHeartbeat(time: Instant): Future[Unit] =
checkpointQueue
- .offer(_ => PersistenceEntry.Checkpoint(LedgerEntry.Checkpoint(time)))
+ .offer(offsets =>
+ storeLedgerEntry(offsets, PersistenceEntry.Checkpoint(LedgerEntry.Checkpoint(time))))
.map(_ => ())(DEC) //this never pushes back, see createQueues above!
override def publishTransaction(
submitterInfo: SubmitterInfo,
transactionMeta: TransactionMeta,
transaction: SubmittedTransaction): Future[SubmissionResult] =
- enqueue { offset =>
- val transactionId = Ref.LedgerString.fromLong(offset)
+ enqueue { offsets =>
+ val transactionId = Ref.LedgerString.fromLong(offsets.offset)
val toAbsCoid: ContractId => AbsoluteContractId =
SandboxEventIdFormatter.makeAbsCoid(transactionId)
@@ -249,7 +259,7 @@ private class SqlLedger(
}
val recordTime = timeProvider.getCurrentTime
- if (recordTime.isAfter(submitterInfo.maxRecordTime.toInstant)) {
+ val entry = if (recordTime.isAfter(submitterInfo.maxRecordTime.toInstant)) {
// This can happen if the DAML-LF computation (i.e. exercise of a choice) takes longer
// than the time window between LET and MRT allows for.
// See https://github.com/digital-asset/daml/issues/987
@@ -281,11 +291,14 @@ private class SqlLedger(
List.empty
)
}
+
+ storeLedgerEntry(offsets, entry)
+
}
- private def enqueue(f: Long => PersistenceEntry): Future[SubmissionResult] =
+ private def enqueue(persist: Offsets => Future[Unit]): Future[SubmissionResult] =
persistenceQueue
- .offer(f)
+ .offer(persist)
.transform {
case Success(Enqueued) =>
Success(SubmissionResult.Acknowledged)
@@ -331,6 +344,35 @@ private class SqlLedger(
UploadPackagesResult.Ok
}(DEC)
}
+
+ override def publishConfiguration(
+ maxRecordTime: Time.Timestamp,
+ submissionId: String,
+ config: Configuration): Future[SubmissionResult] =
+ enqueue { offsets =>
+ val recordTime = timeProvider.getCurrentTime
+ // NOTE(JM): If the generation in the new configuration is invalid
+ // we persist a rejection.
+ ledgerDao
+ .storeConfigurationEntry(
+ offsets.offset,
+ offsets.nextOffset,
+ None,
+ recordTime,
+ submissionId,
+ participantId,
+ config,
+ None
+ )
+ .map(_ => ())(DEC)
+ .recover {
+ case t =>
+ //recovering from the failure so the persistence stream doesn't die
+ logger.error(s"Failed to persist configuration with offsets: $offsets", t)
+ ()
+ }(DEC)
+ }
+
}
private class SqlLedgerFactory(ledgerDao: LedgerDao, loggerFactory: NamedLoggerFactory) {
@@ -343,6 +385,7 @@ private class SqlLedgerFactory(ledgerDao: LedgerDao, loggerFactory: NamedLoggerF
* @param initialLedgerId a random ledger id is generated if none given, if set it's used to initialize the ledger.
* In case the ledger had already been initialized, the given ledger id must not be set or must
* be equal to the one in the database.
+ * @param participantId the participant identifier
* @param timeProvider to get the current time when sequencing transactions
* @param startMode whether we should start with a clean state or continue where we left off
* @param initialLedgerEntries The initial ledger entries -- usually provided by the scenario runner. Will only be
@@ -354,6 +397,7 @@ private class SqlLedgerFactory(ledgerDao: LedgerDao, loggerFactory: NamedLoggerF
*/
def createSqlLedger(
initialLedgerId: Option[LedgerId],
+ participantId: ParticipantId,
timeProvider: TimeProvider,
startMode: SqlStartMode,
acs: InMemoryActiveLedgerState,
@@ -381,6 +425,7 @@ private class SqlLedgerFactory(ledgerDao: LedgerDao, loggerFactory: NamedLoggerF
} yield
new SqlLedger(
ledgerId,
+ participantId,
ledgerEnd,
ledgerDao,
timeProvider,
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/JdbcLedgerDao.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/JdbcLedgerDao.scala
index 63f68a49b3..0ba0d0c995 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/JdbcLedgerDao.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/JdbcLedgerDao.scala
@@ -4,6 +4,7 @@ package com.digitalasset.platform.sandbox.stores.ledger.sql.dao
import java.io.InputStream
import java.sql.Connection
+import java.time.Instant
import java.util.Date
import akka.stream.Materializer
@@ -13,7 +14,12 @@ import anorm.SqlParser._
import anorm.ToStatement.optionToStatement
import anorm.{AkkaStream, BatchSql, Macro, NamedParameter, RowParser, SQL, SqlParser}
import com.daml.ledger.participant.state.index.v2.PackageDetails
-import com.daml.ledger.participant.state.v1.{AbsoluteContractInst, TransactionId}
+import com.daml.ledger.participant.state.v1.{
+ AbsoluteContractInst,
+ Configuration,
+ ParticipantId,
+ TransactionId
+}
import com.digitalasset.daml.lf.archive.Decode
import com.digitalasset.daml.lf.data.Ref.{
ContractIdString,
@@ -40,7 +46,7 @@ import com.digitalasset.platform.sandbox.stores.ActiveLedgerState.{
DivulgedContract
}
import com.digitalasset.platform.sandbox.stores._
-import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry
+import com.digitalasset.platform.sandbox.stores.ledger.{ConfigurationEntry, LedgerEntry}
import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry._
import com.digitalasset.platform.sandbox.stores.ledger.sql.dao.JdbcLedgerDao.{
H2DatabaseQueries,
@@ -141,6 +147,154 @@ private class JdbcLedgerDao(
()
}
+ private val SQL_UPDATE_CURRENT_CONFIGURATION = SQL(
+ "update parameters set configuration={configuration}"
+ )
+ private val SQL_SELECT_CURRENT_CONFIGURATION = SQL("select configuration from parameters")
+
+ private val SQL_GET_CONFIGURATION_ENTRIES = SQL(
+ "select * from configuration_entries where ledger_offset>={startInclusive} and ledger_offset<{endExclusive} order by ledger_offset asc")
+
+ private def updateCurrentConfiguration(configBytes: Array[Byte])(
+ implicit conn: Connection): Unit = {
+ SQL_UPDATE_CURRENT_CONFIGURATION
+ .on("configuration" -> configBytes)
+ .execute()
+ ()
+ }
+
+ private def selectLedgerConfiguration(implicit conn: Connection) =
+ SQL_SELECT_CURRENT_CONFIGURATION
+ .as(byteArray("configuration").?.single)
+ .flatMap(Configuration.decode(_).toOption)
+
+ override def lookupLedgerConfiguration(): Future[Option[Configuration]] =
+ dbDispatcher.executeSql("lookup_configuration")(implicit conn => selectLedgerConfiguration)
+
+ private val configurationAcceptType = "accept"
+ private val configurationRejectType = "reject"
+
+ private val configurationEntryParser: RowParser[(Long, ConfigurationEntry)] =
+ (long("ledger_offset") ~
+ str("typ") ~
+ str("submission_id") ~
+ str("participant_id") ~
+ str("rejection_reason")(emptyStringToNullColumn).? ~
+ byteArray("configuration"))
+ .map(flatten)
+ .map {
+ case (offset, typ, submissionId, participantIdRaw, rejectionReason, configBytes) =>
+ val config = Configuration
+ .decode(configBytes)
+ .fold(err => sys.error(s"Failed to decode configuration: $err"), identity)
+ val participantId = LedgerString
+ .fromString(participantIdRaw)
+ .fold(
+ err => sys.error(s"Failed to decode participant id in configuration entry: $err"),
+ identity)
+
+ offset ->
+ (typ match {
+ case `configurationAcceptType` =>
+ ConfigurationEntry.Accepted(
+ submissionId = submissionId,
+ participantId = participantId,
+ configuration = config
+ )
+ case `configurationRejectType` =>
+ ConfigurationEntry.Rejected(
+ submissionId = submissionId,
+ participantId = participantId,
+ rejectionReason = rejectionReason.getOrElse(""),
+ proposedConfiguration = config
+ )
+
+ case _ =>
+ sys.error(s"getConfigurationEntries: Unknown configuration entry type: $typ")
+ })
+ }
+
+ override def getConfigurationEntries(
+ startInclusive: Long,
+ endExclusive: Long): Source[(Long, ConfigurationEntry), NotUsed] =
+ paginatingStream(
+ startInclusive,
+ endExclusive,
+ PageSize,
+ (startI, endE) => {
+ dbDispatcher.executeSql("load_configuration_entries", Some(s"bounds: [$startI, $endE[")) {
+ implicit conn =>
+ SQL_GET_CONFIGURATION_ENTRIES
+ .on("startInclusive" -> startI, "endExclusive" -> endE)
+ .as(configurationEntryParser.*)
+ }
+ }
+ ).flatMapConcat(Source(_))
+
+ private val SQL_INSERT_CONFIGURATION_ENTRY =
+ SQL(
+ """insert into configuration_entries(ledger_offset, recorded_at, submission_id, participant_id, typ, rejection_reason, configuration)
+ |values({ledger_offset}, {recorded_at}, {submission_id}, {participant_id}, {typ}, {rejection_reason}, {configuration})
+ |""".stripMargin)
+
+ override def storeConfigurationEntry(
+ offset: LedgerOffset,
+ newLedgerEnd: LedgerOffset,
+ externalOffset: Option[ExternalOffset],
+ recordedAt: Instant,
+ submissionId: String,
+ participantId: ParticipantId,
+ configuration: Configuration,
+ rejectionReason: Option[String]
+ ): Future[PersistenceResponse] = {
+ dbDispatcher.executeSql("store_configuration_entry", Some("submissionId=$submissionId")) {
+ implicit conn =>
+ val currentConfig = selectLedgerConfiguration
+ var finalRejectionReason = rejectionReason
+ if (rejectionReason.isEmpty && (currentConfig exists (_.generation + 1 != configuration.generation))) {
+ // If we're not storing a rejection and the new generation is not succ of current configuration, then
+ // we store a rejection. This code path is only expected to be taken in sandbox. This follows the same
+ // pattern as storing transactions.
+ finalRejectionReason = Some(s"Generation mismatch")
+ }
+
+ updateLedgerEnd(newLedgerEnd, externalOffset)
+ val configurationBytes = Configuration.encode(configuration).toByteArray
+ val typ = if (finalRejectionReason.isEmpty) {
+ configurationAcceptType
+ } else {
+ configurationRejectType
+ }
+
+ Try({
+ SQL_INSERT_CONFIGURATION_ENTRY
+ .on(
+ "ledger_offset" -> offset,
+ "recorded_at" -> recordedAt,
+ "submission_id" -> submissionId,
+ "participant_id" -> participantId,
+ "typ" -> typ,
+ "rejection_reason" -> finalRejectionReason.orNull,
+ "configuration" -> configurationBytes
+ )
+ .execute()
+
+ if (typ == configurationAcceptType) {
+ updateCurrentConfiguration(configurationBytes)
+ }
+
+ PersistenceResponse.Ok
+ }).recover {
+ case NonFatal(e) if e.getMessage.contains(queries.DUPLICATE_KEY_ERROR) =>
+ logger.warn(
+ s"Ignoring duplicate configuration submission for submissionId $submissionId, participantId $participantId")
+ conn.rollback()
+ PersistenceResponse.Duplicate
+ }.get
+
+ }
+ }
+
private val SQL_INSERT_CONTRACT_KEY =
SQL(
"insert into contract_keys(package_id, name, value_hash, contract_id) values({package_id}, {name}, {value_hash}, {contract_id})")
@@ -1317,6 +1471,7 @@ private class JdbcLedgerDao(
|truncate contract_key_maintainers cascade;
|truncate parameters cascade;
|truncate contract_keys cascade;
+ |truncate configuration_entries cascade;
""".stripMargin)
override def reset(): Future[Unit] =
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/LedgerDao.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/LedgerDao.scala
index 2ab00cbb51..d48f78f0e7 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/LedgerDao.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/LedgerDao.scala
@@ -3,12 +3,15 @@
package com.digitalasset.platform.sandbox.stores.ledger.sql.dao
+import java.time.Instant
+
import akka.NotUsed
import akka.stream.Materializer
import akka.stream.scaladsl.Source
import com.codahale.metrics.MetricRegistry
import com.daml.ledger.participant.state.index.v2.PackageDetails
import com.daml.ledger.participant.state.v1.{AbsoluteContractInst, TransactionId}
+import com.daml.ledger.participant.state.v1.{Configuration, ParticipantId, TransactionId}
import com.digitalasset.daml.lf.data.Ref.{LedgerString, PackageId, Party}
import com.digitalasset.daml.lf.data.Relation.Relation
import com.digitalasset.daml.lf.transaction.Node
@@ -20,7 +23,7 @@ import com.digitalasset.ledger.api.domain.{LedgerId, PartyDetails}
import com.digitalasset.platform.common.util.DirectExecutionContext
import com.digitalasset.platform.participant.util.EventFilter.TemplateAwareFilter
import com.digitalasset.platform.sandbox.stores.ActiveLedgerState.{ActiveContract, Contract}
-import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry
+import com.digitalasset.platform.sandbox.stores.ledger.{ConfigurationEntry, LedgerEntry}
import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry.Transaction
import scala.collection.immutable
@@ -78,6 +81,14 @@ trait LedgerReadDao extends AutoCloseable {
contractId: AbsoluteContractId,
forParty: Party): Future[Option[Contract]]
+ /** Looks up the current ledger configuration, if it has been set. */
+ def lookupLedgerConfiguration(): Future[Option[Configuration]]
+
+ /** Returns a stream of configuration entries. */
+ def getConfigurationEntries(
+ startInclusive: LedgerOffset,
+ endExclusive: LedgerOffset): Source[(Long, ConfigurationEntry), NotUsed]
+
/**
* Looks up a LedgerEntry at a given offset
*
@@ -200,6 +211,20 @@ trait LedgerWriteDao extends AutoCloseable {
externalOffset: Option[ExternalOffset]
): Future[PersistenceResponse]
+ /**
+ * Store a configuration change or rejection.
+ */
+ def storeConfigurationEntry(
+ offset: LedgerOffset,
+ newLedgerEnd: LedgerOffset,
+ externalOffset: Option[ExternalOffset],
+ recordedAt: Instant,
+ submissionId: String,
+ participantId: ParticipantId,
+ configuration: Configuration,
+ rejectionReason: Option[String]
+ ): Future[PersistenceResponse]
+
/**
* Stores a set of DAML-LF packages
*
diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/MeteredLedgerDao.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/MeteredLedgerDao.scala
index e7c6f5c063..33cdf8aafc 100644
--- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/MeteredLedgerDao.scala
+++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/dao/MeteredLedgerDao.scala
@@ -3,11 +3,13 @@
package com.digitalasset.platform.sandbox.stores.ledger.sql.dao
+import java.time.Instant
+
import akka.NotUsed
import akka.stream.Materializer
import akka.stream.scaladsl.Source
import com.codahale.metrics.MetricRegistry
-import com.daml.ledger.participant.state.v1.TransactionId
+import com.daml.ledger.participant.state.v1.{Configuration, ParticipantId, TransactionId}
import com.digitalasset.daml.lf.data.Ref.{LedgerString, PackageId, Party}
import com.daml.ledger.participant.state.index.v2.PackageDetails
import com.digitalasset.daml.lf.transaction.Node
@@ -18,6 +20,7 @@ import com.digitalasset.platform.participant.util.EventFilter.TemplateAwareFilte
import com.digitalasset.platform.sandbox.metrics.timedFuture
import com.digitalasset.platform.sandbox.stores.ActiveLedgerState.{ActiveContract, Contract}
import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry
+import com.digitalasset.platform.sandbox.stores.ledger.{ConfigurationEntry, LedgerEntry}
import scala.collection.immutable
import scala.concurrent.Future
@@ -32,6 +35,7 @@ private class MeteredLedgerReadDao(ledgerDao: LedgerReadDao, metrics: MetricRegi
val lookupExternalLedgerEnd = metrics.timer("LedgerDao.lookupExternalLedgerEnd")
val lookupLedgerEntry = metrics.timer("LedgerDao.lookupLedgerEntry")
val lookupTransaction = metrics.timer("LedgerDao.lookupTransaction")
+ val lookupLedgerConfiguration = metrics.timer("LedgerDao.lookupLedgerConfiguration")
val lookupKey = metrics.timer("LedgerDao.lookupKey")
val lookupActiveContract = metrics.timer("LedgerDao.lookupActiveContract")
val getParties = metrics.timer("LedgerDao.getParties")
@@ -88,6 +92,16 @@ private class MeteredLedgerReadDao(ledgerDao: LedgerReadDao, metrics: MetricRegi
override def close(): Unit = {
ledgerDao.close()
}
+
+ /** Looks up the current ledger configuration, if it has been set. */
+ override def lookupLedgerConfiguration(): Future[Option[Configuration]] =
+ timedFuture(Metrics.lookupLedgerConfiguration, ledgerDao.lookupLedgerConfiguration())
+
+ /** Get a stream of configuration entries. */
+ override def getConfigurationEntries(
+ startInclusive: LedgerOffset,
+ endExclusive: LedgerOffset): Source[(LedgerOffset, ConfigurationEntry), NotUsed] =
+ ledgerDao.getConfigurationEntries(startInclusive, endExclusive)
}
private class MeteredLedgerDao(ledgerDao: LedgerDao, metrics: MetricRegistry)
@@ -99,7 +113,7 @@ private class MeteredLedgerDao(ledgerDao: LedgerDao, metrics: MetricRegistry)
val storeInitialState = metrics.timer("LedgerDao.storeInitialState")
val uploadLfPackages = metrics.timer("LedgerDao.uploadLfPackages")
val storeLedgerEntry = metrics.timer("LedgerDao.storeLedgerEntry")
-
+ val storeConfigurationEntry = metrics.timer("LedgerDao.storeConfigurationEntry")
}
override def storeLedgerEntry(
offset: Long,
@@ -131,6 +145,29 @@ private class MeteredLedgerDao(ledgerDao: LedgerDao, metrics: MetricRegistry)
externalOffset: Option[ExternalOffset]): Future[PersistenceResponse] =
timedFuture(Metrics.storeParty, ledgerDao.storeParty(party, displayName, externalOffset))
+ override def storeConfigurationEntry(
+ offset: LedgerOffset,
+ newLedgerEnd: LedgerOffset,
+ externalOffset: Option[ExternalOffset],
+ recordTime: Instant,
+ submissionId: String,
+ participantId: ParticipantId,
+ configuration: Configuration,
+ rejectionReason: Option[String]
+ ): Future[PersistenceResponse] =
+ timedFuture(
+ Metrics.storeConfigurationEntry,
+ ledgerDao.storeConfigurationEntry(
+ offset,
+ newLedgerEnd,
+ externalOffset,
+ recordTime,
+ submissionId,
+ participantId,
+ configuration,
+ rejectionReason)
+ )
+
override def uploadLfPackages(
uploadId: String,
packages: List[(Archive, PackageDetails)],
diff --git a/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/LedgerResource.scala b/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/LedgerResource.scala
index e864607b2e..80991dc91c 100644
--- a/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/LedgerResource.scala
+++ b/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/LedgerResource.scala
@@ -5,6 +5,7 @@ package com.digitalasset.platform.sandbox
import akka.stream.Materializer
import com.codahale.metrics.MetricRegistry
+import com.daml.ledger.participant.state.v1.ParticipantId
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.ledger.api.testing.utils.Resource
import com.digitalasset.platform.sandbox.persistence.{PostgresFixture, PostgresResource}
@@ -34,6 +35,7 @@ object LedgerResource {
def inMemory(
ledgerId: LedgerId,
+ participantId: ParticipantId,
timeProvider: TimeProvider,
acs: InMemoryActiveLedgerState = InMemoryActiveLedgerState.empty,
packages: InMemoryPackageStore = InMemoryPackageStore.empty,
@@ -41,12 +43,13 @@ object LedgerResource {
LedgerResource.resource(
() =>
Future.successful(
- Ledger.inMemory(ledgerId, timeProvider, acs, packages, entries)
+ Ledger.inMemory(ledgerId, participantId, timeProvider, acs, packages, entries)
)
)
def postgres(
ledgerId: LedgerId,
+ participantId: ParticipantId,
timeProvider: TimeProvider,
metrics: MetricRegistry,
packages: InMemoryPackageStore = InMemoryPackageStore.empty)(implicit mat: Materializer) = {
@@ -68,6 +71,7 @@ object LedgerResource {
Ledger.jdbcBacked(
postgres.value.jdbcUrl,
ledgerId,
+ participantId,
timeProvider,
InMemoryActiveLedgerState.empty,
packages,
diff --git a/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/TestHelpers.scala b/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/TestHelpers.scala
index b8ae99d8d8..3744eace1f 100644
--- a/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/TestHelpers.scala
+++ b/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/TestHelpers.scala
@@ -8,7 +8,7 @@ import java.time.Instant
import akka.stream.ActorMaterializer
import com.codahale.metrics.MetricRegistry
-import com.daml.ledger.participant.state.v1.ParticipantId
+import com.daml.ledger.participant.state.v1.{ParticipantId, TimeModel}
import com.digitalasset.api.util.{TimeProvider, ToleranceWindow}
import com.digitalasset.daml.lf.archive.DarReader
import com.digitalasset.daml.lf.data.{ImmArray, Ref}
@@ -23,7 +23,6 @@ import com.digitalasset.platform.sandbox.stores.{
InMemoryPackageStore,
SandboxIndexAndWriteService
}
-import com.digitalasset.platform.services.time.TimeModel
import scala.concurrent.ExecutionContext
diff --git a/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/services/SandboxFixture.scala b/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/services/SandboxFixture.scala
index 6b5a92055e..9c2187e2d2 100644
--- a/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/services/SandboxFixture.scala
+++ b/ledger/sandbox/src/test/lib/scala/com/digitalasset/platform/sandbox/services/SandboxFixture.scala
@@ -5,7 +5,6 @@ package com.digitalasset.platform.sandbox.services
import java.io.File
import java.util.concurrent.Executors
-
import akka.actor.ActorSystem
import akka.stream.{ActorMaterializer, Materializer}
import com.digitalasset.daml.bazeltools.BazelRunfiles._
@@ -22,16 +21,16 @@ import com.digitalasset.ledger.api.v1.testing.time_service.TimeServiceGrpc
import com.digitalasset.ledger.client.services.testing.time.StaticTime
import com.digitalasset.platform.common.LedgerIdMode
import com.digitalasset.platform.sandbox.config.SandboxConfig
-import com.digitalasset.platform.services.time.{TimeModel, TimeProviderType}
+import com.digitalasset.platform.services.time.TimeProviderType
import io.grpc.Channel
import org.scalatest.{BeforeAndAfterAll, Suite}
import scalaz.syntax.tag._
-
import scala.concurrent.{Await, ExecutionContext}
import scala.concurrent.duration._
import scala.util.Try
import com.digitalasset.ledger.api.domain.LedgerId
import com.google.common.util.concurrent.ThreadFactoryBuilder
+import com.daml.ledger.participant.state.v1.TimeModel
import org.slf4j.LoggerFactory
trait SandboxFixture extends SuiteResource[Channel] with BeforeAndAfterAll {
diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/ImplicitPartyAdditionIT.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/ImplicitPartyAdditionIT.scala
index 71991cb1d6..895eaadca5 100644
--- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/ImplicitPartyAdditionIT.scala
+++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/ImplicitPartyAdditionIT.scala
@@ -6,7 +6,12 @@ package com.digitalasset.platform.sandbox.stores.ledger
import java.time.Instant
import akka.stream.scaladsl.Sink
-import com.daml.ledger.participant.state.v1.{SubmissionResult, SubmitterInfo, TransactionMeta}
+import com.daml.ledger.participant.state.v1.{
+ ParticipantId,
+ SubmissionResult,
+ SubmitterInfo,
+ TransactionMeta
+}
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time}
import com.digitalasset.daml.lf.transaction.Node._
@@ -60,6 +65,7 @@ class ImplicitPartyAdditionIT
override def timeLimit: Span = scaled(60.seconds)
private val ledgerId: LedgerId = LedgerId("ledgerId")
+ private val participantId: ParticipantId = Ref.LedgerString.assertFromString("participantId")
private val timeProvider = TimeProvider.Constant(Instant.EPOCH.plusSeconds(10))
private val templateId1: Ref.Identifier = Ref.Identifier(
@@ -80,9 +86,9 @@ class ImplicitPartyAdditionIT
override protected def constructResource(index: Int, fixtureId: BackendType): Resource[Ledger] =
fixtureId match {
case BackendType.InMemory =>
- LedgerResource.inMemory(ledgerId, timeProvider)
+ LedgerResource.inMemory(ledgerId, participantId, timeProvider)
case BackendType.Postgres =>
- LedgerResource.postgres(ledgerId, timeProvider, metrics)
+ LedgerResource.postgres(ledgerId, participantId, timeProvider, metrics)
}
private def publishSingleNodeTx(
diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/TransactionMRTComplianceIT.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/TransactionMRTComplianceIT.scala
index 48f5b9dc28..36f2171829 100644
--- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/TransactionMRTComplianceIT.scala
+++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/TransactionMRTComplianceIT.scala
@@ -6,7 +6,12 @@ package com.digitalasset.platform.sandbox.stores.ledger
import java.time.Instant
import akka.stream.scaladsl.Sink
-import com.daml.ledger.participant.state.v1.{SubmissionResult, SubmitterInfo, TransactionMeta}
+import com.daml.ledger.participant.state.v1.{
+ ParticipantId,
+ SubmissionResult,
+ SubmitterInfo,
+ TransactionMeta
+}
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time}
import com.digitalasset.daml.lf.transaction.GenTransaction
@@ -51,6 +56,7 @@ class TransactionMRTComplianceIT
override def timeLimit: Span = scaled(60.seconds)
val ledgerId: LedgerId = LedgerId(Ref.LedgerString.assertFromString("ledgerId"))
+ private val participantId: ParticipantId = Ref.LedgerString.assertFromString("participantId")
val timeProvider = TimeProvider.Constant(Instant.EPOCH.plusSeconds(10))
/** Overriding this provides an easy way to narrow down testing to a single implementation. */
@@ -60,9 +66,9 @@ class TransactionMRTComplianceIT
override protected def constructResource(index: Int, fixtureId: BackendType): Resource[Ledger] =
fixtureId match {
case BackendType.InMemory =>
- LedgerResource.inMemory(ledgerId, timeProvider)
+ LedgerResource.inMemory(ledgerId, participantId, timeProvider)
case BackendType.Postgres =>
- LedgerResource.postgres(ledgerId, timeProvider, metrics)
+ LedgerResource.postgres(ledgerId, participantId, timeProvider, metrics)
}
val LET = Instant.EPOCH.plusSeconds(2)
diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/JdbcLedgerDaoSpec.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/JdbcLedgerDaoSpec.scala
index 76ff04cd7b..094ec2a1bc 100644
--- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/JdbcLedgerDaoSpec.scala
+++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/JdbcLedgerDaoSpec.scala
@@ -11,7 +11,7 @@ import java.util.concurrent.atomic.AtomicLong
import akka.stream.scaladsl.{Sink, Source}
import com.codahale.metrics.MetricRegistry
import com.daml.ledger.participant.state.index.v2
-import com.daml.ledger.participant.state.v1.Offset
+import com.daml.ledger.participant.state.v1.{Configuration, Offset, TimeModel}
import com.digitalasset.daml.bazeltools.BazelRunfiles
import com.digitalasset.daml.lf.archive.DarReader
import com.digitalasset.daml.lf.data.Ref.LedgerString.ordering
@@ -45,7 +45,6 @@ import com.digitalasset.platform.common.logging.NamedLoggerFactory
import com.digitalasset.platform.participant.util.EventFilter
import com.digitalasset.platform.sandbox.persistence.PostgresAroundAll
import com.digitalasset.platform.sandbox.stores.ActiveLedgerState.ActiveContract
-import com.digitalasset.platform.sandbox.stores.ledger.LedgerEntry
import com.digitalasset.platform.sandbox.stores.ledger.sql.dao._
import com.digitalasset.platform.sandbox.stores.ledger.sql.migration.FlywayMigrations
import com.digitalasset.platform.sandbox.stores.ledger.sql.serialisation.{
@@ -55,6 +54,7 @@ import com.digitalasset.platform.sandbox.stores.ledger.sql.serialisation.{
ValueSerializer
}
import com.digitalasset.platform.sandbox.stores.ledger.sql.util.DbDispatcher
+import com.digitalasset.platform.sandbox.stores.ledger.{ConfigurationEntry, LedgerEntry}
import org.scalatest.{AsyncWordSpec, Matchers, OptionValues}
import scala.collection.immutable.TreeMap
@@ -270,6 +270,142 @@ class JdbcLedgerDaoSpec
persistAndLoadRejection(nextExternalOffset())
}
+ val defaultConfig = Configuration(
+ generation = 0,
+ timeModel = TimeModel.reasonableDefault
+ )
+
+ "be able to persist and load configuration" in {
+ val offset = nextOffset()
+ for {
+ startingOffset <- ledgerDao.lookupLedgerEnd()
+ startingConfig <- ledgerDao.lookupLedgerConfiguration()
+
+ response <- ledgerDao.storeConfigurationEntry(
+ offset,
+ offset + 1,
+ None,
+ Instant.EPOCH,
+ "submission-0",
+ Ref.LedgerString.assertFromString("participant-0"),
+ defaultConfig,
+ None
+ )
+ storedConfig <- ledgerDao.lookupLedgerConfiguration()
+ endingOffset <- ledgerDao.lookupLedgerEnd()
+ } yield {
+ response shouldEqual PersistenceResponse.Ok
+ startingConfig shouldEqual None
+ storedConfig shouldEqual Some(defaultConfig)
+ endingOffset shouldEqual (startingOffset + 1)
+ }
+ }
+
+ "be able to persist configuration rejection" in {
+ val offset = nextOffset()
+ val participantId = Ref.LedgerString.assertFromString("participant-0")
+ for {
+ startingConfig <- ledgerDao.lookupLedgerConfiguration()
+ proposedConfig = startingConfig.getOrElse(defaultConfig)
+ response <- ledgerDao.storeConfigurationEntry(
+ offset,
+ offset + 1,
+ None,
+ Instant.EPOCH,
+ "config-rejection-0",
+ participantId,
+ proposedConfig,
+ Some("bad config")
+ )
+ storedConfig <- ledgerDao.lookupLedgerConfiguration()
+ entries <- ledgerDao.getConfigurationEntries(offset, offset + 1).runWith(Sink.seq)
+
+ } yield {
+ response shouldEqual PersistenceResponse.Ok
+ startingConfig shouldEqual storedConfig
+ entries shouldEqual List(
+ offset -> ConfigurationEntry
+ .Rejected("config-rejection-0", participantId, "bad config", proposedConfig)
+ )
+ }
+ }
+
+ "refuse to persist invalid configuration entry" in {
+ val offset0 = nextOffset()
+ val participantId = Ref.LedgerString.assertFromString("participant-0")
+ for {
+ config <- ledgerDao.lookupLedgerConfiguration().map(_.getOrElse(defaultConfig))
+
+ // Store a new configuration with a known submission id
+ resp0 <- ledgerDao.storeConfigurationEntry(
+ offset0,
+ offset0 + 1,
+ None,
+ Instant.EPOCH,
+ "refuse-config-0",
+ participantId,
+ config.copy(generation = config.generation + 1),
+ None
+ )
+ newConfig <- ledgerDao.lookupLedgerConfiguration().map(_.get)
+
+ // Submission with duplicate submissionId is rejected
+ offset1 = nextOffset()
+ resp1 <- ledgerDao.storeConfigurationEntry(
+ offset1,
+ offset1 + 1,
+ None,
+ Instant.EPOCH,
+ "refuse-config-0",
+ participantId,
+ newConfig.copy(generation = config.generation + 1),
+ None
+ )
+
+ // Submission with mismatching generation is rejected
+ offset2 = nextOffset()
+ resp2 <- ledgerDao.storeConfigurationEntry(
+ offset2,
+ offset2 + 1,
+ None,
+ Instant.EPOCH,
+ "refuse-config-1",
+ participantId,
+ config,
+ None
+ )
+
+ // Submission with unique submissionId and correct generation is accepted.
+ offset3 = nextOffset()
+ lastConfig = newConfig.copy(generation = newConfig.generation + 1)
+ resp3 <- ledgerDao.storeConfigurationEntry(
+ offset3,
+ offset3 + 1,
+ None,
+ Instant.EPOCH,
+ "refuse-config-2",
+ participantId,
+ lastConfig,
+ None
+ )
+ lastConfigActual <- ledgerDao.lookupLedgerConfiguration().map(_.get)
+
+ entries <- ledgerDao.getConfigurationEntries(offset0, offset3 + 1).runWith(Sink.seq)
+ } yield {
+ resp0 shouldEqual PersistenceResponse.Ok
+ resp1 shouldEqual PersistenceResponse.Duplicate
+ resp2 shouldEqual PersistenceResponse.Ok
+ resp3 shouldEqual PersistenceResponse.Ok
+ lastConfig shouldEqual lastConfigActual
+ entries.toList shouldEqual List(
+ offset0 -> ConfigurationEntry.Accepted("refuse-config-0", participantId, newConfig),
+ offset2 -> ConfigurationEntry
+ .Rejected("refuse-config-1", participantId, "Generation mismatch", config),
+ offset3 -> ConfigurationEntry.Accepted("refuse-config-2", participantId, lastConfig)
+ )
+ }
+ }
+
"refuse to persist an upload with no packages without external offset" in {
recoverToSucceededIf[IllegalArgumentException] {
ledgerDao.uploadLfPackages(UUID.randomUUID().toString, Nil, None)
diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedgerSpec.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedgerSpec.scala
index 8bfabef689..ab0d789524 100644
--- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedgerSpec.scala
+++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/SqlLedgerSpec.scala
@@ -3,6 +3,7 @@
package com.digitalasset.platform.sandbox.stores.ledger.sql
+import com.daml.ledger.participant.state.v1.ParticipantId
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.data.{ImmArray, Ref}
import com.digitalasset.ledger.api.domain.LedgerId
@@ -31,6 +32,7 @@ class SqlLedgerSpec
private val queueDepth = 128
private val ledgerId: LedgerId = LedgerId(Ref.LedgerString.assertFromString("TheLedger"))
+ private val participantId: ParticipantId = Ref.LedgerString.assertFromString("TheParticipant")
private val loggerFactory = NamedLoggerFactory(this.getClass)
@@ -39,6 +41,7 @@ class SqlLedgerSpec
val ledgerF = SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = None,
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
@@ -58,6 +61,7 @@ class SqlLedgerSpec
val ledgerF = SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = Some(ledgerId),
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
@@ -79,6 +83,7 @@ class SqlLedgerSpec
ledger1 <- SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = Some(ledgerId),
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
@@ -92,6 +97,7 @@ class SqlLedgerSpec
ledger2 <- SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = Some(ledgerId),
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
@@ -105,6 +111,7 @@ class SqlLedgerSpec
ledger3 <- SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = None,
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
@@ -128,6 +135,7 @@ class SqlLedgerSpec
_ <- SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = Some(LedgerId(Ref.LedgerString.assertFromString("TheLedger"))),
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
@@ -140,6 +148,7 @@ class SqlLedgerSpec
_ <- SqlLedger(
jdbcUrl = postgresFixture.jdbcUrl,
ledgerId = Some(LedgerId(Ref.LedgerString.assertFromString("AnotherLedger"))),
+ participantId = participantId,
timeProvider = TimeProvider.UTC,
acs = InMemoryActiveLedgerState.empty,
packages = InMemoryPackageStore.empty,
diff --git a/release/artifacts.yaml b/release/artifacts.yaml
index 24190135e9..3f4891623b 100644
--- a/release/artifacts.yaml
+++ b/release/artifacts.yaml
@@ -1,10 +1,10 @@
- target: //daml-lf/data:data
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/archive:daml_lf_dev_archive_java_proto
type: jar-lib
javadoc-jar: daml_lf_dev_archive_java_proto_javadoc.jar
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/archive:daml_lf_dev_archive_proto_zip
type: zip
location:
@@ -18,7 +18,7 @@
- target: //daml-lf/archive:daml_lf_1.6_archive_java_proto
type: jar-lib
javadoc-jar: daml_lf_1.6_archive_java_proto_javadoc.jar
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/archive:daml_lf_1.6_archive_proto_zip
type: zip
location:
@@ -41,10 +41,10 @@
artifactId: daml-lf-1.7-archive-proto
- target: //daml-lf/archive:daml_lf_archive_reader
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //compiler/damlc/jar:damlc_jar
type: jar-deploy
- platformDependent: True
+ platformDependent: true
- target: //daml-lf/transaction:value_java_proto
type: jar-proto
mavenUpload: true
@@ -56,48 +56,48 @@
mavenUpload: true
- target: //daml-lf/transaction:transaction
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/data-scalacheck:data-scalacheck
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/transaction-scalacheck:transaction-scalacheck
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/language:language
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/interface:interface
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/validation:validation
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/interpreter:interpreter
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/scenario-interpreter:scenario-interpreter
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/engine:engine
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //daml-lf/repl:repl
type: jar
- target: //ledger-api/grpc-definitions:ledger-api-protos-tarball
type: targz
- mavenUpload: True
+ mavenUpload: true
location:
groupId: com.digitalasset
artifactId: ledger-api-protos
- target: //ledger-api/rs-grpc-bridge:rs-grpc-bridge
type: jar-lib
- mavenUpload: True
+ mavenUpload: true
- target: //language-support/java/bindings:bindings-java
type: jar-lib
- mavenUpload: True
+ mavenUpload: true
- target: //language-support/java/bindings-rxjava:bindings-rxjava
type: jar-lib
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/sandbox:sandbox-tarball
type: targz
location:
@@ -107,44 +107,46 @@
type: jar-deploy
- target: //ledger-api/grpc-definitions:ledger-api-scalapb
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger-api/testing-utils:testing-utils
type: jar-scala
+ mavenUpload: true
- target: //language-support/scala/bindings:bindings
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger-api/rs-grpc-akka:rs-grpc-akka
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger-api/rs-grpc-testing-utils:rs-grpc-testing-utils
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/ledger-api-akka:ledger-api-akka
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //scala-protoc-plugins/scala-logging:scala-logging-lib
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/ledger-api-scala-logging:ledger-api-scala-logging
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/ledger-api-client:ledger-api-client
type: jar-scala
mavenUpload: true
- target: //ledger/ledger-api-domain:ledger-api-domain
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/ledger-api-common:ledger-api-common
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/ledger-api-auth:ledger-api-auth
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/ledger-api-auth-client:ledger-api-auth-client
type: jar-lib
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/sandbox:sandbox
type: jar-scala
+ mavenUpload: true
- target: //ledger/ledger-api-test-tool:ledger-api-test-tool
type: jar-deploy
mavenUpload: true
@@ -155,7 +157,7 @@
type: jar
- target: //language-support/scala/bindings-akka:bindings-akka
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //language-support/java/codegen:shaded_binary
type: jar-scala
mavenUpload: true
@@ -172,13 +174,16 @@
mavenUpload: true
- target: //ledger/participant-state:participant-state
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/participant-state-index:participant-state-index
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger/sandbox:ledger-api-server
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
+- target: //ledger/participant-state/protobuf:ledger_configuration_java_proto
+ type: jar-proto
+ mavenUpload: true
- target: //ledger/participant-state/kvutils:daml_kvutils_java_proto
type: jar-proto
mavenUpload: true
@@ -191,7 +196,7 @@
type: jar-scala
- target: //ledger-service/jwt:jwt
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //ledger-service/db-backend:db-backend
type: jar-scala
- target: //ledger-service/http-json:http-json
@@ -205,7 +210,7 @@
mavenUpload: true
- target: //libs-scala/grpc-utils:grpc-utils
type: jar-scala
- mavenUpload: True
+ mavenUpload: true
- target: //libs-scala/timer-utils:timer-utils
type: jar-scala
mavenUpload: true