CORDA-2526 Allow for duplicate signed attachments in devMode (#4691)

* Allow for duplicate (contract class, version) signed attachments in devMode.

* Code clean-up.

* Fix compilation error in test code.

* Additional gating/warning and added Unit test for development mode behaviour.
This commit is contained in:
josecoll 2019-01-31 14:41:23 +00:00 committed by GitHub
parent 12693ab175
commit 099a747ebf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 64 additions and 21 deletions

View File

@ -76,7 +76,7 @@ class AttachmentsClassLoaderStaticContractTests {
private val serviceHub get() = rigorousMock<ServicesForResolution>().also { private val serviceHub get() = rigorousMock<ServicesForResolution>().also {
val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage()) val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations) cordappProviderImpl.start()
doReturn(cordappProviderImpl).whenever(it).cordappProvider doReturn(cordappProviderImpl).whenever(it).cordappProvider
doReturn(networkParametersService).whenever(it).networkParametersService doReturn(networkParametersService).whenever(it).networkParametersService
doReturn(networkParameters).whenever(it).networkParameters doReturn(networkParameters).whenever(it).networkParameters

View File

@ -170,7 +170,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
@Suppress("LeakingThis") @Suppress("LeakingThis")
val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize()
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize() val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database, configuration.devMode).tokenize()
val cryptoService = configuration.makeCryptoService() val cryptoService = configuration.makeCryptoService()
@Suppress("LeakingThis") @Suppress("LeakingThis")
val networkParametersStorage = makeNetworkParametersStorage() val networkParametersStorage = makeNetworkParametersStorage()
@ -371,7 +371,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
networkParametersStorage.setCurrentParameters(signedNetParams, trustRoot) networkParametersStorage.setCurrentParameters(signedNetParams, trustRoot)
identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts)
attachments.start() attachments.start()
cordappProvider.start(netParams.whitelistedContractImplementations) cordappProvider.start()
nodeProperties.start() nodeProperties.start()
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with

View File

@ -35,7 +35,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
*/ */
override val cordapps: List<CordappImpl> get() = cordappLoader.cordapps override val cordapps: List<CordappImpl> get() = cordappLoader.cordapps
fun start(whitelistedContractImplementations: Map<String, List<AttachmentId>>) { fun start() {
cordappAttachments.putAll(loadContractsIntoAttachmentStore()) cordappAttachments.putAll(loadContractsIntoAttachmentStore())
verifyInstalledCordapps() verifyInstalledCordapps()
} }

View File

