Merge pull request #1285 from corda/2018-07-19

OS merge 2018-07-19
This commit is contained in:
Michele Sollecito 2018-07-20 14:12:57 +01:00 committed by GitHub
commit fae30c8703
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1249 additions and 753 deletions

View File

@ -51,6 +51,7 @@ buildscript {
ext.fileupload_version = '1.3.3'
ext.junit_version = '4.12'
ext.mockito_version = '2.18.3'
ext.hamkrest_version = '1.4.2.2'
ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'

View File

@ -23,7 +23,7 @@ import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.eventually
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.freePort
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.testThreadFactory
import net.corda.testing.node.internal.*
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
@ -47,7 +47,10 @@ class RPCStabilityTests {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
private val portAllocation = PortAllocation.Incremental(10000)
@After
fun shutdown() {
pool.shutdown()
@ -87,7 +90,6 @@ class RPCStabilityTests {
private fun runBlockAndCheckThreads(block: () -> Unit) {
val executor = Executors.newScheduledThreadPool(1)
try {
// Warm-up so that all the thread pools & co. created
block()
@ -100,7 +102,7 @@ class RPCStabilityTests {
// This is a less than check because threads from other tests may be shutting down while this test is running.
// This is therefore a "best effort" check. When this test is run on its own this should be a strict equality.
// In case of failure we output the threads along with their stacktraces to get an idea what was running at a time.
require(threadsBefore.keys.size >= threadsAfter.keys.size, { "threadsBefore: $threadsBefore\nthreadsAfter: $threadsAfter" })
require(threadsBefore.keys.size >= threadsAfter.keys.size) { "threadsBefore: $threadsBefore\nthreadsAfter: $threadsAfter" }
} finally {
executor.shutdownNow()
}
@ -337,7 +339,7 @@ class RPCStabilityTests {
@Test
fun `client throws RPCException after initial connection attempt fails`() {
val client = CordaRPCClient(NetworkHostAndPort("localhost", freePort()))
val client = CordaRPCClient(portAllocation.nextHostAndPort())
var exceptionMessage: String? = null
try {
client.start("user", "pass").proxy

View File

@ -81,6 +81,9 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Hamkrest, for fluent, composable matchers
testCompile "com.natpryce:hamkrest:$hamkrest_version"
// Quasar, for suspendable fibres.
compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") {
transitive = false

View File

@ -70,6 +70,7 @@ data class NetworkParameters(
require(epoch > 0) { "epoch must be at least 1" }
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" }
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
}

View File

@ -10,8 +10,8 @@
package net.corda.core.contracts
import net.corda.finance.*
import net.corda.core.contracts.Amount.Companion.sumOrZero
import net.corda.finance.*
import org.junit.Test
import java.math.BigDecimal
import java.util.*

View File

@ -11,13 +11,18 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
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.core.flows.mixins.WithMockNet
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.hash
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.testing.core.ALICE_NAME
@ -25,122 +30,89 @@ import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.startFlow
import org.junit.After
import org.junit.Before
import org.junit.AfterClass
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class AttachmentTests {
lateinit var mockNet: InternalMockNetwork
class AttachmentTests : WithMockNet {
companion object {
val classMockNet = InternalMockNetwork()
@Before
fun setUp() {
mockNet = InternalMockNetwork()
@JvmStatic
@AfterClass
fun cleanUp() = classMockNet.stopNodes()
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
override val mockNet = classMockNet
private fun fakeAttachment(): ByteArray {
val bs = ByteArrayOutputStream()
val js = JarOutputStream(bs)
js.putNextEntry(ZipEntry("file1.txt"))
js.writer().apply { append("Some useful content"); flush() }
js.closeEntry()
js.close()
return bs.toByteArray()
}
// Test nodes
private val aliceNode = makeNode(ALICE_NAME)
private val bobNode = makeNode(BOB_NAME)
private val alice = aliceNode.info.singleIdentity()
@Test
fun `download and store`() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val alice = aliceNode.info.singleIdentity()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction {
aliceNode.attachments.importAttachment(fakeAttachment().inputStream(), "test", null)
}
val id = aliceNode.importAttachment(fakeAttachment())
// Get node one to run a flow to fetch it and insert it.
mockNet.runNetwork()
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
mockNet.runNetwork()
assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size)
assert.that(
bobNode.startAttachmentFlow(id, alice),
willReturn(noAttachments()))
// Verify it was inserted into node one's store.
val attachment = bobNode.database.transaction {
bobNode.attachments.openAttachment(id)!!
}
assertEquals(id, attachment.open().hash())
val attachment = bobNode.getAttachmentWithId(id)
assert.that(attachment, hashesTo(id))
// Shut down node zero and ensure node one can still resolve the attachment.
aliceNode.dispose()
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow()
assertEquals(attachment, response.fromDisk[0])
assert.that(
bobNode.startAttachmentFlow(id, alice),
willReturn(soleAttachment(attachment)))
}
@Test
fun missing() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
val hash: SecureHash = SecureHash.randomSHA256()
// Get node one to fetch a non-existent attachment.
val hash = SecureHash.randomSHA256()
val alice = aliceNode.info.singleIdentity()
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice)
mockNet.runNetwork()
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.resultFuture.getOrThrow() }
assertEquals(hash, e.requested)
assert.that(
bobNode.startAttachmentFlow(hash, alice),
willThrow(withRequestedHash(hash)))
}
fun withRequestedHash(expected: SecureHash) = has(
"requested hash",
FetchDataFlow.HashNotFound::requested,
equalTo(expected))
@Test
fun maliciousResponse() {
// Make a node that doesn't do sanity checking at load time.
val aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME), nodeFactory = { args ->
object : InternalMockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
})
val bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
val alice = aliceNode.info.singleIdentity()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
val attachment = fakeAttachment()
val badAliceNode = makeBadNode(ALICE_NAME)
val badAlice = badAliceNode.info.singleIdentity()
// Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction {
aliceNode.attachments.importAttachment(attachment.inputStream(), "test", null)
}
val attachment = fakeAttachment()
val id = badAliceNode.importAttachment(attachment)
// Corrupt its store.
val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
aliceNode.database.transaction {
session.update(corruptAttachment)
}
badAliceNode.updateAttachment(corruptAttachment)
// Get n1 to fetch the attachment. Should receive corrupted bytes.
mockNet.runNetwork()
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
mockNet.runNetwork()
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.resultFuture.getOrThrow() }
assert.that(
bobNode.startAttachmentFlow(id, badAlice),
willThrow<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
)
}
private fun StartedNode<*>.startAttachmentFlow(hashes: Set<SecureHash>, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes))
@InitiatingFlow
private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() {
@Suspendable
@ -155,4 +127,62 @@ class AttachmentTests {
@Suspendable
override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
}
//region Generators
override fun makeNode(name: CordaX500Name) =
mockNet.createPartyNode(randomise(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)),
nodeFactory = { args ->
object : InternalMockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
private fun fakeAttachment(): ByteArray =
ByteArrayOutputStream().use { baos ->
JarOutputStream(baos).use { jos ->
jos.putNextEntry(ZipEntry("file1.txt"))
jos.writer().apply {
append("Some useful content")
flush()
}
jos.closeEntry()
}
baos.toByteArray()
}
//endregion
//region Operations
private fun StartedNode<*>.importAttachment(attachment: ByteArray) =
attachments.importAttachment(attachment.inputStream(), "test", null)
.andRunNetwork()
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) = database.transaction {
session.update(attachment)
}.andRunNetwork()
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = startFlowAndRunNetwork(
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash)))
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) =
attachments.openAttachment(id)!!
//endregion
//region Matchers
private fun noAttachments() = has(FetchDataFlow.Result<Attachment>::fromDisk, isEmpty)
private fun soleAttachment(attachment: Attachment) = has(FetchDataFlow.Result<Attachment>::fromDisk,
hasSize(equalTo(1)) and
hasElement(attachment))
private fun hashesTo(hash: SecureHash) = has<Attachment, SecureHash>(
"hash",
{ it.open().hash() },
equalTo(hash))
//endregion
}

View File

@ -11,64 +11,105 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
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.core.flows.mixins.WithContracts
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.excludeHostNode
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.singleIdentity
import net.corda.testing.core.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
import net.corda.testing.node.internal.startFlow
import org.junit.After
import org.junit.Before
import org.junit.AfterClass
import org.junit.Test
import kotlin.test.assertFailsWith
class CollectSignaturesFlowTests {
class CollectSignaturesFlowTests : WithContracts {
companion object {
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
private val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock())
private val classMockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.flows"))
private const val MAGIC_NUMBER = 1337
@JvmStatic
@AfterClass
fun tearDown() = classMockNet.stopNodes()
}
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: StartedNode<MockNode>
private lateinit var bobNode: StartedNode<MockNode>
private lateinit var charlieNode: StartedNode<MockNode>
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var charlie: Party
private lateinit var notary: Party
override val mockNet = classMockNet
@Before
fun setup() {
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.flows"))
aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobNode = mockNet.createPartyNode(BOB_NAME)
charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity()
notary = mockNet.defaultNotaryIdentity
private val aliceNode = makeNode(ALICE_NAME)
private val bobNode = makeNode(BOB_NAME)
private val charlieNode = makeNode(CHARLIE_NAME)
private val alice = aliceNode.info.singleIdentity()
private val bob = bobNode.info.singleIdentity()
private val charlie = charlieNode.info.singleIdentity()
@Test
fun `successfully collects three signatures`() {
val bConfidentialIdentity = bobNode.createConfidentialIdentity(bob)
aliceNode.verifyAndRegister(bConfidentialIdentity)
assert.that(
aliceNode.startTestFlow(alice, bConfidentialIdentity.party, charlie),
willReturn(requiredSignatures(3))
)
}
@After
fun tearDown() {
mockNet.stopNodes()
@Test
fun `no need to collect any signatures`() {
val ptx = aliceNode.signDummyContract(alice.ref(1))
assert.that(
aliceNode.collectSignatures(ptx),
willReturn(requiredSignatures(1))
)
}
@Test
fun `fails when not signed by initiator`() {
val ptx = miniCorpServices.signDummyContract(alice.ref(1))
assert.that(
aliceNode.collectSignatures(ptx),
willThrow(errorMessage("The Initiator of CollectSignaturesFlow must have signed the transaction.")))
}
@Test
fun `passes with multiple initial signatures`() {
val signedByA = aliceNode.signDummyContract(
alice.ref(1),
MAGIC_NUMBER,
bob.ref(2),
bob.ref(3))
val signedByBoth = bobNode.addSignatureTo(signedByA)
assert.that(
aliceNode.collectSignatures(signedByBoth),
willReturn(requiredSignatures(2))
)
}
//region Operators
private fun StartedNode<*>.startTestFlow(vararg party: Party) =
startFlowAndRunNetwork(
TestFlow.Initiator(DummyContract.MultiOwnerState(
MAGIC_NUMBER,
listOf(*party)),
mockNet.defaultNotaryIdentity))
//region Test Flow
// With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to
// override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are
// receiving off the wire.
@ -99,7 +140,7 @@ class CollectSignaturesFlowTests {
"There should only be one output state" using (tx.outputs.size == 1)
"There should only be one output state" using (tx.inputs.isEmpty())
val magicNumberState = ltx.outputsOfType<DummyContract.MultiOwnerState>().single()
"Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337)
"Must be $MAGIC_NUMBER or greater" using (magicNumberState.magicNumber >= MAGIC_NUMBER)
}
}
@ -108,64 +149,5 @@ class CollectSignaturesFlowTests {
}
}
}
@Test
fun `successfully collects two signatures`() {
val bConfidentialIdentity = bobNode.database.transaction {
val bobCert = bobNode.services.myInfo.legalIdentitiesAndCerts.single { it.name == bob.name }
bobNode.services.keyManagementService.freshKeyAndCert(bobCert, false)
}
aliceNode.database.transaction {
// Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity
aliceNode.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity)
}
val magicNumber = 1337
val parties = listOf(alice, bConfidentialIdentity.party, charlie)
val state = DummyContract.MultiOwnerState(magicNumber, parties)
val flow = aliceNode.services.startFlow(TestFlow.Initiator(state, notary))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
result.verifyRequiredSignatures()
println(result.tx)
println(result.sigs)
}
@Test
fun `no need to collect any signatures`() {
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract)
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
result.verifyRequiredSignatures()
println(result.tx)
println(result.sigs)
}
@Test
fun `fails when not signed by initiator`() {
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock())
val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
mockNet.runNetwork()
assertFailsWith<IllegalArgumentException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
flow.resultFuture.getOrThrow()
}
}
@Test
fun `passes with multiple initial signatures`() {
val twoPartyDummyContract = DummyContract.generateInitial(1337, notary,
alice.ref(1),
bob.ref(2),
bob.ref(3))
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
val signedByBoth = bobNode.services.addSignature(signedByA)
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet()))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
println(result.tx)
println(result.sigs)
}
//region
}

