diff --git a/docs/source/testnet-explorer-corda.rst b/docs/source/testnet-explorer-corda.rst index ac1b4bb486..e0fdbc2fd4 100644 --- a/docs/source/testnet-explorer-corda.rst +++ b/docs/source/testnet-explorer-corda.rst @@ -34,8 +34,8 @@ couple of resources. .. code-block:: bash - wget https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-finance-contracts-|corda_version|-corda/corda-finance-contracts-|corda_version|-corda.jar - wget https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-finance-workflows-|corda_version|-corda/corda-finance-workflows-|corda_version|-corda.jar + wget https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-finance-contracts/|corda_version|/corda-finance-contracts-|corda_version|.jar + wget https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-finance-workflows/|corda_version|/corda-finance-workflows-|corda_version|.jar This is required to run some flows to check your connections, and to issue/transfer cash to counterparties. Copy it to the Corda installation location: diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst index d37316b531..80b96546be 100644 --- a/docs/source/upgrading-cordapps.rst +++ b/docs/source/upgrading-cordapps.rst @@ -719,3 +719,29 @@ Although not strictly related to versioning, AMQP serialisation dictates that we wildcard * Any superclass must adhere to the same rules, but can be abstract * Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly + + +Testing CorDapp upgrades +------------------------ + +At the time of this writing there is no platform support to test CorDapp upgrades. There are plans to add support in a future version. +This means that it is not possible to write automated tests using just the provided tooling. + +To test an implicit upgrade, you must simulate a network that initially has only nodes with the old version of the CorDapp, and then nodes gradually transition to the new version. +Typically, in such a complex upgrade scenario, there must be a deadline by which time all nodes that want to continue to use the CorDapp must upgrade. +To achieve this, this deadline must be configured in the flow logic which must only use new features afterwards. + +This can be simulated with a scenario like this: + +1. Write and individually test the new version of the state and contract. +2. Setup a network of nodes with the previous version. In the simplest form, `deployNodes` can be used for this purpose. +3. Run some transactions between nodes. +4. Upgrade a couple of nodes to the new version of the CorDapp. +5. Continue running transactions between various combinations of versions. Also make sure transactions that were created between nodes with the new version +are being successfully read by nodes with the old CorDapp. +6. Upgrade all nodes and simulate the deadline expiration. +7. Make sure old transactions can be consumed, and new features are successfully used in new transactions. + + + + diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index f4f442418b..29dba42de8 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -28,6 +28,8 @@ import java.security.PublicKey import java.time.Clock import java.time.Instant import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet import javax.persistence.Tuple import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.CriteriaUpdate @@ -91,7 +93,8 @@ class NodeVaultService( * Maintain a list of contract state interfaces to concrete types stored in the vault * for usage in generic queries of type queryBy or queryBy> */ - private val contractStateTypeMappings = mutableMapOf>().toSynchronised() + @VisibleForTesting + internal val contractStateTypeMappings = ConcurrentHashMap>() override fun start() { bootstrapContractStateTypes() @@ -103,7 +106,7 @@ class NodeVaultService( if (!seen) { val contractTypes = deriveContractTypes(concreteType) contractTypes.map { - val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } + val contractStateType = contractStateTypeMappings.getOrPut(it.name) { CopyOnWriteArraySet() } contractStateType.add(concreteType.name) } } @@ -207,6 +210,9 @@ class NodeVaultService( override val updates: Observable> get() = mutex.locked { _updatesInDbTx } + @VisibleForTesting + internal val publishUpdates get() = mutex.locked { updatesPublisher } + /** Groups adjacent transactions into batches to generate separate net updates per transaction type. */ override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable) { if (statesToRecord == StatesToRecord.NONE || (!txns.any() && !previouslySeenTxns.any())) return @@ -738,7 +744,7 @@ class NodeVaultService( concreteType?.let { val contractTypes = deriveContractTypes(it) contractTypes.map { - val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } + val contractStateType = contractStateTypeMappings.getOrPut(it.name) { CopyOnWriteArraySet() } contractStateType.add(concreteType.name) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 70e31a1355..ebb1ccc264 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -7,6 +7,7 @@ import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.NullKeys +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.* import net.corda.core.internal.NotaryChangeTransactionBuilder @@ -22,6 +23,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.toNonEmptySet import net.corda.finance.* import net.corda.finance.contracts.asset.Cash @@ -951,4 +953,36 @@ class NodeVaultServiceTest { assertTrue(it) } } + + @Test + fun `test concurrent update of contract state type mappings`() { + // no registered contract state types at start-up. + assertEquals(0, vaultService.contractStateTypeMappings.size) + + fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = + StateAndRef( + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), identity.party), Cash.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), + StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) + ) + + val cashIssued = setOf>(makeCash(100.DOLLARS, dummyCashIssuer.party)) + val cashUpdate = Vault.Update(emptySet(), cashIssued) + + val service = Executors.newFixedThreadPool(10) + (1..100).map { + service.submit { + database.transaction { + vaultService.publishUpdates.onNext(cashUpdate) + } + } + }.forEach { it.getOrThrow() } + + vaultService.contractStateTypeMappings.forEach { + println("${it.key} = ${it.value}") + } + // Cash.State and its superclasses and interfaces: FungibleAsset, FungibleState, OwnableState, QueryableState + assertEquals(4, vaultService.contractStateTypeMappings.size) + + service.shutdown() + } } \ No newline at end of file