@ -52,8 +52,13 @@ import javax.persistence.*
class NodeAttachmentService( class NodeAttachmentService(
metrics: MetricRegistry, metrics: MetricRegistry,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
private val database: CordaPersistence private val database: CordaPersistence,
val devMode: Boolean
) : AttachmentStorageInternal, SingletonSerializeAsToken() { ) : AttachmentStorageInternal, SingletonSerializeAsToken() {
constructor(metrics: MetricRegistry,
cacheFactory: NamedCacheFactory,
database: CordaPersistence) : this(metrics, cacheFactory, database, false)
// This is to break the circular dependency. // This is to break the circular dependency.
lateinit var servicesForResolution: ServicesForResolution lateinit var servicesForResolution: ServicesForResolution
@ -356,9 +361,8 @@ class NodeAttachmentService(
val jarSigners = getSigners(bytes) val jarSigners = getSigners(bytes)
val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id) val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id)
val session = currentDBSession() val session = currentDBSession()
if (!devMode)
verifyVersionUniquenessForSignedAttachments(contractClassNames, contractVersion, jarSigners) verifyVersionUniquenessForSignedAttachments(contractClassNames, contractVersion, jarSigners)
val attachment = NodeAttachmentService.DBAttachment( val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(), attId = id.toString(),
content = bytes, content = bytes,
@ -379,7 +383,8 @@ class NodeAttachmentService(
val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
// update the `uploader` field (as the existing attachment may have been resolved from a peer) // update the `uploader` field (as the existing attachment may have been resolved from a peer)
if (attachment.uploader != uploader) { if (attachment.uploader != uploader) {
verifyVersionUniquenessForSignedAttachments(contractClassNames, attachment.version, attachment.signers) if (!devMode)
verifyVersionUniquenessForSignedAttachments(contractClassNames, attachment.version, attachment.signers)
attachment.uploader = uploader attachment.uploader = uploader
log.info("Updated attachment $id with uploader $uploader") log.info("Updated attachment $id with uploader $uploader")
contractClassNames.forEach { contractsCache.invalidate(it) } contractClassNames.forEach { contractsCache.invalidate(it) }
@ -489,11 +494,14 @@ class NodeAttachmentService(
private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>, contractClassName: String): Pair<Version, AttachmentIds> { private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>, contractClassName: String): Pair<Version, AttachmentIds> {
val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) } val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }
check (signed.size <= 1) //sanity check if (!devMode)
check (signed.size <= 1) //sanity check
else
log.warn("(Dev Mode) Multiple signed attachments ${signed.map { it.toString() }} for contract $contractClassName version '${it.key}'.")
val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) } val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }
if (unsigned.size > 1) if (unsigned.size > 1)
log.warn("Selecting attachment ${unsigned.first()} from duplicated, unsigned attachments ${unsigned.map { it.toString() }} for contract $contractClassName version '${it.key}'.") log.warn("Selecting attachment ${unsigned.first()} from duplicated, unsigned attachments ${unsigned.map { it.toString() }} for contract $contractClassName version '${it.key}'.")
return it.key to AttachmentIds(signed.singleOrNull(), unsigned.firstOrNull()) return it.key to AttachmentIds(signed.firstOrNull(), unsigned.firstOrNull())
} }
override fun getLatestContractAttachments(contractClassName: String, minContractVersion: Int): List<AttachmentId> { override fun getLatestContractAttachments(contractClassName: String, minContractVersion: Int): List<AttachmentId> {

View File

@ -77,7 +77,7 @@ class CordappProviderImplTests {
val configProvider = MockCordappConfigProvider() val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs[isolatedCordappName] = validConfig configProvider.cordappConfigs[isolatedCordappName] = validConfig
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start(whitelistedContractImplementations) } val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() }
val expected = provider.getAppContext(provider.cordapps.first()).config val expected = provider.getAppContext(provider.cordapps.first()).config
@ -86,6 +86,6 @@ class CordappProviderImplTests {
private fun newCordappProvider(vararg urls: URL): CordappProviderImpl { private fun newCordappProvider(vararg urls: URL): CordappProviderImpl {
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN)
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start(whitelistedContractImplementations) } return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start() }
} }
} }

View File

@ -44,6 +44,7 @@ import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException import java.nio.file.FileAlreadyExistsException
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path import java.nio.file.Path
import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
@ -55,6 +56,7 @@ class NodeAttachmentServiceTest {
private lateinit var fs: FileSystem private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService private lateinit var storage: NodeAttachmentService
private lateinit var devModeStorage: NodeAttachmentService
private val services = rigorousMock<ServicesForResolution>().also { private val services = rigorousMock<ServicesForResolution>().also {
doReturn(testNetworkParameters()).whenever(it).networkParameters doReturn(testNetworkParameters()).whenever(it).networkParameters
} }
@ -73,6 +75,12 @@ class NodeAttachmentServiceTest {
} }
} }
storage.servicesForResolution = services storage.servicesForResolution = services
devModeStorage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database, true).also {
database.transaction {
it.start()
}
}
devModeStorage.servicesForResolution = services
} }
@After @After
@ -688,6 +696,32 @@ class NodeAttachmentServiceTest {
} }
} }
@Test
fun `development mode - retrieve latest versions of signed contracts - multiple versions of same version id exist in store`() {
SelfCleaningDir().use { file ->
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val (signedContractJarSameVersion, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", versionSeed = Random().nextInt())
signedContractJar.read { devModeStorage.privilegedImportAttachment(it, "app", "contract-signed.jar") }
var attachmentIdSameVersionLatest: AttachmentId? = null
signedContractJarSameVersion.read { attachmentIdSameVersionLatest = devModeStorage.privilegedImportAttachment(it, "app", "contract-signed-same-version.jar") }
// this assertion is only true in development mode
assertEquals(
2,
devModeStorage.queryAttachments(AttachmentsQueryCriteria(
contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")),
versionCondition = Builder.equal(1),
isSignedCondition = Builder.equal(true))).size
)
val latestAttachments = devModeStorage.getLatestContractAttachments("com.example.MyContract")
assertEquals(1, latestAttachments.size)
// should return latest version given by insertion date
assertEquals(attachmentIdSameVersionLatest, latestAttachments[0])
}
}
// Not the real FetchAttachmentsFlow! // Not the real FetchAttachmentsFlow!
private class FetchAttachmentsFlow : FlowLogic<Unit>() { private class FetchAttachmentsFlow : FlowLogic<Unit>() {
@Suspendable @Suspendable

View File

@ -309,7 +309,7 @@ open class MockServices private constructor(
} }
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2)
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also { private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also {
it.start(initialNetworkParameters.whitelistedContractImplementations) it.start()
} }
override val cordappProvider: CordappProvider get() = mockCordappProvider override val cordappProvider: CordappProvider get() = mockCordappProvider
override val networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override val networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)