View File

@ -0,0 +1,141 @@
package net.corda.core.flows
import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.anything
import com.natpryce.hamkrest.assertion.assert
import com.natpryce.hamkrest.has
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.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.User
import net.corda.testing.node.internal.*
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
import org.junit.AfterClass
import org.junit.Test
class ContractUpgradeFlowRPCTest : WithContracts, WithFinality {
companion object {
private val classMockNet = InternalMockNetwork(cordappPackages = listOf(
"net.corda.testing.contracts",
"net.corda.finance.contracts.asset",
"net.corda.core.flows"))
@JvmStatic
@AfterClass
fun tearDown() = classMockNet.stopNodes()
}
override val mockNet = classMockNet
private val aliceNode = makeNode(ALICE_NAME)
private val bobNode = makeNode(BOB_NAME)
private val alice = aliceNode.info.singleIdentity()
private val bob = bobNode.info.singleIdentity()
@Test
fun `2 parties contract upgrade using RPC`() = rpcDriver {
val testUser = createTestUser()
val rpcA = startProxy(aliceNode, testUser)
val rpcB = startProxy(bobNode, testUser)
// Create, sign and finalise dummy contract.
val signedByA = aliceNode.signDummyContract(alice.ref(1), 0, bob.ref(1))
val stx = bobNode.addSignatureTo(signedByA)
assert.that(rpcA.finalise(stx, bob), willReturn())
val atx = aliceNode.getValidatedTransaction(stx)
val btx = bobNode.getValidatedTransaction(stx)
// Cannot upgrade contract without prior authorisation from counterparty
assert.that(
rpcA.initiateDummyContractUpgrade(atx),
willThrow<CordaRuntimeException>())
// Party B authorises the contract state upgrade, and immediately deauthorises the same.
assert.that(rpcB.authoriseDummyContractUpgrade(btx), willReturn())
assert.that(rpcB.deauthoriseContractUpgrade(btx), willReturn())
// Cannot upgrade contract if counterparty has deauthorised a previously-given authority
assert.that(
rpcA.initiateDummyContractUpgrade(atx),
willThrow<CordaRuntimeException>())
// Party B authorise the contract state upgrade.
assert.that(rpcB.authoriseDummyContractUpgrade(btx), willReturn())
// Party A initiates contract upgrade flow, expected to succeed this time.
assert.that(
rpcA.initiateDummyContractUpgrade(atx),
willReturn(
aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction()))
}
//region RPC DSL
private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps {
return startRpcClient<CordaRPCOps>(
rpcAddress = startRpcServer(
rpcUser = user,
ops = node.rpcOps
).get().broker.hostAndPort!!,
username = user.username,
password = user.password
).get()
}
private fun RPCDriverDSL.createTestUser() = rpcTestUser.copy(permissions = setOf(
startFlow<WithFinality.FinalityInvoker>(),
startFlow<ContractUpgradeFlow.Initiate<*, *>>(),
startFlow<ContractUpgradeFlow.Authorise>(),
startFlow<ContractUpgradeFlow.Deauthorise>()
))
//endregion
//region Operations
private fun CordaRPCOps.initiateDummyContractUpgrade(tx: SignedTransaction) =
initiateContractUpgrade(tx, DummyContractV2::class)
private fun CordaRPCOps.authoriseDummyContractUpgrade(tx: SignedTransaction) =
authoriseContractUpgrade(tx, DummyContractV2::class)
//endregion
//region Matchers
private fun StartedNode<*>.hasDummyContractUpgradeTransaction() =
hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()
private inline fun <reified FROM : Any, reified TO: Any> StartedNode<*>.hasContractUpgradeTransaction() =
has<StateAndRef<ContractState>, ContractUpgradeLedgerTransaction>(
"a contract upgrade transaction",
{ getContractUpgradeTransaction(it) },
isUpgrade<FROM, TO>())
private fun StartedNode<*>.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
services.validatedTransactions.getTransaction(state.ref.txhash)!!
.resolveContractUpgradeTransaction(services)
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
private inline fun <reified T: Any> isUpgradeFrom() =
has<ContractUpgradeLedgerTransaction, Any>("input data", { it.inputs.single().state.data }, isA<T>(anything))
private inline fun <reified T: Any> isUpgradeTo() =
has<ContractUpgradeLedgerTransaction, Any>("output data", { it.outputs.single().data }, isA<T>(anything))
//endregion
}

View File

