This commit is contained in:
szymonsztuka
2018-08-01 14:13:39 +01:00
66 changed files with 1216 additions and 398 deletions

View File

@ -10,55 +10,60 @@
package net.corda.confidential package net.corda.confidential
import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.identity.* import net.corda.core.identity.*
import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.internal.matchers.allOf
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.* import kotlin.test.*
import com.natpryce.hamkrest.assertion.assert
import net.corda.core.crypto.DigitalSignature
import net.corda.testing.internal.matchers.hasOnlyEntries
import net.corda.testing.node.internal.TestStartedNode
import org.junit.AfterClass
import java.security.PublicKey
class SwapIdentitiesFlowTests { class SwapIdentitiesFlowTests {
private lateinit var mockNet: InternalMockNetwork companion object {
private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
@Before @AfterClass
fun setup() { @JvmStatic
// We run this in parallel threads to help catch any race conditions that may exist. fun tearDown() = mockNet.stopNodes()
mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
} }
private val aliceNode = mockNet.createPartyNode(makeUnique(ALICE_NAME))
private val bobNode = mockNet.createPartyNode(makeUnique(BOB_NAME))
private val charlieNode = mockNet.createPartyNode(makeUnique(CHARLIE_NAME))
private val alice = aliceNode.info.singleIdentity()
private val bob = bobNode.info.singleIdentity()
@Test @Test
fun `issue key`() { fun `issue key`() {
// Set up values we'll need assert.that(
val aliceNode = mockNet.createPartyNode(ALICE_NAME) aliceNode.services.startFlow(SwapIdentitiesFlow(bob)),
val bobNode = mockNet.createPartyNode(BOB_NAME) willReturn(
val alice = aliceNode.info.singleIdentity() hasOnlyEntries(
val bob = bobNode.services.myInfo.singleIdentity() alice to allOf(
!equalTo<AbstractParty>(alice),
// Run the flows aliceNode.resolvesToWellKnownParty(alice),
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)).resultFuture aliceNode.holdsOwningKey(),
!bobNode.holdsOwningKey()
// Get the results ),
val actual: Map<Party, AnonymousParty> = requesterFlow.getOrThrow().toMap() bob to allOf(
assertEquals(2, actual.size) !equalTo<AbstractParty>(bob),
// Verify that the generated anonymous identities do not match the well known identities bobNode.resolvesToWellKnownParty(bob),
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() bobNode.holdsOwningKey(),
val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() !aliceNode.holdsOwningKey()
assertNotEquals<AbstractParty>(alice, aliceAnonymousIdentity) )
assertNotEquals<AbstractParty>(bob, bobAnonymousIdentity) )
)
// Verify that the anonymous identities look sane )
assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(aliceAnonymousIdentity)!!.name })
assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(bobAnonymousIdentity)!!.name })
// Verify that the nodes have the right anonymous identities
assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
assertTrue { bobAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys }
assertFalse { aliceAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys }
assertFalse { bobAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
mockNet.stopNodes()
} }
/** /**
@ -66,58 +71,101 @@ class SwapIdentitiesFlowTests {
*/ */
@Test @Test
fun `verifies identity name`() { fun `verifies identity name`() {
// Set up values we'll need val notBob = charlieNode.issueFreshKeyAndCert()
val aliceNode = mockNet.createPartyNode(ALICE_NAME) val signature = charlieNode.signSwapIdentitiesFlowData(notBob, notBob.owningKey)
val bobNode = mockNet.createPartyNode(BOB_NAME) assertFailsWith<SwapIdentitiesException>(
val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) "Certificate subject must match counterparty's well known identity.") {
val bob: Party = bobNode.services.myInfo.singleIdentity() aliceNode.validateSwapIdentitiesFlow(bob, notBob, signature)
val notBob = charlieNode.database.transaction {
charlieNode.services.keyManagementService.freshKeyAndCert(charlieNode.services.myInfo.singleIdentityAndCert(), false)
} }
val sigData = SwapIdentitiesFlow.buildDataToSign(notBob)
val signature = charlieNode.services.keyManagementService.sign(sigData, notBob.owningKey)
assertFailsWith<SwapIdentitiesException>("Certificate subject must match counterparty's well known identity.") {
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, notBob, signature.withoutKey())
}
mockNet.stopNodes()
} }
/** /**
* Check that flow is actually validating its the signature presented by the counterparty. * Check that flow is actually validating its the signature presented by the counterparty.
*/ */
@Test @Test
fun `verifies signature`() { fun `verification rejects signature if name is right but key is wrong`() {
// Set up values we'll need val evilBobNode = mockNet.createPartyNode(bobNode.info.singleIdentity().name)
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val alice: PartyAndCertificate = aliceNode.info.singleIdentityAndCert()
val bob: PartyAndCertificate = bobNode.info.singleIdentityAndCert()
// Check that the right name but wrong key is rejected
val evilBobNode = mockNet.createPartyNode(BOB_NAME)
val evilBob = evilBobNode.info.singleIdentityAndCert() val evilBob = evilBobNode.info.singleIdentityAndCert()
evilBobNode.database.transaction { val anonymousEvilBob = evilBobNode.issueFreshKeyAndCert()
val anonymousEvilBob = evilBobNode.services.keyManagementService.freshKeyAndCert(evilBob, false) val signature = evilBobNode.signSwapIdentitiesFlowData(evilBob, anonymousEvilBob.owningKey)
val sigData = SwapIdentitiesFlow.buildDataToSign(evilBob)
val signature = evilBobNode.services.keyManagementService.sign(sigData, anonymousEvilBob.owningKey)
assertFailsWith<SwapIdentitiesException>("Signature does not match the given identity and nonce") {
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob.party, anonymousEvilBob, signature.withoutKey())
}
}
// Check that the right signing key, but wrong identity is rejected
val anonymousAlice: PartyAndCertificate = aliceNode.database.transaction {
aliceNode.services.keyManagementService.freshKeyAndCert(alice, false)
}
bobNode.database.transaction {
bobNode.services.keyManagementService.freshKeyAndCert(bob, false)
}.let { anonymousBob ->
val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousAlice)
val signature = bobNode.services.keyManagementService.sign(sigData, anonymousBob.owningKey)
assertFailsWith<SwapIdentitiesException>("Signature does not match the given identity and nonce.") {
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob.party, anonymousBob, signature.withoutKey())
}
}
mockNet.stopNodes() assertFailsWith<SwapIdentitiesException>(
"Signature does not match the given identity and nonce") {
aliceNode.validateSwapIdentitiesFlow(bob, anonymousEvilBob, signature)
}
} }
@Test
fun `verification rejects signature if key is right but name is wrong`() {
val anonymousAlice = aliceNode.issueFreshKeyAndCert()
val anonymousBob = bobNode.issueFreshKeyAndCert()
val signature = bobNode.signSwapIdentitiesFlowData(anonymousAlice, anonymousBob.owningKey)
assertFailsWith<SwapIdentitiesException>(
"Signature does not match the given identity and nonce.") {
aliceNode.validateSwapIdentitiesFlow(bob, anonymousBob, signature)
}
}
//region Operations
private fun TestStartedNode.issueFreshKeyAndCert() = database.transaction {
services.keyManagementService.freshKeyAndCert(services.myInfo.singleIdentityAndCert(), false)
}
private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey) =
services.keyManagementService.sign(
SwapIdentitiesFlow.buildDataToSign(party),
owningKey)
private fun TestStartedNode.validateSwapIdentitiesFlow(
party: Party,
counterparty: PartyAndCertificate,
signature: DigitalSignature.WithKey) =
SwapIdentitiesFlow.validateAndRegisterIdentity(
services.identityService,
party,
counterparty,
signature.withoutKey()
)
//endregion
//region Matchers
private fun TestStartedNode.resolvesToWellKnownParty(party: Party) = object : Matcher<AnonymousParty> {
override val description = """
is resolved by "${this@resolvesToWellKnownParty.info.singleIdentity().name}" to well-known party "${party.name}"
""".trimIndent()
override fun invoke(actual: AnonymousParty): MatchResult {
val resolvedName = services.identityService.wellKnownPartyFromAnonymous(actual)!!.name
return if (resolvedName == party.name) {
MatchResult.Match
} else {
MatchResult.Mismatch("was resolved to $resolvedName")
}
}
}
private data class HoldsOwningKeyMatcher(val node: TestStartedNode, val negated: Boolean = false) : Matcher<AnonymousParty> {
private fun sayNotIf(negation: Boolean) = if (negation) { "not " } else { "" }
override val description =
"has an owning key which is ${sayNotIf(negated)}held by ${node.info.singleIdentity().name}"
override fun invoke(actual: AnonymousParty) =
if (negated != actual.owningKey in node.services.keyManagementService.keys) {
MatchResult.Match
} else {
MatchResult.Mismatch("""
had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name}
""".trimIndent())
}
override fun not(): Matcher<AnonymousParty> {
return copy(negated=!negated)
}
}
private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this)
//endregion
} }

View File

@ -3,7 +3,7 @@ package net.corda.deterministic.common
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.internal.TEST_UPLOADER import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
@ -18,8 +18,9 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransa
fun toLedgerTransaction(): LedgerTransaction { fun toLedgerTransaction(): LedgerTransaction {
val deps = dependencies.map { it.deserialize() }.associateBy(WireTransaction::id) val deps = dependencies.map { it.deserialize() }.associateBy(WireTransaction::id)
val attachments = attachments.map { it.deserialize<Attachment>() } val attachments = attachments.map { it.deserialize<Attachment>() }
val attachmentMap = attachments.mapNotNull { it as? MockContractAttachment } val attachmentMap = attachments
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader=TEST_UPLOADER) } .mapNotNull { it as? MockContractAttachment }
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) }
val contractAttachmentMap = emptyMap<ContractClassName, ContractAttachment>() val contractAttachmentMap = emptyMap<ContractClassName, ContractAttachment>()
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
return wtxToVerify.deserialize().toLedgerTransaction( return wtxToVerify.deserialize().toLedgerTransaction(

View File

@ -26,15 +26,14 @@ import java.security.CodeSigner
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
// Possible attachment uploaders
const val DEPLOYED_CORDAPP_UPLOADER = "app" const val DEPLOYED_CORDAPP_UPLOADER = "app"
const val RPC_UPLOADER = "rpc" const val RPC_UPLOADER = "rpc"
const val TEST_UPLOADER = "test"
const val P2P_UPLOADER = "p2p" const val P2P_UPLOADER = "p2p"
const val UNKNOWN_UPLOADER = "unknown" const val UNKNOWN_UPLOADER = "unknown"
fun isUploaderTrusted(uploader: String?) = private val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)
uploader?.let { it in listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, TEST_UPLOADER) } ?: false
fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS
@KeepForDJVM @KeepForDJVM
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {

View File

@ -0,0 +1,57 @@
package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappConfig
import net.corda.core.cordapp.CordappContext
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.serialization.SerializationContext
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import org.slf4j.MDC
// *Internal* Corda-specific utilities
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
val currentMinPlatformVersion = networkParameters.minimumPlatformVersion
if (currentMinPlatformVersion < requiredMinPlatformVersion) {
throw ZoneVersionTooLowException(
"$feature requires all nodes on the Corda compatibility zone to be running at least platform version " +
"$requiredMinPlatformVersion. The current zone is only enforcing a minimum platform version of " +
"$currentMinPlatformVersion. Please contact your zone operator."
)
}
}
/** Provide access to internal method for AttachmentClassLoaderTests */
@DeleteForDJVM
fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
return toWireTransactionWithContext(services, serializationContext)
}
/** Provide access to internal method for AttachmentClassLoaderTests */
@DeleteForDJVM
fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction {
return toLedgerTransactionWithContext(services, serializationContext)
}
fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext {
return CordappContext(cordapp, attachmentId, classLoader, config)
}
/** Checks if this flow is an idempotent flow. */
fun Class<out FlowLogic<*>>.isIdempotentFlow(): Boolean {
return IdempotentFlow::class.java.isAssignableFrom(this)
}
/**
* Ensures each log entry from the current thread will contain id of the transaction in the MDC.
*/
internal fun SignedTransaction.pushToLoggingContext() {
MDC.put("tx_id", id.toString())
}

