Cleaned up TimeWindow and added a bit more docs.

This commit is contained in:
Shams Asari 2017-07-20 13:32:53 +01:00
parent bf98f64269
commit e702025f62
18 changed files with 189 additions and 96 deletions

2
.gitignore vendored
View File

@ -67,7 +67,7 @@ lib/dokka.jar
## Plugin-specific files:
# IntelliJ
/out/
**/out/
/classes/
# mpeltonen/sbt-idea plugin

View File

@ -21,7 +21,6 @@ import java.math.BigDecimal
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.time.temporal.Temporal
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
@ -95,9 +94,6 @@ fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
}
}
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
fun <T> List<T>.indexOfOrThrow(item: T): Int {
val i = indexOf(item)

View File

@ -17,7 +17,6 @@ import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
import java.util.jar.JarInputStream
@ -328,63 +327,6 @@ data class AuthenticatedObject<out T : Any>(
)
// DOCEND 6
/**
* A time-window is required for validation/notarization purposes.
* If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be
* between (fromTime, untilTime).
* Usually, a time-window is required to have both sides set (fromTime, untilTime).
* However, some apps may require that a time-window has a start [Instant] (fromTime), but no end [Instant] (untilTime) and vice versa.
* TODO: Consider refactoring using TimeWindow abstraction like TimeWindow.From, TimeWindow.Until, TimeWindow.Between.
*/
@CordaSerializable
class TimeWindow private constructor(
/** The time at which this transaction is said to have occurred is after this moment. */
val fromTime: Instant?,
/** The time at which this transaction is said to have occurred is before this moment. */
val untilTime: Instant?
) {
companion object {
/** Use when the left-side [fromTime] of a [TimeWindow] is only required and we don't need an end instant (untilTime). */
@JvmStatic
fun fromOnly(fromTime: Instant) = TimeWindow(fromTime, null)
/** Use when the right-side [untilTime] of a [TimeWindow] is only required and we don't need a start instant (fromTime). */
@JvmStatic
fun untilOnly(untilTime: Instant) = TimeWindow(null, untilTime)
/** Use when both sides of a [TimeWindow] must be set ([fromTime], [untilTime]). */
@JvmStatic
fun between(fromTime: Instant, untilTime: Instant): TimeWindow {
require(fromTime < untilTime) { "fromTime should be earlier than untilTime" }
return TimeWindow(fromTime, untilTime)
}
/** Use when we have a start time and a period of validity. */
@JvmStatic
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
/**
* When we need to create a [TimeWindow] based on a specific time [Instant] and some tolerance in both sides of this instant.
* The result will be the following time-window: ([time] - [tolerance], [time] + [tolerance]).
*/
@JvmStatic
fun withTolerance(time: Instant, tolerance: Duration) = between(time - tolerance, time + tolerance)
}
/** The midpoint is calculated as fromTime + (untilTime - fromTime)/2. Note that it can only be computed if both sides are set. */
val midpoint: Instant get() = fromTime!! + Duration.between(fromTime, untilTime!!).dividedBy(2)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TimeWindow) return false
return (fromTime == other.fromTime && untilTime == other.untilTime)
}
override fun hashCode() = 31 * (fromTime?.hashCode() ?: 0) + (untilTime?.hashCode() ?: 0)
override fun toString() = "TimeWindow(fromTime=$fromTime, untilTime=$untilTime)"
}
// DOCSTART 5
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for

View File

