Create a unit test for serialization whitelists via driver.

This commit is contained in:
Chris Rankin 2020-02-05 10:46:42 +00:00
parent dc92786d17
commit d89ce6608a
15 changed files with 212 additions and 32 deletions

View File

@ -36,6 +36,7 @@ fun <T: Any> getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Clas
return ClassGraph().overrideClassLoaders(classloader) return ClassGraph().overrideClassLoaders(classloader)
.enableURLScheme(attachmentScheme) .enableURLScheme(attachmentScheme)
.ignoreParentClassLoaders() .ignoreParentClassLoaders()
.disableDirScanning()
.enableClassInfo() .enableClassInfo()
.pooledScan() .pooledScan()
.use { result -> .use { result ->

View File

@ -0,0 +1,38 @@
package net.corda.contracts.serialization.whitelist
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
class WhitelistContract : Contract {
companion object {
const val MAX_VALUE = 2000L
}
override fun verify(tx: LedgerTransaction) {
val states = tx.outputsOfType<State>()
require(states.isNotEmpty()) {
"Requires at least one data state"
}
states.forEach {
require(it.whitelistData in WhitelistData(0)..WhitelistData(MAX_VALUE)) {
"WhitelistData $it exceeds maximum value!"
}
}
}
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
class State(val owner: AbstractParty, val whitelistData: WhitelistData) : ContractState {
override val participants: List<AbstractParty> = listOf(owner)
@Override
override fun toString(): String {
return whitelistData.toString()
}
}
class Operate : CommandData
}

View File

@ -0,0 +1,15 @@
package net.corda.contracts.serialization.whitelist
import net.corda.core.serialization.SerializationWhitelist
data class WhitelistData(val value: Long) : Comparable<WhitelistData> {
override fun compareTo(other: WhitelistData): Int {
return value.compareTo(other.value)
}
override fun toString(): String = "$value things"
}
class Whitelist : SerializationWhitelist {
override val whitelist = listOf(WhitelistData::class.java)
}

View File

@ -6,11 +6,9 @@ import net.corda.core.contracts.Command
import net.corda.core.contracts.CommandData import net.corda.core.contracts.CommandData
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC @StartableByRPC
class SandboxAttachmentFlow(private val command: CommandData) : FlowLogic<SecureHash>() { class SandboxAttachmentFlow(private val command: CommandData) : FlowLogic<SecureHash>() {
@Suspendable @Suspendable

View File

@ -6,11 +6,9 @@ import net.corda.core.contracts.Command
import net.corda.core.contracts.TypeOnlyCommandData import net.corda.core.contracts.TypeOnlyCommandData
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC @StartableByRPC
class NonDeterministicFlow(private val trouble: TypeOnlyCommandData) : FlowLogic<SecureHash>() { class NonDeterministicFlow(private val trouble: TypeOnlyCommandData) : FlowLogic<SecureHash>() {
@Suspendable @Suspendable

View File

@ -6,12 +6,10 @@ import net.corda.core.contracts.Command
import net.corda.core.contracts.CommandData import net.corda.core.contracts.CommandData
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
@InitiatingFlow
@StartableByRPC @StartableByRPC
class DeterministicCryptoFlow( class DeterministicCryptoFlow(
private val command: CommandData, private val command: CommandData,

View File

@ -7,11 +7,9 @@ import net.corda.contracts.djvm.whitelist.WhitelistData
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC @StartableByRPC
class DeterministicWhitelistFlow(private val data: WhitelistData) : FlowLogic<SecureHash>() { class DeterministicWhitelistFlow(private val data: WhitelistData) : FlowLogic<SecureHash>() {
@Suspendable @Suspendable

View File

@ -7,11 +7,9 @@ import net.corda.contracts.fixup.dependent.DependentData
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC @StartableByRPC
class CordappFixupFlow(private val data: DependentData) : FlowLogic<SecureHash>() { class CordappFixupFlow(private val data: DependentData) : FlowLogic<SecureHash>() {
@Suspendable @Suspendable

View File

@ -7,11 +7,9 @@ import net.corda.contracts.serialization.custom.CustomSerializerContract.Purchas
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC @StartableByRPC
class CustomSerializerFlow( class CustomSerializerFlow(
private val purchase: Currantsy private val purchase: Currantsy

View File

@ -7,11 +7,9 @@ import net.corda.contracts.serialization.missing.MissingSerializerContract.Opera
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC @StartableByRPC
class MissingSerializerBuilderFlow(private val value: Long) : FlowLogic<SecureHash>() { class MissingSerializerBuilderFlow(private val value: Long) : FlowLogic<SecureHash>() {
@Suspendable @Suspendable

View File

@ -12,14 +12,12 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.SignatureMetadata
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.createComponentGroups import net.corda.core.internal.createComponentGroups
import net.corda.core.internal.requiredContractClassName import net.corda.core.internal.requiredContractClassName
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
@InitiatingFlow
@StartableByRPC @StartableByRPC
class MissingSerializerFlow(private val value: Long) : FlowLogic<SecureHash>() { class MissingSerializerFlow(private val value: Long) : FlowLogic<SecureHash>() {
@Suspendable @Suspendable

View File

@ -0,0 +1,26 @@
package net.corda.flows.serialization.whitelist
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.serialization.whitelist.WhitelistContract
import net.corda.contracts.serialization.whitelist.WhitelistContract.State
import net.corda.contracts.serialization.whitelist.WhitelistData
import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder
@StartableByRPC
class WhitelistFlow(private val data: WhitelistData) : FlowLogic<SecureHash>() {
@Suspendable
override fun call(): SecureHash {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary)
.addOutputState(State(ourIdentity, data))
.addCommand(Command(WhitelistContract.Operate(), ourIdentity.owningKey))
)
stx.verify(serviceHub, checkSufficientSignatures = false)
return stx.id
}
}

View File

@ -0,0 +1,96 @@
package net.corda.node
import net.corda.client.rpc.CordaRPCClient
import net.corda.contracts.serialization.whitelist.WhitelistData
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.flows.serialization.whitelist.WhitelistFlow
import net.corda.node.services.Permissions
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.User
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.Test
import kotlin.test.assertFailsWith
@Suppress("FunctionName")
class ContractWithSerializationWhitelistTest {
companion object {
const val DATA = 123456L
@JvmField
val contractCordapp = cordappWithPackages("net.corda.contracts.serialization.whitelist").signed()
@JvmField
val workflowCordapp = cordappWithPackages("net.corda.flows.serialization.whitelist").signed()
fun parametersFor(runInProcess: Boolean): DriverParameters {
return DriverParameters(
portAllocation = incrementalPortAllocation(),
startNodesInProcess = runInProcess,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
cordappsForAllNodes = listOf(contractCordapp, workflowCordapp)
)
}
@BeforeClass
@JvmStatic
fun checkData() {
assertNotCordaSerializable<WhitelistData>()
}
}
@Test
fun `test serialization whitelist out-of-process`() {
val user = User("u", "p", setOf(Permissions.all()))
driver(parametersFor(runInProcess = false)) {
val badData = WhitelistData(DATA)
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<ContractRejection> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
client.proxy.startFlow(::WhitelistFlow, badData)
.returnValue
.getOrThrow()
}
}
assertThat(ex)
.hasMessageContaining("WhitelistData $badData exceeds maximum value!")
}
}
@Test
fun `test serialization whitelist in-process`() {
val user = User("u", "p", setOf(Permissions.all()))
driver(parametersFor(runInProcess = true)) {
val badData = WhitelistData(DATA)
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<ContractRejection> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
client.proxy.startFlow(::WhitelistFlow, badData)
.returnValue
.getOrThrow()
}
}
assertThat(ex)
.hasMessageContaining("WhitelistData $badData exceeds maximum value!")
}
}
// @Test
// fun `test serialization whitelist in-process`() {
// assertFailsWith<NotCordappWhitelist> {
// driver(parametersFor(runInProcess = true)) {}
// }
// }
}

View File

@ -219,7 +219,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
// present in the CorDapp. // present in the CorDapp.
val result = scanResult.getClassesWithSuperclass(NotaryService::class) + val result = scanResult.getClassesWithSuperclass(NotaryService::class) +
scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class) scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class)
if(!result.isEmpty()) { if (result.isNotEmpty()) {
logger.info("Found notary service CorDapp implementations: " + result.joinToString(", ")) logger.info("Found notary service CorDapp implementations: " + result.joinToString(", "))
} }
return result.firstOrNull() return result.firstOrNull()
@ -270,10 +270,26 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
private fun findWhitelists(cordappJarPath: RestrictedURL): List<SerializationWhitelist> { private fun findWhitelists(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
val whitelists = URLClassLoader(arrayOf(cordappJarPath.url)).use { val whitelists = URLClassLoader(arrayOf(cordappJarPath.url)).use {
ServiceLoader.load(SerializationWhitelist::class.java, it).toList() ServiceLoader.load(SerializationWhitelist::class.java, it).toList()
}.filter {
it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) && it.javaClass.location == cordappJarPath.url
} }
return whitelists.filter {
it.javaClass.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) whitelists.filterNot {
} + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. it.javaClass.location == cordappJarPath.url
}.apply {
if (isNotEmpty()) {
throw NotCordappWhitelist("Whitelists ${showClasses(this)} not found within ${cordappJarPath.url}")
}
}
return whitelists + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
}
private fun showClasses(items: Iterable<Any>): String {
return items.map {
it::class.java
}.map {
"${it.name} in ${it.protectionDomain.codeSource.location}"
}.toString()
} }
private fun findSerializers(scanResult: RestrictedScanResult): List<SerializationCustomSerializer<*, *>> { private fun findSerializers(scanResult: RestrictedScanResult): List<SerializationCustomSerializer<*, *>> {
@ -381,6 +397,13 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
*/ */
class MultipleCordappsForFlowException(message: String) : Exception(message) class MultipleCordappsForFlowException(message: String) : Exception(message)
/**
* Thrown when a [SerializationWhitelist] is loaded from outside the CorDapp.
* Most likely because you are testing with node-driver and "in-process" nodes.
* Try using "out-of-process" driver nodes instead.
*/
class NotCordappWhitelist(message: String) : Exception(message)
/** /**
* Thrown if an exception occurs whilst parsing version identifiers within cordapp configuration * Thrown if an exception occurs whilst parsing version identifiers within cordapp configuration
*/ */

View File

@ -142,11 +142,7 @@ class DriverDSLImpl(
private lateinit var _notaries: CordaFuture<List<NotaryHandle>> private lateinit var _notaries: CordaFuture<List<NotaryHandle>>
override val notaryHandles: List<NotaryHandle> get() = _notaries.getOrThrow() override val notaryHandles: List<NotaryHandle> get() = _notaries.getOrThrow()
override val cordappsClassLoader: ClassLoader? = if (!startNodesInProcess) { override val cordappsClassLoader: URLClassLoader? = createCordappsClassLoader(cordappsForAllNodes)
createCordappsClassLoader(cordappsForAllNodes)
} else {
null
}
interface Waitable { interface Waitable {
@Throws(InterruptedException::class) @Throws(InterruptedException::class)
@ -195,14 +191,15 @@ class DriverDSLImpl(
} }
override fun shutdown() { override fun shutdown() {
if (waitForAllNodesToFinish) { cordappsClassLoader.use { _ ->
state.locked { if (waitForAllNodesToFinish) {
processes.forEach { it.waitFor() } state.locked {
processes.forEach { it.waitFor() }
}
} }
_shutdownManager?.shutdown()
_executorService?.shutdownNow()
} }
_shutdownManager?.shutdown()
_executorService?.shutdownNow()
(cordappsClassLoader as? AutoCloseable)?.close()
} }
private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> { private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
@ -992,7 +989,7 @@ class DriverDSLImpl(
return config return config
} }
private fun createCordappsClassLoader(cordapps: Collection<TestCordappInternal>?): ClassLoader? { private fun createCordappsClassLoader(cordapps: Collection<TestCordappInternal>?): URLClassLoader? {
if (cordapps == null || cordapps.isEmpty()) { if (cordapps == null || cordapps.isEmpty()) {
return null return null
} }