View File

@ -399,18 +399,6 @@ fun <T, U : T> uncheckedCast(obj: T) = obj as U
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second } fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
/** Provide access to internal method for AttachmentClassLoaderTests */
@DeleteForDJVM
fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
return toWireTransactionWithContext(services, serializationContext)
}
/** Provide access to internal method for AttachmentClassLoaderTests */
@DeleteForDJVM
fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction {
return toLedgerTransactionWithContext(services, serializationContext)
}
/** Returns the location of this class. */ /** Returns the location of this class. */
val Class<*>.location: URL get() = protectionDomain.codeSource.location val Class<*>.location: URL get() = protectionDomain.codeSource.location
@ -510,29 +498,13 @@ fun <T : Any> SerializedBytes<T>.sign(keyPair: KeyPair): SignedData<T> = SignedD
fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) }
fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext {
return CordappContext(cordapp, attachmentId, classLoader, config)
}
val PublicKey.hash: SecureHash get() = encoded.sha256() val PublicKey.hash: SecureHash get() = encoded.sha256()
/** Checks if this flow is an idempotent flow. */
fun Class<out FlowLogic<*>>.isIdempotentFlow(): Boolean {
return IdempotentFlow::class.java.isAssignableFrom(this)
}
/** /**
* Extension method for providing a sumBy method that processes and returns a Long * Extension method for providing a sumBy method that processes and returns a Long
*/ */
fun <T> Iterable<T>.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum() fun <T> Iterable<T>.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum()
/**
* Ensures each log entry from the current thread will contain id of the transaction in the MDC.
*/
internal fun SignedTransaction.pushToLoggingContext() {
MDC.put("tx_id", id.toString())
}
fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): UntrustworthyData<T> { fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): UntrustworthyData<T> {
val payloadData: T = try { val payloadData: T = try {
val serializer = SerializationDefaults.SERIALIZATION_FACTORY val serializer = SerializationDefaults.SERIALIZATION_FACTORY

View File

@ -10,6 +10,7 @@
package net.corda.core.node package net.corda.core.node
import net.corda.core.CordaRuntimeException
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
@ -115,4 +116,10 @@ data class NetworkParameters(
*/ */
@KeepForDJVM @KeepForDJVM
@CordaSerializable @CordaSerializable
data class NotaryInfo(val identity: Party, val validating: Boolean) data class NotaryInfo(val identity: Party, val validating: Boolean)
/**
* When a Corda feature cannot be used due to the node's compatibility zone not enforcing a high enough minimum platform
* version.
*/
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)

View File

@ -11,6 +11,7 @@
package net.corda.core.transactions package net.corda.core.transactions
import co.paralleluniverse.strands.Strand import co.paralleluniverse.strands.Strand
import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider import net.corda.core.cordapp.CordappProvider
@ -19,9 +20,11 @@ import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.ensureMinimumPlatformVersion
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
@ -84,7 +87,7 @@ open class TransactionBuilder @JvmOverloads constructor(
for (t in items) { for (t in items) {
when (t) { when (t) {
is StateAndRef<*> -> addInputState(t) is StateAndRef<*> -> addInputState(t)
is ReferencedStateAndRef<*> -> @Suppress("DEPRECATION") addReferenceState(t) // Will remove when feature finalised. is ReferencedStateAndRef<*> -> addReferenceState(t)
is SecureHash -> addAttachment(t) is SecureHash -> addAttachment(t)
is TransactionState<*> -> addOutputState(t) is TransactionState<*> -> addOutputState(t)
is StateAndContract -> addOutputState(t.state, t.contract) is StateAndContract -> addOutputState(t.state, t.contract)
@ -105,11 +108,18 @@ open class TransactionBuilder @JvmOverloads constructor(
* [HashAttachmentConstraint]. * [HashAttachmentConstraint].
* *
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder]. * @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
*
* @throws ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4.
*/ */
@Throws(MissingContractAttachments::class) @Throws(MissingContractAttachments::class)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services) fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services)
@CordaInternal
internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction { internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction {
val referenceStates = referenceStates()
if (referenceStates.isNotEmpty()) {
services.ensureMinimumPlatformVersion(4, "Reference states")
}
// Resolves the AutomaticHashConstraints to HashAttachmentConstraints or WhitelistedByZoneAttachmentConstraint based on a global parameter. // Resolves the AutomaticHashConstraints to HashAttachmentConstraints or WhitelistedByZoneAttachmentConstraint based on a global parameter.
// The AutomaticHashConstraint allows for less boiler plate when constructing transactions since for the typical case the named contract // The AutomaticHashConstraint allows for less boiler plate when constructing transactions since for the typical case the named contract
@ -119,14 +129,27 @@ open class TransactionBuilder @JvmOverloads constructor(
when { when {
state.constraint !== AutomaticHashConstraint -> state state.constraint !== AutomaticHashConstraint -> state
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint) useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint)
else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let { else -> {
state.copy(constraint = HashAttachmentConstraint(it)) services.cordappProvider.getContractAttachmentID(state.contract)?.let {
} ?: throw MissingContractAttachments(listOf(state)) state.copy(constraint = HashAttachmentConstraint(it))
} ?: throw MissingContractAttachments(listOf(state))
}
} }
} }
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window, referenceStates()), privacySalt) WireTransaction(
WireTransaction.createComponentGroups(
inputStates(),
resolvedOutputs,
commands,
attachments + makeContractAttachments(services.cordappProvider),
notary,
window,
referenceStates
),
privacySalt
)
} }
} }
@ -179,12 +202,9 @@ open class TransactionBuilder @JvmOverloads constructor(
/** /**
* Adds a reference input [StateRef] to the transaction. * Adds a reference input [StateRef] to the transaction.
* *
* This feature was added in version 4 of Corda, so will throw an exception for any Corda networks with a minimum * Note: Reference states are only supported on Corda networks running a minimum platform version of 4.
* platform version less than 4. * [toWireTransaction] will throw an [IllegalStateException] if called in such an environment.
*
* @throws UncheckedVersionException
*/ */
@Deprecated(message = "Feature not yet released. Pending stabilisation.")
open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder {
val stateAndRef = referencedStateAndRef.stateAndRef val stateAndRef = referencedStateAndRef.stateAndRef
referencesWithTransactionState.add(stateAndRef.state) referencesWithTransactionState.add(stateAndRef.state)
@ -293,10 +313,10 @@ open class TransactionBuilder @JvmOverloads constructor(
return this return this
} }
/** Returns an immutable list of input [StateRefs]. */ /** Returns an immutable list of input [StateRef]s. */
fun inputStates(): List<StateRef> = ArrayList(inputs) fun inputStates(): List<StateRef> = ArrayList(inputs)
/** Returns an immutable list of reference input [StateRefs]. */ /** Returns an immutable list of reference input [StateRef]s. */
fun referenceStates(): List<StateRef> = ArrayList(references) fun referenceStates(): List<StateRef> = ArrayList(references)
/** Returns an immutable list of attachment hashes. */ /** Returns an immutable list of attachment hashes. */
@ -312,7 +332,10 @@ open class TransactionBuilder @JvmOverloads constructor(
* Sign the built transaction and return it. This is an internal function for use by the service hub, please use * Sign the built transaction and return it. This is an internal function for use by the service hub, please use
* [ServiceHub.signInitialTransaction] instead. * [ServiceHub.signInitialTransaction] instead.
*/ */
fun toSignedTransaction(keyManagementService: KeyManagementService, publicKey: PublicKey, signatureMetadata: SignatureMetadata, services: ServicesForResolution): SignedTransaction { fun toSignedTransaction(keyManagementService: KeyManagementService,
publicKey: PublicKey,
signatureMetadata: SignatureMetadata,
services: ServicesForResolution): SignedTransaction {
val wtx = toWireTransaction(services) val wtx = toWireTransaction(services)
val signableData = SignableData(wtx.id, signatureMetadata) val signableData = SignableData(wtx.id, signatureMetadata)
val sig = keyManagementService.sign(signableData, publicKey) val sig = keyManagementService.sign(signableData, publicKey)

View File

@ -15,8 +15,8 @@ import com.natpryce.hamkrest.*
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithMockNet import net.corda.core.flows.mixins.WithMockNet
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -26,6 +26,7 @@ import net.corda.core.internal.hash
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.makeUnique
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.InternalMockNodeParameters
@ -130,13 +131,13 @@ class AttachmentTests : WithMockNet {
//region Generators //region Generators
override fun makeNode(name: CordaX500Name) = override fun makeNode(name: CordaX500Name) =
mockNet.createPartyNode(randomise(name)).apply { mockNet.createPartyNode(makeUnique(name)).apply {
registerInitiatedFlow(FetchAttachmentsResponse::class.java) registerInitiatedFlow(FetchAttachmentsResponse::class.java)
} }
// Makes a node that doesn't do sanity checking at load time. // Makes a node that doesn't do sanity checking at load time.
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode( private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
InternalMockNodeParameters(legalName = randomise(name)), InternalMockNodeParameters(legalName = makeUnique(name)),
nodeFactory = { args, _ -> nodeFactory = { args, _ ->
object : InternalMockNetwork.MockNode(args) { object : InternalMockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }

View File

@ -15,8 +15,8 @@ import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.contracts.StateAndContract import net.corda.core.contracts.StateAndContract
import net.corda.core.contracts.requireThat import net.corda.core.contracts.requireThat
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithContracts
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party

View File

@ -8,8 +8,8 @@ import com.natpryce.hamkrest.isA
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.matchers.rpc.willReturn import net.corda.testing.internal.matchers.rpc.willReturn
import net.corda.core.flows.matchers.rpc.willThrow import net.corda.testing.internal.matchers.rpc.willThrow
import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality import net.corda.core.flows.mixins.WithFinality
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps

View File

@ -13,8 +13,8 @@ package net.corda.core.flows
import com.natpryce.hamkrest.* import com.natpryce.hamkrest.*
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
@ -90,9 +90,9 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
// Party A initiates contract upgrade flow, expected to succeed this time. // Party A initiates contract upgrade flow, expected to succeed this time.
assert.that( assert.that(
aliceNode.initiateDummyContractUpgrade(atx), aliceNode.initiateDummyContractUpgrade(atx),
willReturn( willReturn(
aliceNode.hasDummyContractUpgradeTransaction() aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction())) and bobNode.hasDummyContractUpgradeTransaction()))
} }
private fun TestStartedNode.issueCash(amount: Amount<Currency> = Amount(1000, USD)) = private fun TestStartedNode.issueCash(amount: Amount<Currency> = Amount(1000, USD)) =

View File

@ -12,8 +12,8 @@ package net.corda.core.flows
import com.natpryce.hamkrest.and import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.assertion.assert
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithFinality import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction

View File

@ -12,7 +12,7 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.assertion.assert
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.mixins.WithMockNet import net.corda.core.flows.mixins.WithMockNet
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData

View File

@ -1,36 +0,0 @@
package net.corda.core.flows.matchers.flow
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.has
import net.corda.core.flows.matchers.willReturn
import net.corda.core.flows.matchers.willThrow
import net.corda.core.internal.FlowStateMachine
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn() = has(FlowStateMachine<T>::resultFuture, willReturn())
fun <T> willReturn(expected: T): Matcher<FlowStateMachine<out T?>> = net.corda.core.flows.matchers.flow.willReturn(equalTo(expected))
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = has(
FlowStateMachine<out T>::resultFuture,
willReturn(successMatcher))
/**
* Matches a Flow that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = has(
FlowStateMachine<*>::resultFuture,
willThrow(failureMatcher))
/**
* Matches a Flow that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() = has(
FlowStateMachine<*>::resultFuture,
willThrow<E>())

View File

@ -1,31 +0,0 @@
package net.corda.core.flows.matchers.rpc
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.has
import net.corda.core.flows.matchers.willReturn
import net.corda.core.flows.matchers.willThrow
import net.corda.core.messaging.FlowHandle
/**
* Matches a flow handle that succeeds with a result matched by the given matcher
*/
fun <T> willReturn() = has(FlowHandle<T>::returnValue, willReturn())
/**
* Matches a flow handle that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = has(FlowHandle<out T>::returnValue, willReturn(successMatcher))
/**
* Matches a flow handle that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = has(
FlowHandle<*>::returnValue,
willThrow(failureMatcher))
/**
* Matches a flow handle that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() = has(
FlowHandle<*>::returnValue,
willThrow<E>())

View File

@ -9,10 +9,10 @@ import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.testing.core.makeUnique
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -25,12 +25,7 @@ interface WithMockNet {
/** /**
* Create a node using a randomised version of the given name * Create a node using a randomised version of the given name
*/ */
fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(randomise(name)) fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(makeUnique(name))
/**
* Randomise a party name to avoid clashes with other tests
*/
fun randomise(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
/** /**
* Run the mock network before proceeding * Run the mock network before proceeding

View File

@ -0,0 +1,85 @@
package net.corda.core.transactions
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.DummyCommandData
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class TransactionBuilderTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val notary = TestIdentity(DUMMY_NOTARY_NAME).party
private val services = rigorousMock<ServicesForResolution>()
private val contractAttachmentId = SecureHash.randomSHA256()
@Before
fun setup() {
val cordappProvider = rigorousMock<CordappProvider>()
doReturn(cordappProvider).whenever(services).cordappProvider
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters
}
@Test
fun `bare minimum issuance tx`() {
val outputState = TransactionState(
data = DummyState(),
contract = DummyContract.PROGRAM_ID,
notary = notary,
constraint = HashAttachmentConstraint(contractAttachmentId)
)
val builder = TransactionBuilder()
.addOutputState(outputState)
.addCommand(DummyCommandData, notary.owningKey)
val wtx = builder.toWireTransaction(services)
assertThat(wtx.outputs).containsOnly(outputState)
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
}
@Test
fun `automatic hash constraint`() {
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
val builder = TransactionBuilder()
.addOutputState(outputState)
.addCommand(DummyCommandData, notary.owningKey)
val wtx = builder.toWireTransaction(services)
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = HashAttachmentConstraint(contractAttachmentId)))
}
@Test
fun `reference states`() {
val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1)
val builder = TransactionBuilder(notary)
.addReferenceState(StateAndRef(referenceState, referenceStateRef).referenced())
.addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary))
.addCommand(DummyCommandData, notary.owningKey)
doReturn(testNetworkParameters(minimumPlatformVersion = 3)).whenever(services).networkParameters
assertThatThrownBy { builder.toWireTransaction(services) }
.isInstanceOf(ZoneVersionTooLowException::class.java)
.hasMessageContaining("Reference states")
doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters
val wtx = builder.toWireTransaction(services)
assertThat(wtx.references).containsOnly(referenceStateRef)
}
}

View File

@ -6,8 +6,13 @@ release, see :doc:`upgrade-notes`.
Unreleased Unreleased
---------- ----------
* Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714)
* Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour. * Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour.
* "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used
internally in security sensitive code.
* Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``. * Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``.
* Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database. * Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database.
@ -195,7 +200,8 @@ Unreleased
to in a transaction by the contracts of input and output states but whose contract is not executed as part of the to in a transaction by the contracts of input and output states but whose contract is not executed as part of the
transaction verification process and is not consumed when the transaction is committed to the ledger but is checked transaction verification process and is not consumed when the transaction is committed to the ledger but is checked
for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a
normal state when it occurs in an input or output position. normal state when it occurs in an input or output position. *This feature is only available on Corda networks running
with a minimum platform version of 4.*
.. _changelog_v3.1: .. _changelog_v3.1:

View File

@ -105,7 +105,7 @@ absolute path to the node's base directory.
:h2Port: Deprecated. Use ``h2Settings`` instead. :h2Port: Deprecated. Use ``h2Settings`` instead.
:h2Settings: Sets the H2 JDBC server port. See :doc:`node-database-access-h2`. :h2Settings: Sets the H2 JDBC server host and port. See :doc:`node-database-access-h2`. For non-localhost address the database passowrd needs to be set in ``dataSourceProperties``.
:messagingServerAddress: The address of the ArtemisMQ broker instance. If not provided the node will run one locally. :messagingServerAddress: The address of the ArtemisMQ broker instance. If not provided the node will run one locally.

View File

@ -99,6 +99,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
adminAddress "localhost:10013" adminAddress "localhost:10013"
} }
webPort 10004 webPort 10004
extraConfig = ['h2Settings.address' : 'localhost:10014']
cordapps = [] cordapps = []
} }
node { node {
@ -109,6 +110,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
adminAddress "localhost:10016" adminAddress "localhost:10016"
} }
webPort 10007 webPort 10007
extraConfig = ['h2Settings.address' : 'localhost:10017']
cordapps = [] cordapps = []
rpcUsers = [ rpcUsers = [
['username' : "user", ['username' : "user",

View File

@ -52,7 +52,6 @@ class CustomVaultQueryTest {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs", "com.template")) mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs", "com.template"))
nodeA = mockNet.createPartyNode() nodeA = mockNet.createPartyNode()
nodeB = mockNet.createPartyNode() nodeB = mockNet.createPartyNode()
nodeA.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
notary = mockNet.defaultNotaryIdentity notary = mockNet.defaultNotaryIdentity
} }

View File

@ -46,7 +46,6 @@ class WorkflowTransactionBuildTutorialTest {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs")) mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
aliceNode = mockNet.createPartyNode(ALICE_NAME) aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobNode = mockNet.createPartyNode(BOB_NAME) bobNode = mockNet.createPartyNode(BOB_NAME)
aliceNode.registerInitiatedFlow(RecordCompletionFlow::class.java)
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME)
} }

View File

@ -70,7 +70,7 @@ the three node folders. Each node folder has the following structure:
. .
|____corda.jar // The runnable node |____corda.jar // The runnable node
|____corda-webserver.jar // The node's webserver |____corda-webserver.jar // The node's webserver (The notary doesn't need a web server)
|____node.conf // The node's configuration file |____node.conf // The node's configuration file
|____cordapps |____cordapps
|____java/kotlin-source-0.1.jar // Our IOU CorDapp |____java/kotlin-source-0.1.jar // Our IOU CorDapp
@ -85,7 +85,7 @@ Let's start the nodes by running the following commands from the root of the pro
// On Mac // On Mac
build/nodes/runnodes build/nodes/runnodes
This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight This will start a terminal window for each node, and an additional terminal window for each node's webserver - five
terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays
the message, "Welcome to the Corda interactive shell.". the message, "Welcome to the Corda interactive shell.".

View File

@ -32,13 +32,17 @@ If you want H2 to auto-select a port (mimicking the old ``h2Port`` behaviour), y
address: "localhost:0" address: "localhost:0"
} }
If remote access is required, the address can be changed to ``0.0.0.0``. However it is recommended to change the default username and password before doing so. If remote access is required, the address can be changed to ``0.0.0.0``.
The node requires a database password to be set when the database is exposed on the network interface to listen on.
.. sourcecode:: groovy .. sourcecode:: groovy
h2Settings { h2Settings {
address: "0.0.0.0:12345" address: "0.0.0.0:12345"
} }
dataSourceProperties {
dataSource.password : "strongpassword"
}
The previous ``h2Port`` syntax is now deprecated. ``h2Port`` will continue to work but the database The previous ``h2Port`` syntax is now deprecated. ``h2Port`` will continue to work but the database
will only be accessible on localhost. will only be accessible on localhost.

View File

@ -3,7 +3,24 @@ Node database
Default in-memory database Default in-memory database
-------------------------- --------------------------
By default, nodes store their data in an H2 database. You can connect directly to a running node's database to see its By default, nodes store their data in an H2 database.
The database (a file persistence.mv.db) is created at the first node startup with the administrator user 'sa' and a blank password.
The user name and password can be changed in node configuration:
.. sourcecode:: groovy
dataSourceProperties = {
dataSource.user = [USER]
dataSource.password = [PASSWORD]
}
Note, changing user/password for the existing node in node.conf will not update them in the H2 database,
you need to login to the database first to create new user or change the user password.
The database password is required only when the H2 database is exposed on non-localhost address (which is disabled by default).
The node requires the user with administrator permissions in order to creates tables upon the first startup
or after deplying new CorDapps with own tables.
You can connect directly to a running node's database to see its
stored states, transactions and attachments as follows: stored states, transactions and attachments as follows:
* Enable the H2 database access in the node configuration using the following syntax: * Enable the H2 database access in the node configuration using the following syntax:
@ -35,7 +52,8 @@ interface for you to query them using SQL.
The default behaviour is to expose the H2 database on localhost. This can be overridden in the The default behaviour is to expose the H2 database on localhost. This can be overridden in the
node configuration using ``h2Settings.address`` and specifying the address of the network interface to listen on, node configuration using ``h2Settings.address`` and specifying the address of the network interface to listen on,
or simply using ``0.0.0.0:0`` to listen on all interfaces. or simply using ``0.0.0.0:0`` to listen on all interfaces. The node requires a database password to be set when
the database is exposed on the network interface to listen on.
.. _standalone_database_config_examples_ref: .. _standalone_database_config_examples_ref:

View File

@ -42,7 +42,7 @@ class AddressBindingFailureTests: IntegrationTest() {
@Test @Test
fun `H2 address`() { fun `H2 address`() {
assumeTrue(!IntegrationTest.isRemoteDatabaseMode()) // Enterprise only - disable test where running against remote database assumeTrue(!IntegrationTest.isRemoteDatabaseMode()) // Enterprise only - disable test where running against remote database
assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString())) } assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString()), "dataSourceProperties.dataSource.password" to "password") }
} }
private fun assertBindExceptionForOverrides(overrides: (NetworkHostAndPort) -> Map<String, Any?>) { private fun assertBindExceptionForOverrides(overrides: (NetworkHostAndPort) -> Map<String, Any?>) {

View File

@ -0,0 +1,136 @@
package net.corda.node.persistence
import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import org.junit.Test
import java.net.InetAddress
import java.sql.DriverManager
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import kotlin.test.assertTrue
class H2SecurityTests {
companion object {
private val port = PortAllocation.Incremental(21_000)
private fun getFreePort() = port.nextPort()
private const val h2AddressKey = "h2Settings.address"
private const val dbPasswordKey = "dataSourceProperties.dataSource.password"
}
@Test
fun `h2 server starts when h2Settings are set`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
val port = getFreePort()
startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port")).getOrThrow()
DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "").use {
assertTrue(it.createStatement().executeQuery("SELECT 1").next())
}
}
}
@Test
fun `h2 server on the host name requires non-default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostName}:${getFreePort()}")).getOrThrow()
}
}
}
@Test
fun `h2 server on the external host IP requires non-default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostAddress}:${getFreePort()}")).getOrThrow()
}
}
}
@Test
fun `h2 server on host name requires non-blank database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostName}:${getFreePort()}",
dbPasswordKey to " ")).getOrThrow()
}
}
}
@Test
fun `h2 server on external host IP requires non-blank database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostAddress}:${getFreePort()}",
dbPasswordKey to " ")).getOrThrow()
}
}
}
@Test
fun `h2 server on localhost runs with the default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) {
startNode(customOverrides = mapOf(h2AddressKey to "localhost:${getFreePort()}")).getOrThrow()
}
}
@Test
fun `h2 server to loopback IP runs with the default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
startNode(customOverrides = mapOf(h2AddressKey to "127.0.0.1:${getFreePort()}")).getOrThrow()
}
}
@Test
fun `remote code execution via h2 server is disabled`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) {
val port = getFreePort()
startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port", dbPasswordKey to "x")).getOrThrow()
DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "x").use {
assertFailsWith(org.h2.jdbc.JdbcSQLException::class) {
it.createStatement().execute("CREATE ALIAS SET_PROPERTY FOR \"java.lang.System.setProperty\"")
it.createStatement().execute("CALL SET_PROPERTY('abc', '1')")
}
}
assertNull(System.getProperty("abc"))
}
}
@Test
fun `malicious flow tries to enable remote code execution via h2 server`() {
val user = User("mark", "dadada", setOf(Permissions.startFlow<MaliciousFlow>()))
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) {
val port = getFreePort()
val nodeHandle = startNode(rpcUsers = listOf(user), customOverrides = mapOf(h2AddressKey to "localhost:$port",
dbPasswordKey to "x")).getOrThrow()
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
it.proxy.startFlow(::MaliciousFlow).returnValue.getOrThrow()
}
DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "x").use {
assertFailsWith(org.h2.jdbc.JdbcSQLException::class) {
it.createStatement().execute("CREATE ALIAS SET_PROPERTY FOR \"java.lang.System.setProperty\"")
it.createStatement().execute("CALL SET_PROPERTY('abc', '1')")
}
}
assertNull(System.getProperty("abc"))
}
}
@StartableByRPC
class MaliciousFlow : FlowLogic<Boolean>() {
@Suspendable
override fun call(): Boolean {
System.clearProperty("h2.allowedClasses")
return true
}
}
}

View File

@ -202,7 +202,6 @@ abstract class MQSecurityTest : NodeBasedTest() {
protected fun startBobAndCommunicateWithAlice(): Party { protected fun startBobAndCommunicateWithAlice(): Party {
val bob = startNode(BOB_NAME) val bob = startNode(BOB_NAME)
bob.registerInitiatedFlow(ReceiveFlow::class.java)
val bobParty = bob.info.singleIdentity() val bobParty = bob.info.singleIdentity()
// Perform a protocol exchange to force the peer queue to be created // Perform a protocol exchange to force the peer queue to be created
alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow() alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow()

View File

@ -230,7 +230,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private var _started: S? = null private var _started: S? = null
private fun <T : Any> T.tokenize(): T { private fun <T : Any> T.tokenize(): T {
tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finialised") tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised")
return this return this
} }
@ -254,10 +254,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private fun initKeyStore(): X509Certificate { private fun initKeyStore(): X509Certificate {
if (configuration.devMode) { if (configuration.devMode) {
log.warn("The Corda node is running in developer mode. This is not suitable for production usage.")
configuration.configureWithDevSSLCertificate() configuration.configureWithDevSSLCertificate()
} else {
log.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.")
} }
return validateKeyStore() return validateKeyStore()
} }
@ -347,12 +344,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
services.start(nodeInfo, netParams)
networkMapUpdater.start(trustRoot, signedNetParams.raw.hash, signedNodeInfo.raw.hash) networkMapUpdater.start(trustRoot, signedNetParams.raw.hash, signedNodeInfo.raw.hash)
startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams)
// Do all of this in a database transaction so anything that might need a connection has one. // Do all of this in a database transaction so anything that might need a connection has one.
return database.transaction { return database.transaction {
services.start(nodeInfo, netParams)
identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts)
attachments.start() attachments.start()
cordappProvider.start(netParams.whitelistedContractImplementations) cordappProvider.start(netParams.whitelistedContractImplementations)
@ -692,6 +689,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} else { } else {
Observable.empty() Observable.empty()
} }
check(initiatingFlowClass !in flowFactories.keys) {
"$initiatingFlowClass is attempting to register multiple initiated flows"
}
flowFactories[initiatingFlowClass] = flowFactory flowFactories[initiatingFlowClass] = flowFactory
return observable return observable
} }
@ -772,7 +772,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
val props = configuration.dataSourceProperties val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
database.hikariStart(props, configuration.database, schemaService) database.startHikariPool(props, configuration.database, schemaService)
// Now log the vendor string as this will also cause a connection to be tested eagerly. // Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log) logVendorString(database, log)
} }
@ -941,7 +941,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val transactionVerifierService: TransactionVerifierService get() = this@AbstractNode.transactionVerifierService override val transactionVerifierService: TransactionVerifierService get() = this@AbstractNode.transactionVerifierService
override val contractUpgradeService: ContractUpgradeService get() = this@AbstractNode.contractUpgradeService override val contractUpgradeService: ContractUpgradeService get() = this@AbstractNode.contractUpgradeService
override val auditService: AuditService get() = this@AbstractNode.auditService override val auditService: AuditService get() = this@AbstractNode.auditService
override val attachments: AttachmentStorage get() = this@AbstractNode.attachments override val attachments: AttachmentStorageInternal get() = this@AbstractNode.attachments
override val networkService: MessagingService get() = network override val networkService: MessagingService get() = network
override val clock: Clock get() = platformClock override val clock: Clock get() = platformClock
override val configuration: NodeConfiguration get() = this@AbstractNode.configuration override val configuration: NodeConfiguration get() = this@AbstractNode.configuration
@ -1038,7 +1038,7 @@ fun configureDatabase(hikariProperties: Properties,
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
schemaService: SchemaService = NodeSchemaService()): CordaPersistence = schemaService: SchemaService = NodeSchemaService()): CordaPersistence =
createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService) createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService)
.apply { hikariStart(hikariProperties, databaseConfig, schemaService) } .apply { startHikariPool(hikariProperties, databaseConfig, schemaService) }
fun createCordaPersistence(databaseConfig: DatabaseConfig, fun createCordaPersistence(databaseConfig: DatabaseConfig,
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
@ -1053,7 +1053,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, attributeConverters) return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, attributeConverters)
} }
fun CordaPersistence.hikariStart(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemaService: SchemaService) { fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemaService: SchemaService) {
try { try {
val dataSource = DataSourceFactory.createDataSource(hikariProperties) val dataSource = DataSourceFactory.createDataSource(hikariProperties)
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")

View File

@ -28,26 +28,12 @@ import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.RPC_UPLOADER import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.STRUCTURAL_STEP_PREFIX import net.corda.core.internal.STRUCTURAL_STEP_PREFIX
import net.corda.core.internal.sign import net.corda.core.internal.sign
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.*
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowHandleImpl
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.FlowProgressHandleImpl
import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.messaging.RPCReturnsObservables
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
@ -85,11 +71,10 @@ internal class CordaRPCOpsImpl(
} }
override fun acceptNewNetworkParameters(parametersHash: SecureHash) { override fun acceptNewNetworkParameters(parametersHash: SecureHash) {
services.networkMapUpdater.acceptNewNetworkParameters( // TODO When multiple identities design will be better specified this should be signature from node operator.
parametersHash, services.networkMapUpdater.acceptNewNetworkParameters(parametersHash) { hash ->
// TODO When multiple identities design will be better specified this should be signature from node operator. hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) }
{ hash -> hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) } } }
)
} }
override fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> { override fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> {
@ -206,7 +191,7 @@ internal class CordaRPCOpsImpl(
} }
override fun uploadAttachment(jar: InputStream): SecureHash { override fun uploadAttachment(jar: InputStream): SecureHash {
return services.attachments.importAttachment(jar, RPC_UPLOADER, null) return services.attachments.privilegedImportAttachment(jar, RPC_UPLOADER, null)
} }
override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash { override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash {

View File

@ -64,6 +64,7 @@ import net.corda.nodeapi.internal.bridging.BridgeControlListener
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.serialization.internal.* import net.corda.serialization.internal.*
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import org.h2.jdbc.JdbcSQLException import org.h2.jdbc.JdbcSQLException
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -71,11 +72,13 @@ import rx.Observable
import rx.Scheduler import rx.Scheduler
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.net.BindException import java.net.BindException
import java.net.InetAddress
import java.nio.file.Path import java.nio.file.Path
import java.time.Clock import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.management.ObjectName import javax.management.ObjectName
import kotlin.system.exitProcess import kotlin.system.exitProcess
import java.nio.file.Paths
class NodeWithInfo(val node: Node, val info: NodeInfo) { class NodeWithInfo(val node: Node, val info: NodeInfo) {
val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {} val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {}
@ -358,13 +361,20 @@ open class Node(configuration: NodeConfiguration,
if (databaseUrl != null && databaseUrl.startsWith(h2Prefix)) { if (databaseUrl != null && databaseUrl.startsWith(h2Prefix)) {
val effectiveH2Settings = configuration.effectiveH2Settings val effectiveH2Settings = configuration.effectiveH2Settings
//forbid execution of arbitrary code via SQL except those classes required by H2 itself
System.setProperty("h2.allowedClasses", "org.h2.mvstore.db.MVTableEngine,org.locationtech.jts.geom.Geometry,org.h2.server.TcpServer")
if (effectiveH2Settings?.address != null) { if (effectiveH2Settings?.address != null) {
if (!InetAddress.getByName(effectiveH2Settings.address.host).isLoopbackAddress
&& configuration.dataSourceProperties.getProperty("dataSource.password").isBlank()) {
throw CouldNotCreateDataSourceException("Database password is required for H2 server listening on ${InetAddress.getByName(effectiveH2Settings.address.host)}.")
}
val databaseName = databaseUrl.removePrefix(h2Prefix).substringBefore(';') val databaseName = databaseUrl.removePrefix(h2Prefix).substringBefore(';')
val baseDir = Paths.get(databaseName).parent.toString()
val server = org.h2.tools.Server.createTcpServer( val server = org.h2.tools.Server.createTcpServer(
"-tcpPort", effectiveH2Settings.address.port.toString(), "-tcpPort", effectiveH2Settings.address.port.toString(),
"-tcpAllowOthers", "-tcpAllowOthers",
"-tcpDaemon", "-tcpDaemon",
"-baseDir", baseDir,
"-key", "node", databaseName) "-key", "node", databaseName)
// override interface that createTcpServer listens on (which is always 0.0.0.0) // override interface that createTcpServer listens on (which is always 0.0.0.0)
System.setProperty("h2.bindAddress", effectiveH2Settings.address.host) System.setProperty("h2.bindAddress", effectiveH2Settings.address.host)

View File

@ -39,11 +39,11 @@ import net.corda.node.utilities.registration.UnableToRegisterNodeWithDoormanExce
import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToKeyStore
import net.corda.node.utilities.saveToTrustStore import net.corda.node.utilities.saveToTrustStore
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.nodeapi.internal.persistence.DatabaseMigrationException import net.corda.nodeapi.internal.persistence.DatabaseMigrationException
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole import org.fusesource.jansi.AnsiConsole
@ -337,6 +337,8 @@ open class NodeStartup(val args: Array<String>) {
Emoji.renderIfSupported { Emoji.renderIfSupported {
Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.") Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.")
} }
} else {
logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.")
} }
val nodeInfo = node.start() val nodeInfo = node.start()
@ -352,7 +354,7 @@ open class NodeStartup(val args: Array<String>) {
if (conf.shouldStartLocalShell()) { if (conf.shouldStartLocalShell()) {
node.startupComplete.then { node.startupComplete.then {
try { try {
InteractiveShell.runLocalShell({ node.stop() }) InteractiveShell.runLocalShell(node::stop)
} catch (e: Throwable) { } catch (e: Throwable) {
logger.error("Shell failed to start", e) logger.error("Shell failed to start", e)
} }

View File

@ -24,6 +24,7 @@ import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.node.cordapp.CordappLoader import net.corda.node.cordapp.CordappLoader
import net.corda.node.services.persistence.AttachmentStorageInternal
import java.net.URL import java.net.URL
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -99,7 +100,13 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
cordapps.filter { !it.contractClassNames.isEmpty() }.map { cordapps.filter { !it.contractClassNames.isEmpty() }.map {
it.jarPath.openStream().use { stream -> it.jarPath.openStream().use { stream ->
try { try {
attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null) // We can't make attachmentStorage a AttachmentStorageInternal as that ends up requiring
// MockAttachmentStorage to implement it.
if (attachmentStorage is AttachmentStorageInternal) {
attachmentStorage.privilegedImportAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null)
} else {
attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null)
}
} catch (faee: java.nio.file.FileAlreadyExistsException) { } catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!) AttachmentId.parse(faee.message!!)
} }

View File

@ -31,6 +31,7 @@ import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.network.NetworkMapUpdater
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.ExternalEvent
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -115,6 +116,7 @@ interface ServiceHubInternal : ServiceHub {
} }
} }
override val attachments: AttachmentStorageInternal
override val vaultService: VaultServiceInternal override val vaultService: VaultServiceInternal
/** /**
* A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct.

View File

@ -80,9 +80,7 @@ class NetworkMapCacheImpl(
} }
} }
/** /** Database-based network map cache. */
* Extremely simple in-memory cache of the network map.
*/
@ThreadSafe @ThreadSafe
open class PersistentNetworkMapCache(private val database: CordaPersistence) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { open class PersistentNetworkMapCache(private val database: CordaPersistence) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal {
companion object { companion object {

View File

@ -0,0 +1,13 @@
package net.corda.node.services.persistence
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import java.io.InputStream
interface AttachmentStorageInternal : AttachmentStorage {
/**
* This is the same as [importAttachment] expect there are no checks done on the uploader field. This API is internal
* and is only for the node.
*/
fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId
}

View File

@ -22,12 +22,8 @@ import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.*
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.readFully
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.* import net.corda.core.serialization.*
@ -60,10 +56,12 @@ class NodeAttachmentService(
private val database: CordaPersistence, private val database: CordaPersistence,
attachmentContentCacheSize: Long = NodeConfiguration.defaultAttachmentContentCacheSize, attachmentContentCacheSize: Long = NodeConfiguration.defaultAttachmentContentCacheSize,
attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound
) : AttachmentStorage, SingletonSerializeAsToken() { ) : AttachmentStorageInternal, SingletonSerializeAsToken() {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
private val PRIVILEGED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, P2P_UPLOADER, UNKNOWN_UPLOADER)
// Just iterate over the entries with verification enabled: should be good enough to catch mistakes. // Just iterate over the entries with verification enabled: should be good enough to catch mistakes.
// Note that JarInputStream won't throw any kind of error at all if the file stream is in fact not // Note that JarInputStream won't throw any kind of error at all if the file stream is in fact not
// a ZIP! It'll just pretend it's an empty archive, which is kind of stupid but that's how it works. // a ZIP! It'll just pretend it's an empty archive, which is kind of stupid but that's how it works.
@ -238,10 +236,9 @@ class NodeAttachmentService(
} }
} }
private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>( private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>(attachmentCacheBound) { key ->
attachmentCacheBound, Optional.ofNullable(createAttachment(key))
{ key -> Optional.ofNullable(createAttachment(key)) } }
)
private fun createAttachment(key: SecureHash): Attachment? { private fun createAttachment(key: SecureHash): Attachment? {
val content = attachmentContentCache.get(key)!! val content = attachmentContentCache.get(key)!!
@ -268,6 +265,18 @@ class NodeAttachmentService(
} }
override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
require(uploader !in PRIVILEGED_UPLOADERS) { "$uploader is a reserved uploader token" }
if (uploader.startsWith("$P2P_UPLOADER:")) {
// FetchAttachmentsFlow is in core and thus doesn't have access to AttachmentStorageInternal to call
// privilegedImportAttachment
require(Thread.currentThread().stackTrace.any { it.className == FetchAttachmentsFlow::class.java.name }) {
"$P2P_UPLOADER is a reserved uploader token prefix"
}
}
return import(jar, uploader, filename)
}
override fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return import(jar, uploader, filename) return import(jar, uploader, filename)
} }
@ -292,7 +301,13 @@ class NodeAttachmentService(
if (!hasAttachment(id)) { if (!hasAttachment(id)) {
checkIsAValidJAR(bytes.inputStream()) checkIsAValidJAR(bytes.inputStream())
val session = currentDBSession() val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename, contractClassNames = contractClassNames) val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(),
content = bytes,
uploader = uploader,
filename = filename,
contractClassNames = contractClassNames
)
session.save(attachment) session.save(attachment)
attachmentCount.inc() attachmentCount.inc()
log.info("Stored new attachment $id") log.info("Stored new attachment $id")
@ -305,10 +320,12 @@ class NodeAttachmentService(
} }
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId = try { override fun importOrGetAttachment(jar: InputStream): AttachmentId {
import(jar, UNKNOWN_UPLOADER, null) return try {
} catch (faee: java.nio.file.FileAlreadyExistsException) { import(jar, UNKNOWN_UPLOADER, null)
AttachmentId.parse(faee.message!!) } catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
} }
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> { override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {

View File

@ -95,10 +95,10 @@ class NodeVaultService(
log.trace { "State update of type: $concreteType" } log.trace { "State update of type: $concreteType" }
val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) } val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) }
if (!seen) { if (!seen) {
val contractInterfaces = deriveContractInterfaces(concreteType) val contractTypes = deriveContractTypes(concreteType)
contractInterfaces.map { contractTypes.map {
val contractInterface = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
contractInterface.add(concreteType.name) contractStateType.add(concreteType.name)
} }
} }
} }
@ -541,10 +541,10 @@ class NodeVaultService(
null null
} }
concreteType?.let { concreteType?.let {
val contractInterfaces = deriveContractInterfaces(it) val contractTypes = deriveContractTypes(it)
contractInterfaces.map { contractTypes.map {
val contractInterface = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
contractInterface.add(it.name) contractStateType.add(it.name)
} }
} }
} }
@ -553,14 +553,20 @@ class NodeVaultService(
} }
} }
private fun <T : ContractState> deriveContractInterfaces(clazz: Class<T>): Set<Class<T>> { private fun <T : ContractState> deriveContractTypes(clazz: Class<T>): Set<Class<T>> {
val myInterfaces: MutableSet<Class<T>> = mutableSetOf() val myTypes : MutableSet<Class<T>> = mutableSetOf()
clazz.interfaces.forEach { clazz.superclass?.let {
if (it != ContractState::class.java) { if (!it.isInstance(Any::class)) {
myInterfaces.add(uncheckedCast(it)) myTypes.add(uncheckedCast(it))
myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) myTypes.addAll(deriveContractTypes(uncheckedCast(it)))
} }
} }
return myInterfaces clazz.interfaces.forEach {
if (it != ContractState::class.java) {
myTypes.add(uncheckedCast(it))
myTypes.addAll(deriveContractTypes(uncheckedCast(it)))
}
}
return myTypes
} }
} }