@ -10,18 +10,18 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.CordaRuntimeException
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.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.queryBy
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.USD
@ -29,219 +29,126 @@ import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.exceptions.InternalNodeException
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.User
import net.corda.testing.node.internal.*
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
import org.junit.After
import org.junit.Before
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.junit.AfterClass
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class ContractUpgradeFlowTest {
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: StartedNode<MockNode>
private lateinit var bobNode: StartedNode<MockNode>
private lateinit var notary: Party
private lateinit var alice: Party
private lateinit var bob: Party
class ContractUpgradeFlowTest : WithContracts, WithFinality {
companion object {
private val classMockNet = InternalMockNetwork(cordappPackages = listOf(
"net.corda.testing.contracts",
"net.corda.finance.contracts.asset",
"net.corda.core.flows"))
@Before
fun setup() {
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows", "net.corda.finance.schemas"))
aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobNode = mockNet.createPartyNode(BOB_NAME)
notary = mockNet.defaultNotaryIdentity
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
// Process registration
mockNet.runNetwork()
@JvmStatic
@AfterClass
fun tearDown() = classMockNet.stopNodes()
}
@After
fun tearDown() {
mockNet.stopNodes()
}
override val mockNet = classMockNet
private val aliceNode = makeNode(ALICE_NAME)
private val bobNode = makeNode(BOB_NAME)
private val alice = aliceNode.info.singleIdentity()
private val bob = bobNode.info.singleIdentity()
private val notary = mockNet.defaultNotaryIdentity
@Test
fun `2 parties contract upgrade`() {
// Create dummy contract.
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
val stx = bobNode.services.addSignature(signedByA)
val signedByA = aliceNode.signDummyContract(alice.ref(1),0, bob.ref(1))
val stx = bobNode.addSignatureTo(signedByA)
aliceNode.services.startFlow(FinalityFlow(stx, setOf(bob)))
mockNet.runNetwork()
aliceNode.finalise(stx, bob)
val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) }
val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) }
requireNotNull(atx)
requireNotNull(btx)
val atx = aliceNode.getValidatedTransaction(stx)
val btx = bobNode.getValidatedTransaction(stx)
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
val rejectedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java))
mockNet.runNetwork()
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.resultFuture.getOrThrow() }
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
willThrow<UnexpectedFlowEndException>())
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).resultFuture.getOrThrow()
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
assert.that(bobNode.deauthoriseContractUpgrade(btx), willReturn())
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
mockNet.runNetwork()
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.resultFuture.getOrThrow() }
// The request is expected to be rejected because party B has subsequently deauthorised a previously authorised upgrade.
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
willThrow<UnexpectedFlowEndException>())
// Party B authorise the contract state upgrade
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
// Party A initiates contract upgrade flow, expected to succeed this time.
val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
mockNet.runNetwork()
val result = resultFuture.resultFuture.getOrThrow()
fun check(node: StartedNode<MockNode>) {
val upgradeTx = node.database.transaction {
val wtx = node.services.validatedTransactions.getTransaction(result.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(node.services)
}
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
}
check(aliceNode)
check(bobNode)
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
willReturn(
aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction()))
}
private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps {
return startRpcClient<CordaRPCOps>(
rpcAddress = startRpcServer(
rpcUser = user,
ops = node.rpcOps
).get().broker.hostAndPort!!,
username = user.username,
password = user.password
).get()
}
private fun StartedNode<*>.issueCash(amount: Amount<Currency> = Amount(1000, USD)) =
services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary))
.andRunNetwork()
.resultFuture.getOrThrow()
@Test
fun `2 parties contract upgrade using RPC`() {
rpcDriver {
// Create dummy contract.
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
val stx = bobNode.services.addSignature(signedByA)
private fun StartedNode<*>.getBaseStateFromVault() = getStateFromVault(ContractState::class)
val user = rpcTestUser.copy(permissions = setOf(
startFlow<FinalityInvoker>(),
startFlow<ContractUpgradeFlow.Initiate<*, *>>(),
startFlow<ContractUpgradeFlow.Authorise>(),
startFlow<ContractUpgradeFlow.Deauthorise>()
))
val expectedExceptionClass = if (aliceNode.internals.configuration.devMode) CordaRuntimeException::class else InternalNodeException::class
val rpcA = startProxy(aliceNode, user)
val rpcB = startProxy(bobNode, user)
val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bob))
mockNet.runNetwork()
handle.returnValue.getOrThrow()
private fun StartedNode<*>.getCashStateFromVault() = getStateFromVault(CashV2.State::class)
val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) }
val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) }
requireNotNull(atx)
requireNotNull(btx)
private fun hasIssuedAmount(expected: Amount<Issued<Currency>>) =
hasContractState(has(CashV2.State::amount, equalTo(expected)))
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
atx!!.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
private fun belongsTo(vararg recipients: AbstractParty) =
hasContractState(has(CashV2.State::owners, equalTo(recipients.toList())))
mockNet.runNetwork()
assertFailsWith(expectedExceptionClass) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
btx!!.tx.outRef<ContractState>(0),
DummyContractV2::class.java).returnValue
rpcB.startFlow({ stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) },
btx.tx.outRef<ContractState>(0).ref).returnValue
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
val deauthorisedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
atx.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
mockNet.runNetwork()
assertFailsWith(expectedExceptionClass) { deauthorisedFuture.getOrThrow() }
// Party B authorise the contract state upgrade.
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
btx.tx.outRef<ContractState>(0),
DummyContractV2::class.java).returnValue
// Party A initiates contract upgrade flow, expected to succeed this time.
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
atx.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
mockNet.runNetwork()
val result = resultFuture.getOrThrow()
// Check results.
listOf(aliceNode, bobNode).forEach {
val upgradeTx = aliceNode.database.transaction {
val wtx = aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(aliceNode.services)
}
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
}
}
}
private fun <T : ContractState> hasContractState(expectation: Matcher<T>) =
has<StateAndRef<T>, T>(
"contract state",
{ it.state.data },
expectation)
@Test
fun `upgrade Cash to v2`() {
// Create some cash.
val chosenIdentity = alice
val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary))
mockNet.runNetwork()
val stx = result.resultFuture.getOrThrow().stx
val anonymisedRecipient = result.resultFuture.get().recipient!!
val stateAndRef = stx.tx.outRef<Cash.State>(0)
val baseState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<ContractState>().states.single() }
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
// Starts contract upgrade flow.
val upgradeResult = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java))
mockNet.runNetwork()
upgradeResult.resultFuture.getOrThrow()
// Get contract state from the vault.
val upgradedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<CashV2.State>().states.single() }
assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), upgradedStateFromVault.state.data.amount, "Upgraded cash contain the correct amount.")
assertEquals<Collection<AbstractParty>>(listOf(anonymisedRecipient), upgradedStateFromVault.state.data.owners, "Upgraded cash belongs to the right owner.")
// Make sure the upgraded state can be spent
val movedState = upgradedStateFromVault.state.data.copy(amount = upgradedStateFromVault.state.data.amount.times(2))
val spendUpgradedTx = aliceNode.services.signInitialTransaction(
TransactionBuilder(notary)
.addInputState(upgradedStateFromVault)
.addOutputState(
upgradedStateFromVault.state.copy(data = movedState)
)
.addCommand(CashV2.Move(), alice.owningKey)
val cashFlowResult = aliceNode.issueCash()
val anonymisedRecipient = cashFlowResult.recipient!!
val stateAndRef = cashFlowResult.stx.tx.outRef<Cash.State>(0)
)
aliceNode.services.startFlow(FinalityFlow(spendUpgradedTx)).resultFuture.apply {
mockNet.runNetwork()
get()
// The un-upgraded state is Cash.State
assert.that(aliceNode.getBaseStateFromVault(), hasContractState(isA<Cash.State>(anything)))
// Starts contract upgrade flow.
assert.that(aliceNode.initiateContractUpgrade(stateAndRef, CashV2::class), willReturn())
// Get contract state from the vault.
val upgradedState = aliceNode.getCashStateFromVault()
assert.that(upgradedState,
hasIssuedAmount(Amount(1000000, USD) `issued by` (alice.ref(1)))
and belongsTo(anonymisedRecipient))
// Make sure the upgraded state can be spent
val movedState = upgradedState.state.data.copy(amount = upgradedState.state.data.amount.times(2))
val spendUpgradedTx = aliceNode.signInitialTransaction {
addInputState(upgradedState)
addOutputState(
upgradedState.state.copy(data = movedState)
)
addCommand(CashV2.Move(), alice.owningKey)
}
val movedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<CashV2.State>().states.single() }
assertEquals(movedState, movedStateFromVault.state.data)
assert.that(aliceNode.finalise(spendUpgradedTx), willReturn())
assert.that(aliceNode.getCashStateFromVault(), hasContractState(equalTo(movedState)))
}
class CashV2 : UpgradedContractWithLegacyConstraint<Cash.State, CashV2.State> {
@ -266,10 +173,35 @@ class ContractUpgradeFlowTest {
override fun verify(tx: LedgerTransaction) {}
}
@StartableByRPC
class FinalityInvoker(private val transaction: SignedTransaction,
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
}
//region Operations
private fun StartedNode<*>.initiateDummyContractUpgrade(tx: SignedTransaction) =
initiateContractUpgrade(tx, DummyContractV2::class)
private fun StartedNode<*>.authoriseDummyContractUpgrade(tx: SignedTransaction) =
authoriseContractUpgrade(tx, DummyContractV2::class)
//endregion
//region Matchers
private fun StartedNode<*>.hasDummyContractUpgradeTransaction() =
hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()
private inline fun <reified FROM : Any, reified TO: Any> StartedNode<*>.hasContractUpgradeTransaction() =
has<StateAndRef<ContractState>, ContractUpgradeLedgerTransaction>(
"a contract upgrade transaction",
{ getContractUpgradeTransaction(it) },
isUpgrade<FROM, TO>())
private fun StartedNode<*>.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
services.validatedTransactions.getTransaction(state.ref.txhash)!!
.resolveContractUpgradeTransaction(services)
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
private inline fun <reified T: Any> isUpgradeFrom() =
has<ContractUpgradeLedgerTransaction, Any>("input data", { it.inputs.single().state.data }, isA<T>(anything))
private inline fun <reified T: Any> isUpgradeTo() =
has<ContractUpgradeLedgerTransaction, Any>("output data", { it.outputs.single().data }, isA<T>(anything))
//endregion
}

View File

