Add test cases for more non-determinism in Contract.verify().

This commit is contained in:
Chris Rankin 2019-10-14 12:20:57 +01:00
parent 739ffda6c7
commit 01e235f9d3
6 changed files with 199 additions and 101 deletions

View File

@ -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<AbstractParty> get() = emptyList()
}
object Cmd : TypeOnlyCommandData()
}

View File

@ -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<AbstractParty> = listOf(issuer)
}
class InstantNow : TypeOnlyCommandData()
class CurrentTimeMillis : TypeOnlyCommandData()
class NanoTime : TypeOnlyCommandData()
class RandomUUID : TypeOnlyCommandData()
class WithReflection : TypeOnlyCommandData()
class NoOperation : TypeOnlyCommandData()
}

View File

@ -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<Unit>() {
@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<String>().unwrap { require(it == "OK") { "Not OK: $it"} }
}
}

View File

@ -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<SecureHash>() {
@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
}
}

View File

@ -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<DeterministicVerificationException> {
alice.rpc.startFlow(::NonDeterministicFlow, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}
assertThat(ex)
.hasMessageStartingWith("NoSuchMethodError: ")
.hasMessageContaining(" sandbox.java.time.Instant.now()")
}
}
}

View File

@ -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<NonDeterministicContractVerifyTest>()
@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<DeterministicVerificationException> {
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<DeterministicVerificationException> {
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<DeterministicVerificationException> {
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<DeterministicVerificationException> {
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<DeterministicVerificationException> {
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)
}
}
}