View File

@ -0,0 +1,72 @@
package net.corda.node.internal
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.utilities.unwrap
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.StartedMockNode
import org.assertj.core.api.Assertions.assertThatIllegalStateException
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertNotNull
class FlowRegistrationTest {
lateinit var mockNetwork: MockNetwork
lateinit var initiator: StartedMockNode
lateinit var responder: StartedMockNode
@Before
fun setup() {
// no cordapps scanned so it can be tested in isolation
mockNetwork = MockNetwork(emptyList())
initiator = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("initiator", "Reading", "GB")))
responder = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("responder", "Reading", "GB")))
mockNetwork.runNetwork()
}
@After
fun tearDown() {
mockNetwork.stopNodes()
}
@Test
fun `startup fails when two flows initiated by the same flow are registered`() {
// register the same flow twice to invoke the error without causing errors in other tests
responder.registerInitiatedFlow(Responder::class.java)
assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) }
}
@Test
fun `a single initiated flow can be registered without error`() {
responder.registerInitiatedFlow(Responder::class.java)
val result = initiator.startFlow(Initiator(responder.info.singleIdentity()))
mockNetwork.runNetwork()
assertNotNull(result.get())
}
}
@InitiatingFlow
class Initiator(val party: Party) : FlowLogic<String>() {
@Suspendable
override fun call(): String {
return initiateFlow(party).sendAndReceive<String>("Hello there").unwrap { it }
}
}
@InitiatedBy(Initiator::class)
private class Responder(val session: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
session.receive<String>().unwrap { it }
session.send("What's up")
}
}