View File

@ -55,18 +55,18 @@ object ContractJarTestUtils {
} }
@JvmOverloads @JvmOverloads
fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: Int = 1): Pair<Path, PublicKey> { fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: Int = 1, versionSeed: Int = 0): Pair<Path, PublicKey> {
val jarName = makeTestContractJar(workingDir, contractName, true, version) val jarName = makeTestContractJar(workingDir, contractName, true, version, versionSeed)
val signer = workingDir.signWithDummyKey(jarName) val signer = workingDir.signWithDummyKey(jarName)
return workingDir.resolve(jarName) to signer return workingDir.resolve(jarName) to signer
} }
@JvmOverloads @JvmOverloads
fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: Int = 1): Path { fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: Int = 1, versionSeed: Int = 0): Path {
val packages = contractName.split(".") val packages = contractName.split(".")
val jarName = "attachment-${packages.last()}-$version-${(if (signed) "signed" else "")}.jar" val jarName = "attachment-${packages.last()}-$version-$versionSeed-${(if (signed) "signed" else "")}.jar"
val className = packages.last() val className = packages.last()
createTestClass(workingDir, className, packages.subList(0, packages.size - 1)) createTestClass(workingDir, className, packages.subList(0, packages.size - 1), versionSeed)
workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class") workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class")
workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString())) workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString()))
return workingDir.resolve(jarName) return workingDir.resolve(jarName)
@ -81,7 +81,7 @@ object ContractJarTestUtils {
} }
val packages = contractNames.first().split(".") val packages = contractNames.first().split(".")
val jarName = jarFileName ?: "attachment-${packages.last()}-$version-${(if (signed) "signed" else "")}.jar" val jarName = jarFileName ?: "attachment-${packages.last()}-$version-${(if (signed) "signed" else "")}.jar"
workingDir.createJar(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray() ) workingDir.createJar(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray())
if (generateManifest) if (generateManifest)
workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString())) workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString()))
if (signed) if (signed)
@ -89,12 +89,13 @@ object ContractJarTestUtils {
return workingDir.resolve(jarName) return workingDir.resolve(jarName)
} }
private fun createTestClass(workingDir: Path, className: String, packages: List<String>): Path { private fun createTestClass(workingDir: Path, className: String, packages: List<String>, versionSeed: Int = 0): Path {
val newClass = """package ${packages.joinToString(".")}; val newClass = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*; import net.corda.core.contracts.*;
import net.corda.core.transactions.*; import net.corda.core.transactions.*;
public class $className implements Contract { public class $className implements Contract {
private int seed = $versionSeed;
@Override @Override
public void verify(LedgerTransaction tx) throws IllegalArgumentException { public void verify(LedgerTransaction tx) throws IllegalArgumentException {
} }