diff --git a/contracts/build.gradle b/contracts/build.gradle index 015e12b818..ae3daa7713 100644 --- a/contracts/build.gradle +++ b/contracts/build.gradle @@ -80,5 +80,4 @@ dependencies { compile project(':core') testCompile 'junit:junit:4.12' - testCompile "commons-fileupload:commons-fileupload:1.3.1" } \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 7ff2f26c6c..d6551b4e4d 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -21,9 +21,15 @@ repositories { } } +//noinspection GroovyAssignabilityCheck +configurations { + quasar +} + dependencies { testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:3.4.1' + testCompile "commons-fileupload:commons-fileupload:1.3.1" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" @@ -57,4 +63,40 @@ dependencies { // For JSON compile "com.fasterxml.jackson.core:jackson-databind:2.5.5" + + // Quasar: for the bytecode rewriting for state machines. + quasar "co.paralleluniverse:quasar-core:${quasar_version}:jdk8@jar" } + + +tasks.withType(Test) { + jvmArgs "-javaagent:${configurations.quasar.singleFile}" + jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation" +} +tasks.withType(JavaExec) { + jvmArgs "-javaagent:${configurations.quasar.singleFile}" + jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation" +} + + +// These lines tell gradle to run the Quasar suspendables scanner to look for unannotated super methods +// that have @Suspendable sub implementations. These tend to cause NPEs and are not caught by the verifier +// NOTE: need to make sure the output isn't on the classpath or every other run it generates empty results, so +// we explicitly delete to avoid that happening. We also need to turn off what seems to be a spurious warning in the IDE +// +// TODO: Make this task incremental, as it can be quite slow. + +//noinspection GroovyAssignabilityCheck +task quasarScan(dependsOn: ['classes']) << { + ant.taskdef(name:'scanSuspendables', classname:'co.paralleluniverse.fibers.instrument.SuspendablesScanner', + classpath: "${sourceSets.main.output.classesDir}:${sourceSets.main.output.resourcesDir}:${configurations.runtime.asPath}") + delete "$sourceSets.main.output.resourcesDir/META-INF/suspendables", "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers" + ant.scanSuspendables( + auto:false, + suspendablesFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendables", + supersFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers") { + fileset(dir: sourceSets.main.output.classesDir) + } +} + +jar.dependsOn quasarScan \ No newline at end of file diff --git a/contracts/src/main/resources/core/node/isolated.jar b/core/src/main/resources/core/node/isolated.jar similarity index 100% rename from contracts/src/main/resources/core/node/isolated.jar rename to core/src/main/resources/core/node/isolated.jar diff --git a/contracts/src/test/kotlin/core/contracts/TransactionGroupTests.kt b/core/src/test/kotlin/core/contracts/TransactionGroupTests.kt similarity index 65% rename from contracts/src/test/kotlin/core/contracts/TransactionGroupTests.kt rename to core/src/test/kotlin/core/contracts/TransactionGroupTests.kt index b11cf5b9b7..32694c393d 100644 --- a/contracts/src/test/kotlin/core/contracts/TransactionGroupTests.kt +++ b/core/src/test/kotlin/core/contracts/TransactionGroupTests.kt @@ -1,16 +1,43 @@ package core.contracts -import contracts.Cash -import contracts.testing.`owned by` +import core.crypto.Party +import core.crypto.SecureHash import core.node.services.testing.MockStorageService import core.testing.* import org.junit.Test +import java.security.PublicKey +import java.security.SecureRandom import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals +val TEST_PROGRAM_ID = TransactionGroupTests.TestCash() + class TransactionGroupTests { - val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY, DUMMY_NOTARY) + val A_THOUSAND_POUNDS = TestCash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY, DUMMY_NOTARY) + + class TestCash : Contract { + override val legalContractReference = SecureHash.sha256("TestCash") + + override fun verify(tx: TransactionForVerification) { + } + + data class State( + val deposit: PartyAndReference, + val amount: Amount, + override val owner: PublicKey, + override val notary: Party) : OwnableState { + override val contract: Contract = TEST_PROGRAM_ID + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + } + interface Commands : CommandData { + class Move() : TypeOnlyCommandData(), Commands + data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands + data class Exit(val amount: Amount) : Commands + } + } + + infix fun TestCash.State.`owned by`(owner: PublicKey) = copy(owner = owner) @Test fun success() { @@ -22,13 +49,13 @@ class TransactionGroupTests { transaction { input("£1000") output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } } transaction { input("alice's £1000") - arg(ALICE_PUBKEY) { Cash.Commands.Move() } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) } + arg(ALICE_PUBKEY) { TestCash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) } } verify() @@ -40,7 +67,7 @@ class TransactionGroupTests { transactionGroup { val t = transaction { output("cash") { A_THOUSAND_POUNDS } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() } } val conflict1 = transaction { @@ -48,7 +75,7 @@ class TransactionGroupTests { val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY output { HALF } output { HALF } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } } verify() @@ -59,7 +86,7 @@ class TransactionGroupTests { val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY output { HALF } output { HALF } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } } assertNotEquals(conflict1, conflict2) @@ -78,7 +105,7 @@ class TransactionGroupTests { val tg = transactionGroup { transaction { output("cash") { A_THOUSAND_POUNDS } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() } } transaction { @@ -93,7 +120,7 @@ class TransactionGroupTests { tg.txns += TransactionBuilder().apply { addInputState(input) addOutputState(A_THOUSAND_POUNDS) - addCommand(Cash.Commands.Move(), BOB_PUBKEY) + addCommand(TestCash.Commands.Move(), BOB_PUBKEY) }.toWireTransaction() val e = assertFailsWith(TransactionResolutionException::class) { @@ -114,7 +141,7 @@ class TransactionGroupTests { input("£1000") input("£1000") output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } } assertFailsWith(TransactionConflictException::class) { @@ -128,19 +155,19 @@ class TransactionGroupTests { val signedTxns: List = transactionGroup { transaction { output("£1000") { A_THOUSAND_POUNDS } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() } } transaction { input("£1000") output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } } transaction { input("alice's £1000") - arg(ALICE_PUBKEY) { Cash.Commands.Move() } - arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) } + arg(ALICE_PUBKEY) { TestCash.Commands.Move() } + arg(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) } } }.signAll() diff --git a/contracts/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt similarity index 88% rename from contracts/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt rename to core/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt index 4238b17d80..e24e12ef23 100644 --- a/contracts/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt @@ -1,14 +1,8 @@ package core.node -import contracts.DUMMY_PROGRAM_ID -import contracts.DummyContract -import core.contracts.Contract -import core.contracts.ContractState -import core.contracts.PartyAndReference -import core.contracts.TransactionBuilder +import core.contracts.* import core.crypto.Party import core.crypto.SecureHash -import core.node.AttachmentsClassLoader import core.node.services.AttachmentStorage import core.node.services.testing.MockAttachmentStorage import core.serialization.* @@ -30,11 +24,36 @@ interface DummyContractBackdoor { fun inspectState(state: ContractState): Int } +val ATTACHMENT_TEST_PROGRAM_ID = AttachmentClassLoaderTests.AttachmentDummyContract() + class AttachmentClassLoaderTests { companion object { val ISOLATED_CONTRACTS_JAR_PATH = AttachmentClassLoaderTests::class.java.getResource("isolated.jar") } + class AttachmentDummyContract : Contract { + class State(val magicNumber: Int = 0, + override val notary: Party) : ContractState { + override val contract = ATTACHMENT_TEST_PROGRAM_ID + } + + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + } + + override fun verify(tx: TransactionForVerification) { + // Always accepts. + } + + // The "empty contract" + override val legalContractReference: SecureHash = SecureHash.sha256("") + + fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { + val state = State(magicNumber, notary) + return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + } + } + fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) } // These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though @@ -127,7 +146,7 @@ class AttachmentClassLoaderTests { @Test fun `verify that contract DummyContract is in classPath`() { - val contractClass = Class.forName("contracts.DummyContract") + val contractClass = Class.forName("core.node.AttachmentClassLoaderTests\$AttachmentDummyContract") val contract = contractClass.newInstance() as Contract assertNotNull(contract) @@ -190,13 +209,13 @@ class AttachmentClassLoaderTests { @Test fun `test serialization of WireTransaction with statically loaded contract`() { - val tx = DUMMY_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) + val tx = ATTACHMENT_TEST_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) val wireTransaction = tx.toWireTransaction() val bytes = wireTransaction.serialize() val copiedWireTransaction = bytes.deserialize() assertEquals(1, copiedWireTransaction.outputs.size) - assertEquals(42, (copiedWireTransaction.outputs[0] as DummyContract.State).magicNumber) + assertEquals(42, (copiedWireTransaction.outputs[0] as AttachmentDummyContract.State).magicNumber) } @Test diff --git a/core/src/test/kotlin/core/serialization/SerializationTokenTest.kt b/core/src/test/kotlin/core/serialization/SerializationTokenTest.kt index 64431bb830..346364457b 100644 --- a/core/src/test/kotlin/core/serialization/SerializationTokenTest.kt +++ b/core/src/test/kotlin/core/serialization/SerializationTokenTest.kt @@ -74,6 +74,7 @@ class SerializationTokenTest { @Test(expected = IllegalStateException::class) fun `unannotated throws`() { + @Suppress("UNUSED_VARIABLE") val tokenizableBefore = UnannotatedSerializeAsSingletonToken() } } \ No newline at end of file diff --git a/contracts/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/core/serialization/TransactionSerializationTests.kt similarity index 62% rename from contracts/src/test/kotlin/core/serialization/TransactionSerializationTests.kt rename to core/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index d336663026..5c2fd978ae 100644 --- a/contracts/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -1,22 +1,48 @@ package core.serialization -import contracts.Cash -import core.* import core.contracts.* +import core.crypto.Party +import core.crypto.SecureHash import core.node.services.testing.MockStorageService +import core.seconds import core.testing.* import org.junit.Before import org.junit.Test +import java.security.PublicKey +import java.security.SecureRandom import java.security.SignatureException import kotlin.test.assertEquals import kotlin.test.assertFailsWith +val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash() + class TransactionSerializationTests { - // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). + class TestCash : Contract { + override val legalContractReference = SecureHash.sha256("TestCash") + + override fun verify(tx: TransactionForVerification) { + } + + data class State( + val deposit: PartyAndReference, + val amount: Amount, + override val owner: PublicKey, + override val notary: Party) : OwnableState { + override val contract: Contract = TEST_PROGRAM_ID + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + } + interface Commands : CommandData { + class Move() : TypeOnlyCommandData(), Commands + data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands + data class Exit(val amount: Amount) : Commands + } + } + + // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). // It refers to a fake TX/state that we don't bother creating here. val depositRef = MINI_CORP.ref(1) - val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY) - val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY) + val outputState = TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY) + val changeState = TestCash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY) val fakeStateRef = generateStateRef() lateinit var tx: TransactionBuilder @@ -24,7 +50,7 @@ class TransactionSerializationTests { @Before fun setup() { tx = TransactionBuilder().withItems( - fakeStateRef, outputState, changeState, Command(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) + fakeStateRef, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) ) } @@ -61,7 +87,7 @@ class TransactionSerializationTests { // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { val tx2 = TransactionBuilder().withItems(fakeStateRef, outputState, changeState, - Command(Cash.Commands.Move(), TestUtils.keypair2.public)) + Command(TestCash.Commands.Move(), TestUtils.keypair2.public)) tx2.signWith(TestUtils.keypair2) signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()