View File

@ -10,26 +10,34 @@
package net.corda.node.services.persistence package net.corda.node.services.persistence
import co.paralleluniverse.fibers.Suspendable
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import com.google.common.jimfs.Configuration import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs import com.google.common.jimfs.Jimfs
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.Builder import net.corda.core.node.services.vault.Builder
import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.Sort
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.configureDatabase import net.corda.node.internal.configureDatabase
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.internal.LogHelper import net.corda.testing.internal.LogHelper
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets 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
@ -40,7 +48,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNull import kotlin.test.assertNull
class NodeAttachmentStorageTest { class NodeAttachmentServiceTest {
// Use an in memory file system for testing attachment storage. // Use an in memory file system for testing attachment storage.
private lateinit var fs: FileSystem private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence private lateinit var database: CordaPersistence
@ -195,7 +203,7 @@ class NodeAttachmentStorageTest {
@Ignore("We need to be able to restart nodes - make importing attachments idempotent?") @Ignore("We need to be able to restart nodes - make importing attachments idempotent?")
@Test @Test
fun `duplicates not allowed`() { fun `duplicates not allowed`() {
val (testJar, _) = makeTestJar() val (testJar) = makeTestJar()
testJar.read { testJar.read {
storage.importAttachment(it, "test", null) storage.importAttachment(it, "test", null)
} }
@ -208,7 +216,7 @@ class NodeAttachmentStorageTest {
@Test @Test
fun `corrupt entry throws exception`() { fun `corrupt entry throws exception`() {
val (testJar, _) = makeTestJar() val (testJar) = makeTestJar()
val id = database.transaction { val id = database.transaction {
val id = testJar.read { storage.importAttachment(it, "test", null) } val id = testJar.read { storage.importAttachment(it, "test", null) }
@ -243,23 +251,68 @@ class NodeAttachmentStorageTest {
} }
} }
@Test
fun `using reserved uploader tokens`() {
val (testJar) = makeTestJar()
fun assertImportFails(uploader: String) {
testJar.read {
assertThatIllegalArgumentException().isThrownBy {
storage.importAttachment(it, uploader, null)
}.withMessageContaining(uploader)
}
}
database.transaction {
assertImportFails(DEPLOYED_CORDAPP_UPLOADER)
assertImportFails(P2P_UPLOADER)
assertImportFails(RPC_UPLOADER)
assertImportFails(UNKNOWN_UPLOADER)
}
// Import an attachment similar to how net.corda.core.internal.FetchAttachmentsFlow does it.
InternalMockNetwork(threadPerNode = true).use { mockNet ->
val node = mockNet.createNode()
val result = node.services.startFlow(FetchAttachmentsFlow()).resultFuture
assertThatIllegalArgumentException().isThrownBy {
result.getOrThrow()
}.withMessageContaining(P2P_UPLOADER)
}
}
// Not the real FetchAttachmentsFlow!
private class FetchAttachmentsFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val baos = ByteArrayOutputStream()
makeTestJar(baos)
serviceHub.attachments.importAttachment(baos.toByteArray().inputStream(), "$P2P_UPLOADER:${ourIdentity.name}", null)
}
}
private var counter = 0 private var counter = 0
private fun makeTestJar(extraEntries: List<Pair<String, String>> = emptyList()): Pair<Path, SecureHash> { private fun makeTestJar(extraEntries: List<Pair<String, String>> = emptyList()): Pair<Path, SecureHash> {
counter++ counter++
val file = fs.getPath("$counter.jar") val file = fs.getPath("$counter.jar")
file.write { makeTestJar(file.outputStream(), extraEntries)
val jar = JarOutputStream(it)
jar.putNextEntry(JarEntry("test1.txt"))
jar.write("This is some useful content".toByteArray())
jar.closeEntry()
jar.putNextEntry(JarEntry("test2.txt"))
jar.write("Some more useful content".toByteArray())
extraEntries.forEach {
jar.putNextEntry(JarEntry(it.first))
jar.write(it.second.toByteArray())
}
jar.closeEntry()
}
return Pair(file, file.readAll().sha256()) return Pair(file, file.readAll().sha256())
} }
private companion object {
private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
output.use {
val jar = JarOutputStream(it)
jar.putNextEntry(JarEntry("test1.txt"))
jar.write("This is some useful content".toByteArray())
jar.closeEntry()
jar.putNextEntry(JarEntry("test2.txt"))
jar.write("Some more useful content".toByteArray())
extraEntries.forEach {
jar.putNextEntry(JarEntry(it.first))
jar.write(it.second.toByteArray())
}
jar.closeEntry()
}
}
}
} }

View File

@ -63,47 +63,41 @@ class FlowFrameworkTests {
init { init {
LogHelper.setLevel("+net.corda.flow") LogHelper.setLevel("+net.corda.flow")
} }
}
private lateinit var mockNet: InternalMockNetwork private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: TestStartedNode private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode private lateinit var bobNode: TestStartedNode
private lateinit var alice: Party private lateinit var alice: Party
private lateinit var bob: Party private lateinit var bob: Party
private lateinit var notaryIdentity: Party private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>() private val receivedSessionMessages = ArrayList<SessionTransfer>()
@BeforeClass @Before
@JvmStatic fun setUpMockNet() {
fun beforeClass() { mockNet = InternalMockNetwork(
mockNet = InternalMockNetwork( cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), servicePeerAllocationStrategy = RoundRobin()
servicePeerAllocationStrategy = RoundRobin() )
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
// Extract identities // Extract identities
alice = aliceNode.info.singleIdentity() alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity() bob = bobNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
} }
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@AfterClass @JvmStatic
fun afterClass() {
mockNet.stopNodes()
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
} }
@After @After
fun cleanUp() { fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear() receivedSessionMessages.clear()
} }
@ -484,45 +478,38 @@ class FlowFrameworkTripartyTests {
private lateinit var charlie: Party private lateinit var charlie: Party
private lateinit var notaryIdentity: Party private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>() private val receivedSessionMessages = ArrayList<SessionTransfer>()
}
@BeforeClass @Before
@JvmStatic fun setUpGlobalMockNet() {
fun beforeClass() { mockNet = InternalMockNetwork(
mockNet = InternalMockNetwork( cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), servicePeerAllocationStrategy = RoundRobin()
servicePeerAllocationStrategy = RoundRobin() )
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
// Extract identities // Extract identities
alice = aliceNode.info.singleIdentity() alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity() bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity() charlie = charlieNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
@AfterClass @JvmStatic
fun afterClass() {
mockNet.stopNodes()
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
} }
@After @After
fun cleanUp() { fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear() receivedSessionMessages.clear()
} }
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@Test @Test
fun `sending to multiple parties`() { fun `sending to multiple parties`() {

View File

@ -12,12 +12,14 @@ package net.corda.node.services.vault
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.packageName import net.corda.core.internal.packageName
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.finance.* import net.corda.finance.*
@ -37,6 +39,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.TEST_TX_TIME
import net.corda.testing.internal.chooseIdentity
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.* import net.corda.testing.internal.vault.*
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
@ -125,7 +128,8 @@ open class VaultQueryTestRule : ExternalResource(), VaultQueryParties {
"net.corda.finance.contracts", "net.corda.finance.contracts",
CashSchemaV1::class.packageName, CashSchemaV1::class.packageName,
DummyLinearStateSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName,
SampleCashSchemaV3::class.packageName) SampleCashSchemaV3::class.packageName,
VaultQueryTestsBase.MyContractClass::class.packageName)
override lateinit var services: MockServices override lateinit var services: MockServices
override lateinit var vaultFiller: VaultFiller override lateinit var vaultFiller: VaultFiller
@ -263,6 +267,43 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
} }
} }
@Test
fun `query by interface for a contract class extending a parent contract class`() {
database.transaction {
// build custom contract and store in vault
val me = services.myInfo.chooseIdentity()
val state = MyState("myState", listOf(me))
val stateAndContract = StateAndContract(state, MYCONTRACT_ID)
val utx = TransactionBuilder(notary = notaryServices.myInfo.singleIdentity()).withItems(stateAndContract).withItems(dummyCommand())
services.recordTransactions(services.signInitialTransaction(utx))
// query vault by Child class
val criteria = VaultQueryCriteria() // default is UNCONSUMED
val queryByMyState = vaultService.queryBy<MyState>(criteria)
assertThat(queryByMyState.states).hasSize(1)
// query vault by Parent class
val queryByBaseState = vaultService.queryBy<BaseState>(criteria)
assertThat(queryByBaseState.states).hasSize(1)
// query vault by extended Contract Interface
val queryByContract = vaultService.queryBy<MyContractInterface>(criteria)
assertThat(queryByContract.states).hasSize(1)
}
}
// Beware: do not use `MyContractClass::class.qualifiedName` as this returns a fully qualified name using "dot" notation for enclosed class
val MYCONTRACT_ID = "net.corda.node.services.vault.VaultQueryTestsBase\$MyContractClass"
open class MyContractClass : Contract {
override fun verify(tx: LedgerTransaction) {}
}
interface MyContractInterface : ContractState
open class BaseState(override val participants: List<AbstractParty> = emptyList()) : MyContractInterface
data class MyState(val name: String, override val participants: List<AbstractParty> = emptyList()) : BaseState(participants)
@Test @Test
fun `unconsumed states simple`() { fun `unconsumed states simple`() {
database.transaction { database.transaction {

View File

@ -63,6 +63,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10003" address "localhost:10003"
adminAddress "localhost:10004" adminAddress "localhost:10004"
} }
extraConfig = ['h2Settings.address' : 'localhost:10012']
} }
node { node {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
@ -73,6 +74,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10006" address "localhost:10006"
adminAddress "localhost:10007" adminAddress "localhost:10007"
} }
extraConfig = ['h2Settings.address' : 'localhost:10013']
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
@ -84,6 +86,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
webPort 10010 webPort 10010
cordapps = [] cordapps = []
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
extraConfig = ['h2Settings.address' : 'localhost:10014']
} }
} }