@ -23,7 +23,6 @@ import net.corda.core.internal.rootCause
import net.corda.core.utilities.getOrThrow
import org.assertj.core.api.Assertions.catchThrowable
import org.hamcrest.Matchers.lessThanOrEqualTo
import org.junit.After
import org.junit.Assert.assertThat
import org.junit.Test
import java.util.*
@ -41,30 +40,9 @@ class FastThreadLocalTest {
}
private val expensiveObjCount = AtomicInteger()
private lateinit var pool: ExecutorService
private lateinit var scheduler: FiberExecutorScheduler
private fun init(threadCount: Int, threadImpl: (Runnable) -> Thread) {
pool = Executors.newFixedThreadPool(threadCount, threadImpl)
scheduler = FiberExecutorScheduler(null, pool)
}
@After
fun poolShutdown() = try {
pool.shutdown()
} catch (e: UninitializedPropertyAccessException) {
// Do nothing.
}
@After
fun schedulerShutdown() = try {
scheduler.shutdown()
} catch (e: UninitializedPropertyAccessException) {
// Do nothing.
}
@Test
fun `ThreadLocal with plain old Thread is fiber-local`() {
init(3, ::Thread)
fun `ThreadLocal with plain old Thread is fiber-local`() = scheduled(3, ::Thread) {
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
override fun initialValue() = ExpensiveObj()
}
@ -73,8 +51,7 @@ class FastThreadLocalTest {
}
@Test
fun `ThreadLocal with FastThreadLocalThread is fiber-local`() {
init(3, ::FastThreadLocalThread)
fun `ThreadLocal with FastThreadLocalThread is fiber-local`() = scheduled(3, ::FastThreadLocalThread) {
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
override fun initialValue() = ExpensiveObj()
}
@ -83,8 +60,7 @@ class FastThreadLocalTest {
}
@Test
fun `FastThreadLocal with plain old Thread is fiber-local`() {
init(3, ::Thread)
fun `FastThreadLocal with plain old Thread is fiber-local`() = scheduled(3, ::Thread) {
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
override fun initialValue() = ExpensiveObj()
}
@ -93,8 +69,8 @@ class FastThreadLocalTest {
}
@Test
fun `FastThreadLocal with FastThreadLocalThread is not fiber-local`() {
init(3, ::FastThreadLocalThread)
fun `FastThreadLocal with FastThreadLocalThread is not fiber-local`() =
scheduled(3, ::FastThreadLocalThread) {
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
override fun initialValue() = ExpensiveObj()
}
@ -103,7 +79,7 @@ class FastThreadLocalTest {
}
/** @return the number of times a different expensive object was obtained post-suspend. */
private fun runFibers(fiberCount: Int, threadLocalGet: () -> ExpensiveObj): Int {
private fun SchedulerContext.runFibers(fiberCount: Int, threadLocalGet: () -> ExpensiveObj): Int {
val fibers = (0 until fiberCount).map { Fiber(scheduler, FiberTask(threadLocalGet)) }
val startedFibers = fibers.map { it.start() }
return startedFibers.map { it.get() }.count { it }
@ -137,8 +113,7 @@ class FastThreadLocalTest {
}::get)
}
private fun contentIsNotSerialized(threadLocalGet: () -> UnserializableObj) {
init(1, ::FastThreadLocalThread)
private fun contentIsNotSerialized(threadLocalGet: () -> UnserializableObj) = scheduled(1, ::FastThreadLocalThread) {
// Use false like AbstractKryoSerializationScheme, the default of true doesn't work at all:
val serializer = Fiber.getFiberSerializer(false)
val returnValue = UUID.randomUUID()
@ -172,4 +147,21 @@ class FastThreadLocalTest {
return returnValue
}
}
private data class SchedulerContext(private val pool: ExecutorService, val scheduler: FiberExecutorScheduler) {
fun shutdown() {
pool.shutdown()
scheduler.shutdown()
}
}
private fun scheduled(threadCount: Int, threadImpl: (Runnable) -> Thread, test: SchedulerContext.() -> Unit) {
val pool = Executors.newFixedThreadPool(threadCount, threadImpl)
val ctx = SchedulerContext(pool, FiberExecutorScheduler(null, pool))
try {
ctx.test()
} finally {
ctx.shutdown()
}
}
}

View File

@ -10,75 +10,69 @@
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.core.flows.mixins.WithFinality
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.finance.POUNDS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.issuedBy
import net.corda.node.internal.StartedNode
import net.corda.testing.core.*
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.StartedMockNode
import org.junit.After
import org.junit.Before
import net.corda.testing.node.internal.InternalMockNetwork
import org.junit.AfterClass
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class FinalityFlowTests {
class FinalityFlowTests : WithFinality {
companion object {
private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party
private val classMockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
@JvmStatic
@AfterClass
fun tearDown() = classMockNet.stopNodes()
}
private lateinit var mockNet: MockNetwork
private lateinit var aliceNode: StartedMockNode
private lateinit var bobNode: StartedMockNode
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var notary: Party
override val mockNet = classMockNet
@Before
fun setup() {
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobNode = mockNet.createPartyNode(BOB_NAME)
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
notary = mockNet.defaultNotaryIdentity
}
private val aliceNode = makeNode(ALICE_NAME)
private val bobNode = makeNode(BOB_NAME)
@After
fun tearDown() {
mockNet.stopNodes()
}
private val alice = aliceNode.info.singleIdentity()
private val bob = bobNode.info.singleIdentity()
private val notary = mockNet.defaultNotaryIdentity
@Test
fun `finalise a simple transaction`() {
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
val builder = TransactionBuilder(notary)
Cash().generateIssue(builder, amount, bob, notary)
val stx = aliceNode.services.signInitialTransaction(builder)
val flow = aliceNode.startFlow(FinalityFlow(stx))
mockNet.runNetwork()
val notarisedTx = flow.getOrThrow()
notarisedTx.verifyRequiredSignatures()
val transactionSeenByB = bobNode.transaction {
bobNode.services.validatedTransactions.getTransaction(notarisedTx.id)
}
assertEquals(notarisedTx, transactionSeenByB)
val stx = aliceNode.signCashTransactionWith(bob)
assert.that(
aliceNode.finalise(stx),
willReturn(
requiredSignatures(1)
and visibleTo(bobNode)))
}
@Test
fun `reject a transaction with unknown parties`() {
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them
val builder = TransactionBuilder(notary)
Cash().generateIssue(builder, amount, fakeIdentity, notary)
val stx = aliceNode.services.signInitialTransaction(builder)
val flow = aliceNode.startFlow(FinalityFlow(stx))
mockNet.runNetwork()
assertFailsWith<IllegalArgumentException> {
flow.getOrThrow()
}
// Charlie isn't part of this network, so node A won't recognise them
val stx = aliceNode.signCashTransactionWith(CHARLIE)
assert.that(
aliceNode.finalise(stx),
willThrow<IllegalArgumentException>())
}
private fun StartedNode<*>.signCashTransactionWith(other: Party): SignedTransaction {
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
val builder = TransactionBuilder(notary)
Cash().generateIssue(builder, amount, other, notary)
return services.signInitialTransaction(builder)
}
}

View File

@ -11,25 +11,34 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.assertion.assert
import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.isA
import net.corda.core.flows.matchers.flow.willReturn
import net.corda.core.flows.mixins.WithMockNet
import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.AfterClass
import org.junit.Test
class ReceiveMultipleFlowTests {
private val mockNet = InternalMockNetwork()
private val nodes = (0..2).map { mockNet.createPartyNode() }
@After
fun stopNodes() {
mockNet.stopNodes()
class ReceiveMultipleFlowTests : WithMockNet {
companion object {
private val classMockNet = InternalMockNetwork()
@JvmStatic
@AfterClass
fun stopNodes() = classMockNet.stopNodes()
}
override val mockNet = classMockNet
private val nodes = (0..2).map { mockNet.createPartyNode() }
@Test
fun showcase_flows_as_closures() {
val answer = 10.0
@ -59,10 +68,9 @@ class ReceiveMultipleFlowTests {
} as FlowLogic<Unit>
}
val flow = nodes[0].services.startFlow(initiatingFlow)
mockNet.runNetwork()
val receivedAnswer = flow.resultFuture.getOrThrow()
assertThat(receivedAnswer).isEqualTo(answer)
assert.that(
nodes[0].startFlowAndRunNetwork(initiatingFlow),
willReturn(answer as Any))
}
@Test
@ -71,10 +79,10 @@ class ReceiveMultipleFlowTests {
nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue)
val stringValue = "Thriller"
nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue)
val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
assertThat(result).isEqualTo(doubleValue * stringValue.length)
assert.that(
nodes[0].startFlowAndRunNetwork(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity())),
willReturn(doubleValue * stringValue.length))
}
@Test
@ -83,12 +91,10 @@ class ReceiveMultipleFlowTests {
nodes[1].registerAnswer(ParallelAlgorithmList::class, value1)
val value2 = 6.0
nodes[2].registerAnswer(ParallelAlgorithmList::class, value2)
val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
mockNet.runNetwork()
val data = flow.resultFuture.getOrThrow()
assertThat(data[0]).isEqualTo(value1)
assertThat(data[1]).isEqualTo(value2)
assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2)
assert.that(
nodes[0].startFlowAndRunNetwork(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity())),
willReturn(listOf(value1, value2)))
}
class ParallelAlgorithmMap(doubleMember: Party, stringMember: Party) : AlgorithmDefinition(doubleMember, stringMember) {

View File

@ -0,0 +1,72 @@
package net.corda.core.flows.matchers
import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.utilities.getOrThrow
import java.util.concurrent.Future
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn() = object : Matcher<Future<T>> {
override val description: String = "is a future that will succeed"
override fun invoke(actual: Future<T>): MatchResult = try {
actual.getOrThrow()
MatchResult.Match
} catch (e: Exception) {
MatchResult.Mismatch("Failed with $e")
}
}
fun <T> willReturn(expected: T): Matcher<Future<out T?>> = willReturn(equalTo(expected))
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
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())
} catch (e: Exception) {
MatchResult.Mismatch("Failed with $e")
}
}
/**
* Matches a Flow that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = object : Matcher<Future<*>> {
override val description: String
get() = "is a future that will fail with a ${E::class.java.simpleName} that ${failureMatcher.description}"
override fun invoke(actual: Future<*>): MatchResult = try {
actual.getOrThrow()
MatchResult.Mismatch("Succeeded")
} catch (e: Exception) {
when(e) {
is E -> failureMatcher(e)
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
}
}
}
/**
* Matches a Flow that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() = object : Matcher<Future<*>> {
override val description: String
get() = "is a future that will fail with a ${E::class.java}"
override fun invoke(actual: Future<*>): MatchResult = try {
actual.getOrThrow()
MatchResult.Mismatch("Succeeded")
} catch (e: Exception) {
when(e) {
is E -> MatchResult.Match
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
}
}
}

View File

@ -0,0 +1,36 @@
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.willThrow
import net.corda.core.flows.matchers.willReturn
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

@ -0,0 +1,31 @@
package net.corda.core.flows.matchers.rpc
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.has
import net.corda.core.flows.matchers.willThrow
import net.corda.core.flows.matchers.willReturn
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

@ -0,0 +1,83 @@
package net.corda.core.flows.mixins
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PartyAndReference
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.UpgradedContract
import net.corda.core.flows.CollectSignaturesFlow
import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.node.internal.StartedNode
import net.corda.testing.contracts.DummyContract
import kotlin.reflect.KClass
/**
* Mix this interface into a test class to get useful generator and operation functions for working with dummy contracts
*/
interface WithContracts : WithMockNet {
//region Generators
fun createDummyContract(owner: PartyAndReference, magicNumber: Int = 0, vararg others: PartyAndReference) =
DummyContract.generateInitial(
magicNumber,
mockNet.defaultNotaryIdentity,
owner,
*others)
//region
//region Operations
fun StartedNode<*>.signDummyContract(owner: PartyAndReference, magicNumber: Int = 0, vararg others: PartyAndReference) =
services.signDummyContract(owner, magicNumber, *others).andRunNetwork()
fun ServiceHub.signDummyContract(owner: PartyAndReference, magicNumber: Int = 0, vararg others: PartyAndReference) =
signInitialTransaction(createDummyContract(owner, magicNumber, *others))
fun StartedNode<*>.collectSignatures(ptx: SignedTransaction) =
startFlowAndRunNetwork(CollectSignaturesFlow(ptx, emptySet()))
fun StartedNode<*>.addSignatureTo(ptx: SignedTransaction) =
services.addSignature(ptx).andRunNetwork()
fun <T : UpgradedContract<*, *>>
StartedNode<*>.initiateContractUpgrade(tx: SignedTransaction, toClass: KClass<T>) =
initiateContractUpgrade(tx.tx.outRef(0), toClass)
fun <S : ContractState, T : UpgradedContract<S, *>>
StartedNode<*>.initiateContractUpgrade(stateAndRef: StateAndRef<S>, toClass: KClass<T>) =
startFlowAndRunNetwork(ContractUpgradeFlow.Initiate(stateAndRef, toClass.java))
fun <T : UpgradedContract<*, *>> StartedNode<*>.authoriseContractUpgrade(
tx: SignedTransaction, toClass: KClass<T>) =
startFlow(
ContractUpgradeFlow.Authorise(tx.tx.outRef<ContractState>(0), toClass.java)
)
fun StartedNode<*>.deauthoriseContractUpgrade(tx: SignedTransaction) = startFlow(
ContractUpgradeFlow.Deauthorise(tx.tx.outRef<ContractState>(0).ref)
)
// RPC versions of the above
fun <S : ContractState, T : UpgradedContract<S, *>> CordaRPCOps.initiateContractUpgrade(
tx: SignedTransaction, toClass: KClass<T>) =
startFlow(
{ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
tx.tx.outRef<S>(0),
toClass.java)
.andRunNetwork()
fun <S : ContractState, T : UpgradedContract<S, *>> CordaRPCOps.authoriseContractUpgrade(
tx: SignedTransaction, toClass: KClass<T>) =
startFlow(
{ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
tx.tx.outRef<S>(0),
toClass.java)
fun CordaRPCOps.deauthoriseContractUpgrade(tx: SignedTransaction) =
startFlow(
{ stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) },
tx.tx.outRef<ContractState>(0).ref)
//region
}

View File

@ -0,0 +1,44 @@
package net.corda.core.flows.mixins
import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.node.internal.StartedNode
import net.corda.testing.core.singleIdentity
interface WithFinality : WithMockNet {
//region Operations
fun StartedNode<*>.finalise(stx: SignedTransaction, vararg additionalParties: Party) =
startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet()))
fun StartedNode<*>.getValidatedTransaction(stx: SignedTransaction) =
services.validatedTransactions.getTransaction(stx.id)!!
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party) =
startFlow(::FinalityInvoker, stx, parties.toSet())
.andRunNetwork()
//endregion
//region Matchers
fun visibleTo(other: StartedNode<*>) = object : Matcher<SignedTransaction> {
override val description = "has a transaction visible to ${other.info.singleIdentity()}"
override fun invoke(actual: SignedTransaction) =
equalTo(actual)(other.getValidatedTransaction(actual))
}
//endregion
@StartableByRPC
class FinalityInvoker(private val transaction: SignedTransaction,
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
}
}

View File

