mirror of
https://github.com/corda/corda.git
synced 2024-12-30 01:39:04 +00:00
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:
parent
8e8650e27b
commit
6f0bc42098
@ -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.concurrent.openFuture
|
import net.corda.core.internal.concurrent.openFuture
|
||||||
import net.corda.core.internal.elapsedTime
|
import net.corda.core.internal.elapsedTime
|
||||||
import net.corda.core.internal.notary.NotaryInternalException
|
import net.corda.core.internal.notary.*
|
||||||
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.schemas.PersistentStateRef
|
import net.corda.core.schemas.PersistentStateRef
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
@ -32,18 +28,12 @@ import net.corda.nodeapi.internal.persistence.currentDBSession
|
|||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.LinkedHashMap
|
import java.util.*
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
import javax.persistence.Column
|
import javax.persistence.*
|
||||||
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 kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/** A RDBMS backed Uniqueness provider */
|
/** 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
|
* Generates and adds a [CommitRequest] to the request queue. If the request queue is full, this method will block
|
||||||
* until space is available.
|
* 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>) {
|
private fun handleReferenceConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
|
||||||
val session = currentDBSession()
|
|
||||||
if (!previouslyCommitted(txId)) {
|
if (!previouslyCommitted(txId)) {
|
||||||
val conflictError = NotaryError.Conflict(txId, conflictingStates)
|
val conflictError = NotaryError.Conflict(txId, conflictingStates)
|
||||||
log.debug { "Failure, input states already committed: ${conflictingStates.keys}" }
|
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>) {
|
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)
|
val outsideTimeWindowError = validateTimeWindow(clock.instant(), timeWindow)
|
||||||
if (outsideTimeWindowError == null) {
|
if (outsideTimeWindowError == null) {
|
||||||
states.forEach { stateRef ->
|
states.forEach { stateRef ->
|
||||||
@ -285,9 +278,6 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste
|
|||||||
session.persist(CommittedTransaction(txId.toString()))
|
session.persist(CommittedTransaction(txId.toString()))
|
||||||
log.debug { "Successfully committed all input states: $states" }
|
log.debug { "Successfully committed all input states: $states" }
|
||||||
} else {
|
} else {
|
||||||
if (states.isEmpty() && previouslyCommitted(txId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw NotaryInternalException(outsideTimeWindowError)
|
throw NotaryInternalException(outsideTimeWindowError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user