View File

@ -54,10 +54,12 @@ class BankOfCordaCordform : CordformDefinition() {
adminAddress("localhost:10004") adminAddress("localhost:10004")
} }
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:10016"))
} }
node { node {
name(BOC_NAME) name(BOC_NAME)
extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD"))) extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")),
"h2Settings" to mapOf("address" to "localhost:10017"))
p2pPort(10005) p2pPort(10005)
rpcSettings { rpcSettings {
address("localhost:$BOC_RPC_PORT") address("localhost:$BOC_RPC_PORT")
@ -77,6 +79,7 @@ class BankOfCordaCordform : CordformDefinition() {
webPort(10010) webPort(10010)
rpcUsers(User(BIGCORP_RPC_USER, BIGCORP_RPC_PWD, setOf(all()))) rpcUsers(User(BIGCORP_RPC_USER, BIGCORP_RPC_PWD, setOf(all())))
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:10018"))
} }
} }

View File

@ -32,10 +32,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10003 port 10003
adminPort 10004 adminPort 10004
} }
extraConfig = ['h2Settings.address' : 'localhost:10005']
} }
node { node {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
p2pPort 10005 p2pPort 10006
cordapps = [] cordapps = []
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
// This configures the default cordapp for this node // This configures the default cordapp for this node
@ -46,10 +47,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10007 port 10007
adminPort 10008 adminPort 10008
} }
extraConfig = ['h2Settings.address' : 'localhost:10009']
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
p2pPort 10009 p2pPort 10010
cordapps = [] cordapps = []
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
// This configures the default cordapp for this node // This configures the default cordapp for this node
@ -60,5 +62,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10011 port 10011
adminPort 10012 adminPort 10012
} }
extraConfig = ['h2Settings.address' : 'localhost:10013']
} }
} }

