CORDA-2497 fix (#4630)

CORDA-2497 fix
* Added test to show the fix working.
* Now backwards compatible.
* Refactored out some duped code.
* Added better explanations for what's going on.
* Fixed test which was failing due to the serializationEnvRule problem.
* Addressed Tudor's review comments.
This commit is contained in:
Roger Willis
2019-01-24 20:36:33 +00:00
committed by GitHub
parent f7a6463424
commit 0e1c20a883
5 changed files with 187 additions and 77 deletions

View File

@ -47,7 +47,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
* other transactions observed, then the changes are observed "net" of those.
*/
@CordaSerializable
data class Update<U : ContractState>(
data class Update<U : ContractState> @JvmOverloads constructor(
val consumed: Set<StateAndRef<U>>,
val produced: Set<StateAndRef<U>>,
val flowId: UUID? = null,
@ -56,10 +56,11 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
* Notary change transactions only modify the notary field on states, and potentially need to be handled
* differently.
*/
val type: UpdateType = UpdateType.GENERAL
val type: UpdateType = UpdateType.GENERAL,
val references: Set<StateAndRef<U>> = emptySet()
) {
/** Checks whether the update contains a state of the specified type. */
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T }
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T } || references.any { it.state.data is T }
/** Checks whether the update contains a state of the specified type and state status */
fun <T : ContractState> containsType(clazz: Class<T>, status: StateStatus) =
@ -83,7 +84,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val combinedConsumed = consumed + (rhs.consumed - produced)
// The ordering below matters to preserve ordering of consumed/produced Sets when they are insertion order dependent implementations.
val combinedProduced = produced.filter { it !in rhs.consumed }.toSet() + rhs.produced
return copy(consumed = combinedConsumed, produced = combinedProduced)
return copy(consumed = combinedConsumed, produced = combinedProduced, references = references + rhs.references)
}
override fun toString(): String {
@ -99,8 +100,23 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
produced.forEach {
sb.appendln("${it.ref}: ${it.state}")
}
sb.appendln("References:")
references.forEach {
sb.appendln("${it.ref}: ${it.state}")
}
return sb.toString()
}
/** Additional copy method to maintain backwards compatibility. */
fun copy(
consumed: Set<StateAndRef<U>>,
produced: Set<StateAndRef<U>>,
flowId: UUID? = null,
type: UpdateType = UpdateType.GENERAL
): Update<U> {
return Update(consumed, produced, flowId, type, references)
}
}
@CordaSerializable
@ -232,9 +248,9 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
companion object {
@Deprecated("No longer used. The vault does not emit empty updates")
val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL)
val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL, references = emptySet())
@Deprecated("No longer used. The vault does not emit empty updates")
val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE)
val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE, references = emptySet())
}
}
@ -284,7 +300,7 @@ interface VaultService {
val result = trackBy<ContractState>(query)
val snapshot = result.snapshot.states
return if (snapshot.isNotEmpty()) {
doneFuture(Vault.Update(consumed = setOf(snapshot.single()), produced = emptySet()))
doneFuture(Vault.Update(consumed = setOf(snapshot.single()), produced = emptySet(), references = emptySet()))
} else {
result.updates.toFuture()
}

View File

@ -8,31 +8,41 @@ import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.toFuture
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.internal.vault.DUMMY_LINEAR_CONTRACT_PROGRAM_ID
import net.corda.testing.internal.vault.DummyLinearContract
import net.corda.testing.node.StartedMockNode
import net.corda.testing.node.internal.*
import net.corda.testing.node.transaction
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class WithReferencedStatesFlowTests {
companion object {
@JvmStatic
private val mockNet = InternalMockNetwork(
cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()),
threadPerNode = true,
initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)
}
class ReferencedStatesFlowTests {
private val nodes = (0..1).map {
mockNet.createNode(
parameters = InternalMockNodeParameters(version = VersionInfo(4, "Blah", "Blah", "Blah"))
)
var mockNet: InternalMockNetwork = InternalMockNetwork(
cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()),
threadPerNode = true,
initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)
lateinit var nodes: List<TestStartedNode>
@Before
fun setup() {
nodes = (0..1).map {
mockNet.createNode(
parameters = InternalMockNodeParameters(version = VersionInfo(4, "Blah", "Blah", "Blah"))
)
}
}
@After
@ -41,7 +51,7 @@ class WithReferencedStatesFlowTests {
}
@Test
fun test() {
fun `with referenced states flow blocks until the reference state update is received`() {
// 1. Create reference state.
val newRefTx = nodes[0].services.startFlow(CreateRefState()).resultFuture.getOrThrow()
val newRefState = newRefTx.tx.outRefsOfType<RefState.State>().single()
@ -54,7 +64,8 @@ class WithReferencedStatesFlowTests {
val updatedRefState = updatedRefTx.tx.outRefsOfType<RefState.State>().single()
// 4. Try to use the old reference state. This will throw a NotaryException.
val useRefTx = nodes[1].services.startFlow(WithReferencedStatesFlow { UseRefState(newRefState.state.data.linearId) }).resultFuture
val nodeOneIdentity = nodes[1].info.legalIdentities.first()
val useRefTx = nodes[1].services.startFlow(WithReferencedStatesFlow { UseRefState(nodeOneIdentity, newRefState.state.data.linearId) }).resultFuture
// 5. Share the update reference state.
nodes[0].services.startFlow(Initiator(updatedRefState)).resultFuture.getOrThrow()
@ -64,6 +75,40 @@ class WithReferencedStatesFlowTests {
assertEquals(updatedRefState.ref, result.tx.references.single())
}
@Test
fun `check ref state is persisted when used in tx with relevant states`() {
// 1. Create a state to be used as a reference state. Don't share it.
val newRefTx = nodes[0].services.startFlow(CreateRefState()).resultFuture.getOrThrow()
val newRefState = newRefTx.tx.outRefsOfType<RefState.State>().single()
// 2. Use the "newRefState" a transaction involving another party (nodes[1]) which creates a new state. They should store the new state and the reference state.
val newTx = nodes[0].services.startFlow(UseRefState(nodes[1].info.legalIdentities.first(), newRefState.state.data.linearId)).resultFuture.getOrThrow()
// Wait until node 1 stores the new tx.
nodes[1].services.validatedTransactions.updates.filter { it.id == newTx.id }.toFuture().getOrThrow()
// Check that nodes[1] has finished recording the transaction (and updating the vault.. hopefully!).
val allRefStates = nodes[1].services.vaultService.queryBy<RefState.State>()
// nodes[1] should have two states. The newly created output and the reference state created by nodes[0].
assertEquals(2, allRefStates.states.size)
// Now let's find the specific reference state on nodes[1].
val refStateLinearId = newRefState.state.data.linearId
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(refStateLinearId))
val theReferencedState = nodes[1].services.vaultService.queryBy<RefState.State>(query)
// There should be one result - the reference state.
assertEquals(newRefState, theReferencedState.states.single())
println(theReferencedState.statesMetadata.single())
// nodes[0] should also have the same state.
val nodeZeroQuery = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(refStateLinearId))
val theReferencedStateOnNodeZero = nodes[0].services.vaultService.queryBy<RefState.State>(nodeZeroQuery)
assertEquals(newRefState, theReferencedStateOnNodeZero.states.single())
println(theReferencedStateOnNodeZero.statesMetadata.single())
// nodes[0] sends the tx that created the reference state to nodes[1].
nodes[0].services.startFlow(Initiator(newRefState)).resultFuture.getOrThrow()
// Query again.
val theReferencedStateAgain = nodes[1].services.vaultService.queryBy<RefState.State>(query)
// There should be one result - the reference state.
assertEquals(newRefState, theReferencedStateAgain.states.single())
println(theReferencedStateAgain.statesMetadata.single())
}
// A dummy reference state contract.
class RefState : Contract {
companion object {
@ -135,7 +180,8 @@ class WithReferencedStatesFlowTests {
}
// A flow to use a reference state in another transaction.
class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic<SignedTransaction>() {
@InitiatingFlow
class UseRefState(private val participant: Party, private val linearId: UniqueIdentifier) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities.first()
@ -147,10 +193,23 @@ class WithReferencedStatesFlowTests {
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
addReferenceState(referenceState.referenced())
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
addOutputState(RefState.State(participant), RefState.CONTRACT_ID)
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
})
return subFlow(FinalityFlow(stx, emptyList()))
return if (participant != ourIdentity) {
subFlow(FinalityFlow(stx, listOf(initiateFlow(participant))))
} else {
subFlow(FinalityFlow(stx, emptyList()))
}
}
}
@InitiatedBy(UseRefState::class)
class UseRefStateResponder(val otherSession: FlowSession) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
// This should also store the reference state if one is there.
return subFlow(ReceiveFinalityFlow(otherSession, statesToRecord = StatesToRecord.ONLY_RELEVANT))
}
}
}

View File

@ -16,7 +16,7 @@ class VaultUpdateTests {
private companion object {
const val DUMMY_PROGRAM_ID = "net.corda.core.node.VaultUpdateTests.DummyContract"
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val emptyUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL)
val emptyUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL, references = emptySet())
}
object DummyContract : Contract {