mirror of
https://github.com/corda/corda.git
synced 2025-01-18 10:46:38 +00:00
Merge branch 'release/os/4.5' into os_4.5-feature_checkpoint_table_improvements-merge
This commit is contained in:
commit
6baa775e23
@ -187,7 +187,7 @@ buildscript {
|
||||
// See https://github.com/corda/gradle-capsule-plugin
|
||||
classpath "us.kirchmeier:gradle-capsule-plugin:1.0.4_r3"
|
||||
classpath group: "com.r3.testing", name: "gradle-distributed-testing-plugin", version: "1.2-LOCAL-K8S-SHARED-CACHE-SNAPSHOT", changing: true
|
||||
classpath group: "com.r3.dependx", name: "gradle-dependx", version: "0.1.12", changing: true
|
||||
classpath group: "com.r3.dependx", name: "gradle-dependx", version: "0.1.13", changing: true
|
||||
classpath "com.bmuschko:gradle-docker-plugin:5.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.statemachine.StaffedFlowHospital
|
||||
import org.junit.Before
|
||||
import java.sql.SQLTransientConnectionException
|
||||
@ -72,11 +73,12 @@ abstract class AbstractFlowExternalOperationTest {
|
||||
@Suspendable
|
||||
override fun call(): Any {
|
||||
log.info("Started my flow")
|
||||
subFlow(PingPongFlow(party))
|
||||
val result = testCode()
|
||||
val session = initiateFlow(party)
|
||||
session.send("hi there")
|
||||
log.info("ServiceHub value = $serviceHub")
|
||||
session.receive<String>()
|
||||
session.sendAndReceive<String>("hi there").unwrap { it }
|
||||
session.sendAndReceive<String>("hi there").unwrap { it }
|
||||
subFlow(PingPongFlow(party))
|
||||
log.info("Finished my flow")
|
||||
return result
|
||||
}
|
||||
@ -92,8 +94,28 @@ abstract class AbstractFlowExternalOperationTest {
|
||||
class FlowWithExternalOperationResponder(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>()
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("go away")
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("go away")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class PingPongFlow(val party: Party): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(party)
|
||||
session.sendAndReceive<String>("ping pong").unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(PingPongFlow::class)
|
||||
class PingPongResponder(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("I got you bro")
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +133,7 @@ abstract class AbstractFlowExternalOperationTest {
|
||||
fun createFuture(): CompletableFuture<Any> {
|
||||
return CompletableFuture.supplyAsync(Supplier<Any> {
|
||||
log.info("Starting sleep inside of future")
|
||||
Thread.sleep(2000)
|
||||
Thread.sleep(1000)
|
||||
log.info("Finished sleep inside of future")
|
||||
"Here is your return value"
|
||||
}, executorService)
|
||||
|
@ -177,12 +177,14 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() {
|
||||
FlowWithExternalProcess(party) {
|
||||
|
||||
@Suspendable
|
||||
override fun testCode(): Any =
|
||||
await(ExternalAsyncOperation(serviceHub) { _, _ ->
|
||||
override fun testCode(): Any {
|
||||
val e = createException()
|
||||
return await(ExternalAsyncOperation(serviceHub) { _, _ ->
|
||||
CompletableFuture<Any>().apply {
|
||||
completeExceptionally(createException())
|
||||
completeExceptionally(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun createException() = when (exceptionType) {
|
||||
HospitalizeFlowException::class.java -> HospitalizeFlowException("keep it around")
|
||||
|
@ -235,7 +235,10 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() {
|
||||
FlowWithExternalProcess(party) {
|
||||
|
||||
@Suspendable
|
||||
override fun testCode(): Any = await(ExternalOperation(serviceHub) { _, _ -> throw createException() })
|
||||
override fun testCode() {
|
||||
val e = createException()
|
||||
await(ExternalOperation(serviceHub) { _, _ -> throw e })
|
||||
}
|
||||
|
||||
private fun createException() = when (exceptionType) {
|
||||
HospitalizeFlowException::class.java -> HospitalizeFlowException("keep it around")
|
||||
|
@ -380,10 +380,8 @@ abstract class FlowLogic<out T> {
|
||||
@Suspendable
|
||||
@Throws(FlowException::class)
|
||||
open fun <R> subFlow(subLogic: FlowLogic<R>): R {
|
||||
subLogic.stateMachine = stateMachine
|
||||
maybeWireUpProgressTracking(subLogic)
|
||||
logger.debug { "Calling subflow: $subLogic" }
|
||||
val result = stateMachine.subFlow(subLogic)
|
||||
val result = stateMachine.subFlow(this, subLogic)
|
||||
logger.debug { "Subflow finished with result ${result.toString().abbreviate(300)}" }
|
||||
return result
|
||||
}
|
||||
@ -540,18 +538,6 @@ abstract class FlowLogic<out T> {
|
||||
_stateMachine = value
|
||||
}
|
||||
|
||||
private fun maybeWireUpProgressTracking(subLogic: FlowLogic<*>) {
|
||||
val ours = progressTracker
|
||||
val theirs = subLogic.progressTracker
|
||||
if (ours != null && theirs != null && ours != theirs) {
|
||||
if (ours.currentStep == ProgressTracker.UNSTARTED) {
|
||||
logger.debug { "Initializing the progress tracker for flow: ${this::class.java.name}." }
|
||||
ours.nextStep()
|
||||
}
|
||||
ours.setChildProgressTracker(ours.currentStep, theirs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enforceNoDuplicates(sessions: List<FlowSession>) {
|
||||
require(sessions.size == sessions.toSet().size) { "A flow session can only appear once as argument." }
|
||||
}
|
||||
@ -579,12 +565,7 @@ abstract class FlowLogic<out T> {
|
||||
@Suspendable
|
||||
fun <R : Any> await(operation: FlowExternalAsyncOperation<R>): R {
|
||||
// Wraps the passed in [FlowExternalAsyncOperation] so its [CompletableFuture] can be converted into a [CordaFuture]
|
||||
val flowAsyncOperation = object : FlowAsyncOperation<R>, WrappedFlowExternalAsyncOperation<R> {
|
||||
override val operation = operation
|
||||
override fun execute(deduplicationId: String): CordaFuture<R> {
|
||||
return this.operation.execute(deduplicationId).asCordaFuture()
|
||||
}
|
||||
}
|
||||
val flowAsyncOperation = WrappedFlowExternalAsyncOperation(operation)
|
||||
val request = FlowIORequest.ExecuteAsyncOperation(flowAsyncOperation)
|
||||
return stateMachine.suspend(request, false)
|
||||
}
|
||||
@ -598,18 +579,7 @@ abstract class FlowLogic<out T> {
|
||||
*/
|
||||
@Suspendable
|
||||
fun <R : Any> await(operation: FlowExternalOperation<R>): R {
|
||||
val flowAsyncOperation = object : FlowAsyncOperation<R>, WrappedFlowExternalOperation<R> {
|
||||
override val serviceHub = this@FlowLogic.serviceHub as ServiceHubCoreInternal
|
||||
override val operation = operation
|
||||
override fun execute(deduplicationId: String): CordaFuture<R> {
|
||||
// Using a [CompletableFuture] allows unhandled exceptions to be thrown inside the background operation
|
||||
// the exceptions will be set on the future by [CompletableFuture.AsyncSupply.run]
|
||||
return CompletableFuture.supplyAsync(
|
||||
Supplier { this.operation.execute(deduplicationId) },
|
||||
serviceHub.externalOperationExecutor
|
||||
).asCordaFuture()
|
||||
}
|
||||
}
|
||||
val flowAsyncOperation = WrappedFlowExternalOperation(serviceHub as ServiceHubCoreInternal, operation)
|
||||
val request = FlowIORequest.ExecuteAsyncOperation(flowAsyncOperation)
|
||||
return stateMachine.suspend(request, false)
|
||||
}
|
||||
@ -619,21 +589,32 @@ abstract class FlowLogic<out T> {
|
||||
* [WrappedFlowExternalAsyncOperation] is added to allow jackson to properly reference the data stored within the wrapped
|
||||
* [FlowExternalAsyncOperation].
|
||||
*/
|
||||
private interface WrappedFlowExternalAsyncOperation<R : Any> {
|
||||
val operation: FlowExternalAsyncOperation<R>
|
||||
private class WrappedFlowExternalAsyncOperation<R : Any>(val operation: FlowExternalAsyncOperation<R>) : FlowAsyncOperation<R> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<R> {
|
||||
return operation.execute(deduplicationId).asCordaFuture()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [WrappedFlowExternalOperation] is added to allow jackson to properly reference the data stored within the wrapped
|
||||
* [FlowExternalOperation].
|
||||
*
|
||||
* The reference to [ServiceHub] is is also needed by Kryo to properly keep a reference to [ServiceHub] so that
|
||||
* The reference to [ServiceHub] is also needed by Kryo to properly keep a reference to [ServiceHub] so that
|
||||
* [FlowExternalOperation] can be run from the [ServiceHubCoreInternal.externalOperationExecutor] without causing errors when retrying a
|
||||
* flow. A [NullPointerException] is thrown if [FlowLogic.serviceHub] is accessed from [FlowLogic.await] when retrying a flow.
|
||||
*/
|
||||
private interface WrappedFlowExternalOperation<R : Any> {
|
||||
val serviceHub: ServiceHub
|
||||
private class WrappedFlowExternalOperation<R : Any>(
|
||||
val serviceHub: ServiceHubCoreInternal,
|
||||
val operation: FlowExternalOperation<R>
|
||||
) : FlowAsyncOperation<R> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<R> {
|
||||
// Using a [CompletableFuture] allows unhandled exceptions to be thrown inside the background operation
|
||||
// the exceptions will be set on the future by [CompletableFuture.AsyncSupply.run]
|
||||
return CompletableFuture.supplyAsync(
|
||||
Supplier { this.operation.execute(deduplicationId) },
|
||||
serviceHub.externalOperationExecutor
|
||||
).asCordaFuture()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,7 +25,7 @@ interface FlowStateMachine<FLOWRETURN> {
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>)
|
||||
|
||||
@Suspendable
|
||||
fun <SUBFLOWRETURN> subFlow(subFlow: FlowLogic<SUBFLOWRETURN>): SUBFLOWRETURN
|
||||
fun <SUBFLOWRETURN> subFlow(currentFlow: FlowLogic<*>, subFlow: FlowLogic<SUBFLOWRETURN>): SUBFLOWRETURN
|
||||
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
|
||||
|
@ -315,7 +315,10 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun <R> subFlow(subFlow: FlowLogic<R>): R {
|
||||
override fun <R> subFlow(currentFlow: FlowLogic<*>, subFlow: FlowLogic<R>): R {
|
||||
subFlow.stateMachine = this
|
||||
maybeWireUpProgressTracking(currentFlow, subFlow)
|
||||
|
||||
checkpointIfSubflowIdempotent(subFlow.javaClass)
|
||||
processEventImmediately(
|
||||
Event.EnterSubFlow(subFlow.javaClass,
|
||||
@ -338,6 +341,18 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeWireUpProgressTracking(currentFlow: FlowLogic<*>, subFlow: FlowLogic<*>) {
|
||||
val currentFlowProgressTracker = currentFlow.progressTracker
|
||||
val subflowProgressTracker = subFlow.progressTracker
|
||||
if (currentFlowProgressTracker != null && subflowProgressTracker != null && currentFlowProgressTracker != subflowProgressTracker) {
|
||||
if (currentFlowProgressTracker.currentStep == ProgressTracker.UNSTARTED) {
|
||||
logger.debug { "Initializing the progress tracker for flow: ${this::class.java.name}." }
|
||||
currentFlowProgressTracker.nextStep()
|
||||
}
|
||||
currentFlowProgressTracker.setChildProgressTracker(currentFlowProgressTracker.currentStep, subflowProgressTracker)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.isUnrecoverable(): Boolean = this is VirtualMachineError && this !is StackOverflowError
|
||||
|
||||
/**
|
||||
|
@ -624,7 +624,7 @@ class SingleThreadedStateMachineManager(
|
||||
checkpoint = checkpoint,
|
||||
pendingDeduplicationHandlers = deduplicationHandler?.let { listOf(it) } ?: emptyList(),
|
||||
isFlowResumed = false,
|
||||
isTransactionTracked = false,
|
||||
isWaitingForFuture = false,
|
||||
isAnyCheckpointPersisted = existingCheckpoint != null,
|
||||
isStartIdempotent = isStartIdempotent,
|
||||
isRemoved = false,
|
||||
@ -789,7 +789,7 @@ class SingleThreadedStateMachineManager(
|
||||
checkpoint = checkpoint,
|
||||
pendingDeduplicationHandlers = initialDeduplicationHandler?.let { listOf(it) } ?: emptyList(),
|
||||
isFlowResumed = false,
|
||||
isTransactionTracked = false,
|
||||
isWaitingForFuture = false,
|
||||
isAnyCheckpointPersisted = isAnyCheckpointPersisted,
|
||||
isStartIdempotent = isStartIdempotent,
|
||||
isRemoved = false,
|
||||
@ -808,7 +808,7 @@ class SingleThreadedStateMachineManager(
|
||||
checkpoint = checkpoint,
|
||||
pendingDeduplicationHandlers = initialDeduplicationHandler?.let { listOf(it) } ?: emptyList(),
|
||||
isFlowResumed = false,
|
||||
isTransactionTracked = false,
|
||||
isWaitingForFuture = false,
|
||||
isAnyCheckpointPersisted = isAnyCheckpointPersisted,
|
||||
isStartIdempotent = isStartIdempotent,
|
||||
isRemoved = false,
|
||||
|
@ -26,7 +26,7 @@ import java.time.Instant
|
||||
* @param pendingDeduplicationHandlers the list of incomplete deduplication handlers.
|
||||
* @param isFlowResumed true if the control is returned (or being returned) to "user-space" flow code. This is used
|
||||
* to make [Event.DoRemainingWork] idempotent.
|
||||
* @param isTransactionTracked true if a ledger transaction has been tracked as part of a
|
||||
* @param isWaitingForFuture true if the flow is waiting for the completion of a future triggered by one of the statemachine's actions
|
||||
* [FlowIORequest.WaitForLedgerCommit]. This used is to make tracking idempotent.
|
||||
* @param isAnyCheckpointPersisted true if at least a single checkpoint has been persisted. This is used to determine
|
||||
* whether we should DELETE the checkpoint at the end of the flow.
|
||||
@ -43,7 +43,7 @@ data class StateMachineState(
|
||||
val flowLogic: FlowLogic<*>,
|
||||
val pendingDeduplicationHandlers: List<DeduplicationHandler>,
|
||||
val isFlowResumed: Boolean,
|
||||
val isTransactionTracked: Boolean,
|
||||
val isWaitingForFuture: Boolean,
|
||||
val isAnyCheckpointPersisted: Boolean,
|
||||
val isStartIdempotent: Boolean,
|
||||
val isRemoved: Boolean,
|
||||
|
@ -95,9 +95,11 @@ class StartedFlowTransition(
|
||||
}
|
||||
|
||||
private fun waitForLedgerCommitTransition(flowIORequest: FlowIORequest.WaitForLedgerCommit): TransitionResult {
|
||||
return if (!startingState.isTransactionTracked) {
|
||||
// This ensures that the [WaitForLedgerCommit] request is not executed multiple times if extra
|
||||
// [DoRemainingWork] events are pushed onto the fiber's event queue before the flow has really woken up
|
||||
return if (!startingState.isWaitingForFuture) {
|
||||
TransitionResult(
|
||||
newState = startingState.copy(isTransactionTracked = true),
|
||||
newState = startingState.copy(isWaitingForFuture = true),
|
||||
actions = listOf(
|
||||
Action.CreateTransaction,
|
||||
Action.TrackTransaction(flowIORequest.hash),
|
||||
@ -416,13 +418,20 @@ class StartedFlowTransition(
|
||||
}
|
||||
|
||||
private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult {
|
||||
return builder {
|
||||
// This ensures that the [ExecuteAsyncOperation] request is not executed multiple times if extra
|
||||
// [DoRemainingWork] events are pushed onto the fiber's event queue before the flow has really woken up
|
||||
return if (!startingState.isWaitingForFuture) {
|
||||
builder {
|
||||
// The `numberOfSuspends` is added to the deduplication ID in case an async
|
||||
// operation is executed multiple times within the same flow.
|
||||
val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.checkpointState.numberOfSuspends.toString()
|
||||
actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation))
|
||||
currentState = currentState.copy(isWaitingForFuture = true)
|
||||
FlowContinuation.ProcessEvents
|
||||
}
|
||||
} else {
|
||||
TransitionResult(startingState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeForceCheckpoint(): TransitionResult {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.services.statemachine.transitions
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.internal.FlowIORequest
|
||||
import net.corda.core.utilities.Try
|
||||
@ -60,11 +61,8 @@ class TopLevelTransition(
|
||||
private fun transactionCommittedTransition(event: Event.TransactionCommitted): TransitionResult {
|
||||
return builder {
|
||||
val checkpoint = currentState.checkpoint
|
||||
if (currentState.isTransactionTracked &&
|
||||
checkpoint.flowState is FlowState.Started &&
|
||||
checkpoint.flowState.flowIORequest is FlowIORequest.WaitForLedgerCommit &&
|
||||
checkpoint.flowState.flowIORequest.hash == event.transaction.id) {
|
||||
currentState = currentState.copy(isTransactionTracked = false)
|
||||
if (isWaitingForLedgerCommit(currentState, checkpoint, event.transaction.id)) {
|
||||
currentState = currentState.copy(isWaitingForFuture = false)
|
||||
if (isErrored()) {
|
||||
return@builder FlowContinuation.ProcessEvents
|
||||
}
|
||||
@ -76,6 +74,17 @@ class TopLevelTransition(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWaitingForLedgerCommit(
|
||||
currentState: StateMachineState,
|
||||
checkpoint: Checkpoint,
|
||||
transactionId: SecureHash
|
||||
): Boolean {
|
||||
return currentState.isWaitingForFuture &&
|
||||
checkpoint.flowState is FlowState.Started &&
|
||||
checkpoint.flowState.flowIORequest is FlowIORequest.WaitForLedgerCommit &&
|
||||
checkpoint.flowState.flowIORequest.hash == transactionId
|
||||
}
|
||||
|
||||
private fun softShutdownTransition(): TransitionResult {
|
||||
val lastState = startingState.copy(isRemoved = true)
|
||||
return TransitionResult(
|
||||
|
@ -68,13 +68,13 @@ class TransitionBuilder(val context: TransitionContext, initialState: StateMachi
|
||||
|
||||
fun resumeFlowLogic(result: Any?): FlowContinuation {
|
||||
actions.add(Action.CreateTransaction)
|
||||
currentState = currentState.copy(isFlowResumed = true)
|
||||
currentState = currentState.copy(isFlowResumed = true, isWaitingForFuture = false)
|
||||
return FlowContinuation.Resume(result)
|
||||
}
|
||||
|
||||
fun resumeFlowLogic(result: Throwable): FlowContinuation {
|
||||
actions.add(Action.CreateTransaction)
|
||||
currentState = currentState.copy(isFlowResumed = true)
|
||||
currentState = currentState.copy(isFlowResumed = true, isWaitingForFuture = false)
|
||||
return FlowContinuation.Throw(result)
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class UniquenessProviderTests(
|
||||
}
|
||||
|
||||
/*
|
||||
There are 6 types of transactions to test:
|
||||
There are 7 types of transaction to test:
|
||||
|
||||
A B C D E F G
|
||||
================== === === === === === === ===
|
||||
@ -95,27 +95,91 @@ class UniquenessProviderTests(
|
||||
/* Group A: only time window */
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `commits transaction with valid time window`() {
|
||||
val inputState1 = generateStateRef()
|
||||
fun `rejects transaction before time window is valid`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes))
|
||||
val result = uniquenessProvider.commit(listOf(inputState1), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result is UniquenessProvider.Result.Success)
|
||||
val timeWindow = TimeWindow.between(
|
||||
Clock.systemUTC().instant().plus(30.minutes),
|
||||
Clock.systemUTC().instant().plus(60.minutes))
|
||||
val result = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result is UniquenessProvider.Result.Failure)
|
||||
val error = (result as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid
|
||||
assertEquals(timeWindow, error.txTimeWindow)
|
||||
|
||||
// Idempotency: can re-notarise successfully later.
|
||||
// Once time window behaviour has changed, we should add an additional test case here to check
|
||||
// that retry within time window still fails. We can't do that now because currently it will
|
||||
// succeed and that will result in the past time window case succeeding too.
|
||||
|
||||
// Retry still fails after advancing past time window
|
||||
testClock.advanceBy(90.minutes)
|
||||
val result2 = uniquenessProvider.commit(listOf(inputState1), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result2 is UniquenessProvider.Result.Success)
|
||||
val result2 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result2 is UniquenessProvider.Result.Failure)
|
||||
val error2 = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid
|
||||
assertEquals(timeWindow, error2.txTimeWindow)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `rejects transaction with invalid time window`() {
|
||||
val inputState1 = generateStateRef()
|
||||
fun `commits transaction within time window`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val invalidTimeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes))
|
||||
val result = uniquenessProvider.commit(listOf(inputState1), firstTxId, identity, requestSignature, invalidTimeWindow).get()
|
||||
val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes))
|
||||
val result = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result is UniquenessProvider.Result.Success)
|
||||
|
||||
// Retry is successful whilst still within time window
|
||||
testClock.advanceBy(10.minutes)
|
||||
val result2 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result2 is UniquenessProvider.Result.Success)
|
||||
|
||||
// Retry is successful after time window has expired
|
||||
testClock.advanceBy(80.minutes)
|
||||
val result3 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result3 is UniquenessProvider.Result.Success)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `rejects transaction after time window has expired`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes))
|
||||
val result = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result is UniquenessProvider.Result.Failure)
|
||||
val error = (result as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid
|
||||
assertEquals(invalidTimeWindow, error.txTimeWindow)
|
||||
assertEquals(timeWindow, error.txTimeWindow)
|
||||
|
||||
// Retry still fails at a later time
|
||||
testClock.advanceBy(10.minutes)
|
||||
val result2 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow).get()
|
||||
assert(result2 is UniquenessProvider.Result.Failure)
|
||||
val error2 = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid
|
||||
assertEquals(timeWindow, error2.txTimeWindow)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `time window only transactions are processed correctly when duplicate requests occur in succession`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val secondTxId = SecureHash.randomSHA256()
|
||||
val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes))
|
||||
val invalidTimeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes))
|
||||
|
||||
val validFuture1 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow)
|
||||
val validFuture2 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, timeWindow)
|
||||
val invalidFuture1 = uniquenessProvider.commit(
|
||||
emptyList(), secondTxId, identity, requestSignature, invalidTimeWindow)
|
||||
val invalidFuture2 = uniquenessProvider.commit(
|
||||
emptyList(), secondTxId, identity, requestSignature, invalidTimeWindow)
|
||||
|
||||
// Ensure that transactions are processed correctly and duplicates get the same responses to original
|
||||
assert(validFuture1.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture2.get() is UniquenessProvider.Result.Success)
|
||||
assert(invalidFuture1.get() is UniquenessProvider.Result.Failure)
|
||||
assert(invalidFuture2.get() is UniquenessProvider.Result.Failure)
|
||||
}
|
||||
|
||||
/* Group B: only reference states */
|
||||
@ -154,6 +218,52 @@ class UniquenessProviderTests(
|
||||
assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `commits retry transaction when reference states were spent since initial transaction`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val referenceState = generateStateRef()
|
||||
|
||||
val result = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
.get()
|
||||
assert(result is UniquenessProvider.Result.Success)
|
||||
|
||||
// Spend reference state
|
||||
val secondTxId = SecureHash.randomSHA256()
|
||||
val result2 = uniquenessProvider.commit(
|
||||
listOf(referenceState), secondTxId, identity, requestSignature, references = emptyList())
|
||||
.get()
|
||||
assert(result2 is UniquenessProvider.Result.Success)
|
||||
|
||||
// Retry referencing the now spent state still succeeds
|
||||
val result3 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
.get()
|
||||
assert(result3 is UniquenessProvider.Result.Success)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `reference state only transactions are processed correctly when duplicate requests occur in succession`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val secondTxId = SecureHash.randomSHA256()
|
||||
val referenceState = generateStateRef()
|
||||
|
||||
val validFuture3 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
val validFuture4 = uniquenessProvider.commit(
|
||||
emptyList(), firstTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
val validFuture1 = uniquenessProvider.commit(
|
||||
emptyList(), secondTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
val validFuture2 = uniquenessProvider.commit(
|
||||
emptyList(), secondTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
|
||||
// Ensure that transactions are processed correctly and duplicates get the same responses to original
|
||||
assert(validFuture1.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture2.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture3.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture4.get() is UniquenessProvider.Result.Success)
|
||||
}
|
||||
|
||||
/* Group C: reference states & time window */
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@ -262,6 +372,28 @@ class UniquenessProviderTests(
|
||||
assertEquals(firstTxId.sha256(), conflictCause.hashOfTransactionId)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `input state only transactions are processed correctly when duplicate requests occur in succession`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val secondTxId = SecureHash.randomSHA256()
|
||||
val inputState = generateStateRef()
|
||||
|
||||
val validFuture1 = uniquenessProvider.commit(
|
||||
listOf(inputState), firstTxId, identity, requestSignature)
|
||||
val validFuture2 = uniquenessProvider.commit(
|
||||
listOf(inputState), firstTxId, identity, requestSignature)
|
||||
val invalidFuture1 = uniquenessProvider.commit(
|
||||
listOf(inputState), secondTxId, identity, requestSignature)
|
||||
val invalidFuture2 = uniquenessProvider.commit(
|
||||
listOf(inputState), secondTxId, identity, requestSignature)
|
||||
|
||||
// Ensure that transactions are processed correctly and duplicates get the same responses to original
|
||||
assert(validFuture1.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture2.get() is UniquenessProvider.Result.Success)
|
||||
assert(invalidFuture1.get() is UniquenessProvider.Result.Failure)
|
||||
assert(invalidFuture2.get() is UniquenessProvider.Result.Failure)
|
||||
}
|
||||
|
||||
/* Group E: input states & time window */
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@ -346,6 +478,37 @@ class UniquenessProviderTests(
|
||||
assert(result2 is UniquenessProvider.Result.Success)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `re-notarise after reference state is spent`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val inputState = generateStateRef()
|
||||
val referenceState = generateStateRef()
|
||||
val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes))
|
||||
|
||||
val result = uniquenessProvider.commit(
|
||||
listOf(inputState), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState))
|
||||
.get()
|
||||
assert(result is UniquenessProvider.Result.Success)
|
||||
|
||||
// Spend the reference state.
|
||||
val referenceSpend = uniquenessProvider.commit(
|
||||
listOf(referenceState),
|
||||
SecureHash.randomSHA256(),
|
||||
identity,
|
||||
requestSignature,
|
||||
timeWindow,
|
||||
emptyList()).get()
|
||||
assert(referenceSpend is UniquenessProvider.Result.Success)
|
||||
|
||||
// Idempotency: can re-notarise successfully
|
||||
testClock.advanceBy(90.minutes)
|
||||
val result2 = uniquenessProvider.commit(
|
||||
listOf(inputState), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState))
|
||||
.get()
|
||||
// Known failure - this should return success. Will be fixed in a future release.
|
||||
assert(result2 is UniquenessProvider.Result.Failure)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `rejects transaction with unused reference states and used input states`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
@ -387,6 +550,31 @@ class UniquenessProviderTests(
|
||||
assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `input and reference state transactions are processed correctly when duplicate requests occur in succession`() {
|
||||
val firstTxId = SecureHash.randomSHA256()
|
||||
val secondTxId = SecureHash.randomSHA256()
|
||||
val referenceState = generateStateRef()
|
||||
|
||||
// Ensure batch contains duplicates
|
||||
val validFuture1 = uniquenessProvider.commit(
|
||||
emptyList(), secondTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
val validFuture2 = uniquenessProvider.commit(
|
||||
emptyList(), secondTxId, identity, requestSignature, references = listOf(referenceState))
|
||||
val validFuture3 = uniquenessProvider.commit(
|
||||
listOf(referenceState), firstTxId, identity, requestSignature)
|
||||
|
||||
// Attempt to use the reference state after it has been consumed
|
||||
val validFuture4 = uniquenessProvider.commit(
|
||||
emptyList(), SecureHash.randomSHA256(), identity, requestSignature, references = listOf(referenceState))
|
||||
|
||||
// Ensure that transactions are processed correctly and duplicates get the same responses to original
|
||||
assert(validFuture1.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture2.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture3.get() is UniquenessProvider.Result.Success)
|
||||
assert(validFuture4.get() is UniquenessProvider.Result.Failure)
|
||||
}
|
||||
|
||||
/* Group G: input, reference states and time window – covered by previous tests. */
|
||||
|
||||
/* Transaction signing tests. */
|
||||
|
Loading…
Reference in New Issue
Block a user