View File

@ -80,6 +80,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapps = ["${project(":finance").group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
useTestClock true useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10024']
} }
node { node {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
@ -91,6 +92,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapps = ["${project(":finance").group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
useTestClock true useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10027']
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
@ -102,6 +104,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
useTestClock true useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10030']
} }
node { node {
name "O=Regulator,L=Moscow,C=RU" name "O=Regulator,L=Moscow,C=RU"
@ -114,6 +117,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapps = ["${project(":finance").group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
useTestClock true useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10033']
} }
} }

View File

@ -24,7 +24,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10003 port 10003
adminPort 10004 adminPort 10004
} }
h2Port 20004 extraConfig = ['h2Settings.address' : 'localhost:20004']
} }
node { node {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
@ -35,6 +35,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10007 port 10007
adminPort 10008 adminPort 10008
} }
extraConfig = ['h2Settings.address' : 'localhost:0']
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
@ -45,5 +46,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10011 port 10011
adminPort 10012 adminPort 10012
} }
extraConfig = ['h2Settings.address' : 'localhost:0']
} }
} }

View File

@ -52,7 +52,7 @@ by using the H2 web console:
Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a **notary** node is running, Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a **notary** node is running,
look for the following string: look for the following string:
``Database connection url is : jdbc:h2:tcp://10.18.0.150:56736/node`` ``Database connection url is : jdbc:h2:tcp://localhost:56736/node``
You can use the string on the right to connect to the h2 database: just paste it into the `JDBC URL` field and click *Connect*. You can use the string on the right to connect to the h2 database: just paste it into the `JDBC URL` field and click *Connect*.
You will be presented with a web application that enumerates all the available tables and provides an interface for you to query them using SQL You will be presented with a web application that enumerates all the available tables and provides an interface for you to query them using SQL