@ -0,0 +1,87 @@
package net.corda.core.contracts
import net.corda.core.internal.div
import net.corda.core.internal.until
import net.corda.core.serialization.CordaSerializable
import java.time.Duration
import java.time.Instant
/**
* A time-window is required for validation/notarization purposes. If present in a transaction, contains a time that was
* verified by the uniqueness service. The true time must be in the time interval `[fromTime, untilTime)`.
*
* Usually a time-window is required to have both sides defined. However some apps may require a time-window which is
* open-ended on one of the two sides.
*/
@CordaSerializable
abstract class TimeWindow {
companion object {
/** Creates a [TimeWindow] with null [untilTime], i.e. the time interval `[fromTime, ∞]`. [midpoint] will return null. */
@JvmStatic
fun fromOnly(fromTime: Instant): TimeWindow = From(fromTime)
/** Creates a [TimeWindow] with null [fromTime], i.e. the time interval `[∞, untilTime)`. [midpoint] will return null. */
@JvmStatic
fun untilOnly(untilTime: Instant): TimeWindow = Until(untilTime)
/**
* Creates a [TimeWindow] with the time interval `[fromTime, untilTime)`. [midpoint] will return
* `fromTime + (untilTime - fromTime) / 2`.
* @throws IllegalArgumentException If [fromTime] [untilTime]
*/
@JvmStatic
fun between(fromTime: Instant, untilTime: Instant): TimeWindow = Between(fromTime, untilTime)
/**
* Creates a [TimeWindow] with the time interval `[fromTime, fromTime + duration)`. [midpoint] will return
* `fromTime + duration / 2`
*/
@JvmStatic
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
/**
* Creates a [TimeWindow] which is centered around [instant] with the given [tolerance] on both sides, i.e the
* time interval `[instant - tolerance, instant + tolerance)`. [midpoint] will return [instant].
*/
@JvmStatic
fun withTolerance(instant: Instant, tolerance: Duration) = between(instant - tolerance, instant + tolerance)
}
/** Returns the inclusive lower-bound of this [TimeWindow]'s interval, with null implying infinity. */
abstract val fromTime: Instant?
/** Returns the exclusive upper-bound of this [TimeWindow]'s interval, with null implying infinity. */
abstract val untilTime: Instant?
/**
* Returns the midpoint of [fromTime] and [untilTime] if both are non-null, calculated as
* `fromTime + (untilTime - fromTime)/2`, otherwise returns null.
*/
abstract val midpoint: Instant?
/** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */
abstract operator fun contains(instant: Instant): Boolean
private data class From(override val fromTime: Instant) : TimeWindow() {
override val untilTime: Instant? get() = null
override val midpoint: Instant? get() = null
override fun contains(instant: Instant): Boolean = instant >= fromTime
override fun toString(): String = "[$fromTime, ∞]"
}
private data class Until(override val untilTime: Instant) : TimeWindow() {
override val fromTime: Instant? get() = null
override val midpoint: Instant? get() = null
override fun contains(instant: Instant): Boolean = instant < untilTime
override fun toString(): String = "[∞, $untilTime)"
}
private data class Between(override val fromTime: Instant, override val untilTime: Instant) : TimeWindow() {
init {
require(fromTime < untilTime) { "fromTime must be earlier than untilTime" }
}
override val midpoint: Instant get() = fromTime + (fromTime until untilTime) / 2
override fun contains(instant: Instant): Boolean = instant >= fromTime && instant < untilTime
override fun toString(): String = "[$fromTime, $untilTime)"
}
}

View File

@ -7,9 +7,16 @@ import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
import java.time.temporal.Temporal
import java.util.stream.Stream
import kotlin.reflect.KClass
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
operator fun Duration.div(divider: Long): Duration = dividedBy(divider)
operator fun Duration.times(multiplicand: Long): Duration = multipliedBy(multiplicand)
/**
* Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform
* separator problems.

View File

@ -7,16 +7,5 @@ import java.time.Clock
* Checks if the current instant provided by the input clock falls within the provided time-window.
*/
class TimeWindowChecker(val clock: Clock = Clock.systemUTC()) {
fun isValid(timeWindow: TimeWindow): Boolean {
val fromTime = timeWindow.fromTime
val untilTime = timeWindow.untilTime
val now = clock.instant()
// We don't need to test for (fromTime == null && untilTime == null) or backwards bounds because the TimeWindow
// constructor already checks that.
if (fromTime != null && now < fromTime) return false
if (untilTime != null && now > untilTime) return false
return true
}
fun isValid(timeWindow: TimeWindow): Boolean = clock.instant() in timeWindow
}

View File