@ -0,0 +1,96 @@
package net.corda.core.flows.mixins
import com.natpryce.hamkrest.*
import net.corda.core.contracts.ContractState
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
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.node.internal.StartedNode
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import java.util.*
import kotlin.reflect.KClass
/**
* Mix this interface into a test to provide functions useful for working with a mock network
*/
interface WithMockNet {
val mockNet: InternalMockNetwork
/**
* 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()}")
/**
* Run the mock network before proceeding
*/
fun <T: Any> T.andRunNetwork(): T = apply { mockNet.runNetwork() }
//region Operations
/**
* Sign an initial transaction
*/
fun StartedNode<*>.signInitialTransaction(build: TransactionBuilder.() -> TransactionBuilder) =
services.signInitialTransaction(TransactionBuilder(mockNet.defaultNotaryIdentity).build())
/**
* Retrieve the sole instance of a state of a particular class from the node's vault
*/
fun <S: ContractState> StartedNode<*>.getStateFromVault(stateClass: KClass<S>) =
services.vaultService.queryBy(stateClass.java).states.single()
/**
* Start a flow
*/
fun <T> StartedNode<*>.startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = services.startFlow(logic)
/**
* Start a flow and run the network immediately afterwards
*/
fun <T> StartedNode<*>.startFlowAndRunNetwork(logic: FlowLogic<T>): FlowStateMachine<T> =
startFlow(logic).andRunNetwork()
fun StartedNode<*>.createConfidentialIdentity(party: Party) =
services.keyManagementService.freshKeyAndCert(
services.myInfo.legalIdentitiesAndCerts.single { it.name == party.name },
false)
fun StartedNode<*>.verifyAndRegister(identity: PartyAndCertificate) =
services.identityService.verifyAndRegisterIdentity(identity)
//endregion
//region Matchers
/**
* The transaction has the required number of verified signatures
*/
fun requiredSignatures(count: Int = 1) = object : Matcher<SignedTransaction> {
override val description: String = "A transaction with valid required signatures"
override fun invoke(actual: SignedTransaction): MatchResult = try {
actual.verifyRequiredSignatures()
has(SignedTransaction::sigs, hasSize(equalTo(count)))(actual)
} catch (e: Exception) {
MatchResult.Mismatch("$e")
}
}
/**
* The exception has the expected error message
*/
fun errorMessage(expected: String) = has(
Exception::message,
equalTo(expected))
//endregion
}

View File

@ -18,7 +18,10 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds
import net.corda.finance.POUNDS
import net.corda.testing.core.*
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.generateStateRef
import net.corda.testing.internal.TEST_TX_TIME
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices

View File

