@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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" }
|
||||
}
|
||||
|
||||
|
@ -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.*
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
@ -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>())
|
@ -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>())
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
-------------------------------------
|
||||
|
||||
|
@ -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. It’s 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
|
||||
root’s 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.
|
87
docs/source/key-concepts-tearoffs.rst
Normal 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. It’s 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
|
||||
root’s 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
|
@ -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:
|
||||
|
@ -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
|
||||
---------------------------------
|
||||
|
@ -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.
|
@ -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"
|
||||
}
|
||||
|
BIN
docs/source/resources/SubMerkleTree_Notary.png
Normal file
After Width: | Height: | Size: 146 KiB |
BIN
docs/source/resources/SubMerkleTree_Oracle.png
Normal file
After Width: | Height: | Size: 138 KiB |
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 365 KiB |
BIN
docs/source/resources/merkleTreeFull.png
Normal file
After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 367 KiB |
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.4 MiB |
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
|
@ -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() }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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?,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -21,6 +21,7 @@ private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory {
|
||||
serializersByType = mutableMapOf(),
|
||||
serializersByDescriptor = mutableMapOf(),
|
||||
customSerializers = ArrayList(),
|
||||
customSerializersCache = mutableMapOf(),
|
||||
transformsCache = mutableMapOf()
|
||||
)
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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) }
|
||||
|