@ -0,0 +1,67 @@
package net.corda.core.contracts
import net.corda.core.utilities.millis
import net.corda.core.utilities.minutes
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset.UTC
class TimeWindowTest {
private val now = Instant.now()
@Test
fun fromOnly() {
val timeWindow = TimeWindow.fromOnly(now)
assertThat(timeWindow.fromTime).isEqualTo(now)
assertThat(timeWindow.untilTime).isNull()
assertThat(timeWindow.midpoint).isNull()
assertThat(timeWindow.contains(now - 1.millis)).isFalse()
assertThat(timeWindow.contains(now)).isTrue()
assertThat(timeWindow.contains(now + 1.millis)).isTrue()
}
@Test
fun untilOnly() {
val timeWindow = TimeWindow.untilOnly(now)
assertThat(timeWindow.fromTime).isNull()
assertThat(timeWindow.untilTime).isEqualTo(now)
assertThat(timeWindow.midpoint).isNull()
assertThat(timeWindow.contains(now - 1.millis)).isTrue()
assertThat(timeWindow.contains(now)).isFalse()
assertThat(timeWindow.contains(now + 1.millis)).isFalse()
}
@Test
fun between() {
val today = LocalDate.now()
val fromTime = today.atTime(12, 0).toInstant(UTC)
val untilTime = today.atTime(12, 30).toInstant(UTC)
val timeWindow = TimeWindow.between(fromTime, untilTime)
assertThat(timeWindow.fromTime).isEqualTo(fromTime)
assertThat(timeWindow.untilTime).isEqualTo(untilTime)
assertThat(timeWindow.midpoint).isEqualTo(today.atTime(12, 15).toInstant(UTC))
assertThat(timeWindow.contains(fromTime - 1.millis)).isFalse()
assertThat(timeWindow.contains(fromTime)).isTrue()
assertThat(timeWindow.contains(fromTime + 1.millis)).isTrue()
assertThat(timeWindow.contains(untilTime)).isFalse()
assertThat(timeWindow.contains(untilTime + 1.millis)).isFalse()
}
@Test
fun fromStartAndDuration() {
val timeWindow = TimeWindow.fromStartAndDuration(now, 10.minutes)
assertThat(timeWindow.fromTime).isEqualTo(now)
assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes)
assertThat(timeWindow.midpoint).isEqualTo(now + 5.minutes)
}
@Test
fun withTolerance() {
val timeWindow = TimeWindow.withTolerance(now, 10.minutes)
assertThat(timeWindow.fromTime).isEqualTo(now - 10.minutes)
assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes)
assertThat(timeWindow.midpoint).isEqualTo(now)
}
}

View File

