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
This commit is contained in:
Andrius Dagys 2018-12-21 16:30:53 +00:00
parent 8e8650e27b
commit 6f0bc42098
3 changed files with 399 additions and 188 deletions

View File

@ -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<StateRef, StateConsumptionDetails>) {
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<StateRef>, txId: SecureHash, commitLog: AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef>) {
// 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)
}
}

View File

@ -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)
}
}

View File

@ -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<UniquenessProviderFactory> = 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()
}
}