@ -10,9 +10,9 @@
package net.corda.core.utilities
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import kotlin.test.assertEquals
import org.assertj.core.api.Assertions.assertThatThrownBy
class NetworkHostAndPortTest {
/**

View File

@ -10,12 +10,12 @@
package net.corda.core.utilities
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFails
import org.assertj.core.api.Assertions.*
class ProgressTrackerTest {
object SimpleSteps {

View File

@ -7,6 +7,10 @@ release, see :doc:`upgrade-notes`.
Unreleased
----------
* ``freeLocalHostAndPort``, ``freePort``, and ``getFreeLocalPorts`` from ``TestUtils`` have been deprecated as they
don't provide any guarantee the returned port will be available which can result in flaky tests. Use ``PortAllocation.Incremental``
instead.
* The ``corda-bridgserver.jar`` has been renamed to ``corda-firewall.jar`` to be more consistent
with marketing materials and purpose of the jar. Further to this we have also renamed ``bridge.conf`` to ``firewall.conf``
and within that file the ``bridgeMode`` propety has been modified to ``firewallMode`` for overall consistency.

View File

@ -26,21 +26,23 @@ This will create you an account with the Testnet onboarding application which wi
.. image:: resources/testnet-account-type.png
Fill in the form with your details. This helps prioritise requests to join the private beta.
Fill in the form with your details.
.. note::
Testnet is currently invitation only. If your request is approved you will receive an email. Please fill in as many details as possible as it helps us proritise requests. The approval process will take place daily by a member of the r3 operations team reviewing all invite requests and making a decision based on current rate of onboarding of new customers.
.. image:: resources/testnet-form.png
Note we currently only support federated login using either Google email accounts or Github enabled email accounts. Please ensure the email you use to register is either set up as a Google or Github account and that you use this email to log in with the appropriate service.
.. note::
To enable your email for Google please see https://support.google.com/accounts/answer/176347?hl=en
To set up a Github account please see https://github.com/join
We currently only support federated login using Google email accounts. Please ensure the email you use to register is a Gmail account or is set up as a Google account and that you use this email to log in.
Gmail is recommended. If you want to use a non-Gmail account you can enable your email for Google: https://support.google.com/accounts/answer/176347?hl=en
Once you have been approved to join the beta you will receive an email. Follow the link in the email to sign in or click on "I have an invitation" on the https://testnet.corda.network
Once you have been approved, navigate to https://testnet.corda.network and click on "I have an invitation".
Sign in using either the Google or Github login services:
Sign in using the Google login service:
.. image:: resources/testnet-signin.png
@ -48,7 +50,9 @@ When prompted approve the Testnet application:
.. image:: resources/testnet-signin-auth.png
At this point you may need to verify your email address is valid. If prompted check your email and click on the link to validate then return to the sign in page and sign in again.
.. note::
At this point you may need to verify your email address is valid (if you are not using a Gmail address). If prompted check your email and click on the link to validate then return to the sign in page and sign in again.
Next agree to the terms of service:
@ -56,18 +60,19 @@ Next agree to the terms of service:
You can now choose how to deploy your Corda node to the Corda Testnet. We strongly recommend hosting your Corda node on a public cloud resource.
.. note:: If you host your node on your own machine or a corporate server you must ensure it is reachable from the public internet at a specific IP address. This will typically require port forwarding on your router.
Select the cloud provider you wish to use for documentation on how to specifically configure Corda for that environment.
.. image:: resources/testnet-platform-clean.png
Once your cloud instance is set up you can install and run your Testnet pre-provisioned Corda node by clicking on "Copy" and pasting the one time link into your cloud shell.
Once your cloud instance is set up you can install and run your Testnet pre-provisioned Corda node by clicking on "Copy" and pasting the one time link into your remote cloud terminal.
The installation script will download the Corda binaries as well as your PKI certificates, private keys and suporting files and will install and run Corda on your fresh cloud VM. Your node will register itself with the Corda Testnet when it first runs and be added to the global network map and be visible to counterparties after approximately 5 minutes.
Hosting a Corda node locally is possible but will require manually configuring firewall and port forwarding on your local router. If you want this option then click on the "Download" button to download a Zip file with a pre-configured Corda node.
.. note:: If you host your node on your own machine or a corporate server you must ensure it is reachable from the public internet at a specific IP address. Please follow the instructions here: :doc:`deploy-locally`.
A note on identities on Corda Testnet
-------------------------------------

View File

@ -31,52 +31,12 @@ If a node wishes to use a given fact in a transaction, they request a command as
the oracle considers the fact to be true, they send back the required command. The node then includes the command in
their transaction, and the oracle will sign the transaction to assert that the fact is true.
For privacy purposes, the oracle does not require to have access on every part of the transaction and the only
information it needs to see is their embedded, related to this oracle, command(s). We should also provide
guarantees that all of the commands requiring a signature from this oracle should be visible to
the oracle entity, but not the rest. To achieve that we use filtered transactions, in which the transaction proposer(s)
uses a nested Merkle tree approach to "tear off" the unrelated parts of the transaction. See :doc:`key-concepts-tearoffs`
for more information on how transaction tear-offs work.
If they wish to monetize their services, oracles can choose to only sign a transaction and attest to the validity of
the fact it contains for a fee.
Transaction tear-offs
---------------------
To sign a transaction, the only information the oracle needs to see is their embedded command. Providing any
additional transaction data to the oracle would constitute a privacy leak. Similarly, a non-validating notary only
needs to see a transaction's input states.
To combat this, the transaction proposer(s) uses a Merkle tree to "tear off" any parts of the transaction that the
oracle/notary doesn't need to see before presenting it to them for signing. A Merkle tree is a well-known cryptographic
scheme that is commonly used to provide proofs of inclusion and data integrity. Merkle trees are widely used in
peer-to-peer networks, blockchain systems and git.
The advantage of a Merkle tree is that the parts of the transaction that were torn off when presenting the transaction
to the oracle cannot later be changed without also invalidating the oracle's digital signature.
Transaction Merkle trees
^^^^^^^^^^^^^^^^^^^^^^^^
A Merkle tree is constructed from a transaction by splitting the transaction into leaves, where each leaf contains
either an input, an output, a command, or an attachment. The Merkle tree also contains the other fields of the
``WireTransaction``, such as the time-window, the notary, the type and the signers.
Next, the Merkle tree is built in the normal way by hashing the concatenation of nodes hashes below the current one
together. Its visible on the example image below, where ``H`` denotes sha256 function, "+" - concatenation.
.. image:: resources/merkleTree.png
The transaction has two input states, one output state, one attachment, one command and a time-window. For brevity
we didn't include all leaves on the diagram (type, notary and signers are presented as one leaf labelled Rest - in
reality they are separate leaves). Notice that if a tree is not a full binary tree, leaves are padded to the nearest
power of 2 with zero hash (since finding a pre-image of sha256(x) == 0 is hard computational task) - marked light
green above. Finally, the hash of the root is the identifier of the transaction, it's also used for signing and
verification of data integrity. Every change in transaction on a leaf level will change its identifier.
Hiding data
^^^^^^^^^^^
Hiding data and providing the proof that it formed a part of a transaction is done by constructing Partial Merkle Trees
(or Merkle branches). A Merkle branch is a set of hashes, that given the leaves data, is used to calculate the
roots hash. Then that hash is compared with the hash of a whole transaction and if they match it means that data we
obtained belongs to that particular transaction.
.. image:: resources/partialMerkle.png
In the example above, the node ``H(f)`` is the one holding command data for signing by Oracle service. Blue leaf
``H(g)`` is also included since it's holding time-window information. Nodes labelled ``Provided`` form the Partial
Merkle Tree, black ones are omitted. Having time-window with the command that should be in a violet node place and
branch we are able to calculate root of this tree and compare it with original transaction identifier - we have a
proof that this command and time-window belong to this transaction.
the fact it contains for a fee.

View File

@ -0,0 +1,87 @@
Transaction tear-offs
=====================
.. topic:: Summary
* *Hide transaction components for privacy purposes*
* *Oracles and non-validating notaries can only see their "related" transaction components, but not the full transaction details*
Overview
--------
There are cases where some of the entities involved on the transaction could only have partial visibility on the
transaction parts. For instance, when an oracle should sign a transaction, the only information it needs to see is their
embedded, related to this oracle, command(s). Similarly, a non-validating notary only needs to see a transaction's input
states. Providing any additional transaction data to the oracle would constitute a privacy leak.
To combat this, we use the concept of filtered transactions, in which the transaction proposer(s) uses a nested Merkle
tree approach to "tear off" any parts of the transaction that the oracle/notary doesn't need to see before presenting it
to them for signing. A Merkle tree is a well-known cryptographic scheme that is commonly used to provide proofs of
inclusion and data integrity. Merkle trees are widely used in peer-to-peer networks, blockchain systems and git.
The advantage of a Merkle tree is that the parts of the transaction that were torn off when presenting the transaction
to the oracle cannot later be changed without also invalidating the oracle's digital signature.
Transaction Merkle trees
^^^^^^^^^^^^^^^^^^^^^^^^
A Merkle tree is constructed from a transaction by splitting the transaction into leaves, where each leaf contains
either an input, an output, a command, or an attachment. The final nested tree structure also contains the
other fields of the transaction, such as the time-window, the notary and the required signers. As shown in the picture
below, the only component type that is requiring two trees instead of one is the command, which is split into
command data and required signers for visibility purposes.
Corda is using a patent-pending approach using nested Merkle trees per component type. Briefly, a component sub-tree
is generated for each component type (i.e., inputs, outputs, attachments). Then, the roots of these sub-trees
form the leaves of the top Merkle tree and finally the root of this tree represents the transaction id.
Another important feature is that a nonce is deterministically generated for each component in a way that each nonce
is independent. Then, we use the nonces along with their corresponding components to calculate the component hash,
which is the actual Merkle tree leaf. Nonces are required to protect against brute force attacks that otherwise would
reveal the content of low-entropy hashed values (i.e., a single-word text attachment).
After computing the leaves, each Merkle tree is built in the normal way by hashing the concatenation of nodes hashes
below the current one together. Its visible on the example image below, where ``H`` denotes sha256 function, "+" - concatenation.
.. image:: resources/merkleTreeFull.png
:scale: 35%
:align: center
The transaction has three input states, two output states, two commands, one attachment, a notary and a time-window.
Notice that if a tree is not a full binary tree, leaves are padded to the nearest
power of 2 with zero hash (since finding a pre-image of sha256(x) == 0 is hard computational task) - marked light
green above. Finally, the hash of the root is the identifier of the transaction, it's also used for signing and
verification of data integrity. Every change in transaction on a leaf level will change its identifier.
Hiding data
^^^^^^^^^^^
Hiding data and providing the proof that it formed a part of a transaction is done by constructing partial Merkle trees
(or Merkle branches). A Merkle branch is a set of hashes, that given the leaves data, is used to calculate the
roots hash. Then, that hash is compared with the hash of a whole transaction and if they match it means that data we
obtained belongs to that particular transaction. In the following we provide concrete examples on the data visible to a
an oracle and a non-validating notary, respectively.
Let's assume that only the first command should be visible to an Oracle. We should also provide guarantees that all of
the commands requiring a signature from this oracle should be visible to the oracle entity, but not the rest. Here is how
this filtered transaction will be represented in the Merkle tree structure.
.. image:: resources/SubMerkleTree_Oracle.png
:scale: 35%
:align: center
Blue nodes and ``H(c2)`` are provided to the Oracle service, while the black ones are omitted. ``H(c2)`` is required, so
that the Oracle can compute ``H(commandData)`` without being to able to see the second command, but at the same time
ensuring ``CommandData1`` is part of the transaction. It is highlighted that all signers are visible, so as to have a
proof that no related command (that the Oracle should see) has been maliciously filtered out. Additionally, hashes of
sub-trees (violet nodes) are also provided in the current Corda protocol. The latter is required for special cases, i.e.,
when required to know if a component group is empty or not.
Having all of the aforementioned data, one can calculate the root of the top tree and compare it with original
transaction identifier - we have a proof that this command and time-window belong to this transaction.
Along the same lines, if we want to send the same transaction to a non-validating notary we should hide all components
apart from input states, time-window and the notary information. This data is enough for the notary to know which
input states should be checked for double-spending, if the time-window is valid and if this transaction should be
notarised by this notary.
.. image:: resources/SubMerkleTree_Notary.png
:scale: 35%
:align: center

View File

@ -23,6 +23,7 @@ This section should be read in order:
key-concepts-time-windows
key-concepts-oracles
key-concepts-node
key-concepts-tearoffs
key-concepts-tradeoffs
The detailed thinking and rationale behind these concepts are presented in two white papers:

View File

@ -124,10 +124,8 @@ The current set of network parameters:
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node.
More parameters will be added in future releases to regulate things like allowed port numbers, how long a node can be
offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required
cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to
SGX and so on.
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on.
Network parameters update process
---------------------------------

View File

@ -18,6 +18,7 @@ overridden by specifying the full network address (interface and port), using th
syntax in the node configuration:
.. sourcecode:: groovy
h2Settings {
address: "localhost:12345"
}
@ -25,3 +26,6 @@ syntax in the node configuration:
The configuration above will restrict the H2 service to run on localhost. 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.
The previous ``h2Port`` syntax is now deprecated. ``h2Port`` will continue to work but the database
will only be accessible on localhost.

View File

@ -9,6 +9,7 @@ stored states, transactions and attachments as follows:
* Enable the H2 database access in the node configuration using the following syntax:
.. sourcecode:: groovy
h2Settings {
address: "localhost:0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -360,7 +360,7 @@ class NetworkBootstrapper
notaries = notaryInfos,
modifiedTime = Instant.now(),
maxMessageSize = 10485760,
maxTransactionSize = Int.MAX_VALUE,
maxTransactionSize = 10485760,
whitelistedContractImplementations = whitelist,
epoch = 1,
eventHorizon = 30.days

View File

@ -76,6 +76,8 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
private fun logInfoWithMDC(msg: String) = withMDC { log.info(msg) }
private fun logWarnWithMDC(msg: String, ex: Throwable? = null) = withMDC { log.warn(msg, ex) }
private fun logErrorWithMDC(msg: String, ex: Throwable? = null) = withMDC { log.error(msg, ex) }
val connection: Connection
@ -318,6 +320,16 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
}
}
override fun onLinkRemoteClose(e: Event) {
val link = e.link
if(link.remoteCondition != null) {
logWarnWithMDC("Connection closed due to error on remote side: `${link.remoteCondition.description}`")
transport.condition = link.condition
transport.close_tail()
transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code)
}
}
override fun onLinkFinal(event: Event) {
val link = event.link
if (link is Sender) {

View File

@ -28,7 +28,11 @@ import net.corda.nodeapi.internal.bridging.AMQPBridgeManager
import net.corda.nodeapi.internal.bridging.BridgeManager
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
import net.corda.testing.core.*
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.MAX_MESSAGE_SIZE
import net.corda.testing.core.TestIdentity
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.rigorousMock
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.RoutingType
@ -54,11 +58,9 @@ class AMQPBridgeTest {
private val BOB = TestIdentity(BOB_NAME)
private val artemisPort = freePort()
private val artemisPort2 = freePort()
private val amqpPort = freePort()
private val artemisAddress = NetworkHostAndPort("localhost", artemisPort)
private val amqpAddress = NetworkHostAndPort("localhost", amqpPort)
private val portAllocation = PortAllocation.Incremental(10000)
private val artemisAddress = portAllocation.nextHostAndPort()
private val amqpAddress = portAllocation.nextHostAndPort()
private abstract class AbstractNodeConfiguration : NodeConfiguration
@ -261,8 +263,10 @@ class AMQPBridgeTest {
doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration
}
artemisConfig.configureWithDevSSLCertificate()
val artemisServer = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), MAX_MESSAGE_SIZE)
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE, confirmationWindowSize = 10 * 1024)
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisAddress.copy(host = "0.0.0.0"), MAX_MESSAGE_SIZE)
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
artemisServer.start()
artemisClient.start()
val bridgeManager = AMQPBridgeManager(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
@ -314,7 +318,7 @@ class AMQPBridgeTest {
override val maxMessageSize: Int = maxMessageSize
}
return AMQPServer("0.0.0.0",
amqpPort,
amqpAddress.port,
amqpConfig
)
}

View File

@ -19,7 +19,11 @@ import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
import net.corda.testing.core.*
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.MAX_MESSAGE_SIZE
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.DEV_INTERMEDIATE_CA
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.internal.rigorousMock
@ -66,7 +70,8 @@ class CertificateRevocationListNodeTests {
private val ROOT_CA = DEV_ROOT_CA
private lateinit var INTERMEDIATE_CA: CertificateAndKeyPair
private val serverPort = freePort()
private val portAllocation = PortAllocation.Incremental(10000)
private val serverPort = portAllocation.nextPort()
private lateinit var server: CrlServer

View File

@ -34,7 +34,11 @@ import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
import net.corda.testing.core.*
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.MAX_MESSAGE_SIZE
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.internal.rigorousMock
import org.apache.activemq.artemis.api.core.RoutingType
@ -44,7 +48,6 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.*
import kotlin.concurrent.thread
@ -56,9 +59,10 @@ class ProtonWrapperTests {
@JvmField
val temporaryFolder = TemporaryFolder()
private val serverPort = freePort()
private val serverPort2 = freePort()
private val artemisPort = freePort()
private val portAllocation = PortAllocation.Incremental(10000)
private val serverPort = portAllocation.nextPort()
private val serverPort2 = portAllocation.nextPort()
private val artemisPort = portAllocation.nextPort()
private abstract class AbstractNodeConfiguration : NodeConfiguration
@ -386,6 +390,31 @@ class ProtonWrapperTests {
}
}
@Test
fun `Message sent from AMQP to non-existent Artemis inbox is rejected and client disconnects`() {
val (server, artemisClient) = createArtemisServerAndClient()
val amqpClient = createClient()
var connected = false
amqpClient.onConnection.subscribe { change ->
connected = change.connected
}
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
assertEquals(true, clientConnected.get().connected)
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
val sendAddress = P2P_PREFIX + "Test"
val testData = "Test".toByteArray()
val testProperty = mutableMapOf<String, Any?>()
testProperty["TestProp"] = "1"
val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
amqpClient.write(message)
assertEquals(MessageStatus.Rejected, message.onComplete.get())
assertEquals(false, connected)
amqpClient.stop()
artemisClient.stop()
server.stop()
}
private fun createArtemisServerAndClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory

View File

@ -34,6 +34,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.netty.*
import net.corda.testing.core.*
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.rigorousMock
import org.apache.activemq.artemis.api.core.RoutingType
import org.junit.After
@ -50,10 +51,11 @@ class SocksTests {
@JvmField
val temporaryFolder = TemporaryFolder()
private val socksPort = freePort()
private val serverPort = freePort()
private val serverPort2 = freePort()
private val artemisPort = freePort()
private val portAllocator = PortAllocation.Incremental(10000)
private val socksPort = portAllocator.nextPort()
private val serverPort = portAllocator.nextPort()
private val serverPort2 = portAllocator.nextPort()
private val artemisPort = portAllocator.nextPort()
private abstract class AbstractNodeConfiguration : NodeConfiguration

View File

@ -25,6 +25,7 @@ import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.*
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
@ -61,7 +62,9 @@ class ArtemisMessagingTest {
@JvmField
val temporaryFolder = TemporaryFolder()
private val serverPort = freePort()
// THe
private val portAllocation = PortAllocation.Incremental(10000)
private val serverPort = portAllocation.nextPort()
private val identity = generateKeyPair()
private lateinit var config: NodeConfiguration
@ -107,7 +110,7 @@ class ArtemisMessagingTest {
@Test
fun `client should connect to remote server`() {
val remoteServerAddress = freeLocalHostAndPort()
val remoteServerAddress = portAllocation.nextHostAndPort()
createMessagingServer(remoteServerAddress.port).start()
createMessagingClient(server = remoteServerAddress)
@ -116,8 +119,8 @@ class ArtemisMessagingTest {
@Test
fun `client should throw if remote server not found`() {
val serverAddress = freeLocalHostAndPort()
val invalidServerAddress = freeLocalHostAndPort()
val serverAddress = portAllocation.nextHostAndPort()
val invalidServerAddress = portAllocation.nextHostAndPort()
createMessagingServer(serverAddress.port).start()
@ -190,169 +193,6 @@ class ArtemisMessagingTest {
assertThat(received.platformVersion).isEqualTo(3)
}
@Test
fun `we can fake send and receive`() {
val (messagingClient, receivedMessages) = createAndStartClientAndServer()
val message = messagingClient.createMessage(TOPIC, data = "first msg".toByteArray())
val fakeMsg = messagingClient.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient.deliver(fakeMsg)
val received = receivedMessages.take()
assertThat(String(received.data.bytes, Charsets.UTF_8)).isEqualTo("first msg")
}
@Test
fun `redelivery from same client is ignored`() {
val (messagingClient, receivedMessages) = createAndStartClientAndServer()
val message = messagingClient.createMessage(TOPIC, data = "first msg".toByteArray())
val fakeMsg = messagingClient.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient.deliver(fakeMsg)
messagingClient.deliver(fakeMsg)
val received = receivedMessages.take()
assertThat(String(received.data.bytes, Charsets.UTF_8)).isEqualTo("first msg")
val received2 = receivedMessages.poll()
assertThat(received2).isNull()
}
// Redelivery from a sender who stops and restarts (some re-sends from the sender, with sender state reset with exception of recovered checkpoints)
@Test
fun `re-send from different client is ignored`() {
val (messagingClient1, receivedMessages) = createAndStartClientAndServer()
val message = messagingClient1.createMessage(TOPIC, data = "first msg".toByteArray())
val fakeMsg = messagingClient1.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient1.deliver(fakeMsg)
// Now change the sender
try {
val messagingClient2 = createMessagingClient()
startNodeMessagingClient()
val fakeMsg2 = messagingClient2.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg2!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient1.deliver(fakeMsg2)
val received = receivedMessages.take()
assertThat(String(received.data.bytes, Charsets.UTF_8)).isEqualTo("first msg")
val received2 = receivedMessages.poll()
assertThat(received2).isNull()
} finally {
messagingClient1.stop()
}
}
// Redelivery to a receiver who stops and restarts (some re-deliveries from Artemis, but with receiver state reset)
@Test
fun `re-receive from different client is ignored`() {
val (messagingClient1, receivedMessages) = createAndStartClientAndServer()
val message = messagingClient1.createMessage(TOPIC, data = "first msg".toByteArray())
val fakeMsg = messagingClient1.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient1.deliver(fakeMsg)
// Now change the receiver
try {
val messagingClient2 = createMessagingClient()
messagingClient2.addMessageHandler(TOPIC) { msg, _, handle ->
database.transaction { handle.insideDatabaseTransaction() }
handle.afterDatabaseTransaction() // We ACK first so that if it fails we won't get a duplicate in [receivedMessages]
receivedMessages.add(msg)
}
startNodeMessagingClient()
messagingClient2.deliver(fakeMsg)
val received = receivedMessages.take()
assertThat(String(received.data.bytes, Charsets.UTF_8)).isEqualTo("first msg")
val received2 = receivedMessages.poll()
assertThat(received2).isNull()
} finally {
messagingClient1.stop()
}
}
// Redelivery to a receiver who stops and restarts (some re-deliveries from Artemis, but with receiver state reset), but the original
// messages were recorded as consumed out of order, and only the *second* message was acked.
@Test
fun `re-receive from different client is not ignored when acked out of order`() {
// Don't ack first message, pretend we exit before that happens (but after second message is acked).
val (messagingClient1, receivedMessages) = createAndStartClientAndServer(dontAckCondition = { received -> String(received.data.bytes, Charsets.UTF_8) == "first msg" })
val message1 = messagingClient1.createMessage(TOPIC, data = "first msg".toByteArray())
val message2 = messagingClient1.createMessage(TOPIC, data = "second msg".toByteArray())
val fakeMsg1 = messagingClient1.messagingExecutor!!.cordaToArtemisMessage(message1)
val fakeMsg2 = messagingClient1.messagingExecutor!!.cordaToArtemisMessage(message2)
fakeMsg1!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
fakeMsg2!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient1.deliver(fakeMsg1)
messagingClient1.deliver(fakeMsg2)
// Now change the receiver
try {
val messagingClient2 = createMessagingClient()
messagingClient2.addMessageHandler(TOPIC) { msg, _, handle ->
// The try-finally causes the test to fail if there's a duplicate insert (which, naturally, is an error but otherwise gets swallowed).
try {
database.transaction { handle.insideDatabaseTransaction() }
} finally {
handle.afterDatabaseTransaction() // We ACK first so that if it fails we won't get a duplicate in [receivedMessages]
receivedMessages.add(msg)
}
}
startNodeMessagingClient()
messagingClient2.deliver(fakeMsg1)
messagingClient2.deliver(fakeMsg2)
// Should receive 2 and then 1 (and not 2 again).
val received = receivedMessages.take()
assertThat(received.senderSeqNo).isEqualTo(1)
val received2 = receivedMessages.poll()
assertThat(received2.senderSeqNo).isEqualTo(0)
val received3 = receivedMessages.poll()
assertThat(received3).isNull()
} finally {
messagingClient1.stop()
}
}
// Re-receive on different client from re-started sender
@Test
fun `re-send from different client and re-receive from different client is ignored`() {
val (messagingClient1, receivedMessages) = createAndStartClientAndServer()
val message = messagingClient1.createMessage(TOPIC, data = "first msg".toByteArray())
val fakeMsg = messagingClient1.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
messagingClient1.deliver(fakeMsg)
// Now change the send *and* receiver
val messagingClient2 = createMessagingClient()
try {
startNodeMessagingClient()
val fakeMsg2 = messagingClient2.messagingExecutor!!.cordaToArtemisMessage(message)
fakeMsg2!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
val messagingClient3 = createMessagingClient()
messagingClient3.addMessageHandler(TOPIC) { msg, _, handle ->
database.transaction { handle.insideDatabaseTransaction() }
handle.afterDatabaseTransaction() // We ACK first so that if it fails we won't get a duplicate in [receivedMessages]
receivedMessages.add(msg)
}
startNodeMessagingClient()
messagingClient3.deliver(fakeMsg2)
val received = receivedMessages.take()
assertThat(String(received.data.bytes, Charsets.UTF_8)).isEqualTo("first msg")
val received2 = receivedMessages.poll()
assertThat(received2).isNull()
} finally {
messagingClient1.stop()
messagingClient2.stop()
}
}
private fun startNodeMessagingClient() {
messagingClient!!.start()
}

View File

@ -94,7 +94,7 @@ class LargeTransactionsTest : IntegrationTest() {
driver(DriverParameters(
startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),
networkParameters = testNetworkParameters(maxTransactionSize = 13.MB.toInt())
networkParameters = testNetworkParameters(maxMessageSize = 15.MB.toInt(), maxTransactionSize = 13.MB.toInt())
)) {
val rpcUser = User("admin", "admin", setOf("ALL"))
val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow()

View File

@ -30,12 +30,15 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.freeLocalHostAndPort
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.hamcrest.Matchers.instanceOf
import org.junit.*
import org.junit.After
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.time.Clock
import java.time.Instant
import java.util.concurrent.CompletableFuture
@ -49,8 +52,10 @@ class RaftTransactionCommitLogTests {
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private lateinit var cluster: List<Member>
private val databases: MutableList<CordaPersistence> = mutableListOf()
private val portAllocation = PortAllocation.Incremental(10000)
private lateinit var cluster: List<Member>
@Before
fun setup() {
@ -149,9 +154,9 @@ class RaftTransactionCommitLogTests {
}
private fun setUpCluster(nodeCount: Int = 3): List<Member> {
val clusterAddress = freeLocalHostAndPort()
val clusterAddress = portAllocation.nextHostAndPort()
val cluster = mutableListOf(createReplica(clusterAddress))
for (i in 1..nodeCount) cluster.add(createReplica(freeLocalHostAndPort(), clusterAddress))
for (i in 1..nodeCount) cluster.add(createReplica(portAllocation.nextHostAndPort(), clusterAddress))
return cluster.map { it.getOrThrow() }
}

View File

@ -15,6 +15,9 @@ import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions
import io.netty.channel.unix.Errors
import joptsimple.OptionParser
import joptsimple.util.PathConverter
import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.Crypto
import net.corda.core.internal.Emoji
@ -51,6 +54,7 @@ import org.fusesource.jansi.AnsiConsole
import org.slf4j.bridge.SLF4JBridgeHandler
import sun.misc.VMSupport
import java.io.Console
import java.io.File
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
import java.net.InetAddress
@ -65,8 +69,10 @@ open class NodeStartup(val args: Array<String>) {
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
const val LOGS_DIRECTORY_NAME = "logs"
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
}
/**
* @return true if the node startup was successful. This value is intended to be the exit code of the process.
*/
@ -77,13 +83,23 @@ open class NodeStartup(val args: Array<String>) {
println("Corda will now exit...")
return false
}
val cmdlineOptions = NodeArgsParser().parseOrExit(*args)
val registrationMode = checkRegistrationMode()
val cmdlineOptions: CmdLineOptions = if (registrationMode && !args.contains("--initial-registration")) {
"Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.".let {
println(it)
logger.info(it)
}
// Pretend that the node was started with `--initial-registration` to help prevent user error.
NodeArgsParser().parseOrExit(*args.plus("--initial-registration"))
} else {
NodeArgsParser().parseOrExit(*args)
}
// We do the single node check before we initialise logging so that in case of a double-node start it
// doesn't mess with the running node's logs.
enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory)
initLogging(cmdlineOptions)
// Register all cryptography [Provider]s.
// Required to install our [SecureRandom] before e.g., UUID asks for one.
// This needs to go after initLogging(netty clashes with our logging).
@ -145,6 +161,8 @@ open class NodeStartup(val args: Array<String>) {
if (cmdlineOptions.nodeRegistrationOption != null) {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
registerWithNetwork(conf, versionInfo, cmdlineOptions.nodeRegistrationOption)
// At this point the node registration was succesfull. We can delete the marker file.
deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory)
return true
}
logStartupInfo(versionInfo, cmdlineOptions, conf)
@ -196,6 +214,49 @@ open class NodeStartup(val args: Array<String>) {
return true
}
private fun checkRegistrationMode(): Boolean {
// Parse the command line args just to get the base directory. The base directory is needed to determine
// if the node registration marker file exists, _before_ we call NodeArgsParser.parse().
// If it does exist, we call NodeArgsParser with `--initial-registration` added to the argument list. This way
// we make sure that the initial registration is completed, even if the node was restarted before the first
// attempt to register succeeded and the node administrator forgets to specify `--initial-registration` upon
// restart.
val optionParser = OptionParser()
optionParser.allowsUnrecognizedOptions()
val baseDirectoryArg = optionParser
.accepts("base-directory", "The node working directory where all the files are kept")
.withRequiredArg()
.withValuesConvertedBy(PathConverter())
.defaultsTo(Paths.get("."))
val isRegistrationArg =
optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
val optionSet = optionParser.parse(*args)
val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
// If the node was started with `--initial-registration`, create marker file.
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
val marker = File((baseDirectory / INITIAL_REGISTRATION_MARKER).toUri())
if (!optionSet.has(isRegistrationArg) && !marker.exists()) {
return false
}
try {
marker.createNewFile()
} catch (e: Exception) {
logger.warn("Could not create marker file for `--initial-registration`.", e)
}
return true
}
private fun deleteNodeRegistrationMarker(baseDir: Path) {
try {
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
if (marker.exists()) {
marker.delete()
}
} catch (e: Exception) {
logger.warn("Could not delete the marker file that was created for `--initial-registration`.", e)
}
}
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)

