From 6f0bc42098bfb3f944a56b2c5510d497ea439960 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 21 Dec 2018 16:30:53 +0000 Subject: [PATCH] ENT-2813: Fix uniqueness provider double insertion issue Fix an issue where a transction id is committed twice if a reference-only transaction gets re-notarised. Added more tests. add fix --- .../PersistentUniquenessProvider.kt | 26 +- .../PersistentUniquenessProviderTests.kt | 170 -------- .../transactions/UniquenessProviderTests.kt | 391 ++++++++++++++++++ 3 files changed, 399 insertions(+), 188 deletions(-) delete mode 100644 node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/transactions/UniquenessProviderTests.kt diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 6b87723305..85b054aa6b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -14,11 +14,7 @@ import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.elapsedTime -import net.corda.core.internal.notary.NotaryInternalException -import net.corda.core.internal.notary.NotaryServiceFlow -import net.corda.core.internal.notary.UniquenessProvider -import net.corda.core.internal.notary.isConsumedByTheSameTx -import net.corda.core.internal.notary.validateTimeWindow +import net.corda.core.internal.notary.* import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -32,18 +28,12 @@ import net.corda.nodeapi.internal.persistence.currentDBSession import java.time.Clock import java.time.Duration import java.time.Instant -import java.util.LinkedHashMap +import java.util.* import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import javax.annotation.concurrent.ThreadSafe -import javax.persistence.Column -import javax.persistence.EmbeddedId -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.Lob -import javax.persistence.MappedSuperclass +import javax.persistence.* import kotlin.concurrent.thread /** A RDBMS backed Uniqueness provider */ @@ -174,7 +164,6 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste ) } - /** * Generates and adds a [CommitRequest] to the request queue. If the request queue is full, this method will block * until space is available. @@ -255,7 +244,6 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste } private fun handleReferenceConflicts(txId: SecureHash, conflictingStates: LinkedHashMap) { - val session = currentDBSession() if (!previouslyCommitted(txId)) { val conflictError = NotaryError.Conflict(txId, conflictingStates) log.debug { "Failure, input states already committed: ${conflictingStates.keys}" } @@ -276,6 +264,11 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste } private fun handleNoConflicts(timeWindow: TimeWindow?, states: List, txId: SecureHash, commitLog: AppendOnlyPersistentMap) { + // Skip if this is a re-notarisation of a reference-only transaction + if (states.isEmpty() && previouslyCommitted(txId)) { + return + } + val outsideTimeWindowError = validateTimeWindow(clock.instant(), timeWindow) if (outsideTimeWindowError == null) { states.forEach { stateRef -> @@ -285,9 +278,6 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste session.persist(CommittedTransaction(txId.toString())) log.debug { "Successfully committed all input states: $states" } } else { - if (states.isEmpty() && previouslyCommitted(txId)) { - return - } throw NotaryInternalException(outsideTimeWindowError) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt deleted file mode 100644 index 544fa78fa7..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ /dev/null @@ -1,170 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.contracts.TimeWindow -import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.NullKeys -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sha256 -import net.corda.core.flows.NotarisationRequestSignature -import net.corda.core.flows.NotaryError -import net.corda.core.flows.StateConsumptionDetails -import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.notary.UniquenessProvider -import net.corda.core.utilities.minutes -import net.corda.node.services.schema.NodeSchemaService -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.core.TestIdentity -import net.corda.testing.core.generateStateRef -import net.corda.testing.internal.LogHelper -import net.corda.testing.internal.TestingNamedCacheFactory -import net.corda.testing.internal.configureDatabase -import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.TestClock -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import java.time.Clock -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class PersistentUniquenessProviderTests { - @Rule - @JvmField - val testSerialization = SerializationEnvironmentRule(inheritable = true) - private val identity = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party - private val txID = SecureHash.randomSHA256() - private val requestSignature = NotarisationRequestSignature(DigitalSignature.WithKey(NullKeys.NullPublicKey, ByteArray(32)), 0) - - private lateinit var database: CordaPersistence - - @Before - fun setUp() { - LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(extraSchemas = setOf(NodeNotarySchemaV1))) - } - - @After - fun tearDown() { - database.close() - LogHelper.reset(PersistentUniquenessProvider::class) - } - - @Test - fun `should successfully commit a transaction with unused inputs`() { - val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) - val inputState = generateStateRef() - - val result = provider.commit(listOf(inputState), txID, identity, requestSignature).get() - assertEquals(UniquenessProvider.Result.Success, result) - } - - @Test - fun `should report a conflict for a transaction with previously used inputs`() { - val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) - val inputState = generateStateRef() - - val inputs = listOf(inputState) - val firstTxId = txID - val result = provider.commit(inputs, firstTxId, identity, requestSignature).get() - assertEquals(UniquenessProvider.Result.Success, result) - - val secondTxId = SecureHash.randomSHA256() - - val response: UniquenessProvider.Result = provider.commit(inputs, secondTxId, identity, requestSignature).get() - val error = (response as UniquenessProvider.Result.Failure).error as NotaryError.Conflict - - val conflictCause = error.consumedStates[inputState]!! - assertEquals(firstTxId.sha256(), conflictCause.hashOfTransactionId) - } - - @Test - fun `rejects transaction with invalid time window`() { - val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) - val inputState1 = generateStateRef() - val firstTxId = SecureHash.randomSHA256() - val timeWindow = TimeWindow.fromOnly(Clock.systemUTC().instant().plus(30.minutes)) - val result = provider.commit(listOf(inputState1), firstTxId, identity, requestSignature, timeWindow).get() - val error = (result as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid - assertEquals(timeWindow, error.txTimeWindow) - } - - @Test - fun `handles transaction with valid time window`() { - val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) - val inputState1 = generateStateRef() - val firstTxId = SecureHash.randomSHA256() - val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) - val result = provider.commit(listOf(inputState1), firstTxId, identity, requestSignature, timeWindow).get() - assertEquals(UniquenessProvider.Result.Success, result) - } - - @Test - fun `handles transaction with valid time window without inputs`() { - val testClock = TestClock(Clock.systemUTC()) - val provider = PersistentUniquenessProvider(testClock, database, TestingNamedCacheFactory()) - val firstTxId = SecureHash.randomSHA256() - val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) - val result = provider.commit(emptyList(), firstTxId, identity, requestSignature, timeWindow).get() - assertEquals(UniquenessProvider.Result.Success, result) - - // Re-notarisation works outside the specified time window. - testClock.advanceBy(90.minutes) - val result2 = provider.commit(emptyList(), firstTxId, identity, requestSignature, timeWindow).get() - assertEquals(UniquenessProvider.Result.Success, result2) - } - - @Test - fun `handles reference states`() { - val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) - val inputState1 = generateStateRef() - val inputState2 = generateStateRef() - val firstTxId = SecureHash.randomSHA256() - val secondTxId = SecureHash.randomSHA256() - - // Conflict free transaction goes through. - val result1 = provider.commit(listOf(inputState1), firstTxId, identity, requestSignature, references = listOf(inputState2)).get() - assertEquals(UniquenessProvider.Result.Success, result1) - - // Referencing a spent state results in a conflict. - val result2 = provider.commit(listOf(inputState2), secondTxId, identity, requestSignature, references = listOf(inputState1)).get() - val error = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict - val conflictCause = error.consumedStates[inputState1]!! - assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) - assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type) - - // Re-notarisation works. - val result3 = provider.commit(listOf(inputState1), firstTxId, identity, requestSignature, references = listOf(inputState2)).get() - assertEquals(UniquenessProvider.Result.Success, result3) - } - - @Test - fun `handles transaction with reference states only`() { - val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) - val inputState1 = generateStateRef() - val firstTxId = SecureHash.randomSHA256() - val secondTxId = SecureHash.randomSHA256() - val thirdTxId = SecureHash.randomSHA256() - - // Conflict free transaction goes through. - val result1 = provider.commit(emptyList(), firstTxId, identity, requestSignature, references = listOf(inputState1)).get() - assertEquals(UniquenessProvider.Result.Success, result1) - - // Commit state 1. - val result2 = provider.commit(listOf(inputState1), secondTxId, identity, requestSignature).get() - assertEquals(UniquenessProvider.Result.Success, result2) - - // Re-notarisation works. - val result3 = provider.commit(emptyList(), firstTxId, identity, requestSignature, references = listOf(inputState1)).get() - assertEquals(UniquenessProvider.Result.Success, result3) - - // Transaction referencing the spent sate fails. - val result4 = provider.commit(emptyList(), thirdTxId, identity, requestSignature, references = listOf(inputState1)).get() - val error = (result4 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict - val conflictCause = error.consumedStates[inputState1]!! - assertEquals(conflictCause.hashOfTransactionId, secondTxId.sha256()) - assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type) - } -} diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/UniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/UniquenessProviderTests.kt new file mode 100644 index 0000000000..298119c1c7 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/transactions/UniquenessProviderTests.kt @@ -0,0 +1,391 @@ +package net.corda.node.services.transactions + +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.NullKeys +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryError +import net.corda.core.flows.StateConsumptionDetails +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.notary.UniquenessProvider +import net.corda.core.utilities.minutes +import net.corda.node.services.schema.NodeSchemaService +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.generateStateRef +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.TestClock +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.time.Clock +import kotlin.test.assertEquals + +@RunWith(Parameterized::class) +class UniquenessProviderTests( + private val uniquenessProviderFactory: UniquenessProviderFactory +) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): Collection = listOf( + PersistentUniquenessProviderFactory() + ) + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(inheritable = true) + private val identity = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party + private val txID = SecureHash.randomSHA256() + private val requestSignature = NotarisationRequestSignature(DigitalSignature.WithKey(NullKeys.NullPublicKey, ByteArray(32)), 0) + private lateinit var testClock: TestClock + private lateinit var uniquenessProvider: UniquenessProvider + + @Before + fun setUp() { + testClock = TestClock(Clock.systemUTC()) + uniquenessProvider = uniquenessProviderFactory.create(testClock) + LogHelper.setLevel(uniquenessProvider::class) + } + + @After + fun tearDown() { + uniquenessProviderFactory.cleanUp() + LogHelper.reset(uniquenessProvider::class) + } + + /* + There are 6 types of transactions to test: + + A B C D E F G + ================== === === === === === === === + Input states 0 0 0 1 1 1 1 + Reference states 0 1 1 0 0 1 1 + Time window 1 0 1 0 1 0 1 + ================== === === === === === === === + + Here "0" indicates absence, and "1" – presence of components. + */ + + /* Group A: only time window */ + + @Test + fun `commits transaction with valid time window`() { + val inputState1 = generateStateRef() + 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() + assertEquals(UniquenessProvider.Result.Success, result) + + // Idempotency: can re-notarise successfully later. + testClock.advanceBy(90.minutes) + val result2 = uniquenessProvider.commit(listOf(inputState1), firstTxId, identity, requestSignature, timeWindow).get() + assertEquals(UniquenessProvider.Result.Success, result2) + } + + @Test + fun `rejects transaction with invalid time window`() { + val inputState1 = generateStateRef() + 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 error = (result as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid + assertEquals(invalidTimeWindow, error.txTimeWindow) + } + + /* Group B: only reference states */ + + @Test + fun `commits transaction with unused reference states`() { + val firstTxId = SecureHash.randomSHA256() + val referenceState = generateStateRef() + + val result = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, references = listOf(referenceState)) + .get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Idempotency: can re-notarise successfully. + val result2 = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, references = listOf(referenceState)) + .get() + assertEquals(UniquenessProvider.Result.Success, result2) + } + + @Test + fun `rejects transaction with previously used reference states`() { + val firstTxId = SecureHash.randomSHA256() + val referenceState = generateStateRef() + + val result = uniquenessProvider.commit(listOf(referenceState), firstTxId, identity, requestSignature, references = emptyList()) + .get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Transaction referencing the spent sate fails. + val secondTxId = SecureHash.randomSHA256() + val result2 = uniquenessProvider.commit(emptyList(), secondTxId, identity, requestSignature, references = listOf(referenceState)) + .get() + val error = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + val conflictCause = error.consumedStates[referenceState]!! + assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) + assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type) + } + + /* Group C: reference states & time window */ + + @Test + fun `commits transaction with unused reference states and valid time window`() { + val firstTxId = SecureHash.randomSHA256() + val referenceState = generateStateRef() + val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) + + val result = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState)) + .get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Idempotency: can re-notarise successfully. + testClock.advanceBy(90.minutes) + val result2 = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState)) + .get() + assertEquals(UniquenessProvider.Result.Success, result2) + } + + @Test + fun `rejects transaction with unused reference states and invalid time window`() { + val firstTxId = SecureHash.randomSHA256() + val referenceState = generateStateRef() + val invalidTimeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes)) + + val result = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, invalidTimeWindow, references = listOf(referenceState)) + .get() + val error = (result as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid + assertEquals(invalidTimeWindow, error.txTimeWindow) + } + + @Test + fun `rejects transaction with previously used reference states and valid time window`() { + val firstTxId = SecureHash.randomSHA256() + val referenceState = generateStateRef() + + val result = uniquenessProvider.commit(listOf(referenceState), firstTxId, identity, requestSignature, references = emptyList()) + .get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Transaction referencing the spent sate fails. + val secondTxId = SecureHash.randomSHA256() + val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) + val result2 = uniquenessProvider.commit(emptyList(), secondTxId, identity, requestSignature, timeWindow, references = listOf(referenceState)) + .get() + val error = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + val conflictCause = error.consumedStates[referenceState]!! + assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) + assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type) + } + + @Test + fun `rejects transaction with previously used reference states and invalid time window`() { + val firstTxId = SecureHash.randomSHA256() + val referenceState = generateStateRef() + + val result = uniquenessProvider.commit(listOf(referenceState), firstTxId, identity, requestSignature, references = emptyList()) + .get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Transaction referencing the spent sate fails. + val secondTxId = SecureHash.randomSHA256() + val invalidTimeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes)) + val result2 = uniquenessProvider.commit(emptyList(), secondTxId, identity, requestSignature, invalidTimeWindow, references = listOf(referenceState)) + .get() + val error = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + val conflictCause = error.consumedStates[referenceState]!! + assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) + assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type) + } + + /* Group D: only input states */ + + @Test + fun `commits transaction with unused inputs`() { + val inputState = generateStateRef() + + val result = uniquenessProvider.commit(listOf(inputState), txID, identity, requestSignature).get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Idempotency: can re-notarise successfully. + val result2 = uniquenessProvider.commit(listOf(inputState), txID, identity, requestSignature).get() + assertEquals(UniquenessProvider.Result.Success, result2) + } + + @Test + fun `rejects transaction with previously used inputs`() { + val inputState = generateStateRef() + + val inputs = listOf(inputState) + val firstTxId = txID + val result = uniquenessProvider.commit(inputs, firstTxId, identity, requestSignature).get() + assertEquals(UniquenessProvider.Result.Success, result) + + val secondTxId = SecureHash.randomSHA256() + + val response: UniquenessProvider.Result = uniquenessProvider.commit(inputs, secondTxId, identity, requestSignature).get() + val error = (response as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + + val conflictCause = error.consumedStates[inputState]!! + assertEquals(firstTxId.sha256(), conflictCause.hashOfTransactionId) + } + + /* Group E: input states & time window */ + + @Test + fun `commits transaction with unused inputs and valid time window`() { + val inputState = generateStateRef() + val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) + + val result = uniquenessProvider.commit(listOf(inputState), txID, identity, requestSignature, timeWindow).get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Idempotency: can re-notarise successfully later. + testClock.advanceBy(90.minutes) + val result2 = uniquenessProvider.commit(listOf(inputState), txID, identity, requestSignature, timeWindow).get() + assertEquals(UniquenessProvider.Result.Success, result2) + } + + @Test + fun `rejects transaction with unused inputs and invalid time window`() { + val inputState = generateStateRef() + val invalidTimeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes)) + + val result = uniquenessProvider.commit(listOf(inputState), txID, identity, requestSignature, invalidTimeWindow).get() + val error = (result as UniquenessProvider.Result.Failure).error as NotaryError.TimeWindowInvalid + assertEquals(invalidTimeWindow, error.txTimeWindow) + } + + @Test + fun `rejects transaction with previously used inputs and valid time window`() { + val inputState = generateStateRef() + val inputs = listOf(inputState) + val firstTxId = txID + val result = uniquenessProvider.commit(inputs, firstTxId, identity, requestSignature).get() + assertEquals(UniquenessProvider.Result.Success, result) + + val secondTxId = SecureHash.randomSHA256() + + val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) + val response: UniquenessProvider.Result = uniquenessProvider.commit(inputs, secondTxId, identity, requestSignature, timeWindow) + .get() + val error = (response as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + + val conflictCause = error.consumedStates[inputState]!! + assertEquals(firstTxId.sha256(), conflictCause.hashOfTransactionId) + } + + @Test + fun `rejects transaction with previously used inputs and invalid time window`() { + val inputState = generateStateRef() + val inputs = listOf(inputState) + val firstTxId = txID + val result = uniquenessProvider.commit(inputs, firstTxId, identity, requestSignature).get() + assertEquals(UniquenessProvider.Result.Success, result) + + val secondTxId = SecureHash.randomSHA256() + + val invalidTimeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().minus(30.minutes)) + val response: UniquenessProvider.Result = uniquenessProvider.commit(inputs, secondTxId, identity, requestSignature, invalidTimeWindow) + .get() + val error = (response as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + + val conflictCause = error.consumedStates[inputState]!! + assertEquals(firstTxId.sha256(), conflictCause.hashOfTransactionId) + } + + /* Group F: input & reference states */ + + @Test + fun `commits transaction with unused input & reference states`() { + 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() + assertEquals(UniquenessProvider.Result.Success, result) + + // Idempotency: can re-notarise successfully. + testClock.advanceBy(90.minutes) + val result2 = uniquenessProvider.commit(listOf(inputState), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState)) + .get() + assertEquals(UniquenessProvider.Result.Success, result2) + } + + @Test + fun `rejects transaction with unused reference states and used input states`() { + val firstTxId = SecureHash.randomSHA256() + val inputState = generateStateRef() + val referenceState = generateStateRef() + + val result = uniquenessProvider.commit(listOf(inputState), firstTxId, identity, requestSignature, references = emptyList()).get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Transaction referencing the spent sate fails. + val secondTxId = SecureHash.randomSHA256() + val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) + val result2 = uniquenessProvider.commit(listOf(inputState), secondTxId, identity, requestSignature, timeWindow, references = listOf(referenceState)) + .get() + val error = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + val conflictCause = error.consumedStates[inputState]!! + assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) + assertEquals(StateConsumptionDetails.ConsumedStateType.INPUT_STATE, conflictCause.type) + } + + @Test + fun `rejects transaction with used reference states and unused input states`() { + val firstTxId = SecureHash.randomSHA256() + val inputState = generateStateRef() + val referenceState = generateStateRef() + + val result = uniquenessProvider.commit(listOf(referenceState), firstTxId, identity, requestSignature, references = emptyList()) + .get() + assertEquals(UniquenessProvider.Result.Success, result) + + // Transaction referencing the spent sate fails. + val secondTxId = SecureHash.randomSHA256() + val timeWindow = TimeWindow.untilOnly(Clock.systemUTC().instant().plus(30.minutes)) + val result2 = uniquenessProvider.commit(listOf(inputState), secondTxId, identity, requestSignature, timeWindow, references = listOf(referenceState)) + .get() + val error = (result2 as UniquenessProvider.Result.Failure).error as NotaryError.Conflict + val conflictCause = error.consumedStates[referenceState]!! + assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) + assertEquals(StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE, conflictCause.type) + } + + /* Group G: input, reference states and time window – covered by previous tests. */ +} + +interface UniquenessProviderFactory { + fun create(clock: Clock): UniquenessProvider + fun cleanUp() {} +} + +class PersistentUniquenessProviderFactory : UniquenessProviderFactory { + private var database: CordaPersistence? = null + + override fun create(clock: Clock): UniquenessProvider { + database?.close() + database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(extraSchemas = setOf(NodeNotarySchemaV1))) + return PersistentUniquenessProvider(clock, database!!, TestingNamedCacheFactory()) + } + + override fun cleanUp() { + database?.close() + } +}