@ -5,12 +5,12 @@ import net.corda.core.utilities.seconds
import org.junit.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class TimeWindowCheckerTests {
val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
val clock: Clock = Clock.fixed(Instant.now(), ZoneOffset.UTC)
val timeWindowChecker = TimeWindowChecker(clock)
@Test

View File

@ -72,7 +72,7 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
*/
override fun verify(tx: LedgerTransaction) {
val command = tx.commands.requireSingleCommand<TradeApprovalContract.Commands>()
require(tx.timeWindow?.midpoint != null) { "must have a time-window" }
requireNotNull(tx.timeWindow) { "must have a time-window" }
when (command.value) {
is Commands.Issue -> {
requireThat {

View File

@ -2,8 +2,10 @@ package net.corda.services.messaging
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.*
import net.corda.core.crypto.random63BitValue
import net.corda.core.elapsedTime
import net.corda.core.getOrThrow
import net.corda.core.internal.times
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
@ -43,7 +45,7 @@ class P2PMessagingTest : NodeBasedTest() {
// Start the network map a second time - this will restore message queues from the journal.
// This will hang and fail prior the fix. https://github.com/corda/corda/issues/37
stopAllNodes()
startNodes().getOrThrow(timeout = startUpDuration.multipliedBy(3))
startNodes().getOrThrow(timeout = startUpDuration * 3)
}
// https://github.com/corda/corda/issues/71

View File

@ -3,12 +3,12 @@ package net.corda.node.utilities
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.SettableFuture
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.internal.until
import net.corda.core.then
import rx.Observable
import rx.Subscriber
import rx.subscriptions.Subscriptions
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicLong
@ -80,7 +80,7 @@ fun Clock.awaitWithDeadline(deadline: Instant, future: Future<*> = GuavaSettable
} else {
null
}
nanos = Duration.between(this.instant(), deadline).toNanos()
nanos = (instant() until deadline).toNanos()
if (nanos > 0) {
try {
// This will return when it times out, or when the clock mutates or when when the original future completes.

View File

@ -1,10 +1,13 @@
package net.corda.node.utilities
import net.corda.core.internal.until
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.serialization.SingletonSerializationToken
import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken
import java.time.*
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import javax.annotation.concurrent.ThreadSafe
/**
@ -21,7 +24,7 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC
val currentDate = LocalDate.now(this)
if (currentDate.isBefore(date)) {
// It's ok to increment
delegateClock = Clock.offset(delegateClock, Duration.between(currentDate.atStartOfDay(), date.atStartOfDay()))
delegateClock = Clock.offset(delegateClock, currentDate.atStartOfDay() until date.atStartOfDay())
notifyMutationObservers()
return true
}

View File

@ -522,7 +522,7 @@ class InterestRateSwap : Contract {
outputs: List<ContractState>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Unit?): Set<Commands> {
require(tx.timeWindow?.midpoint != null) { "must be have a time-window)" }
requireNotNull(tx.timeWindow) { "must be have a time-window)" }
// We return an empty set because we don't process any commands
return emptySet()
}
@ -584,8 +584,7 @@ class InterestRateSwap : Contract {
"There is only one change in the IRS floating leg payment schedule" using (paymentDifferences.size == 1)
}
val changedRates = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = changedRates
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
val fixValue = command.value.fix
// Need to check that everything is the same apart from the new fixed rate entry.
requireThat {

View File

@ -23,7 +23,7 @@ data class OGTrade(override val legalContractReference: SecureHash = SecureHash.
outputs: List<ContractState>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Unit?): Set<Commands> {
require(tx.timeWindow?.midpoint != null) { "must have a time-window" }
requireNotNull(tx.timeWindow) { "must have a time-window" }
// We return an empty set because we don't process any commands
return emptySet()
}
@ -43,7 +43,7 @@ data class OGTrade(override val legalContractReference: SecureHash = SecureHash.
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Agree>()
require(inputs.size == 0) { "Inputs must be empty" }
require(inputs.isEmpty()) { "Inputs must be empty" }
require(outputs.size == 1) { "" }
require(outputs[0].buyer != outputs[0].seller)
require(outputs[0].participants.containsAll(outputs[0].participants))

View File

@ -25,7 +25,7 @@ data class PortfolioSwap(override val legalContractReference: SecureHash = Secur
outputs: List<ContractState>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Unit?): Set<Commands> {
require(tx.timeWindow?.midpoint != null) { "must have a time-window)" }
requireNotNull(tx.timeWindow) { "must have a time-window)" }
// We return an empty set because we don't process any commands
return emptySet()
}
@ -69,7 +69,7 @@ data class PortfolioSwap(override val legalContractReference: SecureHash = Secur
val command = tx.commands.requireSingleCommand<Commands.Agree>()
requireThat {
"there are no inputs" using (inputs.size == 0)
"there are no inputs" using (inputs.isEmpty())
"there is one output" using (outputs.size == 1)
"valuer must be a party" using (outputs[0].participants.contains(outputs[0].valuer))
}

View File

@ -16,6 +16,7 @@ import net.corda.core.crypto.appendToCommonName
import net.corda.core.crypto.commonName
import net.corda.core.identity.Party
import net.corda.core.internal.div
import net.corda.core.internal.times
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceInfo
@ -348,7 +349,7 @@ fun <A> poll(
override fun run() {
if (resultFuture.isCancelled) return // Give up, caller can no longer get the result.
if (++counter == warnCount) {
log.warn("Been polling $pollName for ${pollInterval.multipliedBy(warnCount.toLong()).seconds} seconds...")
log.warn("Been polling $pollName for ${(pollInterval * warnCount.toLong()).seconds} seconds...")
}
try {
val checkResult = check()

View File

@ -1,8 +1,8 @@
package net.corda.testing.node
import net.corda.core.internal.until
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.serialization.SingletonSerializationToken
import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken
import net.corda.node.utilities.MutableClock
import java.time.Clock
@ -35,7 +35,7 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC
*
* This will only be approximate due to the time ticking away, but will be some time shortly after the requested [Instant].
*/
@Synchronized fun setTo(newInstant: Instant) = advanceBy(Duration.between(instant(), newInstant))
@Synchronized fun setTo(newInstant: Instant) = advanceBy(instant() until newInstant)
@Synchronized override fun instant(): Instant {
return delegateClock.instant()

View File

@ -5,7 +5,7 @@ import com.google.common.util.concurrent.RateLimiter
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.catch
import net.corda.core.utilities.minutes
import net.corda.core.until
import net.corda.core.internal.until
import net.corda.core.utilities.loggerFor
import net.corda.demobench.model.NodeConfig
import net.corda.demobench.readErrorLines