View File

@ -10,13 +10,12 @@
package net.corda.node.services.rpc
import io.netty.channel.unix.Errors
import net.corda.core.internal.errors.AddressBindingException
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.node.internal.artemis.*
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECURITY_CONFIG
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG
import net.corda.core.internal.errors.AddressBindingException
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration
@ -30,7 +29,7 @@ import java.nio.file.Path
import java.security.KeyStoreException
import javax.security.auth.login.AppConfigurationEntry
internal class ArtemisRpcBroker internal constructor(
class ArtemisRpcBroker internal constructor(
address: NetworkHostAndPort,
private val adminAddressOptional: NetworkHostAndPort?,
private val sslOptions: BrokerRpcSslOptions?,

View File

@ -30,11 +30,11 @@ import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.MOCK_VERSION_INFO
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.Assertions.*
import org.junit.After
import org.junit.Test
import java.nio.file.Path
import java.time.Instant
import kotlin.test.assertFails
class NetworkParametersTest {
@ -84,6 +84,19 @@ class NetworkParametersTest {
}
}
@Test
fun `maxTransactionSize must be bigger than maxMesssageSize`() {
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2000,
2001,
Instant.now(),
1,
emptyMap())
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
}
// Helpers
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
NetworkParametersCopier(params).install(dir)

View File

@ -19,7 +19,6 @@ import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createNodeInfoAndSigned
@ -49,9 +48,9 @@ class NetworkMapClientTest {
@Before
fun setUp() {
server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort())
val hostAndPort = server.start()
networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_ROOT_CA.certificate)
server = NetworkMapServer(cacheTimeout)
val address = server.start()
networkMapClient = NetworkMapClient(URL("http://$address"), DEV_ROOT_CA.certificate)
}
@After

