diff --git a/build.gradle b/build.gradle index 20cee98ce9..dd55457ea2 100644 --- a/build.gradle +++ b/build.gradle @@ -34,20 +34,6 @@ buildscript { ext.capsule_version = '1.0.1' ext.asm_version = '5.0.4' - - /* - * TODO Upgrade to version 2.4 for large message streaming support - * - * Due to a memory leak in the connection handling code in Artemis, we are - * temporarily downgrading to version 2.2.0. - * - * The memory leak essentially triggers an out-of-memory exception within - * less than 10 seconds and can take down a node if a non-TLS connection is - * attempted against the P2P port. - * - * The issue has been reported to upstream: - * https://issues.apache.org/jira/browse/ARTEMIS-1559 - */ ext.artemis_version = '2.5.0' ext.jackson_version = '2.9.5' ext.jetty_version = '9.4.7.v20170914' diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt index de961cbcdc..e88f72c972 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt @@ -26,7 +26,7 @@ import net.corda.core.serialization.CordaSerializable @CordaSerializable interface ContractState { /** - * A _participant_ is any party that is able to consume this state in a valid transaction. + * A _participant_ is any party that should be notified when the state is created or consumed. * * The list of participants is required for certain types of transactions. For example, when changing the notary * for this state, every participant has to be involved and approve the transaction diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt index 2028be75f6..d30a4e5f1e 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt @@ -48,7 +48,7 @@ class CashIssueAndPaymentFlow(val amount: Amount, @Suspendable override fun call(): Result { subFlow(CashIssueFlow(amount, issueRef, notary)) - return subFlow(CashPaymentFlow(amount, recipient, anonymous)) + return subFlow(CashPaymentFlow(amount, recipient, anonymous, notary)) } @CordaSerializable @@ -57,4 +57,4 @@ class CashIssueAndPaymentFlow(val amount: Amount, val recipient: Party, val notary: Party, val anonymous: Boolean) : AbstractRequest(amount) -} \ No newline at end of file +} diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index 28654db19f..e67ddc30aa 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -34,6 +34,7 @@ import java.util.* * @param recipient the party to pay the currency to. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. * @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false + * @param notary if not specified, the first notary of the network map is selected * for testing purposes. */ @StartableByRPC @@ -42,14 +43,17 @@ open class CashPaymentFlow( val recipient: Party, val anonymous: Boolean, progressTracker: ProgressTracker, - val issuerConstraint: Set = emptySet()) : AbstractCashFlow(progressTracker) { + val issuerConstraint: Set = emptySet(), + val notary: Party? = null) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) - constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint) + constructor(amount: Amount, recipient: Party, anonymous: Boolean, notary: Party) : this(amount, recipient, anonymous, tracker(), notary = notary) + + constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint, request.notary) @Suspendable override fun call(): AbstractCashFlow.Result { @@ -61,7 +65,7 @@ open class CashPaymentFlow( } val anonymousRecipient = txIdentities[recipient] ?: recipient progressTracker.currentStep = GENERATING_TX - val builder = TransactionBuilder(notary = null) + val builder = TransactionBuilder(notary = notary ?: serviceHub.networkMapCache.notaryIdentities.first()) logger.info("Generating spend for: ${builder.lockId}") // TODO: Have some way of restricting this to states the caller controls val (spendTX, keysForSigning) = try { @@ -90,5 +94,6 @@ open class CashPaymentFlow( class PaymentRequest(amount: Amount, val recipient: Party, val anonymous: Boolean, - val issuerConstraint: Set = emptySet()) : AbstractRequest(amount) + val issuerConstraint: Set = emptySet(), + val notary: Party? = null) : AbstractRequest(amount) } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1948b9074f..758de960ec 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowTests.kt index b2f46fadfd..8c1d5a8137 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowTests.kt @@ -21,6 +21,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.FlowIORequest import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.internal.notary.UniquenessProvider @@ -55,8 +56,9 @@ import org.junit.rules.ExternalResource import org.junit.rules.RuleChain import org.slf4j.MDC import java.security.PublicKey +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.assertEquals import kotlin.test.assertNotEquals class TimedFlowTestRule(val clusterSize: Int) : ExternalResource() { @@ -159,13 +161,11 @@ class TimedFlowTests { val flow = NotaryFlow.Client(issueTx) val progressTracker = flow.progressTracker assertNotEquals(ProgressTracker.DONE, progressTracker.currentStep) + val progressTrackerDone = getDoneFuture(progressTracker) + val notarySignatures = services.startFlow(flow).resultFuture.get() (issueTx + notarySignatures).verifyRequiredSignatures() - assertEquals( - ProgressTracker.DONE, - progressTracker.currentStep, - "Ensure the same progress tracker object is re-used after flow restart" - ) + progressTrackerDone.get() } } @@ -178,14 +178,12 @@ class TimedFlowTests { } val flow = FinalityFlow(issueTx) val progressTracker = flow.progressTracker + assertNotEquals(ProgressTracker.DONE, progressTracker.currentStep) + val progressTrackerDone = getDoneFuture(flow.progressTracker) val stx = services.startFlow(flow).resultFuture.get() stx.verifyRequiredSignatures() - assertEquals( - ProgressTracker.DONE, - progressTracker.currentStep, - "Ensure the same progress tracker object is re-used after flow restart" - ) + progressTrackerDone.get() } } @@ -198,6 +196,13 @@ class TimedFlowTests { ) } + /** Returns a future that completes when the [progressTracker] reaches the [ProgressTracker.DONE] step. */ + private fun getDoneFuture(progressTracker: ProgressTracker): Future { + return progressTracker.changes.takeFirst { + it.progressTracker.currentStep == ProgressTracker.DONE + }.bufferUntilSubscribed().toBlocking().toFuture() + } + @CordaService private class TestNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val uniquenessProvider = mock() @@ -226,4 +231,4 @@ class TimedFlowTests { return TransactionParts(stx.id, stx.inputs, stx.tx.timeWindow, stx.notary) } } -} \ No newline at end of file +} diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index 861708f395..78456a62fd 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -35,8 +35,16 @@ public class CordaCaplet extends Capsule { private Config parseConfigFile(List args) { String baseDirOption = getOption(args, "--base-directory"); + // Ensure consistent behaviour with NodeArgsParser.kt, see CORDA-1598. + if (null == baseDirOption || baseDirOption.isEmpty()) { + baseDirOption = getOption(args, "-base-directory"); + } this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString(); String config = getOption(args, "--config-file"); + // Same as for baseDirOption. + if (null == config || config.isEmpty()) { + config = getOption(args, "-config-file"); + } File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config); try { ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false); diff --git a/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt b/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt index f8b4294a8a..d4cdf6b2d7 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt @@ -3,20 +3,73 @@ package net.corda.node.utilities import net.corda.core.flows.FlowLogic import net.corda.core.utilities.ProgressTracker import net.corda.node.services.statemachine.StateMachineManagerInternal +import java.lang.reflect.Field /** * The flow de-serialized from the checkpoint will contain a new instance of the progress tracker, which means that * any existing flow observers would be lost. We need to replace it with the old progress tracker to ensure progress * updates are correctly sent out after the flow is retried. + * + * If the new tracker contains any child trackers from sub-flows, we need to attach those to the old tracker as well. */ -fun StateMachineManagerInternal.injectOldProgressTracker(oldProgressTracker: ProgressTracker?, newFlowLogic: FlowLogic<*>) { - if (oldProgressTracker != null) { - try { - val field = newFlowLogic::class.java.getDeclaredField("progressTracker") - field.isAccessible = true - field.set(newFlowLogic, oldProgressTracker) - } catch (e: NoSuchFieldException) { - // The flow does not use a progress tracker. +//TODO: instead of replacing the progress tracker after constructing the flow logic, we should inject it during fiber deserialization +fun StateMachineManagerInternal.injectOldProgressTracker(oldTracker: ProgressTracker?, newFlowLogic: FlowLogic<*>) { + if (oldTracker != null) { + val newTracker = newFlowLogic.progressTracker + if (newTracker != null) { + attachNewChildren(oldTracker, newTracker) + replaceTracker(newFlowLogic, oldTracker) } } +} + +private fun attachNewChildren(oldTracker: ProgressTracker, newTracker: ProgressTracker) { + oldTracker.currentStep = newTracker.currentStep + oldTracker.steps.forEachIndexed { index, step -> + val newStep = newTracker.steps[index] + val newChildTracker = newTracker.getChildProgressTracker(newStep) + newChildTracker?.let { child -> + oldTracker.setChildProgressTracker(step, child) + } + } + resubscribeToChildren(oldTracker) +} + +/** + * Re-subscribes to child tracker observables. When a nested progress tracker is deserialized from a checkpoint, + * it retains the child links, but does not automatically re-subscribe to the child changes. + */ +private fun resubscribeToChildren(tracker: ProgressTracker) { + tracker.steps.forEach { + val childTracker = tracker.getChildProgressTracker(it) + if (childTracker != null) { + tracker.setChildProgressTracker(it, childTracker) + resubscribeToChildren(childTracker) + } + } +} + +/** Replaces the deserialized [ProgressTracker] in the [newFlowLogic] with the old one to retain old subscribers. */ +private fun replaceTracker(newFlowLogic: FlowLogic<*>, oldProgressTracker: ProgressTracker?) { + val field = getProgressTrackerField(newFlowLogic) + field?.apply { + isAccessible = true + set(newFlowLogic, oldProgressTracker) + } +} + +private fun getProgressTrackerField(newFlowLogic: FlowLogic<*>): Field? { + var clazz: Class<*> = newFlowLogic::class.java + var field: Field? = null + // The progress tracker field may have been overridden in an abstract superclass, so we have to traverse up + // the hierarchy. + while (clazz != FlowLogic::class.java) { + field = clazz.declaredFields.firstOrNull { it.name == "progressTracker" } + if (field == null) { + clazz = clazz.superclass + } else { + break + } + } + return field } \ No newline at end of file diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 30be478ab1..7c7a0fbd10 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -137,11 +137,6 @@ distributions { into 'cordapps' fileMode = 0444 } - from(project(':samples:bank-of-corda-demo').jar) { - rename 'bank-of-corda-demo-(.*)', 'bank-of-corda.jar' - into 'cordapps' - fileMode = 0444 - } } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt index b47628aa95..06290d4479 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt @@ -25,7 +25,6 @@ class CordappController : Controller() { private val jvm by inject() private val cordappDir: Path = jvm.applicationDir.resolve(NodeConfig.cordappDirName) - private val bankOfCorda: Path = cordappDir.resolve("bank-of-corda.jar") private val finance: Path = cordappDir.resolve("corda-finance.jar") /** @@ -40,11 +39,6 @@ class CordappController : Controller() { finance.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) log.info("Installed 'Finance' cordapp") } - // Nodes cannot issue cash unless they contain the "Bank of Corda" cordapp. - if (config.nodeConfig.issuableCurrencies.isNotEmpty() && bankOfCorda.exists()) { - bankOfCorda.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Bank of Corda' cordapp") - } } /** @@ -54,14 +48,12 @@ class CordappController : Controller() { fun useCordappsFor(config: HasCordapps): List { if (!config.cordappsDir.isDirectory()) return emptyList() return config.cordappsDir.walk(1) { paths -> - paths - .filter(Path::isCordapp) - .filter { !bankOfCorda.endsWith(it.fileName) } - .filter { !finance.endsWith(it.fileName) } - .toList() + paths.filter(Path::isCordapp) + .filter { !finance.endsWith(it.fileName) } + .toList() } } } -fun Path.isCordapp(): Boolean = this.isReadable && this.fileName.toString().endsWith(".jar") -fun Path.inCordappsDir(): Boolean = (this.parent != null) && this.parent.endsWith("cordapps/") +val Path.isCordapp: Boolean get() = this.isReadable && this.fileName.toString().endsWith(".jar") +val Path.inCordappsDir: Boolean get() = (this.parent != null) && this.parent.endsWith("cordapps/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index d20a7873ef..5bdb39f111 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -125,7 +125,7 @@ class ProfileController : Controller() { // Now extract all of the plugins from the ZIP file, // and copy them to a temporary location. StreamSupport.stream(fs.rootDirectories.spliterator(), false) - .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inCordappsDir() && p.isCordapp() && attr.isRegularFile }) } + .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inCordappsDir && p.isCordapp && attr.isRegularFile }) } .forEach { cordapp -> val config = nodeIndex[cordapp.getName(0).toString()] ?: return@forEach diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index 839f901f7a..579b37784c 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -62,7 +62,7 @@ task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').co } } -build.dependsOn buildExplorerJAR +assemble.dependsOn buildExplorerJAR artifacts { runtimeArtifacts buildExplorerJAR diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index 5ca8d61930..f370c921ea 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -153,6 +153,8 @@ class NewTransaction : Fragment() { } } + private fun selectNotary(): Party = notaries.first().value!! + private fun newTransactionDialog(window: Window) = Dialog().apply { dialogPane = root initOwner(window) @@ -162,8 +164,8 @@ class NewTransaction : Fragment() { val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef when (it) { executeButton -> when (transactionTypeCB.value) { - CashTransaction.Issue -> IssueAndPaymentRequest(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.party, notaries.first().value!!, anonymous) - CashTransaction.Pay -> PaymentRequest(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.party, anonymous = anonymous) + CashTransaction.Issue -> IssueAndPaymentRequest(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.party, selectNotary(), anonymous) + CashTransaction.Pay -> PaymentRequest(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.party, anonymous = anonymous, notary = selectNotary()) CashTransaction.Exit -> ExitRequest(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef) else -> null }