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

View File

@ -10,55 +10,60 @@
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.utilities.getOrThrow
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.startFlow
import org.junit.Before
import org.junit.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 {
private lateinit var mockNet: InternalMockNetwork
companion object {
private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
@Before
fun setup() {
// We run this in parallel threads to help catch any race conditions that may exist.
mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
@AfterClass
@JvmStatic
fun tearDown() = mockNet.stopNodes()
}
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
fun `issue key`() {
// Set up values we'll need
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val alice = aliceNode.info.singleIdentity()
val bob = bobNode.services.myInfo.singleIdentity()
// Run the flows
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)).resultFuture
// Get the results
val actual: Map<Party, AnonymousParty> = requesterFlow.getOrThrow().toMap()
assertEquals(2, actual.size)
// Verify that the generated anonymous identities do not match the well known identities
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException()
val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException()
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()
assert.that(
aliceNode.services.startFlow(SwapIdentitiesFlow(bob)),
willReturn(
hasOnlyEntries(
alice to allOf(
!equalTo<AbstractParty>(alice),
aliceNode.resolvesToWellKnownParty(alice),
aliceNode.holdsOwningKey(),
!bobNode.holdsOwningKey()
),
bob to allOf(
!equalTo<AbstractParty>(bob),
bobNode.resolvesToWellKnownParty(bob),
bobNode.holdsOwningKey(),
!aliceNode.holdsOwningKey()
)
)
)
)
}
/**
@ -66,58 +71,101 @@ class SwapIdentitiesFlowTests {
*/
@Test
fun `verifies identity name`() {
// Set up values we'll need
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
val bob: Party = bobNode.services.myInfo.singleIdentity()
val notBob = charlieNode.database.transaction {
charlieNode.services.keyManagementService.freshKeyAndCert(charlieNode.services.myInfo.singleIdentityAndCert(), false)
val notBob = charlieNode.issueFreshKeyAndCert()
val signature = charlieNode.signSwapIdentitiesFlowData(notBob, notBob.owningKey)
assertFailsWith<SwapIdentitiesException>(
"Certificate subject must match counterparty's well known identity.") {
aliceNode.validateSwapIdentitiesFlow(bob, notBob, signature)
}
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.
*/
@Test
fun `verifies signature`() {
// Set up values we'll need
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)
fun `verification rejects signature if name is right but key is wrong`() {
val evilBobNode = mockNet.createPartyNode(bobNode.info.singleIdentity().name)
val evilBob = evilBobNode.info.singleIdentityAndCert()
evilBobNode.database.transaction {
val anonymousEvilBob = evilBobNode.services.keyManagementService.freshKeyAndCert(evilBob, false)
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())
}
}
val anonymousEvilBob = evilBobNode.issueFreshKeyAndCert()
val signature = evilBobNode.signSwapIdentitiesFlowData(evilBob, anonymousEvilBob.owningKey)
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.ContractAttachment
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.SerializedBytes
import net.corda.core.serialization.deserialize
@ -18,8 +18,9 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransa
fun toLedgerTransaction(): LedgerTransaction {
val deps = dependencies.map { it.deserialize() }.associateBy(WireTransaction::id)
val attachments = attachments.map { it.deserialize<Attachment>() }
val attachmentMap = attachments.mapNotNull { it as? MockContractAttachment }
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader=TEST_UPLOADER) }
val attachmentMap = attachments
.mapNotNull { it as? MockContractAttachment }
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) }
val contractAttachmentMap = emptyMap<ContractClassName, ContractAttachment>()
@Suppress("DEPRECATION")
return wtxToVerify.deserialize().toLedgerTransaction(

View File

@ -26,15 +26,14 @@ import java.security.CodeSigner
import java.security.cert.X509Certificate
import java.util.jar.JarInputStream
// Possible attachment uploaders
const val DEPLOYED_CORDAPP_UPLOADER = "app"
const val RPC_UPLOADER = "rpc"
const val TEST_UPLOADER = "test"
const val P2P_UPLOADER = "p2p"
const val UNKNOWN_UPLOADER = "unknown"
fun isUploaderTrusted(uploader: String?) =
uploader?.let { it in listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, TEST_UPLOADER) } ?: false
private val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)
fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS
@KeepForDJVM
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 }
/** 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. */
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 createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext {
return CordappContext(cordapp, attachmentId, classLoader, config)
}
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
*/
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> {
val payloadData: T = try {
val serializer = SerializationDefaults.SERIALIZATION_FACTORY

View File

@ -10,6 +10,7 @@
package net.corda.core.node
import net.corda.core.CordaRuntimeException
import net.corda.core.KeepForDJVM
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
@ -115,4 +116,10 @@ data class NetworkParameters(
*/
@KeepForDJVM
@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
import co.paralleluniverse.strands.Strand
import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.*
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.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.ensureMinimumPlatformVersion
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub
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.KeyManagementService
import net.corda.core.serialization.SerializationContext
@ -84,7 +87,7 @@ open class TransactionBuilder @JvmOverloads constructor(
for (t in items) {
when (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 TransactionState<*> -> addOutputState(t)
is StateAndContract -> addOutputState(t.state, t.contract)
@ -105,11 +108,18 @@ open class TransactionBuilder @JvmOverloads constructor(
* [HashAttachmentConstraint].
*
* @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)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services)
@CordaInternal
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.
// 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 {
state.constraint !== AutomaticHashConstraint -> state
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint)
else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let {
state.copy(constraint = HashAttachmentConstraint(it))
} ?: throw MissingContractAttachments(listOf(state))
else -> {
services.cordappProvider.getContractAttachmentID(state.contract)?.let {
state.copy(constraint = HashAttachmentConstraint(it))
} ?: throw MissingContractAttachments(listOf(state))
}
}
}
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.
*
* This feature was added in version 4 of Corda, so will throw an exception for any Corda networks with a minimum
* platform version less than 4.
*
* @throws UncheckedVersionException
* Note: Reference states are only supported on Corda networks running a minimum platform version of 4.
* [toWireTransaction] will throw an [IllegalStateException] if called in such an environment.
*/
@Deprecated(message = "Feature not yet released. Pending stabilisation.")
open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder {
val stateAndRef = referencedStateAndRef.stateAndRef
referencesWithTransactionState.add(stateAndRef.state)
@ -293,10 +313,10 @@ open class TransactionBuilder @JvmOverloads constructor(
return this
}
/** Returns an immutable list of input [StateRefs]. */
/** Returns an immutable list of input [StateRef]s. */
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)
/** 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
* [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 signableData = SignableData(wtx.id, signatureMetadata)
val sig = keyManagementService.sign(signableData, publicKey)

View File

@ -15,8 +15,8 @@ import com.natpryce.hamkrest.*
import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithMockNet
import net.corda.core.identity.CordaX500Name
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.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.makeUnique
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
@ -130,13 +131,13 @@ class AttachmentTests : WithMockNet {
//region Generators
override fun makeNode(name: CordaX500Name) =
mockNet.createPartyNode(randomise(name)).apply {
mockNet.createPartyNode(makeUnique(name)).apply {
registerInitiatedFlow(FetchAttachmentsResponse::class.java)
}
// Makes a node that doesn't do sanity checking at load time.
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
InternalMockNodeParameters(legalName = randomise(name)),
InternalMockNodeParameters(legalName = makeUnique(name)),
nodeFactory = { args, _ ->
object : InternalMockNetwork.MockNode(args) {
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.StateAndContract
import net.corda.core.contracts.requireThat
import net.corda.core.flows.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts
import net.corda.core.identity.CordaX500Name
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.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.matchers.rpc.willReturn
import net.corda.core.flows.matchers.rpc.willThrow
import net.corda.testing.internal.matchers.rpc.willReturn
import net.corda.testing.internal.matchers.rpc.willThrow
import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality
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.assertion.assert
import net.corda.core.contracts.*
import net.corda.core.flows.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality
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.
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
willReturn(
aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction()))
willReturn(
aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction()))
}
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.assertion.assert
import net.corda.core.flows.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction

View File

@ -12,7 +12,7 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
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.identity.Party
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.transactions.SignedTransaction
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.TestStartedNode
import net.corda.testing.node.internal.startFlow
import java.util.*
import kotlin.reflect.KClass
/**
@ -25,12 +25,7 @@ interface WithMockNet {
/**
* Create a node using a randomised version of the given name
*/
fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(randomise(name))
/**
* Randomise a party name to avoid clashes with other tests
*/
fun randomise(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(makeUnique(name))
/**
* 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
----------
* 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.
* "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``.
* 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
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
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:

View File

@ -105,7 +105,7 @@ absolute path to the node's base directory.
: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.

View File

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

View File

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

View File

@ -46,7 +46,6 @@ class WorkflowTransactionBuildTutorialTest {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobNode = mockNet.createPartyNode(BOB_NAME)
aliceNode.registerInitiatedFlow(RecordCompletionFlow::class.java)
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_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-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
|____cordapps
|____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
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
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"
}
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
h2Settings {
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
will only be accessible on localhost.

View File

@ -3,7 +3,24 @@ Node 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:
* 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
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:

View File

@ -42,7 +42,7 @@ class AddressBindingFailureTests: IntegrationTest() {
@Test
fun `H2 address`() {
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?>) {

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 {
val bob = startNode(BOB_NAME)
bob.registerInitiatedFlow(ReceiveFlow::class.java)
val bobParty = bob.info.singleIdentity()
// Perform a protocol exchange to force the peer queue to be created
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 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
}
@ -254,10 +254,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private fun initKeyStore(): X509Certificate {
if (configuration.devMode) {
log.warn("The Corda node is running in developer mode. This is not suitable for production usage.")
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()
}
@ -347,12 +344,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
services.start(nodeInfo, netParams)
networkMapUpdater.start(trustRoot, signedNetParams.raw.hash, signedNodeInfo.raw.hash)
startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams)
// Do all of this in a database transaction so anything that might need a connection has one.
return database.transaction {
services.start(nodeInfo, netParams)
identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts)
attachments.start()
cordappProvider.start(netParams.whitelistedContractImplementations)
@ -692,6 +689,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} else {
Observable.empty()
}
check(initiatingFlowClass !in flowFactories.keys) {
"$initiatingFlowClass is attempting to register multiple initiated flows"
}
flowFactories[initiatingFlowClass] = flowFactory
return observable
}
@ -772,7 +772,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
val props = configuration.dataSourceProperties
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.
logVendorString(database, log)
}
@ -941,7 +941,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val transactionVerifierService: TransactionVerifierService get() = this@AbstractNode.transactionVerifierService
override val contractUpgradeService: ContractUpgradeService get() = this@AbstractNode.contractUpgradeService
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 clock: Clock get() = platformClock
override val configuration: NodeConfiguration get() = this@AbstractNode.configuration
@ -1038,7 +1038,7 @@ fun configureDatabase(hikariProperties: Properties,
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
schemaService: SchemaService = NodeSchemaService()): CordaPersistence =
createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService)
.apply { hikariStart(hikariProperties, databaseConfig, schemaService) }
.apply { startHikariPool(hikariProperties, databaseConfig, schemaService) }
fun createCordaPersistence(databaseConfig: DatabaseConfig,
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
@ -1053,7 +1053,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
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 {
val dataSource = DataSourceFactory.createDataSource(hikariProperties)
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.STRUCTURAL_STEP_PREFIX
import net.corda.core.internal.sign
import net.corda.core.messaging.CordaRPCOps
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.messaging.*
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.AttachmentQueryCriteria
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.node.services.vault.*
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
@ -85,11 +71,10 @@ internal class CordaRPCOpsImpl(
}
override fun acceptNewNetworkParameters(parametersHash: SecureHash) {
services.networkMapUpdater.acceptNewNetworkParameters(
parametersHash,
// TODO When multiple identities design will be better specified this should be signature from node operator.
{ hash -> hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) } }
)
// TODO When multiple identities design will be better specified this should be signature from node operator.
services.networkMapUpdater.acceptNewNetworkParameters(parametersHash) { hash ->
hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) }
}
}
override fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> {
@ -206,7 +191,7 @@ internal class CordaRPCOpsImpl(
}
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 {

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.crypto.X509Utilities
import net.corda.serialization.internal.*
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import org.h2.jdbc.JdbcSQLException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -71,11 +72,13 @@ import rx.Observable
import rx.Scheduler
import rx.schedulers.Schedulers
import java.net.BindException
import java.net.InetAddress
import java.nio.file.Path
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
import javax.management.ObjectName
import kotlin.system.exitProcess
import java.nio.file.Paths
class NodeWithInfo(val node: Node, val info: NodeInfo) {
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)) {
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 (!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 baseDir = Paths.get(databaseName).parent.toString()
val server = org.h2.tools.Server.createTcpServer(
"-tcpPort", effectiveH2Settings.address.port.toString(),
"-tcpAllowOthers",
"-tcpDaemon",
"-baseDir", baseDir,
"-key", "node", databaseName)
// override interface that createTcpServer listens on (which is always 0.0.0.0)
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.saveToTrustStore
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.persistence.DatabaseMigrationException
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
@ -337,6 +337,8 @@ open class NodeStartup(val args: Array<String>) {
Emoji.renderIfSupported {
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()
@ -352,7 +354,7 @@ open class NodeStartup(val args: Array<String>) {
if (conf.shouldStartLocalShell()) {
node.startupComplete.then {
try {
InteractiveShell.runLocalShell({ node.stop() })
InteractiveShell.runLocalShell(node::stop)
} catch (e: Throwable) {
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.utilities.contextLogger
import net.corda.node.cordapp.CordappLoader
import net.corda.node.services.persistence.AttachmentStorageInternal
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
@ -99,7 +100,13 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
cordapps.filter { !it.contractClassNames.isEmpty() }.map {
it.jarPath.openStream().use { stream ->
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) {
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.messaging.MessagingService
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.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -115,6 +116,7 @@ interface ServiceHubInternal : ServiceHub {
}
}
override val attachments: AttachmentStorageInternal
override val vaultService: VaultServiceInternal
/**
* 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(
}
}
/**
* Extremely simple in-memory cache of the network map.
*/
/** Database-based network map cache. */
@ThreadSafe
open class PersistentNetworkMapCache(private val database: CordaPersistence) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal {
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.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.readFully
import net.corda.core.internal.*
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.AttachmentSort
import net.corda.core.serialization.*
@ -60,10 +56,12 @@ class NodeAttachmentService(
private val database: CordaPersistence,
attachmentContentCacheSize: Long = NodeConfiguration.defaultAttachmentContentCacheSize,
attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound
) : AttachmentStorage, SingletonSerializeAsToken() {
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
companion object {
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.
// 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.
@ -238,10 +236,9 @@ class NodeAttachmentService(
}
}
private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>(
attachmentCacheBound,
{ key -> Optional.ofNullable(createAttachment(key)) }
)
private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>(attachmentCacheBound) { key ->
Optional.ofNullable(createAttachment(key))
}
private fun createAttachment(key: SecureHash): Attachment? {
val content = attachmentContentCache.get(key)!!
@ -268,6 +265,18 @@ class NodeAttachmentService(
}
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)
}
@ -292,7 +301,13 @@ class NodeAttachmentService(
if (!hasAttachment(id)) {
checkIsAValidJAR(bytes.inputStream())
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)
attachmentCount.inc()
log.info("Stored new attachment $id")
@ -305,10 +320,12 @@ class NodeAttachmentService(
}
@Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId = try {
import(jar, UNKNOWN_UPLOADER, null)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
return try {
import(jar, UNKNOWN_UPLOADER, null)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
}
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {

View File

@ -95,10 +95,10 @@ class NodeVaultService(
log.trace { "State update of type: $concreteType" }
val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) }
if (!seen) {
val contractInterfaces = deriveContractInterfaces(concreteType)
contractInterfaces.map {
val contractInterface = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
contractInterface.add(concreteType.name)
val contractTypes = deriveContractTypes(concreteType)
contractTypes.map {
val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
contractStateType.add(concreteType.name)
}
}
}
@ -541,10 +541,10 @@ class NodeVaultService(
null
}
concreteType?.let {
val contractInterfaces = deriveContractInterfaces(it)
contractInterfaces.map {
val contractInterface = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
contractInterface.add(it.name)
val contractTypes = deriveContractTypes(it)
contractTypes.map {
val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
contractStateType.add(it.name)
}
}
}
@ -553,14 +553,20 @@ class NodeVaultService(
}
}
private fun <T : ContractState> deriveContractInterfaces(clazz: Class<T>): Set<Class<T>> {
val myInterfaces: MutableSet<Class<T>> = mutableSetOf()
clazz.interfaces.forEach {
if (it != ContractState::class.java) {
myInterfaces.add(uncheckedCast(it))
myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it)))
private fun <T : ContractState> deriveContractTypes(clazz: Class<T>): Set<Class<T>> {
val myTypes : MutableSet<Class<T>> = mutableSetOf()
clazz.superclass?.let {
if (!it.isInstance(Any::class)) {
myTypes.add(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
import co.paralleluniverse.fibers.Suspendable
import com.codahale.metrics.MetricRegistry
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.*
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.Builder
import net.corda.core.node.services.vault.Sort
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.configureDatabase
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.internal.LogHelper
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.Before
import org.junit.Ignore
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException
import java.nio.file.FileSystem
@ -40,7 +48,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class NodeAttachmentStorageTest {
class NodeAttachmentServiceTest {
// Use an in memory file system for testing attachment storage.
private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence
@ -195,7 +203,7 @@ class NodeAttachmentStorageTest {
@Ignore("We need to be able to restart nodes - make importing attachments idempotent?")
@Test
fun `duplicates not allowed`() {
val (testJar, _) = makeTestJar()
val (testJar) = makeTestJar()
testJar.read {
storage.importAttachment(it, "test", null)
}
@ -208,7 +216,7 @@ class NodeAttachmentStorageTest {
@Test
fun `corrupt entry throws exception`() {
val (testJar, _) = makeTestJar()
val (testJar) = makeTestJar()
val id = database.transaction {
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 fun makeTestJar(extraEntries: List<Pair<String, String>> = emptyList()): Pair<Path, SecureHash> {
counter++
val file = fs.getPath("$counter.jar")
file.write {
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()
}
makeTestJar(file.outputStream(), extraEntries)
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 {
LogHelper.setLevel("+net.corda.flow")
}
}
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>()
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>()
@BeforeClass
@JvmStatic
fun beforeClass() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
@Before
fun setUpMockNet() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@AfterClass @JvmStatic
fun afterClass() {
mockNet.stopNodes()
}
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@After
fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear()
}
@ -484,45 +478,38 @@ class FlowFrameworkTripartyTests {
private lateinit var charlie: Party
private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>()
}
@BeforeClass
@JvmStatic
fun beforeClass() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
@Before
fun setUpGlobalMockNet() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
@AfterClass @JvmStatic
fun afterClass() {
mockNet.stopNodes()
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
@After
fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear()
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@Test
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.crypto.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.packageName
import net.corda.core.node.services.*
import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.*
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.testing.core.*
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.vault.*
import net.corda.testing.node.MockServices
@ -125,7 +128,8 @@ open class VaultQueryTestRule : ExternalResource(), VaultQueryParties {
"net.corda.finance.contracts",
CashSchemaV1::class.packageName,
DummyLinearStateSchemaV1::class.packageName,
SampleCashSchemaV3::class.packageName)
SampleCashSchemaV3::class.packageName,
VaultQueryTestsBase.MyContractClass::class.packageName)
override lateinit var services: MockServices
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
fun `unconsumed states simple`() {
database.transaction {

View File

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

View File

@ -54,10 +54,12 @@ class BankOfCordaCordform : CordformDefinition() {
adminAddress("localhost:10004")
}
devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:10016"))
}
node {
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)
rpcSettings {
address("localhost:$BOC_RPC_PORT")
@ -77,6 +79,7 @@ class BankOfCordaCordform : CordformDefinition() {
webPort(10010)
rpcUsers(User(BIGCORP_RPC_USER, BIGCORP_RPC_PWD, setOf(all())))
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
adminPort 10004
}
extraConfig = ['h2Settings.address' : 'localhost:10005']
}
node {
name "O=Bank A,L=London,C=GB"
p2pPort 10005
p2pPort 10006
cordapps = []
rpcUsers = ext.rpcUsers
// This configures the default cordapp for this node
@ -46,10 +47,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10007
adminPort 10008
}
extraConfig = ['h2Settings.address' : 'localhost:10009']
}
node {
name "O=Bank B,L=New York,C=US"
p2pPort 10009
p2pPort 10010
cordapps = []
rpcUsers = ext.rpcUsers
// This configures the default cordapp for this node
@ -60,5 +62,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10011
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"]
rpcUsers = rpcUsersList
useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10024']
}
node {
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"]
rpcUsers = rpcUsersList
useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10027']
}
node {
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"]
rpcUsers = rpcUsersList
useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10030']
}
node {
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"]
rpcUsers = rpcUsersList
useTestClock true
extraConfig = ['h2Settings.address' : 'localhost:10033']
}
}

View File

@ -24,7 +24,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10003
adminPort 10004
}
h2Port 20004
extraConfig = ['h2Settings.address' : 'localhost:20004']
}
node {
name "O=Bank A,L=London,C=GB"
@ -35,6 +35,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10007
adminPort 10008
}
extraConfig = ['h2Settings.address' : 'localhost:0']
}
node {
name "O=Bank B,L=New York,C=US"
@ -45,5 +46,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
port 10011
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,
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 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)
devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
}
node {
name(BOB_NAME)
@ -54,11 +55,13 @@ class BFTNotaryCordform : CordformDefinition() {
adminAddress("localhost:10106")
}
devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
}
val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) }
fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node {
name(notaryNames[replicaId])
notary(NotaryConfig(validating = false, serviceLegalName = clusterName, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses)))
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
configure()
}
notaryNode(0) {

View File

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

View File

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

View File

@ -39,6 +39,7 @@ class SingleNotaryCordform : CordformDefinition() {
}
rpcUsers(notaryDemoUser)
devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
}
node {
name(BOB_NAME)
@ -48,6 +49,7 @@ class SingleNotaryCordform : CordformDefinition() {
adminAddress("localhost:10106")
}
devMode(true)
extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0"))
}
node {
name(DUMMY_NOTARY_NAME)
@ -58,6 +60,7 @@ class SingleNotaryCordform : CordformDefinition() {
}
notary(NotaryConfig(validating = 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 = [
custom: [
jvmArgs: ["-Xmx1g"]
]
],
'h2Settings.address' : 'localhost:10038'
]
}
node {
@ -122,7 +123,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
],
'h2Settings.address' : 'localhost:10039'
]
}
node {
@ -140,7 +142,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
],
'h2Settings.address' : 'localhost:10040'
]
}
node {
@ -158,7 +161,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
],
'h2Settings.address' : 'localhost:10041'
]
}
}

View File

@ -64,6 +64,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10003"
adminAddress "localhost:10004"
}
extraConfig = ['h2Settings.address' : 'localhost:10014']
cordapps = ["$project.group:finance:$corda_release_version"]
}
node {
@ -75,6 +76,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10006"
adminAddress "localhost:10007"
}
extraConfig = ['h2Settings.address' : 'localhost:10015']
}
node {
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"
adminAddress "localhost:10010"
}
extraConfig = ['h2Settings.address' : 'localhost:10016']
}
node {
name "O=BankOfCorda,L=New York,C=US"
@ -95,6 +98,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address "localhost:10012"
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.
*/
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. */
@ -421,7 +433,7 @@ open class MockNetwork(
forcedID: Int? = null,
entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()),
configOverrides: (NodeConfiguration) -> Any? = {},
additionalCordapps: Set<TestCorDapp> = emptySet()): UnstartedMockNode {
additionalCordapps: Set<TestCorDapp>): UnstartedMockNode {
val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps)
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 networkParameters: NetworkParameters = testNetworkParameters(),
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 {
// Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS.
// 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 {
return TestStartedNodeImpl(
@ -562,6 +562,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
fun waitQuiescent() {
busyLatch.await()
}
override fun close() = stopNodes()
}
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 "org.mockito:mockito-core:$mockito_version"
compile "org.assertj:assertj-core:$assertj_version"
compile "com.natpryce:hamkrest:$hamkrest_version"
// Guava: Google test library (collections test suite)
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.PublicKey
import java.security.cert.X509Certificate
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
/**
@ -118,6 +119,18 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party
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
* of utility methods for use during testing.

View File

@ -298,7 +298,7 @@ data class TestLedgerDSLInterpreter private constructor(
copy().dsl()
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 {

View File

@ -13,12 +13,11 @@ package net.corda.testing.internal
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
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.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.services.MockAttachmentStorage
import java.nio.file.Paths
@ -60,7 +59,7 @@ class MockCordappProvider(
return if (!existingAttachment.isEmpty()) {
existingAttachment.keys.first()
} 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.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.utilities.getOrThrow
import net.corda.testing.internal.matchers.modifyMismatchDescription
import java.util.concurrent.Future
/**
@ -16,7 +17,7 @@ fun <T> willReturn() = object : Matcher<Future<T>> {
actual.getOrThrow()
MatchResult.Match
} 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 fun invoke(actual: Future<out T>): MatchResult = try {
successMatcher(actual.getOrThrow())
successMatcher(actual.getOrThrow()).modifyMismatchDescription { "succeeded with value that $it" }
} 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 {
actual.getOrThrow()
MatchResult.Mismatch("Succeeded")
MatchResult.Mismatch("succeeded")
} catch (e: Exception) {
when(e) {
is E -> failureMatcher(e)
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
is E -> failureMatcher(e).modifyMismatchDescription { "failed with ${E::class.java.simpleName} that $it" }
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 {
actual.getOrThrow()
MatchResult.Mismatch("Succeeded")
MatchResult.Mismatch("succeeded")
} catch (e: Exception) {
when(e) {
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
)
}
}