diff --git a/.gitignore b/.gitignore index a19702ff4a..ba7883cc9c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ tags .gradle /build/ /contracts/build -/standalone.test/build +/contracts/isolated/build /core/build /docs/build/doctrees diff --git a/build.gradle b/build.gradle index 8c943be0cc..adb018f4dd 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,9 @@ repositories { configurations { quasar + + // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment + runtime.exclude module: 'isolated' } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -48,6 +51,7 @@ configurations { dependencies { compile project(':contracts') + testCompile project(':contracts:isolated') compile "com.google.code.findbugs:jsr305:3.0.1" compile "org.slf4j:slf4j-jdk14:1.7.13" diff --git a/contracts/isolated/build.gradle b/contracts/isolated/build.gradle new file mode 100644 index 0000000000..37df6202ee --- /dev/null +++ b/contracts/isolated/build.gradle @@ -0,0 +1,80 @@ +import com.google.common.io.ByteStreams +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.nio.file.attribute.FileTime +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import java.util.zip.ZipFile + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath "com.google.guava:guava:19.0" + } +} + +// Custom Gradle plugin that attempts to make the resulting jar file deterministic. +// Ie. same contract definition should result when compiled in same jar file. +// This is done by removing date time stamps from the files inside the jar. +class CanonicalizerPlugin implements Plugin { + void apply(Project project) { + + project.getTasks().getByName('jar').doLast() { + + def zipPath = (String) project.jar.archivePath + def destPath = Files.createTempFile("processzip", null) + + def zeroTime = FileTime.fromMillis(0) + + def input = new ZipFile(zipPath) + def entries = input.entries().toList().sort { it.name } + + def output = new ZipOutputStream(new FileOutputStream(destPath.toFile())) + output.setMethod(ZipOutputStream.DEFLATED) + + entries.each { + def newEntry = new ZipEntry( it.name ) + + newEntry.setLastModifiedTime(zeroTime) + newEntry.setCreationTime(zeroTime) + newEntry.compressedSize = -1 + newEntry.size = it.size + newEntry.crc = it.crc + + output.putNextEntry(newEntry) + + ByteStreams.copy(input.getInputStream(it), output) + + output.closeEntry() + } + output.close() + input.close() + + Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING) + } + + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' + +apply plugin: CanonicalizerPlugin + +repositories { + mavenCentral() + mavenLocal() + mavenCentral() + jcenter() + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + } +} + +dependencies { + compile project(':core') +} \ No newline at end of file diff --git a/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt b/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt new file mode 100644 index 0000000000..bec6b01cfe --- /dev/null +++ b/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package contracts.isolated + +import core.Contract +import core.ContractState +import core.TransactionForVerification +import core.crypto.SecureHash + +// The dummy contract doesn't do anything useful. It exists for testing purposes. + +val ANOTHER_DUMMY_PROGRAM_ID = SecureHash.sha256("dummy") + +class AnotherDummyContract : Contract { + class State : ContractState { + override val programRef: SecureHash = ANOTHER_DUMMY_PROGRAM_ID + } + + override fun verify(tx: TransactionForVerification) { + // Always accepts. + } + + // The "empty contract" + override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") +} \ No newline at end of file diff --git a/core/src/main/kotlin/core/serialization/Kryo.kt b/core/src/main/kotlin/core/serialization/Kryo.kt index ff59a34767..ffbf602e25 100644 --- a/core/src/main/kotlin/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/core/serialization/Kryo.kt @@ -87,7 +87,7 @@ inline fun OpaqueBytes.deserialize(kryo: Kryo = THREAD_LOCAL_K // The more specific deserialize version results in the bytes being cached, which is faster. @JvmName("SerializedBytesWireTransaction") fun SerializedBytes.deserialize(): WireTransaction = WireTransaction.deserialize(this) -inline fun SerializedBytes.deserialize(): T = bits.deserialize() +inline fun SerializedBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get(), includeClassName: Boolean = false): T = bits.deserialize(kryo, includeClassName) /** * Can be called on any object to convert it to a byte array (wrapped by [SerializedBytes]), regardless of whether diff --git a/settings.gradle b/settings.gradle index 4a8157a88d..6178f9132b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'r3prototyping' include 'contracts' +include 'contracts:isolated' include 'core' include 'standalone.test' \ No newline at end of file diff --git a/src/test/kotlin/core/node/ClassLoaderTests.kt b/src/test/kotlin/core/node/ClassLoaderTests.kt index 5240309189..1c10317dc3 100644 --- a/src/test/kotlin/core/node/ClassLoaderTests.kt +++ b/src/test/kotlin/core/node/ClassLoaderTests.kt @@ -3,19 +3,36 @@ package core.node import core.Contract import core.MockAttachmentStorage import core.crypto.SecureHash +import core.serialization.createKryo +import core.serialization.deserialize +import core.serialization.serialize import org.apache.commons.io.IOUtils -import org.junit.Ignore import org.junit.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.FileInputStream +import java.net.URL +import java.net.URLClassLoader import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull class ClassLoaderTests { + val ISOLATED_CONTRACTS_JAR_PATH = "contracts/isolated/build/libs/isolated.jar" + + @Test + fun `dynamically load AnotherDummyContract from isolated contracts jar`() { + var child = URLClassLoader(arrayOf(URL("file", "", ISOLATED_CONTRACTS_JAR_PATH))) + + var contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child) + var contract = contractClass.newInstance() as Contract + + assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference) + } + fun fakeAttachment(filepath: String, content: String): ByteArray { val bs = ByteArrayOutputStream() val js = JarOutputStream(bs) @@ -29,7 +46,7 @@ class ClassLoaderTests { @Test fun `test MockAttachmentStorage open as jar`() { val storage = MockAttachmentStorage() - val key = storage.importAttachment( FileInputStream("contracts/build/libs/contracts.jar") ) + val key = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) val attachment = storage.openAttachment(key)!! val jar = attachment.openAsJAR() @@ -42,7 +59,7 @@ class ClassLoaderTests { var storage = MockAttachmentStorage() - var att0 = storage.importAttachment( FileInputStream("contracts/build/libs/contracts.jar") ) + var att0 = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) var att1 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file.txt", "some data")) ) var att2 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file.txt", "some other data")) ) @@ -58,7 +75,7 @@ class ClassLoaderTests { var storage = MockAttachmentStorage() - var att0 = storage.importAttachment( FileInputStream("contracts/build/libs/contracts.jar") ) + var att0 = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) var att1 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file1.txt", "some data")) ) var att2 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")) ) @@ -69,26 +86,91 @@ class ClassLoaderTests { } @Test - fun `loading class Cash`() { + fun `loading class AnotherDummyContract`() { var storage = MockAttachmentStorage() - var att0 = storage.importAttachment( FileInputStream("contracts/build/libs/contracts.jar") ) + var att0 = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) var att1 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file1.txt", "some data")) ) var att2 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")) ) ClassLoader.create( arrayOf( att0, att1, att2 ).map { storage.openAttachment(it)!! } ).use { - var contractClass = Class.forName("contracts.Cash", true, it) + var contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, it) var contract = contractClass.newInstance() as Contract - assertEquals(SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html"), contract.legalContractReference) + assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference) } } - @Ignore @Test - fun `testing Kryo with ClassLoader`() { - assert(false) // todo + fun `verify that contract AnotherDummyContract is not in classPath`() { + assertFailsWith(ClassNotFoundException::class) { + var contractClass = Class.forName("contracts.isolated.AnotherDummyContract") + contractClass.newInstance() as Contract + } + } + + @Test + fun `verify that contract DummyContract is in classPath`() { + var contractClass = Class.forName("contracts.DummyContract") + var contract = contractClass.newInstance() as Contract + + assertNotNull(contract) + } + + fun createContract2Cash() : Contract { + var child = URLClassLoader(arrayOf(URL("file", "", ISOLATED_CONTRACTS_JAR_PATH))) + + var contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child) + var contract = contractClass.newInstance() as Contract + return contract + } + + @Test + fun `testing Kryo with ClassLoader (with top level class name)`() { + val contract = createContract2Cash() + + val bytes = contract.serialize(includeClassName = true) + + var storage = MockAttachmentStorage() + + var att0 = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) + var att1 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file1.txt", "some data")) ) + var att2 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")) ) + + val clsLoader = ClassLoader.create( arrayOf( att0, att1, att2 ).map { storage.openAttachment(it)!! } ) + + val kryo = createKryo() + kryo.classLoader = clsLoader + + val state2 = bytes.deserialize(kryo, true) + + assertNotNull(state2) + } + + // top level wrapper + class Data( val contract: Contract ) + + @Test + fun `testing Kryo with ClassLoader (without top level class name)`() { + val data = Data( createContract2Cash() ) + + val bytes = data.serialize() + + var storage = MockAttachmentStorage() + + var att0 = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) + var att1 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file1.txt", "some data")) ) + var att2 = storage.importAttachment( ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")) ) + + val clsLoader = ClassLoader.create( arrayOf( att0, att1, att2 ).map { storage.openAttachment(it)!! } ) + + val kryo = createKryo() + kryo.classLoader = clsLoader + + val state2 = bytes.deserialize(kryo) + + assertNotNull(state2) } } \ No newline at end of file diff --git a/standalone.test/build.gradle b/standalone.test/build.gradle deleted file mode 100644 index 059f3fb260..0000000000 --- a/standalone.test/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath "com.google.guava:guava:19.0" - } -} - -apply plugin: 'java' -apply plugin: 'kotlin' - -repositories { - mavenCentral() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } -} - -dependencies { - testCompile 'junit:junit:4.12' - - compile project(':core') -} \ No newline at end of file diff --git a/standalone.test/src/test/kotlin/LoaderTests.kt b/standalone.test/src/test/kotlin/LoaderTests.kt deleted file mode 100644 index 4400d4abc3..0000000000 --- a/standalone.test/src/test/kotlin/LoaderTests.kt +++ /dev/null @@ -1,21 +0,0 @@ -import core.Contract -import core.crypto.SecureHash -import org.junit.Test -import java.net.URL -import java.net.URLClassLoader -import java.util.jar.JarInputStream -import kotlin.test.assertEquals - -class LoaderTests { - - @Test - fun `dynamically load Cash class from contracts jar`() { - var child = URLClassLoader(arrayOf(URL("file", "", "../contracts/build/libs/contracts.jar"))) - - var contractClass = Class.forName("contracts.Cash", true, child) - var contract = contractClass.newInstance() as Contract - - assertEquals(SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html"), contract.legalContractReference) - } - -} \ No newline at end of file