View File

@ -45,6 +45,7 @@ class BFTNotaryCordform : CordformDefinition() {
} }
rpcUsers(notaryDemoUser) rpcUsers(notaryDemoUser)
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
node { node {
name(BOB_NAME) name(BOB_NAME)
@ -54,11 +55,13 @@ class BFTNotaryCordform : CordformDefinition() {
adminAddress("localhost:10106") adminAddress("localhost:10106")
} }
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) } val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) }
fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node {
name(notaryNames[replicaId]) name(notaryNames[replicaId])
notary(NotaryConfig(validating = false, serviceLegalName = clusterName, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses))) notary(NotaryConfig(validating = false, serviceLegalName = clusterName, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses)))
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
configure() configure()
} }
notaryNode(0) { notaryNode(0) {

View File

@ -33,6 +33,7 @@ class CustomNotaryCordform : CordformDefinition() {
} }
rpcUsers(notaryDemoUser) rpcUsers(notaryDemoUser)
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
node { node {
name(BOB_NAME) name(BOB_NAME)
@ -42,6 +43,7 @@ class CustomNotaryCordform : CordformDefinition() {
adminAddress("localhost:10106") adminAddress("localhost:10106")
} }
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
node { node {
name(DUMMY_NOTARY_NAME) name(DUMMY_NOTARY_NAME)
@ -52,6 +54,7 @@ class CustomNotaryCordform : CordformDefinition() {
} }
notary(NotaryConfig(validating = true, custom = true)) notary(NotaryConfig(validating = true, custom = true))
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
} }

View File

@ -45,6 +45,7 @@ class RaftNotaryCordform : CordformDefinition() {
} }
rpcUsers(notaryDemoUser) rpcUsers(notaryDemoUser)
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
node { node {
name(BOB_NAME) name(BOB_NAME)
@ -54,11 +55,13 @@ class RaftNotaryCordform : CordformDefinition() {
adminAddress("localhost:10106") adminAddress("localhost:10106")
} }
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
fun notaryNode(index: Int, nodePort: Int, clusterPort: Int? = null, configure: CordformNode.() -> Unit) = node { fun notaryNode(index: Int, nodePort: Int, clusterPort: Int? = null, configure: CordformNode.() -> Unit) = node {
name(notaryNames[index]) name(notaryNames[index])
val clusterAddresses = if (clusterPort != null) listOf(NetworkHostAndPort("localhost", clusterPort)) else emptyList() val clusterAddresses = if (clusterPort != null) listOf(NetworkHostAndPort("localhost", clusterPort)) else emptyList()
notary(NotaryConfig(validating = true, serviceLegalName = clusterName, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses))) notary(NotaryConfig(validating = true, serviceLegalName = clusterName, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses)))
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
configure() configure()
devMode(true) devMode(true)
} }

View File

@ -39,6 +39,7 @@ class SingleNotaryCordform : CordformDefinition() {
} }
rpcUsers(notaryDemoUser) rpcUsers(notaryDemoUser)
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
node { node {
name(BOB_NAME) name(BOB_NAME)
@ -48,6 +49,7 @@ class SingleNotaryCordform : CordformDefinition() {
adminAddress("localhost:10106") adminAddress("localhost:10106")
} }
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
node { node {
name(DUMMY_NOTARY_NAME) name(DUMMY_NOTARY_NAME)
@ -58,6 +60,7 @@ class SingleNotaryCordform : CordformDefinition() {
} }
notary(NotaryConfig(validating = true)) notary(NotaryConfig(validating = true))
devMode(true) devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
} }
} }

View File

@ -104,7 +104,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [ extraConfig = [
custom: [ custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ],
'h2Settings.address' : 'localhost:10038'
] ]
} }
node { node {
@ -122,7 +123,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [ extraConfig = [
custom: [ custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ],
'h2Settings.address' : 'localhost:10039'
] ]
} }
node { node {
@ -140,7 +142,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [ extraConfig = [
custom: [ custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ],
'h2Settings.address' : 'localhost:10040'
] ]
} }
node { node {
@ -158,7 +161,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [ extraConfig = [
custom: [ custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ],
'h2Settings.address' : 'localhost:10041'
] ]
} }
} }

View File

@ -64,6 +64,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10003" address "localhost:10003"
adminAddress "localhost:10004" adminAddress "localhost:10004"
} }
extraConfig = ['h2Settings.address' : 'localhost:10014']
cordapps = ["$project.group:finance:$corda_release_version"] cordapps = ["$project.group:finance:$corda_release_version"]
} }
node { node {
@ -75,6 +76,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10006" address "localhost:10006"
adminAddress "localhost:10007" adminAddress "localhost:10007"
} }
extraConfig = ['h2Settings.address' : 'localhost:10015']
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
@ -85,6 +87,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10009" address "localhost:10009"
adminAddress "localhost:10010" adminAddress "localhost:10010"
} }
extraConfig = ['h2Settings.address' : 'localhost:10016']
} }
node { node {
name "O=BankOfCorda,L=New York,C=US" name "O=BankOfCorda,L=New York,C=US"
@ -95,6 +98,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10012" address "localhost:10012"
adminAddress "localhost:10013" adminAddress "localhost:10013"
} }
extraConfig = ['h2Settings.address' : 'localhost:10017']
} }
} }

View File

@ -138,6 +138,18 @@ class UnstartedMockNode private constructor(private val node: InternalMockNetwor
* @return A [StartedMockNode] object. * @return A [StartedMockNode] object.
*/ */
fun start(): StartedMockNode = StartedMockNode.create(node.start()) fun start(): StartedMockNode = StartedMockNode.create(node.start())
/**
* A [StartedMockNode] object for this running node.
* @throws [IllegalStateException] if the node is not running yet.
*/
val started: StartedMockNode
get() = StartedMockNode.create(node.started ?: throw IllegalStateException("Node ID=$id is not running"))
/**
* Whether this node has been started yet.
*/
val isStarted: Boolean get() = node.started != null
} }
/** A class that represents a started mock node for testing. */ /** A class that represents a started mock node for testing. */
@ -421,7 +433,7 @@ open class MockNetwork(
forcedID: Int? = null, forcedID: Int? = null,
entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()),
configOverrides: (NodeConfiguration) -> Any? = {}, configOverrides: (NodeConfiguration) -> Any? = {},
additionalCordapps: Set<TestCorDapp> = emptySet()): UnstartedMockNode { additionalCordapps: Set<TestCorDapp>): UnstartedMockNode {
val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps) val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps)
return UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(InternalMockNodeParameters(parameters))) return UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(InternalMockNodeParameters(parameters)))
} }

View File