View File

@ -32,7 +32,6 @@ import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.*
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createNodeInfoAndSigned
@ -72,9 +71,9 @@ class NetworkMapUpdaterTest {
@Before
fun setUp() {
server = NetworkMapServer(cacheExpiryMs.millis, PortAllocation.Incremental(10000).nextHostAndPort())
val hostAndPort = server.start()
networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_ROOT_CA.certificate)
server = NetworkMapServer(cacheExpiryMs.millis)
val address = server.start()
networkMapClient = NetworkMapClient(URL("http://$address"), DEV_ROOT_CA.certificate)
}
@After

View File

@ -23,7 +23,6 @@ import net.corda.node.internal.NetworkParametersReader
import net.corda.nodeapi.internal.network.*
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
@ -42,7 +41,7 @@ class NetworkParametersReaderTest {
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix())
private val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix())
private val cacheTimeout = 100000.seconds
private lateinit var server: NetworkMapServer
@ -50,9 +49,9 @@ class NetworkParametersReaderTest {
@Before
fun setUp() {
server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort())
val hostAndPort = server.start()
networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_ROOT_CA.certificate)
server = NetworkMapServer(cacheTimeout)
val address = server.start()
networkMapClient = NetworkMapClient(URL("http://$address"), DEV_ROOT_CA.certificate)
}
@After

View File

@ -21,6 +21,7 @@ private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory {
serializersByType = mutableMapOf(),
serializersByDescriptor = mutableMapOf(),
customSerializers = ArrayList(),
customSerializersCache = mutableMapOf(),
transformsCache = mutableMapOf()
)
}

View File

@ -35,6 +35,8 @@ import javax.annotation.concurrent.ThreadSafe
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
@KeepForDJVM
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
@KeepForDJVM
data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type)
/**
* Factory of serializers designed to be shared across threads and invocations.
@ -66,6 +68,7 @@ open class SerializerFactory(
private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>,
val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>,
private val customSerializers: MutableList<SerializerFor>,
private val customSerializersCache: MutableMap<CustomSerializersCacheKey, AMQPSerializer<Any>?>,
val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>,
private val onlyCustomSerializers: Boolean = false
) {
@ -84,6 +87,7 @@ open class SerializerFactory(
ConcurrentHashMap(),
CopyOnWriteArrayList(),
ConcurrentHashMap(),
ConcurrentHashMap(),
onlyCustomSerializers
)
@ -387,6 +391,13 @@ open class SerializerFactory(
}
internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
return customSerializersCache.computeIfAbsent(CustomSerializersCacheKey(clazz, declaredType), ::doFindCustomSerializer)
}
private fun doFindCustomSerializer(key: CustomSerializersCacheKey): AMQPSerializer<Any>? {
val (clazz, declaredType) = key
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
// AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
// super type. Could be done, but do we need it?
@ -399,7 +410,7 @@ open class SerializerFactory(
|| !customSerializer.isSerializerFor(declaredSuperClass)
|| !customSerializer.revealSubclassesInSchema
) {
logger.info ("action=\"Using custom serializer\", class=${clazz.typeName}, " +
logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " +
"declaredType=${declaredType.typeName}")
@Suppress("UNCHECKED_CAST")

View File

@ -27,8 +27,8 @@ import net.corda.nodeapi.internal.config.toConfig
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.getFreeLocalPorts
import net.corda.testing.internal.IntegrationTest
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.testThreadFactory
import net.corda.testing.node.User
import org.apache.logging.log4j.Level
@ -57,6 +57,7 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
private lateinit var defaultNetworkParameters: NetworkParametersCopier
private val startedNodes = mutableListOf<StartedNode<Node>>()
private val nodeInfos = mutableListOf<NodeInfo>()
private val portAllocation = PortAllocation.Incremental(10000)
init {
System.setProperty("consoleLogLevel", Level.DEBUG.name().toLowerCase())
@ -96,8 +97,7 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
rpcUsers: List<User> = emptyList(),
configOverrides: Map<String, Any> = emptyMap()): Node {
val baseDirectory = baseDirectory(legalName).createDirectories()
val localPort = getFreeLocalPorts("localhost", 3)
val p2pAddress = configOverrides["p2pAddress"] ?: localPort[0].toString()
val p2pAddress = configOverrides["p2pAddress"] ?: portAllocation.nextHostAndPort().toString()
val config = ConfigHelper.loadConfig(
baseDirectory = baseDirectory,
allowMissingConfig = true,
@ -106,8 +106,8 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
"myLegalName" to legalName.toString(),
"p2pAddress" to p2pAddress,
"devMode" to true,
"rpcSettings.address" to localPort[1].toString(),
"rpcSettings.adminAddress" to localPort[2].toString(),
"rpcSettings.address" to portAllocation.nextHostAndPort().toString(),
"rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString(),
"rpcUsers" to rpcUsers.map { it.toConfig().root().unwrapped() }
) + configOverrides
)

View File

@ -46,7 +46,7 @@ import javax.ws.rs.core.Response.ok
import javax.ws.rs.core.Response.status
class NetworkMapServer(private val pollInterval: Duration,
hostAndPort: NetworkHostAndPort,
hostAndPort: NetworkHostAndPort = NetworkHostAndPort("localhost", 0),
private val networkMapCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa(),
private val myHostNameValue: String = "test.host.name",
vararg additionalServices: Any) : Closeable {

View File

@ -59,13 +59,17 @@ import java.util.concurrent.atomic.AtomicInteger
fun generateStateRef(): StateRef = StateRef(SecureHash.randomSHA256(), 0)
private val freePortCounter = AtomicInteger(30000)
/**
* Returns a localhost address with a free port.
*
* Unsafe for getting multiple ports!
* Use [getFreeLocalPorts] for getting multiple ports.
*/
fun freeLocalHostAndPort() = NetworkHostAndPort("localhost", freePort())
@Suppress("DEPRECATION")
@Deprecated("Returned port is not guaranteed to be free when used, which can result in flaky tests. Instead use a port " +
"range that's unlikely to be used by the rest of the system, such as PortAllocation.Incremental(10000).")
fun freeLocalHostAndPort(): NetworkHostAndPort = NetworkHostAndPort("localhost", freePort())
/**
* Returns a free port.
@ -73,6 +77,8 @@ fun freeLocalHostAndPort() = NetworkHostAndPort("localhost", freePort())
* Unsafe for getting multiple ports!
* Use [getFreeLocalPorts] for getting multiple ports.
*/
@Deprecated("Returned port is not guaranteed to be free when used, which can result in flaky tests. Instead use a port " +
"range that's unlikely to be used by the rest of the system, such as PortAllocation.Incremental(10000).")
fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + 1) % 10000 }
/**
@ -81,6 +87,8 @@ fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (
* Unlikely, but in the time between running this function and handing the ports
* to the Node, some other process else could allocate the returned ports.
*/
@Deprecated("Returned port is not guaranteed to be free when used, which can result in flaky tests. Instead use a port " +
"range that's unlikely to be used by the rest of the system, such as PortAllocation.Incremental(10000).")
fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<NetworkHostAndPort> {
val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 }
return (0 until numberToAlloc).map { NetworkHostAndPort(hostName, freePort + it) }