diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/NonDeterministicContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/NonDeterministicContract.kt deleted file mode 100644 index e4fc1b90f1..0000000000 --- a/node/src/integration-test/kotlin/net/corda/contracts/djvm/NonDeterministicContract.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.corda.contracts.djvm - -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.TypeOnlyCommandData -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction -import java.time.Instant - -class NonDeterministicContract : Contract { - override fun verify(tx: LedgerTransaction) { - Instant.now().toString() - } - - class State : ContractState { - override val participants: List get() = emptyList() - } - - object Cmd : TypeOnlyCommandData() -} diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt new file mode 100644 index 0000000000..a74928b174 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt @@ -0,0 +1,50 @@ +package net.corda.contracts.djvm.broken + +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.LedgerTransaction +import java.time.Instant +import java.util.* + +class NonDeterministicContract : Contract { + override fun verify(tx: LedgerTransaction) { + when { + tx.commandsOfType(InstantNow::class.java).isNotEmpty() -> verifyInstantNow() + tx.commandsOfType(CurrentTimeMillis::class.java).isNotEmpty() -> verifyCurrentTimeMillis() + tx.commandsOfType(NanoTime::class.java).isNotEmpty() -> verifyNanoTime() + tx.commandsOfType(RandomUUID::class.java).isNotEmpty() -> UUID.randomUUID() + tx.commandsOfType(WithReflection::class.java).isNotEmpty() -> verifyNoReflection() + else -> {} + } + } + + private fun verifyInstantNow() { + Instant.now() + } + + private fun verifyCurrentTimeMillis() { + System.currentTimeMillis() + } + + private fun verifyNanoTime() { + System.nanoTime() + } + + private fun verifyNoReflection() { + Date::class.java.getDeclaredConstructor().newInstance() + } + + @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") + class State(val issuer: AbstractParty) : ContractState { + override val participants: List = listOf(issuer) + } + + class InstantNow : TypeOnlyCommandData() + class CurrentTimeMillis : TypeOnlyCommandData() + class NanoTime : TypeOnlyCommandData() + class RandomUUID : TypeOnlyCommandData() + class WithReflection : TypeOnlyCommandData() + class NoOperation : TypeOnlyCommandData() +} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/NonDeterministicFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/NonDeterministicFlow.kt deleted file mode 100644 index 8e07dad85b..0000000000 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/NonDeterministicFlow.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.corda.flows.djvm - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.djvm.NonDeterministicContract -import net.corda.core.contracts.Command -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.Party -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.unwrap - -@InitiatingFlow -@StartableByRPC -class NonDeterministicFlow(private val otherSide: Party) : FlowLogic() { - @Suspendable - override fun call() { - val notary = serviceHub.networkMapCache.notaryIdentities[0] - val stx = serviceHub.signInitialTransaction( - TransactionBuilder(notary) - .addOutputState(NonDeterministicContract.State()) - .addCommand(Command(NonDeterministicContract.Cmd, ourIdentity.owningKey)) - ) - stx.verify(serviceHub, checkSufficientSignatures = false) - val session = initiateFlow(otherSide) - subFlow(FinalityFlow(stx, session)) - // It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws - session.receive().unwrap { require(it == "OK") { "Not OK: $it"} } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt new file mode 100644 index 0000000000..ca9ddad1ef --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt @@ -0,0 +1,27 @@ +package net.corda.flows.djvm.broken + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.djvm.broken.NonDeterministicContract +import net.corda.core.contracts.Command +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.TransactionBuilder + +@InitiatingFlow +@StartableByRPC +class NonDeterministicFlow(private val trouble: TypeOnlyCommandData) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + val notary = serviceHub.networkMapCache.notaryIdentities[0] + val stx = serviceHub.signInitialTransaction( + TransactionBuilder(notary) + .addOutputState(NonDeterministicContract.State(ourIdentity)) + .addCommand(Command(trouble, ourIdentity.owningKey)) + ) + stx.verify(serviceHub, checkSufficientSignatures = false) + return stx.id + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractVerifyTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractVerifyTest.kt deleted file mode 100644 index 9a9075b993..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractVerifyTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.corda.node.services - -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.flows.djvm.NonDeterministicFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.singleIdentity -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.internal.cordappsForPackages -import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat -import org.junit.ClassRule -import org.junit.Test -import org.junit.jupiter.api.assertThrows - -class DeterministicContractVerifyTest { - companion object { - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - } - - @Test - fun `test DJVM rejects non-deterministic contract`() { - driver(DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = false, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), - cordappsForAllNodes = cordappsForPackages( - "net.corda.contracts.djvm", - "net.corda.flows.djvm" - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - )) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::NonDeterministicFlow, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageStartingWith("NoSuchMethodError: ") - .hasMessageContaining(" sandbox.java.time.Instant.now()") - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt new file mode 100644 index 0000000000..825d7eddac --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt @@ -0,0 +1,122 @@ +package net.corda.node.services + +import net.corda.contracts.djvm.broken.NonDeterministicContract.* +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.flows.djvm.broken.NonDeterministicFlow +import net.corda.node.DeterministicSourcesRule +import net.corda.node.internal.djvm.DeterministicVerificationException +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.internal.cordappsForPackages +import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat +import org.junit.ClassRule +import org.junit.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class NonDeterministicContractVerifyTest { + companion object { + val logger = loggerFor() + + @ClassRule + @JvmField + val djvmSources = DeterministicSourcesRule() + + fun parametersFor(djvmSources: DeterministicSourcesRule): DriverParameters { + return DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess =false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = cordappsForPackages( + "net.corda.contracts.djvm.broken", + "net.corda.flows.djvm.broken" + ), + djvmBootstrapSource = djvmSources.bootstrap, + djvmCordaSource = djvmSources.corda + ) + } + } + + @Test + fun `test DJVM rejects contract that uses Instant now`() { + driver(parametersFor(djvmSources)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val ex = assertThrows { + alice.rpc.startFlow(::NonDeterministicFlow, InstantNow()) + .returnValue.getOrThrow() + } + assertThat(ex) + .hasMessageStartingWith("NoSuchMethodError: sandbox.java.time.Instant.now()Lsandbox/java/time/Instant;, ") + } + } + + @Test + fun `test DJVM rejects contract that uses System currentTimeMillis`() { + driver(parametersFor(djvmSources)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val ex = assertThrows { + alice.rpc.startFlow(::NonDeterministicFlow, CurrentTimeMillis()) + .returnValue.getOrThrow() + } + assertThat(ex) + .hasMessageStartingWith("NoSuchMethodError: sandbox.java.lang.System.currentTimeMillis()J, ") + } + } + + @Test + fun `test DJVM rejects contract that uses System nanoTime`() { + driver(parametersFor(djvmSources)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val ex = assertThrows { + alice.rpc.startFlow(::NonDeterministicFlow, NanoTime()) + .returnValue.getOrThrow() + } + assertThat(ex) + .hasMessageStartingWith("NoSuchMethodError: sandbox.java.lang.System.nanoTime()J, ") + } + } + + @Test + fun `test DJVM rejects contract that uses UUID randomUUID`() { + driver(parametersFor(djvmSources)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val ex = assertThrows { + alice.rpc.startFlow(::NonDeterministicFlow, RandomUUID()) + .returnValue.getOrThrow() + } + assertThat(ex) + .hasMessageStartingWith("NoSuchMethodError: sandbox.java.util.UUID.randomUUID()Lsandbox/java/util/UUID;, ") + } + } + + @Test + fun `test DJVM rejects contract that uses reflection`() { + driver(parametersFor(djvmSources)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val ex = assertThrows { + alice.rpc.startFlow(::NonDeterministicFlow, WithReflection()) + .returnValue.getOrThrow() + } + assertThat(ex) + .hasMessageStartingWith("RuleViolationError: Disallowed reference to API; java.lang.Class.getDeclaredConstructor(Class[]), ") + } + } + + @Test + fun `test DJVM can succeed`() { + driver(parametersFor(djvmSources)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val txId = assertDoesNotThrow { + alice.rpc.startFlow(::NonDeterministicFlow, NoOperation()) + .returnValue.getOrThrow() + } + logger.info("TX-ID: {}", txId) + } + } +}