@ -157,7 +157,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()),
val networkParameters: NetworkParameters = testNetworkParameters(), val networkParameters: NetworkParameters = testNetworkParameters(),
val defaultFactory: (MockNodeArgs, CordappLoader?) -> MockNode = { args, cordappLoader -> cordappLoader?.let { MockNode(args, it) } ?: MockNode(args) }, val defaultFactory: (MockNodeArgs, CordappLoader?) -> MockNode = { args, cordappLoader -> cordappLoader?.let { MockNode(args, it) } ?: MockNode(args) },
val cordappsForAllNodes: Set<TestCorDapp> = emptySet()) { val cordappsForAllNodes: Set<TestCorDapp> = emptySet()) : AutoCloseable {
init { init {
// Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS.
// This SFTP support loads BouncyCastle, which we want to avoid. // This SFTP support loads BouncyCastle, which we want to avoid.
@ -347,7 +347,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
} }
} }
override val started: TestStartedNode? get() = uncheckedCast(super.started) override val started: TestStartedNode? get() = super.started
override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): TestStartedNode { override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): TestStartedNode {
return TestStartedNodeImpl( return TestStartedNodeImpl(
@ -562,6 +562,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
fun waitQuiescent() { fun waitQuiescent() {
busyLatch.await() busyLatch.await()
} }
override fun close() = stopNodes()
} }
abstract class MessagingServiceSpy { abstract class MessagingServiceSpy {

View File

@ -0,0 +1,47 @@
package net.corda.testing.node
import net.corda.testing.core.*
import org.assertj.core.api.Assertions.*
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
class MockNetworkTest {
private companion object {
private const val NODE_ID = 101
}
private lateinit var mockNetwork: MockNetwork
@Before
fun setup() {
mockNetwork = MockNetwork(cordappPackages = emptyList())
}
@After
fun done() {
mockNetwork.stopNodes()
}
@Test
fun `with a started node`() {
val unstarted = mockNetwork.createUnstartedNode(DUMMY_BANK_A_NAME, forcedID = NODE_ID)
assertFalse(unstarted.isStarted)
mockNetwork.startNodes()
assertTrue(unstarted.isStarted)
val started = unstarted.started
assertEquals(NODE_ID, started.id)
assertEquals(DUMMY_BANK_A_NAME, started.info.identityFromX500Name(DUMMY_BANK_A_NAME).name)
assertFailsWith<IllegalArgumentException> { started.info.identityFromX500Name(DUMMY_BANK_B_NAME) }
}
@Test
fun `with an unstarted node`() {
val unstarted = mockNetwork.createUnstartedNode(DUMMY_BANK_A_NAME, forcedID = NODE_ID)
val ex = assertFailsWith<IllegalStateException> { unstarted.started }
assertThat(ex).hasMessage("Node ID=$NODE_ID is not running")
}
}

View File

@ -34,6 +34,7 @@ dependencies {
compile 'com.nhaarman:mockito-kotlin:1.5.0' compile 'com.nhaarman:mockito-kotlin:1.5.0'
compile "org.mockito:mockito-core:$mockito_version" compile "org.mockito:mockito-core:$mockito_version"
compile "org.assertj:assertj-core:$assertj_version" compile "org.assertj:assertj-core:$assertj_version"
compile "com.natpryce:hamkrest:$hamkrest_version"
// Guava: Google test library (collections test suite) // Guava: Google test library (collections test suite)
compile "com.google.guava:guava-testlib:$guava_version" compile "com.google.guava:guava-testlib:$guava_version"

View File

@ -35,6 +35,7 @@ import java.math.BigInteger
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
/** /**
@ -118,6 +119,18 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party
return getTestPartyAndCertificate(Party(name, publicKey)) return getTestPartyAndCertificate(Party(name, publicKey))
} }
private val count = AtomicInteger(0)
/**
* Randomise a party name to avoid clashes with other tests
*/
fun makeUnique(name: CordaX500Name) = name.copy(commonName =
if (name.commonName == null) {
count.incrementAndGet().toString()
} else {
"${ name.commonName }_${ count.incrementAndGet() }"
})
/** /**
* A class that encapsulates a test identity containing a [CordaX500Name] and a [KeyPair], alongside a range * A class that encapsulates a test identity containing a [CordaX500Name] and a [KeyPair], alongside a range
* of utility methods for use during testing. * of utility methods for use during testing.

View File

@ -298,7 +298,7 @@ data class TestLedgerDSLInterpreter private constructor(
copy().dsl() copy().dsl()
override fun attachment(attachment: InputStream): SecureHash { override fun attachment(attachment: InputStream): SecureHash {
return services.attachments.importAttachment(attachment, UNKNOWN_UPLOADER, null) return services.attachments.importAttachment(attachment, "TestDSL", null)
} }
override fun verifies(): EnforceVerifyOrFail { override fun verifies(): EnforceVerifyOrFail {

View File

@ -13,12 +13,11 @@ package net.corda.testing.internal
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.TEST_UPLOADER import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentStorage
import net.corda.node.cordapp.CordappLoader import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import java.nio.file.Paths import java.nio.file.Paths
@ -60,7 +59,7 @@ class MockCordappProvider(
return if (!existingAttachment.isEmpty()) { return if (!existingAttachment.isEmpty()) {
existingAttachment.keys.first() existingAttachment.keys.first()
} else { } else {
attachments.importContractAttachment(contractClassNames, TEST_UPLOADER, data.inputStream()) attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream())
} }
} }
} }

View File

@ -0,0 +1,99 @@
package net.corda.testing.internal.matchers
import com.natpryce.hamkrest.*
internal fun indent(description: String) = description.lineSequence().map { "\t$it" }.joinToString("\n")
fun hasEntrySetSize(expected: Int) = object : Matcher<Map<*, *>> {
override val description = "is a map of size $expected"
override fun invoke(actual: Map<*, *>) =
if (actual.size == expected) {
MatchResult.Match
} else {
MatchResult.Mismatch("was a map of size ${actual.size}")
}
}
fun <T> Matcher<T>.redescribe(redescriber: (String) -> String) = object : Matcher<T> {
override val description = redescriber(this@redescribe.description)
override fun invoke(actual: T) = this@redescribe(actual)
}
fun <T> Matcher<T>.redescribeMismatch(redescriber: (String) -> String) = object : Matcher<T> {
override val description = this@redescribeMismatch.description
override fun invoke(actual: T) = this@redescribeMismatch(actual).modifyMismatchDescription(redescriber)
}
fun MatchResult.modifyMismatchDescription(modify: (String) -> String) = when(this) {
is MatchResult.Match -> MatchResult.Match
is MatchResult.Mismatch -> MatchResult.Mismatch(modify(this.description))
}
fun <O, I> Matcher<I>.extrude(projection: (O) -> I) = object : Matcher<O> {
override val description = this@extrude.description
override fun invoke(actual: O) = this@extrude(projection(actual))
}
internal fun <K, V> hasAnEntry(key: K, valueMatcher: Matcher<V>) = object : Matcher<Map<K, V>> {
override val description = "$key: ${valueMatcher.description}"
override fun invoke(actual: Map<K, V>): MatchResult =
actual[key]?.let { valueMatcher(it) }?.let { when(it) {
is MatchResult.Match -> it
is MatchResult.Mismatch -> MatchResult.Mismatch("$key: ${it.description}")
}} ?: MatchResult.Mismatch("$key was not present")
}
fun <K, V> hasEntry(key: K, valueMatcher: Matcher<V>) =
hasAnEntry(key, valueMatcher).redescribe { "Is a map containing the entry:\n${indent(it)}"}
fun <K, V> hasOnlyEntries(vararg entryMatchers: Pair<K, Matcher<V>>) = hasOnlyEntries(entryMatchers.toList())
fun <K, V> hasOnlyEntries(entryMatchers: Collection<Pair<K, Matcher<V>>>) =
allOf(
hasEntrySetSize(entryMatchers.size),
hasEntries(entryMatchers)
)
fun <K, V> hasEntries(vararg entryMatchers: Pair<K, Matcher<V>>) = hasEntries(entryMatchers.toList())
fun <K, V> hasEntries(entryMatchers: Collection<Pair<K, Matcher<V>>>) = object : Matcher<Map<K, V>> {
override val description =
"is a map containing the entries:\n" +
entryMatchers.asSequence()
.joinToString("\n") { indent("${it.first}: ${it.second.description}") }
override fun invoke(actual: Map<K, V>): MatchResult {
val mismatches = entryMatchers.map { hasAnEntry(it.first, it.second)(actual) }
.filterIsInstance<MatchResult.Mismatch>()
return if (mismatches.isEmpty()) {
MatchResult.Match
} else {
MatchResult.Mismatch(
"had entries which did not meet criteria:\n" +
mismatches.joinToString("\n") { indent(it.description) })
}
}
}
fun <T> allOf(vararg matchers: Matcher<T>) = allOf(matchers.toList())
fun <T> allOf(matchers: Collection<Matcher<T>>) = object : Matcher<T> {
override val description =
"meets all of the criteria:\n" +
matchers.asSequence()
.joinToString("\n") { indent(it.description) }
override fun invoke(actual: T) : MatchResult {
val mismatches = matchers.map { it(actual) }
.filterIsInstance<MatchResult.Mismatch>()
return if (mismatches.isEmpty()) {
MatchResult.Match
} else {
MatchResult.Mismatch(
"did not meet criteria:\n" +
mismatches.joinToString("\n") { indent(it.description) })
}
}
}

View File

@ -0,0 +1,38 @@
package net.corda.testing.internal.matchers.flow
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.internal.FlowStateMachine
import net.corda.testing.internal.matchers.*
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(): Matcher<FlowStateMachine<T>> = net.corda.testing.internal.matchers.future.willReturn<T>()
.extrude(FlowStateMachine<T>::resultFuture)
.redescribe { "is a flow that will return" }
fun <T> willReturn(expected: T): Matcher<FlowStateMachine<T>> = willReturn(equalTo(expected))
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = net.corda.testing.internal.matchers.future.willReturn(successMatcher)
.extrude(FlowStateMachine<out T>::resultFuture)
.redescribe { "is a flow that will return with a value that ${successMatcher.description}" }
/**
* Matches a Flow that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) =
net.corda.testing.internal.matchers.future.willThrow(failureMatcher)
.extrude(FlowStateMachine<*>::resultFuture)
.redescribe { "is a flow that will fail, throwing an exception that ${failureMatcher.description}" }
/**
* Matches a Flow that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() =
net.corda.testing.internal.matchers.future.willThrow<E>()
.extrude(FlowStateMachine<*>::resultFuture)
.redescribe { "is a flow that will fail with an exception of type ${E::class.java.simpleName}" }

View File

@ -1,9 +1,10 @@
package net.corda.core.flows.matchers package net.corda.testing.internal.matchers.future
import com.natpryce.hamkrest.MatchResult import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.equalTo
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.testing.internal.matchers.modifyMismatchDescription
import java.util.concurrent.Future import java.util.concurrent.Future
/** /**
@ -16,7 +17,7 @@ fun <T> willReturn() = object : Matcher<Future<T>> {
actual.getOrThrow() actual.getOrThrow()
MatchResult.Match MatchResult.Match
} catch (e: Exception) { } catch (e: Exception) {
MatchResult.Mismatch("Failed with $e") MatchResult.Mismatch("failed with $e")
} }
} }
@ -29,9 +30,9 @@ fun <T> willReturn(successMatcher: Matcher<T>) = object : Matcher<Future<out T>>
override val description: String = "is a future that will succeed with a value that ${successMatcher.description}" override val description: String = "is a future that will succeed with a value that ${successMatcher.description}"
override fun invoke(actual: Future<out T>): MatchResult = try { override fun invoke(actual: Future<out T>): MatchResult = try {
successMatcher(actual.getOrThrow()) successMatcher(actual.getOrThrow()).modifyMismatchDescription { "succeeded with value that $it" }
} catch (e: Exception) { } catch (e: Exception) {
MatchResult.Mismatch("Failed with $e") MatchResult.Mismatch("failed with $e")
} }
} }
@ -44,11 +45,11 @@ inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = object
override fun invoke(actual: Future<*>): MatchResult = try { override fun invoke(actual: Future<*>): MatchResult = try {
actual.getOrThrow() actual.getOrThrow()
MatchResult.Mismatch("Succeeded") MatchResult.Mismatch("succeeded")
} catch (e: Exception) { } catch (e: Exception) {
when(e) { when(e) {
is E -> failureMatcher(e) is E -> failureMatcher(e).modifyMismatchDescription { "failed with ${E::class.java.simpleName} that $it" }
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") else -> MatchResult.Mismatch("failed with ${e.javaClass}")
} }
} }
} }
@ -62,11 +63,11 @@ inline fun <reified E: Exception> willThrow() = object : Matcher<Future<*>> {
override fun invoke(actual: Future<*>): MatchResult = try { override fun invoke(actual: Future<*>): MatchResult = try {
actual.getOrThrow() actual.getOrThrow()
MatchResult.Mismatch("Succeeded") MatchResult.Mismatch("succeeded")
} catch (e: Exception) { } catch (e: Exception) {
when(e) { when(e) {
is E -> MatchResult.Match is E -> MatchResult.Match
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") else -> MatchResult.Mismatch("failed with ${e.javaClass}")
} }
} }
} }

View File

@ -0,0 +1,36 @@
package net.corda.testing.internal.matchers.rpc
import com.natpryce.hamkrest.Matcher
import net.corda.core.messaging.FlowHandle
import net.corda.testing.internal.matchers.extrude
import net.corda.testing.internal.matchers.redescribe
/**
* Matches a flow handle that succeeds with a result matched by the given matcher
*/
fun <T> willReturn() = net.corda.testing.internal.matchers.future.willReturn<T>()
.extrude(FlowHandle<T>::returnValue)
.redescribe { "is an RPG flow handle that will return" }
/**
* Matches a flow handle that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = net.corda.testing.internal.matchers.future.willReturn(successMatcher)
.extrude(FlowHandle<T>::returnValue)
.redescribe { "is an RPG flow handle that will return a value that ${successMatcher.description}" }
/**
* Matches a flow handle that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) =
net.corda.testing.internal.matchers.future.willThrow(failureMatcher)
.extrude(FlowHandle<*>::returnValue)
.redescribe { "is an RPG flow handle that will fail with an exception that ${failureMatcher.description}" }
/**
* Matches a flow handle that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() =
net.corda.testing.internal.matchers.future.willThrow<E>()
.extrude(FlowHandle<*>::returnValue)
.redescribe { "is an RPG flow handle that will fail with an exception of type ${E::class.java.simpleName}" }

View File

@ -0,0 +1,58 @@
package net.corda.testing.internal
import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.equalTo
import net.corda.testing.internal.matchers.hasEntries
import org.junit.Test
import kotlin.test.assertEquals
class MatcherTests {
@Test
fun `nested items indent`() {
val nestedMap = mapOf(
"a" to mapOf(
"apple" to "vegetable",
"aardvark" to "animal",
"anthracite" to "mineral"),
"b" to mapOf(
"broccoli" to "mineral",
"bison" to "animal",
"bauxite" to "vegetable")
)
val matcher = hasEntries(
"a" to hasEntries(
"aardvark" to equalTo("animal"),
"anthracite" to equalTo("mineral")
),
"b" to hasEntries(
"bison" to equalTo("animal"),
"bauxite" to equalTo("mineral")
)
)
println(matcher.description)
println((matcher(nestedMap) as MatchResult.Mismatch).description)
assertEquals(
"""
is a map containing the entries:
a: is a map containing the entries:
aardvark: is equal to "animal"
anthracite: is equal to "mineral"
b: is a map containing the entries:
bison: is equal to "animal"
bauxite: is equal to "mineral"
""".trimIndent().replace(" ", "\t"),
matcher.description)
assertEquals(
"""
had entries which did not meet criteria:
b: had entries which did not meet criteria:
bauxite: was: "vegetable"
""".trimIndent().replace(" ", "\t"),
(matcher(nestedMap) as MatchResult.Mismatch).description
)
}
}