diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 6892aeae53..6b10e1b8b0 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1576,24 +1576,8 @@ public interface net.corda.core.node.services.KeyManagementService @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SignableData, java.security.PublicKey) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.DigitalSignature$WithKey sign(byte[], java.security.PublicKey) ## -public interface net.corda.core.node.services.NetworkMapCache - public abstract void clearNetworkMapCache() - @org.jetbrains.annotations.NotNull public abstract List getAllNodes() - @org.jetbrains.annotations.NotNull public abstract rx.Observable getChanged() - @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByAddress(net.corda.core.utilities.NetworkHostAndPort) +public interface net.corda.core.node.services.NetworkMapCache extends net.corda.core.node.services.NetworkMapCacheBase @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalIdentity(net.corda.core.identity.AbstractParty) - @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name) - @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady() - @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey) - @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalName(net.corda.core.identity.CordaX500Name) - @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name) - @org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities() - @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party) - @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name) - @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate getPeerCertificateByLegalName(net.corda.core.identity.CordaX500Name) - public abstract boolean isNotary(net.corda.core.identity.Party) - public abstract boolean isValidatingNotary(net.corda.core.identity.Party) - @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() ## public abstract static class net.corda.core.node.services.NetworkMapCache$MapChange extends java.lang.Object @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo getNode() @@ -1627,6 +1611,24 @@ public static final class net.corda.core.node.services.NetworkMapCache$MapChange public int hashCode() public String toString() ## +public interface net.corda.core.node.services.NetworkMapCacheBase + public abstract void clearNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract List getAllNodes() + @org.jetbrains.annotations.NotNull public abstract rx.Observable getChanged() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByAddress(net.corda.core.utilities.NetworkHostAndPort) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady() + @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate getPeerCertificateByLegalName(net.corda.core.identity.CordaX500Name) + public abstract boolean isNotary(net.corda.core.identity.Party) + public abstract boolean isValidatingNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() +## public abstract class net.corda.core.node.services.NotaryService extends net.corda.core.serialization.SingletonSerializeAsToken public () @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowLogic createServiceFlow(net.corda.core.flows.FlowSession) @@ -2435,6 +2437,17 @@ public final class net.corda.core.serialization.MissingAttachmentsException exte public (List) @org.jetbrains.annotations.NotNull public final List getIds() ## +public final class net.corda.core.serialization.ObjectWithCompatibleContext extends java.lang.Object + public (Object, net.corda.core.serialization.SerializationContext) + @org.jetbrains.annotations.NotNull public final Object component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.ObjectWithCompatibleContext copy(Object, net.corda.core.serialization.SerializationContext) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getContext() + @org.jetbrains.annotations.NotNull public final Object getObj() + public int hashCode() + public String toString() +## public final class net.corda.core.serialization.SerializationAPIKt extends java.lang.Object @org.jetbrains.annotations.NotNull public static final net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationFactory, net.corda.core.serialization.SerializationContext) ## @@ -2476,6 +2489,7 @@ public abstract class net.corda.core.serialization.SerializationFactory extends public () public final Object asCurrent(kotlin.jvm.functions.Function1) @org.jetbrains.annotations.NotNull public abstract Object deserialize(net.corda.core.utilities.ByteSequence, Class, net.corda.core.serialization.SerializationContext) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.ObjectWithCompatibleContext deserializeWithCompatibleContext(net.corda.core.utilities.ByteSequence, Class, net.corda.core.serialization.SerializationContext) @org.jetbrains.annotations.Nullable public final net.corda.core.serialization.SerializationContext getCurrentContext() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getDefaultContext() @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationContext) @@ -2923,7 +2937,7 @@ public static final class net.corda.core.utilities.NonEmptySet$iterator$1 extend ## public class net.corda.core.utilities.OpaqueBytes extends net.corda.core.utilities.ByteSequence public (byte[]) - @org.jetbrains.annotations.NotNull public byte[] getBytes() + @org.jetbrains.annotations.NotNull public final byte[] getBytes() public int getOffset() public int getSize() public static final net.corda.core.utilities.OpaqueBytes$Companion Companion diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index 7694d995dd..e9ac25cf6d 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -10,13 +10,14 @@ if [ ! -f $apiCurrent ]; then exit -1 fi -diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt` -echo "Diff contents:" +# Remove the two header lines from the diff output. +diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail --lines=+3` +echo "Diff contents:" echo "$diffContents" echo # A removed line means that an API was either deleted or modified. -removals=$(echo "$diffContents" | grep "^-\s") +removals=$(echo "$diffContents" | grep "^-") removalCount=`grep -v "^$" < + + diff --git a/build.gradle b/build.gradle index f8a21b7789..70abd216ca 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { file("$projectDir/constants.properties").withInputStream { constants.load(it) } // Our version: bump this on release. - ext.corda_release_version = "1.1-SNAPSHOT" + ext.corda_release_version = "2.0-SNAPSHOT" // Increment this on any release that changes public APIs anywhere in the Corda platform // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal ext.corda_platform_version = 1 @@ -231,7 +231,6 @@ tasks.withType(Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=GB" node { name "O=Controller,OU=corda,L=London,C=GB" notary = [validating : true] @@ -294,12 +293,16 @@ artifactory { publish { contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' repository { - repoKey = 'corda-releases' + repoKey = 'corda-dev' username = 'teamcity' password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') } + defaults { - publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities') + // Root project applies the plugin (for this block) but does not need to be published + if(project != rootProject) { + publications(project.extensions.publish.name()) + } } } } diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index e596a61418..4c0245462d 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -28,11 +28,13 @@ import static kotlin.test.AssertionsKt.assertEquals; import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.contracts.GetBalances.getCashBalance; import static net.corda.node.services.FlowPermissions.startFlowPermission; -import static net.corda.testing.CoreTestUtils.setCordappPackages; -import static net.corda.testing.CoreTestUtils.unsetCordappPackages; import static net.corda.testing.TestConstants.getALICE; public class CordaRPCJavaClientTest extends NodeBasedTest { + public CordaRPCJavaClientTest() { + super(Arrays.asList("net.corda.finance.contracts", CashSchemaV1.class.getPackage().getName())); + } + private List perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class)); private Set permSet = new HashSet<>(perms); private User rpcUser = new User("user1", "test", permSet); @@ -49,17 +51,14 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws ExecutionException, InterruptedException { - setCordappPackages("net.corda.finance.contracts"); CordaFuture> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true); node = nodeFuture.get(); - node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE)); client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); } @After public void done() throws IOException { connection.close(); - unsetCordappPackages(); } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt index 776b96f87f..89264f2e05 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt @@ -23,7 +23,7 @@ import org.junit.rules.ExpectedException @CordaSerializable data class Packet(val x: () -> Long) -class BlacklistKotlinClosureTest : NodeBasedTest() { +class BlacklistKotlinClosureTest : NodeBasedTest(listOf("net.corda.client.rpc")) { companion object { @Suppress("UNUSED") val logger = loggerFor() const val EVIL: Long = 666 @@ -66,7 +66,6 @@ class BlacklistKotlinClosureTest : NodeBasedTest() { @Before fun setUp() { - setCordappPackages("net.corda.client.rpc") aliceNode = startNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() bobNode = startNode(BOB.name, rpcUsers = listOf(rpcUser)).getOrThrow() bobNode.registerInitiatedFlow(RemoteFlowC::class.java) @@ -78,7 +77,6 @@ class BlacklistKotlinClosureTest : NodeBasedTest() { connection?.close() bobNode.internals.stop() aliceNode.internals.stop() - unsetCordappPackages() } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 422f08f8f3..277769354c 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -2,6 +2,7 @@ package net.corda.client.rpc import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowInitiator +import net.corda.core.internal.packageName import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow @@ -23,8 +24,6 @@ import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.chooseIdentity import net.corda.testing.node.NodeBasedTest -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.After @@ -34,7 +33,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest() { +class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) { private val rpcUser = User("user1", "test", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -49,16 +48,13 @@ class CordaRPCClientTest : NodeBasedTest() { @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts") node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() - node.internals.registerCustomSchemas(setOf(CashSchemaV1)) client = CordaRPCClient(node.internals.configuration.rpcAddress!!) } @After fun done() { connection?.close() - unsetCordappPackages() } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 7ccf1913d5..a25881f4ec 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -92,6 +92,7 @@ class RPCStabilityTests { startAndStop() } val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor) + assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter) executor.shutdownNow() } diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 70ae26e1b6..31daccc9c2 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -74,9 +74,9 @@ public class StandaloneCordaRPCJavaClientTest { } private void copyFinanceCordapp() { - Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins")); + Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps")); try { - Files.createDirectories(pluginsDir); + Files.createDirectories(cordappsDir); } catch (IOException ex) { fail("Failed to create directories"); } @@ -84,7 +84,7 @@ public class StandaloneCordaRPCJavaClientTest { paths.forEach(file -> { if (file.toString().contains("corda-finance")) { try { - Files.copy(file, pluginsDir.resolve(file.getFileName())); + Files.copy(file, cordappsDir.resolve(file.getFileName())); } catch (IOException ex) { fail("Failed to copy finance jar"); } diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 1d487f762b..d6d305f542 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -89,12 +89,12 @@ class StandaloneCordaRPClientTest { } private fun copyFinanceCordapp() { - val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories() + val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories() // Find the finance jar file for the smoke tests of this module val financeJar = Paths.get("build", "resources", "smokeTest").list { it.filter { "corda-finance" in it.toString() }.toList().single() } - financeJar.copyToDirectory(pluginsDir) + financeJar.copyToDirectory(cordappsDir) } @Test diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index 7482825440..b760817afe 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -28,26 +28,24 @@ class IdentitySyncFlowTests { @Before fun before() { - setCordappPackages("net.corda.finance.contracts.asset") // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `sync confidential identities`() { // Set up values we'll need - mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(ALICE.name) - val bobNode = mockNet.createPartyNode(BOB.name) - val alice: Party = aliceNode.services.myInfo.chooseIdentity() - val bob: Party = bobNode.services.myInfo.chooseIdentity() - val notary = aliceNode.services.getDefaultNotary() + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val alice: Party = aliceNode.info.singleIdentity() + val bob: Party = bobNode.info.singleIdentity() + val notary = notaryNode.services.getDefaultNotary() bobNode.internals.registerInitiatedFlow(Receive::class.java) // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about @@ -73,12 +71,12 @@ class IdentitySyncFlowTests { fun `don't offer other's identities confidential identities`() { // Set up values we'll need val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(ALICE.name) - val bobNode = mockNet.createPartyNode(BOB.name) - val charlieNode = mockNet.createPartyNode(CHARLIE.name) - val alice: Party = aliceNode.services.myInfo.chooseIdentity() - val bob: Party = bobNode.services.myInfo.chooseIdentity() - val charlie: Party = charlieNode.services.myInfo.chooseIdentity() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) + val alice: Party = aliceNode.info.singleIdentity() + val bob: Party = bobNode.info.singleIdentity() + val charlie: Party = charlieNode.info.singleIdentity() val notary = notaryNode.services.getDefaultNotary() bobNode.internals.registerInitiatedFlow(Receive::class.java) diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 55e9ef9d26..8e73042899 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -13,14 +13,14 @@ class SwapIdentitiesFlowTests { @Test fun `issue key`() { // We run this in parallel threads to help catch any race conditions that may exist. - val mockNet = MockNetwork(false, true) + val mockNet = MockNetwork(threadPerNode = true) // Set up values we'll need val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) - val alice: Party = aliceNode.services.myInfo.chooseIdentity() - val bob: Party = bobNode.services.myInfo.chooseIdentity() + val alice = aliceNode.info.singleIdentity() + val bob = bobNode.services.myInfo.singleIdentity() // Run the flows val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)) @@ -53,13 +53,13 @@ class SwapIdentitiesFlowTests { @Test fun `verifies identity name`() { // We run this in parallel threads to help catch any race conditions that may exist. - val mockNet = MockNetwork(false, true) + val mockNet = MockNetwork(threadPerNode = true) // Set up values we'll need val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name) val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) - val bob: Party = bobNode.services.myInfo.chooseIdentity() + val bob: Party = bobNode.services.myInfo.singleIdentity() val notBob = notaryNode.database.transaction { notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false) } @@ -78,13 +78,13 @@ class SwapIdentitiesFlowTests { @Test fun `verifies signature`() { // We run this in parallel threads to help catch any race conditions that may exist. - val mockNet = MockNetwork(false, true) + val mockNet = MockNetwork(threadPerNode = true) // Set up values we'll need val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name) val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) - val bob: Party = bobNode.services.myInfo.chooseIdentity() + val bob: Party = bobNode.services.myInfo.singleIdentity() // Check that the wrong signature is rejected notaryNode.database.transaction { notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false) diff --git a/constants.properties b/constants.properties index 4591e03dea..37f3b33003 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=2.0.1 +gradlePluginsVersion=2.0.5 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index b55055e529..e55a91d546 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -2,6 +2,7 @@ package net.corda.core.contracts import net.corda.core.identity.Party import net.corda.core.internal.extractFile +import net.corda.core.serialization.CordaSerializable import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream @@ -17,6 +18,7 @@ import java.util.jar.JarInputStream * - Legal documents * - Facts generated by oracles which might be reused a lot */ +@CordaSerializable interface Attachment : NamedByHash { fun open(): InputStream fun openAsJAR(): JarInputStream { diff --git a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt index c32792a998..514abb986a 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt @@ -10,5 +10,6 @@ enum class ComponentGroupEnum { COMMANDS_GROUP, // ordinal = 2. ATTACHMENTS_GROUP, // ordinal = 3. NOTARY_GROUP, // ordinal = 4. - TIMEWINDOW_GROUP // ordinal = 5. + TIMEWINDOW_GROUP, // ordinal = 5. + SIGNERS_GROUP // ordinal = 6. } diff --git a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt index c8c650257d..8126292138 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt @@ -64,6 +64,17 @@ abstract class TimeWindow { */ abstract val midpoint: Instant? + /** + * Returns the duration between [fromTime] and [untilTime] if both are non-null. Otherwise returns null. + */ + val length: Duration? get() { + return if (fromTime == null || untilTime == null) { + null + } else { + Duration.between(fromTime, untilTime) + } + } + /** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */ abstract operator fun contains(instant: Instant): Boolean diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index eb935965b3..26145b38a1 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -19,7 +19,7 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str class ContractConstraintRejection(txId: SecureHash, contractClass: String) : TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null) - class MissingAttachmentRejection(txId: SecureHash, contractClass: String) + class MissingAttachmentRejection(txId: SecureHash, val contractClass: String) : TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null) class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 58b68bb176..f2c0a45918 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -2,7 +2,10 @@ package net.corda.core.crypto import net.corda.core.internal.X509EdDSAEngine import net.corda.core.serialization.serialize -import net.i2p.crypto.eddsa.* +import net.i2p.crypto.eddsa.EdDSAEngine +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.EdDSASecurityProvider import net.i2p.crypto.eddsa.math.GroupElement import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable @@ -39,8 +42,6 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec import java.math.BigInteger import java.security.* -import java.security.KeyFactory -import java.security.KeyPairGenerator import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec @@ -148,7 +149,7 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) - /** Corda composite key type */ + /** Corda composite key type. */ @JvmField val COMPOSITE_KEY = SignatureScheme( 6, @@ -823,7 +824,7 @@ object Crypto { @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) - // custom key pair generator from entropy. + // Custom key pair generator from entropy. private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair { val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length. @@ -882,7 +883,7 @@ object Crypto { } } - // return true if EdDSA publicKey is point at infinity. + // Return true if EdDSA publicKey is point at infinity. // For EdDSA a custom function is required as it is not supported by the I2P implementation. private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean { return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) @@ -894,7 +895,7 @@ object Crypto { return signatureScheme.schemeCodeName in signatureSchemeMap } - // validate a key, by checking its algorithmic params. + // Validate a key, by checking its algorithmic params. private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { return when (key) { is PublicKey -> validatePublicKey(signatureScheme, key) @@ -903,7 +904,7 @@ object Crypto { } } - // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). + // Check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { return when (key) { is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key) @@ -912,7 +913,7 @@ object Crypto { } } - // check if a private key satisfies algorithm specs. + // Check if a private key satisfies algorithm specs. private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { return when (key) { is BCECPrivateKey -> key.parameters == signatureScheme.algSpec @@ -924,7 +925,6 @@ object Crypto { /** * Convert a public key to a supported implementation. - * * @param key a public key. * @return a supported implementation of the input public key. * @throws IllegalArgumentException on not supported scheme or if the given key specification diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 32b14cffcd..17bd078862 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -20,9 +20,19 @@ import java.security.* * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign)) +/** + * Utility to simplify the act of signing a byte array and return a [DigitalSignature.WithKey] object. + * Note that there is no check if the public key matches with the signing private key. + * @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the [DigitalSignature.WithKey] object on the input message [bytesToSign] and [publicKey]. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ +@Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) /** @@ -33,10 +43,13 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public) +/** Helper function to sign the bytes of [bytesToSign] with a key pair. */ +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes) + /** * Helper function for signing a [SignableData] object. * @param signableData the object to be signed. @@ -56,8 +69,8 @@ fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSi * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ -// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`, -@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class) +// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`. +@Throws(SignatureException::class, InvalidKeyException::class) fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content) /** @@ -70,9 +83,10 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d * signature). * @throws SignatureException if the signature is invalid (i.e. damaged). * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. + * @throws IllegalStateException if this is a [CompositeKey], because verification of composite key signatures is not supported. * @return whether the signature is correct for this key. */ -@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(SignatureException::class, InvalidKeyException::class) fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean { if (this is CompositeKey) throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification. @@ -82,9 +96,12 @@ fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean /** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */ fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() +/** Return a [Set] of the contained keys if this is a [CompositeKey]; otherwise, return a [Set] with a single element (this [PublicKey]). */ val PublicKey.keys: Set get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) +/** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) +/** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKeys: Iterable): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys) /** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */ @@ -98,8 +115,9 @@ fun Iterable.byKeys() = map { it.by }.toSet() // Allow Kotlin destructuring: // val (private, public) = keyPair +/* The [PrivateKey] of this [KeyPair] .*/ operator fun KeyPair.component1(): PrivateKey = this.private - +/* The [PublicKey] of this [KeyPair] .*/ operator fun KeyPair.component2(): PublicKey = this.public /** A simple wrapper that will make it easier to swap out the EC algorithm we use in future. */ @@ -122,7 +140,7 @@ fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEnt * if this signatureData algorithm is unable to process the input data provided, etc. * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. */ -@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData) /** @@ -135,7 +153,7 @@ fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = * if this signatureData algorithm is unable to process the input data provided, etc. * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. */ -@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData) /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index 77e06bf1c2..b46eb7f631 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -158,4 +158,41 @@ class PartialMerkleTree(val root: PartialTree) { return false return (verifyRoot == merkleRootHash) } + + /** + * Method to return the index of the input leaf in the partial Merkle tree structure. + * @param leaf the component hash to check. + * @return leaf-index of this component (starting from zero). + * @throws MerkleTreeException if the provided hash is not in the tree. + */ + @Throws(MerkleTreeException::class) + internal fun leafIndex(leaf: SecureHash): Int { + // Special handling if the tree consists of one node only. + if (root is PartialTree.IncludedLeaf && root.hash == leaf) return 0 + val flagPath = mutableListOf() + if (!leafIndexHelper(leaf, this.root, flagPath)) throw MerkleTreeException("The provided hash $leaf is not in the tree.") + return indexFromFlagPath(flagPath) + } + + // Helper function to compute the path. False means go to the left and True to the right. + // Because the path is updated recursively, the path is returned in reverse order. + private fun leafIndexHelper(leaf: SecureHash, node: PartialTree, path: MutableList): Boolean { + if (node is PartialTree.IncludedLeaf) { + return node.hash == leaf + } else if (node is PartialTree.Node) { + if (leafIndexHelper(leaf, node.left, path)) { + path.add(false) + return true + } + if (leafIndexHelper(leaf, node.right, path)) { + path.add(true) + return true + } + } + return false + } + + // Return the leaf index from the path boolean list. + private fun indexFromFlagPath(pathList: List) = + pathList.mapIndexed { index, value -> if (value) (1 shl index) else 0 }.sum() } diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index 16bf533550..6555ac6af7 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -19,39 +19,87 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { } } + /** + * Convert the hash value to an uppercase hexadecimal [String]. + */ override fun toString(): String = bytes.toHexString() + /** + * Returns the first [prefixLen] hexadecimal digits of the [SecureHash] value. + * @param prefixLen The number of characters in the prefix. + */ fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen) + + /** + * Append a second hash value to this hash value, and then compute the SHA-256 hash of the result. + * @param other The hash to append to this one. + */ fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256() // Like static methods in Java, except the 'companion' is a singleton that can have state. companion object { + /** + * Converts a SHA-256 hash value represented as a hexadecimal [String] into a [SecureHash]. + * @param str A sequence of 64 hexadecimal digits that represents a SHA-256 hash value. + * @throws IllegalArgumentException The input string does not contain 64 hexadecimal digits, or it contains incorrectly-encoded characters. + */ @JvmStatic - fun parse(str: String) = str.toUpperCase().parseAsHex().let { - when (it.size) { - 32 -> SHA256(it) - else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str") + fun parse(str: String): SHA256 { + return str.toUpperCase().parseAsHex().let { + when (it.size) { + 32 -> SHA256(it) + else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str") + } } } + /** + * Computes the SHA-256 hash value of the [ByteArray]. + * @param bytes The [ByteArray] to hash. + */ @JvmStatic fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes)) + /** + * Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash. + * @param bytes The [ByteArray] to hash. + */ @JvmStatic fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes) + /** + * Computes the SHA-256 hash of the [String]'s UTF-8 byte contents. + * @param str [String] whose UTF-8 contents will be hashed. + */ @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) + /** + * Generates a random SHA-256 value. + */ @JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) + /** + * A SHA-256 hash value consisting of 32 0x00 bytes. + */ val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) + + /** + * A SHA-256 hash value consisting of 32 0xFF bytes. + */ val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() })) } // In future, maybe SHA3, truncated hashes etc. } +/** + * Compute the SHA-256 hash for the contents of the [ByteArray]. + */ fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this) + +/** + * Compute the SHA-256 hash for the contents of the [OpaqueBytes]. + */ fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bytes) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 419c918753..0239c81f20 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -1,6 +1,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.Strand import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -16,6 +17,8 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.debug import org.slf4j.Logger +import java.time.Duration +import java.time.Instant /** * A sub-class of [FlowLogic] implements a flow using direct, straight line blocking code. Thus you @@ -43,6 +46,34 @@ abstract class FlowLogic { /** This is where you should log things to. */ val logger: Logger get() = stateMachine.logger + companion object { + /** + * Return the outermost [FlowLogic] instance, or null if not in a flow. + */ + @JvmStatic + val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic + + /** + * If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise, + * just sleep for [duration]. This sleep function is not designed to aid scheduling, for which you should + * consider using [SchedulableState]. It is designed to aid with managing contention for which you have not + * managed via another means. + * + * Warning: long sleeps and in general long running flows are highly discouraged, as there is currently no + * support for flow migration! This method will throw an exception if you attempt to sleep for longer than + * 5 minutes. + */ + @Suspendable + @JvmStatic + @Throws(FlowException::class) + fun sleep(duration: Duration) { + if (duration.compareTo(Duration.ofMinutes(5)) > 0) { + throw FlowException("Attempt to sleep for longer than 5 minutes is not supported. Consider using SchedulableState.") + } + (Strand.currentStrand() as? FlowStateMachine<*>)?.sleepUntil(Instant.now() + duration) ?: Strand.sleep(duration.toMillis()) + } + } + /** * Returns a wrapped [java.util.UUID] object that identifies this state machine run (i.e. subflows have the same * identifier as their parents). diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index 261af49f02..a2b0e2fd15 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -10,6 +10,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger +import java.time.Instant /** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */ interface FlowStateMachine { @@ -35,6 +36,9 @@ interface FlowStateMachine { @Suspendable fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction + @Suspendable + fun sleepUntil(until: Instant) + fun checkFlowPermission(permissionName: String, extraAuditData: Map) fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) @@ -45,6 +49,7 @@ interface FlowStateMachine { @Suspendable fun persistFlowStackSnapshot(flowClass: Class>) + val logic: FlowLogic val serviceHub: ServiceHub val logger: Logger val id: StateMachineRunId diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 5f26ef2e48..2f101b869f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -300,3 +300,6 @@ fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serial * @suppress */ fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext) + +/** Convenience method to get the package name of a class literal. */ +val KClass<*>.packageName get() = java.`package`.name diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index cf3e5aed2e..07e6c550be 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -1,5 +1,6 @@ package net.corda.core.node +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.serialization.CordaSerializable @@ -42,4 +43,9 @@ data class NodeInfo(val addresses: List, /** Returns true if [party] is one of the identities of this node, else false. */ fun isLegalIdentity(party: Party): Boolean = party in legalIdentities + + fun identityFromX500Name(name: CordaX500Name): Party { + val identity = legalIdentitiesAndCerts.singleOrNull { it.name == name } ?: throw IllegalArgumentException("Node does not have an identity \"$name\"") + return identity.party + } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 792baadb39..7e6f959e4f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -18,8 +18,7 @@ import java.security.PublicKey * from an authoritative service, and adds easy lookup of the data stored within it. Generally it would be initialised * with a specified network map service, which it fetches data from and then subscribes to updates of. */ -interface NetworkMapCache { - +interface NetworkMapCache : NetworkMapCacheBase { @CordaSerializable sealed class MapChange { abstract val node: NodeInfo @@ -29,6 +28,22 @@ interface NetworkMapCache { data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange() } + /** + * Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party + * is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this + * returns null. + * Notice that when there are more than one node for a given party (in case of distributed services) first service node + * found will be returned. See also: [NetworkMapCache.getNodesByLegalIdentityKey]. + * + * @param party party to retrieve node information for. + * @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is + * no node for the party, only that this cache is unaware of it. + */ + fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? +} + +/** Subset of [NetworkMapCache] that doesn't depend on an [IdentityService]. */ +interface NetworkMapCacheBase { // DOCSTART 1 /** * A list of notary services available on the network. @@ -40,7 +55,7 @@ interface NetworkMapCache { // DOCEND 1 /** Tracks changes to the network map cache. */ - val changed: Observable + val changed: Observable /** Future to track completion of the NetworkMapService registration. */ val nodeReady: CordaFuture @@ -48,20 +63,7 @@ interface NetworkMapCache { * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the * first subscriber is registered so as to avoid racing with early updates. */ - fun track(): DataFeed, MapChange> - - /** - * Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party - * is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this - * returns null. - * Notice that when there are more than one node for a given party (in case of distributed services) first service node - * found will be returned. See also: [getNodesByLegalIdentityKey]. - * - * @param party party to retrieve node information for. - * @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is - * no node for the party, only that this cache is unaware of it. - */ - fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? + fun track(): DataFeed, NetworkMapCache.MapChange> /** * Look up the node info for a legal name. diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 574008d6d8..8241fcd13c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -1,5 +1,6 @@ package net.corda.core.node.services +import com.google.common.primitives.Booleans import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.* @@ -15,12 +16,13 @@ import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { companion object { const val ID_PREFIX = "corda.notary." - fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false): String { - require(!raft || !bft) + fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { + require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } return StringBuffer(ID_PREFIX).apply { append(if (validating) "validating" else "simple") if (raft) append(".raft") if (bft) append(".bft") + if (custom) append(".custom") }.toString() } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index 0eccaf6ffb..2b5705a187 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -1,5 +1,3 @@ -@file:JvmName("SerializationAPI") - package net.corda.core.serialization import net.corda.core.crypto.SecureHash @@ -8,6 +6,9 @@ import net.corda.core.internal.WriteOnceProperty import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.sequence +import java.sql.Blob + +data class ObjectWithCompatibleContext(val obj: T, val context: SerializationContext) /** * An abstraction for serializing and deserializing objects, with support for versioning of the wire format via @@ -23,6 +24,16 @@ abstract class SerializationFactory { */ abstract fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + * @return deserialized object along with [SerializationContext] to identify encoding used. + */ + abstract fun deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): ObjectWithCompatibleContext + /** * Serialize an object to bytes using the preferred serialization format version from the context. * @@ -88,6 +99,8 @@ abstract class SerializationFactory { } } +typealias VersionHeader = ByteSequence + /** * Parameters to serialization and deserialization. */ @@ -95,7 +108,7 @@ interface SerializationContext { /** * When serializing, use the format this header sequence represents. */ - val preferredSerializationVersion: ByteSequence + val preferredSerializationVersion: VersionHeader /** * The class loader to use for deserialization. */ @@ -148,7 +161,7 @@ interface SerializationContext { /** * Helper method to return a new context based on this context but with serialization using the format this header sequence represents. */ - fun withPreferredSerializationVersion(versionHeader: ByteSequence): SerializationContext + fun withPreferredSerializationVersion(versionHeader: VersionHeader): SerializationContext /** * The use case that we are serializing for, since it influences the implementations chosen. @@ -175,6 +188,15 @@ inline fun ByteSequence.deserialize(serializationFactory: Seri return serializationFactory.deserialize(this, T::class.java, context) } +/** + * Additionally returns [SerializationContext] which was used for encoding. + * It might be helpful to know [SerializationContext] to use the same encoding in the reply. + */ +inline fun ByteSequence.deserializeWithCompatibleContext(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, + context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext { + return serializationFactory.deserializeWithCompatibleContext(this, T::class.java, context) +} + /** * Convenience extension method for deserializing SerializedBytes with type matching, utilising the defaults. */ @@ -187,6 +209,11 @@ inline fun SerializedBytes.deserialize(serializationFactory */ inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context) +/** + * Convenience extension method for deserializing a JDBC Blob, utilising the defaults. + */ +inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) + /** * Convenience extension method for serializing an object of type T, utilising the defaults. */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 8f2c476004..ba7cb8de1f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -25,7 +25,7 @@ abstract class TraversableTransaction(open val componentGroups: List> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ - val commands: List> = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) + val commands: List> = deserialiseCommands() override val notary: Party? = let { val notaries: List = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes(it).deserialize() }) @@ -74,6 +74,31 @@ abstract class TraversableTransaction(open val componentGroups: List> { + // TODO: we could avoid deserialising unrelated signers. + // However, current approach ensures the transaction is not malformed + // and it will throw if any of the signers objects is not List of public keys). + val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes>(it).deserialize() }) + val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) + val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal } + if (group is FilteredComponentGroup) { + check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" } + val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) } + val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) } + if (leafIndices.isNotEmpty()) + check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" } + return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) } + } else { + // It is a WireTransaction + // or a FilteredTransaction with no Commands (in which case group is null). + check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" } + return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) } + } + } } /** @@ -111,11 +136,12 @@ class FilteredTransaction private constructor( val filteredSerialisedComponents: MutableMap> = hashMapOf() val filteredComponentNonces: MutableMap> = hashMapOf() val filteredComponentHashes: MutableMap> = hashMapOf() // Required for partial Merkle tree generation. + var signersIncluded = false fun filter(t: T, componentGroupIndex: Int, internalIndex: Int) { if (filtering.test(t)) { val group = filteredSerialisedComponents[componentGroupIndex] - // Because the filter passed, we know there is a match. We also use first vs single as the init function + // Because the filter passed, we know there is a match. We also use first Vs single as the init function // of WireTransaction ensures there are no duplicated groups. val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] if (group == null) { @@ -132,6 +158,17 @@ class FilteredTransaction private constructor( filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) } + // If at least one command is visible, then all command-signers should be visible as well. + // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. + if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) { + signersIncluded = true + val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal + // There exist commands, thus the signers group is not empty. + val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } + filteredSerialisedComponents.put(signersGroupIndex, signersGroupComponents.components.toMutableList()) + filteredComponentNonces.put(signersGroupIndex, wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList()) + filteredComponentHashes.put(signersGroupIndex, wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList()) + } } } @@ -142,6 +179,10 @@ class FilteredTransaction private constructor( wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) } if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0) if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0) + // It is highlighted that because there is no a signers property in TraversableTransaction, + // one cannot specifically filter them in or out. + // The above is very important to ensure someone won't filter out the signers component group if at least one + // command is included in a FilteredTransaction. // It's sometimes possible that when we receive a WireTransaction for which there is a new or more unknown component groups, // we decide to filter and attach this field to a FilteredTransaction. @@ -207,7 +248,9 @@ class FilteredTransaction private constructor( /** * Function that checks if all of the components in a particular group are visible. * This functionality is required on non-Validating Notaries to check that all inputs are visible. - * It might also be applied in Oracles, where an Oracle should know it can see all commands. + * It might also be applied in Oracles or any other entity requiring [Command] visibility, but because this method + * cannot distinguish between related and unrelated to the signer [Command]s, one should use the + * [checkCommandVisibility] method, which is specifically designed for [Command] visibility purposes. * The logic behind this algorithm is that we check that the root of the provided group partialMerkleTree matches with the * root of a fullMerkleTree if computed using all visible components. * Note that this method is usually called after or before [verify], to also ensure that the provided partial Merkle @@ -229,18 +272,54 @@ class FilteredTransaction private constructor( visibilityCheck(group.groupIndex < groupHashes.size) { "There is no matching component group hash for group ${group.groupIndex}" } val groupPartialRoot = groupHashes[group.groupIndex] val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash - visibilityCheck(groupPartialRoot == groupFullRoot) { "The partial Merkle tree root does not match with the received root for group ${group.groupIndex}" } + visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" } + // Verify the top level Merkle tree from groupHashes. + visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" } } } - inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any): Unit { + /** + * Function that checks if all of the commands that should be signed by the input public key are visible. + * This functionality is required from Oracles to check that all of the commands they should sign are visible. + * This algorithm uses the [ComponentGroupEnum.SIGNERS_GROUP] to count how many commands should be signed by the + * input [PublicKey] and it then matches it with the size of received [commands]. + * Note that this method does not throw if there are no commands for this key to sign in the original [WireTransaction]. + * @param publicKey signer's [PublicKey] + * @throws ComponentVisibilityException if not all of the related commands are visible. + */ + @Throws(ComponentVisibilityException::class) + fun checkCommandVisibility(publicKey: PublicKey) { + val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal } + val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners) + val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size + visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" } + } + + // Function to return number of expected commands to sign. + private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int { + checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) + if (commandSigners == null) return 0 + fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List { + try { + return SerializedBytes>(opaqueBytes.bytes).deserialize() + } catch (e: Exception) { + throw Exception("Malformed transaction, signers at index $internalIndex cannot be deserialised", e) + } + } + + return commandSigners.components + .mapIndexed { internalIndex, opaqueBytes -> signersKeys(internalIndex, opaqueBytes) } + .filter { signers -> publicKey in signers }.size + } + + inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any) { if (!value) { val message = lazyMessage() throw FilteredTransactionVerificationException(id, message.toString()) } } - inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any): Unit { + inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) { if (!value) { val message = lazyMessage() throw ComponentVisibilityException(id, message.toString()) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index 1909168198..ab25d68064 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -1,8 +1,6 @@ package net.corda.core.transactions -import net.corda.core.contracts.ContractState import net.corda.core.contracts.NamedByHash -import net.corda.core.contracts.TransactionState import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.transactions.SignedTransaction.SignaturesMissingException diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 2325344286..7112369667 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -6,9 +6,10 @@ import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.Emoji import net.corda.core.node.ServicesForResolution -import net.corda.core.serialization.* -import net.corda.core.utilities.OpaqueBytes import net.corda.core.node.services.AttachmentId +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.serialize +import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey import java.security.SignatureException import java.util.function.Predicate @@ -213,10 +214,14 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr val componentGroupMap: MutableList = mutableListOf() if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() })) if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() })) - if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() })) + // Adding commandData only to the commands group. Signers are added in their own group. + if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() })) if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() })) if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize()))) if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize()))) + // Adding signers to their own group. This is required for command visibility purposes: a party receiving + // a FilteredTransaction can now verify it sees all the commands it should sign. + if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() })) return componentGroupMap } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index a2b74a11ff..cf8b1f915d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -118,8 +118,11 @@ sealed class ByteSequence : Comparable { * In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such * functionality to Java, but it won't arrive for a few years yet! */ -open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() { +open class OpaqueBytes(bytes: ByteArray) : ByteSequence() { companion object { + /** + * Create [OpaqueBytes] from a sequence of [Byte] values. + */ @JvmStatic fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b)) } @@ -128,13 +131,35 @@ open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() { require(bytes.isNotEmpty()) } - override val size: Int get() = bytes.size - override val offset: Int get() = 0 + /** + * The bytes are always cloned so that this object becomes immutable. This has been done + * to prevent tampering with entities such as [SecureHash] and [PrivacySalt], as well as + * preserve the integrity of our hash constants [zeroHash] and [allOnesHash]. + * + * Cloning like this may become a performance issue, depending on whether or not the JIT + * compiler is ever able to optimise away the clone. In which case we may need to revisit + * this later. + */ + override final val bytes: ByteArray = bytes + get() = field.clone() + override val size: Int = bytes.size + override val offset: Int = 0 } +/** + * Copy [size] bytes from this [ByteArray] starting from [offset] into a new [ByteArray]. + */ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of(this, offset, size) +/** + * Converts this [ByteArray] into a [String] of hexadecimal digits. + */ fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this) + +/** + * Converts this [String] of hexadecimal digits into a [ByteArray]. + * @throws IllegalArgumentException if the [String] contains incorrectly-encoded characters. + */ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this) /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index 2c1854c897..2fd31c9590 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -3,9 +3,8 @@ package net.corda.core.utilities import net.corda.core.crypto.Base58 +import net.corda.core.crypto.Crypto import net.corda.core.crypto.sha256 -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize import java.nio.charset.Charset import java.security.PublicKey import java.util.* @@ -15,11 +14,13 @@ import javax.xml.bind.DatatypeConverter // [ByteArray] encoders +/** Convert a byte array to a Base58 encoded [String]. */ fun ByteArray.toBase58(): String = Base58.encode(this) +/** Convert a byte array to a Base64 encoded [String]. */ fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) -/** Convert a byte array to a hex (base 16) capitalized encoded string.*/ +/** Convert a byte array to a hex (Base16) capitalized encoded [String]. */ fun ByteArray.toHex(): String = DatatypeConverter.printHexBinary(this) @@ -65,7 +66,15 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64() // TODO We use for both CompositeKeys and EdDSAPublicKey custom serializers and deserializers. We need to specify encoding. // TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition // structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519). -fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize() +/** + * Method to return the [PublicKey] object given its Base58-[String] representation. + * @param base58String the Base58 encoded format of the serialised [PublicKey]. + * @return the resulted [PublicKey] after decoding the [base58String] input and then deserialising to a [PublicKey] object. + */ +fun parsePublicKeyBase58(base58String: String): PublicKey = Crypto.decodePublicKey(base58String.base58ToByteArray()) -fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58() -fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised). +/** Return the Base58 representation of the serialised public key. */ +fun PublicKey.toBase58String(): String = this.encoded.toBase58() + +/** Return the bytes of the SHA-256 output for this public key. */ +fun PublicKey.toSHA256Bytes(): ByteArray = this.encoded.sha256().bytes diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 897ae4d1d8..b12ea8353d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -1,5 +1,3 @@ -@file:JvmName("KotlinUtils") - package net.corda.core.utilities import net.corda.core.internal.concurrent.get diff --git a/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt index 7ed31d3d38..b50aa66b99 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt @@ -1,13 +1,9 @@ package net.corda.core.contracts import net.corda.core.contracts.ComponentGroupEnum.* -import net.corda.core.crypto.MerkleTree -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.secureRandomBytes +import net.corda.core.crypto.* import net.corda.core.serialization.serialize -import net.corda.core.transactions.ComponentGroup -import net.corda.core.transactions.ComponentVisibilityException -import net.corda.core.transactions.WireTransaction +import net.corda.core.transactions.* import net.corda.core.utilities.OpaqueBytes import net.corda.testing.* import net.corda.testing.contracts.DummyContract @@ -34,22 +30,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) } private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) } - private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }) } + private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) } private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty. private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) } private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) } + private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) } - private val newUnknownComponentGroup = ComponentGroup(20, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8)))) - private val newUnknownComponentEmptyGroup = ComponentGroup(21, emptyList()) + private val newUnknownComponentGroup = ComponentGroup(100, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8)))) + private val newUnknownComponentEmptyGroup = ComponentGroup(101, emptyList()) // Do not add attachments (empty list). private val componentGroupsA by lazy { listOf( - inputGroup, - outputGroup, - commandGroup, - notaryGroup, - timeWindowGroup + inputGroup, + outputGroup, + commandGroup, + notaryGroup, + timeWindowGroup, + signersGroup ) } private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) } @@ -74,7 +72,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { commandGroup, attachmentGroup, notaryGroup, - timeWindowGroup + timeWindowGroup, + signersGroup ) assertFails { WireTransaction(componentGroups = componentGroupsEmptyAttachment, privacySalt = privacySalt) } @@ -86,7 +85,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { outputGroup, commandGroup, notaryGroup, - timeWindowGroup + timeWindowGroup, + signersGroup ) val wireTransaction1ShuffledInputs = WireTransaction(componentGroups = componentGroupsB, privacySalt = privacySalt) // The ID has changed due to change of the internal ordering in inputs. @@ -106,7 +106,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { inputGroup, commandGroup, notaryGroup, - timeWindowGroup + timeWindowGroup, + signersGroup ) assertEquals(wireTransactionA, WireTransaction(componentGroups = shuffledComponentGroupsA, privacySalt = privacySalt)) } @@ -123,7 +124,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { commandGroup, ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components), notaryGroup, - timeWindowGroup + timeWindowGroup, + signersGroup ) assertFails { WireTransaction(componentGroupsB, privacySalt) } @@ -134,7 +136,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { commandGroup, // First commandsGroup. commandGroup, // Second commandsGroup. notaryGroup, - timeWindowGroup + timeWindowGroup, + signersGroup ) assertFails { WireTransaction(componentGroupsDuplicatedCommands, privacySalt) } @@ -144,7 +147,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { outputGroup, commandGroup, notaryGroup, - timeWindowGroup + timeWindowGroup, + signersGroup ) assertFails { WireTransaction(componentGroupsC, privacySalt) } @@ -154,23 +158,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { commandGroup, notaryGroup, timeWindowGroup, - newUnknownComponentGroup // A new unknown component with ordinal 20 that we cannot process. + signersGroup, + newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process. ) // The old client (receiving more component types than expected) is still compatible. val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt) assertEquals(wireTransactionCompatibleA.availableComponentGroups, wireTransactionA.availableComponentGroups) // The known components are the same. assertNotEquals(wireTransactionCompatibleA, wireTransactionA) // But obviously, its Merkle root has changed Vs wireTransactionA (which doesn't include this extra component). - assertEquals(6, wireTransactionCompatibleA.componentGroups.size) - // The old client will trhow if receiving an empty component (even if this unknown). + // The old client will throw if receiving an empty component (even if this is unknown). val componentGroupsCompatibleEmptyNew = listOf( inputGroup, outputGroup, commandGroup, notaryGroup, timeWindowGroup, - newUnknownComponentEmptyGroup // A new unknown component with ordinal 21 that we cannot process. + signersGroup, + newUnknownComponentEmptyGroup // A new unknown component with ordinal 101 that we cannot process. ) assertFails { WireTransaction(componentGroupsCompatibleEmptyNew, privacySalt) } } @@ -179,7 +184,9 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { fun `FilteredTransaction constructors and compatibility`() { // Filter out all of the components. val ftxNothing = wireTransactionA.buildFilteredTransaction(Predicate { false }) // Nothing filtered. - assertEquals(6, ftxNothing.groupHashes.size) // Although nothing filtered, we still receive the group hashes for the top level Merkle tree. + // Although nothing filtered, we still receive the group hashes for the top level Merkle tree. + // Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes. + assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size) ftxNothing.verify() // Include all of the components. @@ -191,6 +198,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP) ftxAll.checkAllComponentsVisible(NOTARY_GROUP) ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP) + ftxAll.checkAllComponentsVisible(SIGNERS_GROUP) // Filter inputs only. fun filtering(elem: Any): Boolean { @@ -222,12 +230,14 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree. // The old client (receiving more component types than expected) is still compatible. - val componentGroupsCompatibleA = listOf(inputGroup, + val componentGroupsCompatibleA = listOf( + inputGroup, outputGroup, commandGroup, notaryGroup, timeWindowGroup, - newUnknownComponentGroup // A new unknown component with ordinal 10,000 that we cannot process. + signersGroup, + newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process. ) val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt) val ftxCompatible = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filtering)) @@ -245,9 +255,288 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { ftxCompatibleAll.verify() assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id) - // Check we received the last (6th) element that we cannot process (backwards compatibility). - assertEquals(6, ftxCompatibleAll.filteredComponentGroups.size) + // Check we received the last element that we cannot process (backwards compatibility). + assertEquals(wireTransactionCompatibleA.componentGroups.size, ftxCompatibleAll.filteredComponentGroups.size) + // Hide one component group only. + // Filter inputs only. + fun filterOutInputs(elem: Any): Boolean { + return when (elem) { + is StateRef -> false + else -> true + } + } + val ftxCompatibleNoInputs = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filterOutInputs)) + ftxCompatibleNoInputs.verify() + assertFailsWith { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) } + assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size) + assertEquals(wireTransactionCompatibleA.componentGroups.map { it.groupIndex }.max()!!, ftxCompatibleNoInputs.groupHashes.size - 1) + } + + @Test + fun `Command visibility tests`() { + // 1st and 3rd commands require a signature from KEY_1. + val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public)) + val componentGroups = listOf( + inputGroup, + outputGroup, + ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }), + notaryGroup, + timeWindowGroup, + ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }), + newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process. + ) + val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt()) + + // Filter all commands. + fun filterCommandsOnly(elem: Any): Boolean { + return when (elem) { + is Command<*> -> true // Even if one Command is filtered, all signers are automatically filtered as well + else -> false + } + } + + // Filter out commands only. + fun filterOutCommands(elem: Any): Boolean { + return when (elem) { + is Command<*> -> false + else -> true + } + } + + // Filter KEY_1 commands. + fun filterKEY1Commands(elem: Any): Boolean { + return when (elem) { + is Command<*> -> DUMMY_KEY_1.public in elem.signers + else -> false + } + } + + // Filter only one KEY_1 command. + fun filterTwoSignersCommands(elem: Any): Boolean { + return when (elem) { + is Command<*> -> elem.signers.size == 2 // dummyCommand(DUMMY_KEY_1.public) is filtered out. + else -> false + } + } + + // Again filter only one KEY_1 command. + fun filterSingleSignersCommands(elem: Any): Boolean { + return when (elem) { + is Command<*> -> elem.signers.size == 1 // dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public) is filtered out. + else -> false + } + } + + val allCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterCommandsOnly)) + val noCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterOutCommands)) + val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands)) + val oneKey1CommandFtxA = wtx.buildFilteredTransaction(Predicate(::filterTwoSignersCommands)) + val oneKey1CommandFtxB = wtx.buildFilteredTransaction(Predicate(::filterSingleSignersCommands)) + + allCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public) + assertFailsWith { noCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public) } + key1CommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public) + assertFailsWith { oneKey1CommandFtxA.checkCommandVisibility(DUMMY_KEY_1.public) } + assertFailsWith { oneKey1CommandFtxB.checkCommandVisibility(DUMMY_KEY_1.public) } + + allCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) + assertFailsWith { noCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) } // If we filter out all commands, signers are not sent as well. + key1CommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible. + oneKey1CommandFtxA.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible. + oneKey1CommandFtxB.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible. + + // We don't send a list of signers. + val componentGroupsCompatible = listOf( + inputGroup, + outputGroup, + ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }), + notaryGroup, + timeWindowGroup, + // ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }), + newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process. + ) + + // Invalid Transaction. Sizes of CommandData and Signers (empty) do not match. + assertFailsWith { WireTransaction(componentGroups = componentGroupsCompatible, privacySalt = PrivacySalt()) } + + // We send smaller list of signers. + val componentGroupsLessSigners = listOf( + inputGroup, + outputGroup, + ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }), + notaryGroup, + timeWindowGroup, + ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }.subList(0, 1)), // Send first signer only. + newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process. + ) + + // Invalid Transaction. Sizes of CommandData and Signers (empty) do not match. + assertFailsWith { WireTransaction(componentGroups = componentGroupsLessSigners, privacySalt = PrivacySalt()) } + + // Test if there is no command to sign. + val commandsNoKey1= listOf(dummyCommand(DUMMY_KEY_2.public)) + + val componentGroupsNoKey1ToSign = listOf( + inputGroup, + outputGroup, + ComponentGroup(COMMANDS_GROUP.ordinal, commandsNoKey1.map { it.value.serialize() }), + notaryGroup, + timeWindowGroup, + ComponentGroup(SIGNERS_GROUP.ordinal, commandsNoKey1.map { it.signers.serialize() }), + newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process. + ) + + val wtxNoKey1 = WireTransaction(componentGroups = componentGroupsNoKey1ToSign, privacySalt = PrivacySalt()) + val allCommandsNoKey1Ftx= wtxNoKey1.buildFilteredTransaction(Predicate(::filterCommandsOnly)) + allCommandsNoKey1Ftx.checkCommandVisibility(DUMMY_KEY_1.public) // This will pass, because there are indeed no commands to sign in the original transaction. + } + + @Test + fun `FilteredTransaction signer manipulation tests`() { + // Required to call the private constructor. + val ftxConstructor = FilteredTransaction::class.java.declaredConstructors[1] + ftxConstructor.isAccessible = true + + // 1st and 3rd commands require a signature from KEY_1. + val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public)) + val componentGroups = listOf( + inputGroup, + outputGroup, + ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }), + notaryGroup, + timeWindowGroup, + ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }) + ) + val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt()) + + // Filter KEY_1 commands (commands 1 and 3). + fun filterKEY1Commands(elem: Any): Boolean { + return when (elem) { + is Command<*> -> DUMMY_KEY_1.public in elem.signers + else -> false + } + } + + // Filter KEY_2 commands (commands 1 and 2). + fun filterKEY2Commands(elem: Any): Boolean { + return when (elem) { + is Command<*> -> DUMMY_KEY_2.public in elem.signers + else -> false + } + } + + val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands)) + val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands)) + + // val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components + val commandDataHashes = wtx.availableComponentHashes[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!! + val noLastCommandDataPMT = PartialMerkleTree.build( + MerkleTree.getMerkleTree(commandDataHashes), + commandDataHashes.subList(0, 1) + ) + val noLastCommandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components.subList(0, 1) + val noLastCommandDataNonces = key1CommandsFtx.filteredComponentGroups[0].nonces.subList(0, 1) + val noLastCommandDataGroup = FilteredComponentGroup( + ComponentGroupEnum.COMMANDS_GROUP.ordinal, + noLastCommandDataComponents, + noLastCommandDataNonces, + noLastCommandDataPMT + ) + + val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components + val signerHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!! + val noLastSignerPMT = PartialMerkleTree.build( + MerkleTree.getMerkleTree(signerHashes), + signerHashes.subList(0, 2) + ) + val noLastSignerComponents = key1CommandsFtx.filteredComponentGroups[1].components.subList(0, 2) + val noLastSignerNonces = key1CommandsFtx.filteredComponentGroups[1].nonces.subList(0, 2) + val noLastSignerGroup = FilteredComponentGroup( + ComponentGroupEnum.SIGNERS_GROUP.ordinal, + noLastSignerComponents, + noLastSignerNonces, + noLastSignerPMT + ) + val noLastSignerGroupSamePartialTree = FilteredComponentGroup( + ComponentGroupEnum.SIGNERS_GROUP.ordinal, + noLastSignerComponents, + noLastSignerNonces, + key1CommandsFtx.filteredComponentGroups[1].partialMerkleTree) // We don't update that, so we can catch the index mismatch. + + val updatedFilteredComponentsNoSignersKey2 = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroup) + val updatedFilteredComponentsNoSignersKey2SamePMT = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree) + + // There are only two components in key1CommandsFtx (commandData and signers). + assertEquals(2, key1CommandsFtx.componentGroups.size) + + // Remove last signer for which there is a pointer from a visible commandData. This is the case of Key1. + // This will result to an invalid transaction. + // A command with no corresponding signer detected + // because the pointer of CommandData (3rd leaf) cannot find a corresponding (3rd) signer. + val updatedFilteredComponentsNoSignersKey1SamePMT = listOf(key1CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree) + assertFails { ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoSignersKey1SamePMT, key1CommandsFtx.groupHashes) } + + // Remove both last signer (KEY1) and related command. + // Update partial Merkle tree for signers. + val updatedFilteredComponentsNoLastCommandAndSigners = listOf(noLastCommandDataGroup, noLastSignerGroup) + val ftxNoLastCommandAndSigners = ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoLastCommandAndSigners, key1CommandsFtx.groupHashes) as FilteredTransaction + // verify() will pass as the transaction is well-formed. + ftxNoLastCommandAndSigners.verify() + // checkCommandVisibility() will not pass, because checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) will fail. + assertFailsWith { ftxNoLastCommandAndSigners.checkCommandVisibility(DUMMY_KEY_1.public) } + + // Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2. + // Do not change partial Merkle tree for signers. + // This time the object can be constructed as there is no pointer mismatch. + val ftxNoLastSigner = ftxConstructor.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2SamePMT, key2CommandsFtx.groupHashes) as FilteredTransaction + // verify() will fail as we didn't change the partial Merkle tree. + assertFailsWith { ftxNoLastSigner.verify() } + // checkCommandVisibility() will not pass. + assertFailsWith { ftxNoLastSigner.checkCommandVisibility(DUMMY_KEY_2.public) } + + // Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2. + // Update partial Merkle tree for signers. + val ftxNoLastSignerB = ftxConstructor.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2, key2CommandsFtx.groupHashes) as FilteredTransaction + // verify() will pass, the transaction is well-formed. + ftxNoLastSignerB.verify() + // But, checkAllComponentsVisible() will not pass. + assertFailsWith { ftxNoLastSignerB.checkCommandVisibility(DUMMY_KEY_2.public) } + + // Modify last signer (we have a pointer from commandData). + // Update partial Merkle tree for signers. + val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice. + val alterSignersHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2]) + val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes) + val alterSignerPMTK = PartialMerkleTree.build( + alterMTree, + alterSignersHashes + ) + + val alterSignerGroup = FilteredComponentGroup( + ComponentGroupEnum.SIGNERS_GROUP.ordinal, + alterSignerComponents, + key1CommandsFtx.filteredComponentGroups[1].nonces, + alterSignerPMTK + ) + val alterFilteredComponents = listOf(key1CommandsFtx.filteredComponentGroups[0], alterSignerGroup) + + // Do not update groupHashes. + val ftxAlterSigner = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes) as FilteredTransaction + // Visible components in signers group cannot be verified against their partial Merkle tree. + assertFailsWith { ftxAlterSigner.verify() } + // Also, checkAllComponentsVisible() will not pass (groupHash matching will fail). + assertFailsWith { ftxAlterSigner.checkCommandVisibility(DUMMY_KEY_1.public) } + + // Update groupHashes. + val ftxAlterSignerB = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes.subList(0, 6) + alterMTree.hash) as FilteredTransaction + // Visible components in signers group cannot be verified against their partial Merkle tree. + assertFailsWith { ftxAlterSignerB.verify() } + // Also, checkAllComponentsVisible() will not pass (top level Merkle tree cannot be verified against transaction's id). + assertFailsWith { ftxAlterSignerB.checkCommandVisibility(DUMMY_KEY_1.public) } + + ftxConstructor.isAccessible = false } } + diff --git a/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt b/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt index 453b01eb65..f0da2499b9 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt @@ -1,9 +1,12 @@ package net.corda.core.contracts +import net.corda.core.internal.div +import net.corda.core.internal.times import net.corda.core.utilities.millis import net.corda.core.utilities.minutes import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset.UTC @@ -17,6 +20,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isEqualTo(now) assertThat(timeWindow.untilTime).isNull() assertThat(timeWindow.midpoint).isNull() + assertThat(timeWindow.length).isNull() assertThat(timeWindow.contains(now - 1.millis)).isFalse() assertThat(timeWindow.contains(now)).isTrue() assertThat(timeWindow.contains(now + 1.millis)).isTrue() @@ -28,6 +32,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isNull() assertThat(timeWindow.untilTime).isEqualTo(now) assertThat(timeWindow.midpoint).isNull() + assertThat(timeWindow.length).isNull() assertThat(timeWindow.contains(now - 1.millis)).isTrue() assertThat(timeWindow.contains(now)).isFalse() assertThat(timeWindow.contains(now + 1.millis)).isFalse() @@ -42,6 +47,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isEqualTo(fromTime) assertThat(timeWindow.untilTime).isEqualTo(untilTime) assertThat(timeWindow.midpoint).isEqualTo(today.atTime(12, 15).toInstant(UTC)) + assertThat(timeWindow.length).isEqualTo(Duration.between(fromTime, untilTime)) assertThat(timeWindow.contains(fromTime - 1.millis)).isFalse() assertThat(timeWindow.contains(fromTime)).isTrue() assertThat(timeWindow.contains(fromTime + 1.millis)).isTrue() @@ -51,17 +57,21 @@ class TimeWindowTest { @Test fun fromStartAndDuration() { - val timeWindow = TimeWindow.fromStartAndDuration(now, 10.minutes) + val duration = 10.minutes + val timeWindow = TimeWindow.fromStartAndDuration(now, duration) assertThat(timeWindow.fromTime).isEqualTo(now) - assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes) - assertThat(timeWindow.midpoint).isEqualTo(now + 5.minutes) + assertThat(timeWindow.untilTime).isEqualTo(now + duration) + assertThat(timeWindow.midpoint).isEqualTo(now + duration / 2) + assertThat(timeWindow.length).isEqualTo(duration) } @Test fun withTolerance() { - val timeWindow = TimeWindow.withTolerance(now, 10.minutes) - assertThat(timeWindow.fromTime).isEqualTo(now - 10.minutes) - assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes) + val tolerance = 10.minutes + val timeWindow = TimeWindow.withTolerance(now, tolerance) + assertThat(timeWindow.fromTime).isEqualTo(now - tolerance) + assertThat(timeWindow.untilTime).isEqualTo(now + tolerance) assertThat(timeWindow.midpoint).isEqualTo(now) + assertThat(timeWindow.length).isEqualTo(tolerance * 2) } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index a82e698a97..e476f64cb5 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto - import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.identity.Party @@ -14,10 +13,12 @@ import net.corda.testing.* import org.junit.Test import java.security.PublicKey import java.util.function.Predicate +import java.util.stream.IntStream +import kotlin.streams.toList import kotlin.test.* class PartialMerkleTreeTest : TestDependencyInjectionBase() { - val nodes = "abcdef" + private val nodes = "abcdef" private val hashed = nodes.map { initialiseTestSerialization() try { @@ -115,16 +116,18 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() { val d = testTx.serialize().deserialize() assertEquals(testTx.id, d.id) - val mt = testTx.buildFilteredTransaction(Predicate(::filtering)) + val ftx = testTx.buildFilteredTransaction(Predicate(::filtering)) - assertEquals(4, mt.filteredComponentGroups.size) - assertEquals(1, mt.inputs.size) - assertEquals(0, mt.attachments.size) - assertEquals(1, mt.outputs.size) - assertEquals(1, mt.commands.size) - assertNull(mt.notary) - assertNotNull(mt.timeWindow) - mt.verify() + // We expect 5 and not 4 component groups, because there is at least one command in the ftx and thus, + // the signers component is also sent (required for visibility purposes). + assertEquals(5, ftx.filteredComponentGroups.size) + assertEquals(1, ftx.inputs.size) + assertEquals(0, ftx.attachments.size) + assertEquals(1, ftx.outputs.size) + assertEquals(1, ftx.commands.size) + assertNull(ftx.notary) + assertNotNull(ftx.timeWindow) + ftx.verify() } @Test @@ -246,4 +249,50 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() { privacySalt = privacySalt ) } + + @Test + fun `Find leaf index`() { + // A Merkle tree with 20 leaves. + val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { SecureHash.sha256(it.toString()) } + val merkleTree = MerkleTree.getMerkleTree(sampleLeaves) + + // Provided hashes are not in the tree. + assertFailsWith { PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("20"))) } + // One of the provided hashes is not in the tree. + assertFailsWith { PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("20"), SecureHash.sha256("1"), SecureHash.sha256("5"))) } + + val pmt = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("1"), SecureHash.sha256("5"), SecureHash.sha256("0"), SecureHash.sha256("19"))) + // First leaf. + assertEquals(0, pmt.leafIndex(SecureHash.sha256("0"))) + // Second leaf. + assertEquals(1, pmt.leafIndex(SecureHash.sha256("1"))) + // A random leaf. + assertEquals(5, pmt.leafIndex(SecureHash.sha256("5"))) + // The last leaf. + assertEquals(19, pmt.leafIndex(SecureHash.sha256("19"))) + // The provided hash is not in the tree. + assertFailsWith { pmt.leafIndex(SecureHash.sha256("10")) } + // The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree). + assertFailsWith { pmt.leafIndex(SecureHash.sha256("30")) } + + val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("0"))) + assertEquals(0, pmtFirstElementOnly.leafIndex(SecureHash.sha256("0"))) + // The provided hash is not in the tree. + assertFailsWith { pmtFirstElementOnly.leafIndex(SecureHash.sha256("10")) } + + val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("19"))) + assertEquals(19, pmtLastElementOnly.leafIndex(SecureHash.sha256("19"))) + // The provided hash is not in the tree. + assertFailsWith { pmtLastElementOnly.leafIndex(SecureHash.sha256("10")) } + + val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("5"))) + assertEquals(5, pmtOneElement.leafIndex(SecureHash.sha256("5"))) + // The provided hash is not in the tree. + assertFailsWith { pmtOneElement.leafIndex(SecureHash.sha256("10")) } + + val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves) + for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(SecureHash.sha256(i.toString()))) + // The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree). + assertFailsWith { pmtAllIncluded.leafIndex(SecureHash.sha256("30")) } + } } diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 6a2bf1ef8b..aae7f6fc0d 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -12,12 +12,12 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE +import net.corda.testing.ALICE_NAME import net.corda.testing.BOB -import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork +import net.corda.testing.singleIdentity import org.junit.After import org.junit.Before import org.junit.Test @@ -55,13 +55,13 @@ class AttachmentTests { @Test fun `download and store`() { - mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() aliceNode.internals.ensureRegistered() + val alice = aliceNode.info.singleIdentity() aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -73,7 +73,7 @@ class AttachmentTests { // Get node one to run a flow to fetch it and insert it. mockNet.runNetwork() - val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) mockNet.runNetwork() assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size) @@ -87,13 +87,12 @@ class AttachmentTests { // Shut down node zero and ensure node one can still resolve the attachment. aliceNode.dispose() - val response: FetchDataFlow.Result = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()).resultFuture.getOrThrow() + val response: FetchDataFlow.Result = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow() assertEquals(attachment, response.fromDisk[0]) } @Test fun `missing`() { - mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) @@ -107,7 +106,8 @@ class AttachmentTests { // Get node one to fetch a non-existent attachment. val hash = SecureHash.randomSHA256() mockNet.runNetwork() - val bobFlow = bobNode.startAttachmentFlow(setOf(hash), aliceNode.info.chooseIdentity()) + val alice = aliceNode.info.singleIdentity() + val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice) mockNet.runNetwork() val e = assertFailsWith { bobFlow.resultFuture.getOrThrow() } assertEquals(hash, e.requested) @@ -130,6 +130,7 @@ class AttachmentTests { // Ensure that registration was successful before progressing any further mockNet.runNetwork() aliceNode.internals.ensureRegistered() + val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -146,12 +147,12 @@ class AttachmentTests { val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) aliceNode.database.transaction { - DatabaseTransactionManager.current().session.update(corruptAttachment) + session.update(corruptAttachment) } // Get n1 to fetch the attachment. Should receive corrupted bytes. mockNet.runNetwork() - val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) mockNet.runNetwork() assertFailsWith { bobFlow.resultFuture.getOrThrow() } } diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 619ddec8a6..a0b2c0020c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -23,29 +23,37 @@ import kotlin.reflect.KClass import kotlin.test.assertFailsWith class CollectSignaturesFlowTests { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts") + } + lateinit var mockNet: MockNetwork lateinit var aliceNode: StartedNode lateinit var bobNode: StartedNode lateinit var charlieNode: StartedNode + lateinit var alice: Party + lateinit var bob: Party + lateinit var charlie: Party lateinit var notary: Party @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = cordappPackages) val notaryNode = mockNet.createNotaryNode() aliceNode = mockNet.createPartyNode(ALICE.name) bobNode = mockNet.createPartyNode(BOB.name) charlieNode = mockNet.createPartyNode(CHARLIE.name) mockNet.runNetwork() - notary = notaryNode.services.getDefaultNotary() aliceNode.internals.ensureRegistered() + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + charlie = charlieNode.info.singleIdentity() + notary = notaryNode.services.getDefaultNotary() } @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } private fun registerFlowOnAllNodes(flowClass: KClass>) { @@ -141,7 +149,8 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { val bConfidentialIdentity = bobNode.database.transaction { - bobNode.services.keyManagementService.freshKeyAndCert(bobNode.info.chooseIdentityAndCert(), false) + 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 @@ -149,7 +158,7 @@ class CollectSignaturesFlowTests { } registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 - val parties = listOf(aliceNode.info.chooseIdentity(), bConfidentialIdentity.party, charlieNode.info.chooseIdentity()) + val parties = listOf(alice, bConfidentialIdentity.party, charlie) val state = DummyContract.MultiOwnerState(magicNumber, parties) val flow = aliceNode.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() @@ -161,7 +170,7 @@ class CollectSignaturesFlowTests { @Test fun `no need to collect any signatures`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) + 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() @@ -173,8 +182,8 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) - val miniCorpServices = MockServices(MINI_CORP_KEY) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) + val miniCorpServices = MockServices(cordappPackages, MINI_CORP_KEY) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() @@ -186,9 +195,9 @@ class CollectSignaturesFlowTests { @Test fun `passes with multiple initial signatures`() { val twoPartyDummyContract = DummyContract.generateInitial(1337, notary, - aliceNode.info.chooseIdentity().ref(1), - bobNode.info.chooseIdentity().ref(2), - bobNode.info.chooseIdentity().ref(3)) + 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())) diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 0720557b1f..eba602a47e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -40,8 +40,7 @@ class ContractUpgradeFlowTest { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows")) val notaryNode = mockNet.createNotaryNode() aliceNode = mockNet.createPartyNode(ALICE.name) bobNode = mockNet.createPartyNode(BOB.name) @@ -56,7 +55,6 @@ class ContractUpgradeFlowTest { @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } @Test @@ -121,7 +119,7 @@ class ContractUpgradeFlowTest { return startRpcClient( rpcAddress = startRpcServer( rpcUser = user, - ops = CordaRPCOpsImpl(node.services, node.smm, node.database) + ops = CordaRPCOpsImpl(node.services, node.smm, node.database, node.services) ).get().broker.hostAndPort!!, username = user.username, password = user.password diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index fb865fd291..eec08b4c6b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -6,7 +6,7 @@ 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.node.services.api.StartedNodeServices import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -16,53 +16,57 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class FinalityFlowTests { - lateinit var mockNet: MockNetwork - lateinit var aliceNode: StartedNode - lateinit var bobNode: StartedNode - lateinit var notary: Party + private lateinit var mockNet: MockNetwork + private lateinit var aliceServices: StartedNodeServices + private lateinit var bobServices: StartedNodeServices + private lateinit var alice: Party + private lateinit var bob: Party + private lateinit var notary: Party @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork() - mockNet.createNotaryNode() - aliceNode = mockNet.createPartyNode(ALICE.name) - bobNode = mockNet.createPartyNode(BOB.name) + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) mockNet.runNetwork() aliceNode.internals.ensureRegistered() - notary = aliceNode.services.getDefaultNotary() + aliceServices = aliceNode.services + bobServices = bobNode.services + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + notary = notaryNode.services.getDefaultNotary() } @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `finalise a simple transaction`() { - val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0)) + val amount = 1000.POUNDS.issuedBy(alice.ref(0)) val builder = TransactionBuilder(notary) - Cash().generateIssue(builder, amount, bobNode.info.chooseIdentity(), notary) - val stx = aliceNode.services.signInitialTransaction(builder) - val flow = aliceNode.services.startFlow(FinalityFlow(stx)) + Cash().generateIssue(builder, amount, bob, notary) + val stx = aliceServices.signInitialTransaction(builder) + val flow = aliceServices.startFlow(FinalityFlow(stx)) mockNet.runNetwork() val notarisedTx = flow.resultFuture.getOrThrow() notarisedTx.verifyRequiredSignatures() - val transactionSeenByB = bobNode.services.database.transaction { - bobNode.services.validatedTransactions.getTransaction(notarisedTx.id) + val transactionSeenByB = bobServices.database.transaction { + bobServices.validatedTransactions.getTransaction(notarisedTx.id) } assertEquals(notarisedTx, transactionSeenByB) } @Test fun `reject a transaction with unknown parties`() { - val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0)) + 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.services.startFlow(FinalityFlow(stx)) + val stx = aliceServices.signInitialTransaction(builder) + val flow = aliceServices.startFlow(FinalityFlow(stx)) mockNet.runNetwork() assertFailsWith { flow.resultFuture.getOrThrow() diff --git a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt index b2f31384db..0a2fb69f26 100644 --- a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt @@ -61,6 +61,7 @@ class InternalUtilsTest { assertArrayEquals(intArrayOf(1, 2, 3, 4), (1 until 5).stream().toArray()) assertArrayEquals(intArrayOf(1, 3), (1..4 step 2).stream().toArray()) assertArrayEquals(intArrayOf(1, 3), (1..3 step 2).stream().toArray()) + @Suppress("EmptyRange") // It's supposed to be empty. assertArrayEquals(intArrayOf(), (1..0).stream().toArray()) assertArrayEquals(intArrayOf(1, 0), (1 downTo 0).stream().toArray()) assertArrayEquals(intArrayOf(3, 1), (3 downTo 0 step 2).stream().toArray()) diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index bf16c93f53..e7f43c3096 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -36,8 +36,7 @@ class ResolveTransactionsFlowTest { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.createNotaryNode() megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name) miniCorpNode = mockNet.createPartyNode(MINI_CORP.name) @@ -52,7 +51,6 @@ class ResolveTransactionsFlowTest { @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } // DOCEND 3 diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 89bc81dad9..f6d1eec6b2 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -16,7 +16,7 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.currentDBSession import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork @@ -54,7 +54,7 @@ private fun StartedNode<*>.hackAttachment(attachmentId: SecureHash, content: Str * @see NodeAttachmentService.importAttachment */ private fun updateAttachment(attachmentId: SecureHash, data: ByteArray) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) attachment?.let { attachment.content = data diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 88ef89b662..6a3324e6dd 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -12,11 +12,12 @@ import org.junit.Before import org.junit.Test import java.security.SignatureException import java.util.* +import kotlin.reflect.jvm.javaField import kotlin.test.assertEquals import kotlin.test.assertFailsWith class TransactionSerializationTests : TestDependencyInjectionBase() { - val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash" + private val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash" class TestCash : Contract { override fun verify(tx: LedgerTransaction) { @@ -65,7 +66,10 @@ class TransactionSerializationTests : TestDependencyInjectionBase() { stx.verifyRequiredSignatures() // Corrupt the data and ensure the signature catches the problem. - stx.id.bytes[5] = stx.id.bytes[5].inc() + val bytesField = stx.id::bytes.javaField?.apply { setAccessible(true) } + val bytes = bytesField?.get(stx.id) as ByteArray + bytes[5] = bytes[5].inc() + assertFailsWith(SignatureException::class) { stx.verifyRequiredSignatures() } diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 84afad26a5..c2c6e88e46 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -7,6 +7,8 @@ API: Persistence ================ +.. contents:: + Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping* (ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins @@ -79,10 +81,10 @@ Custom schema registration Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan for schemas (any class that extends the ``MappedSchema`` interface) in the `plugins` configuration directory in your CorDapp jar. -For testing purposes it is necessary to manually register custom schemas as follows: +For testing purposes it is necessary to manually register the packages containing custom schemas as follows: -- Tests using ``MockNetwork`` and ``MockNode`` must explicitly register custom schemas using the `registerCustomSchemas()` method of ``MockNode`` -- Tests using ``MockServices`` must explicitly register schemas using `customSchemas` attribute of the ``MockServices`` `makeTestDatabaseAndMockServices()` helper method. +- Tests using ``MockNetwork`` and ``MockNode`` must explicitly register packages using the `cordappPackages` parameter of ``MockNetwork`` +- Tests using ``MockServices`` must explicitly register packages using the `cordappPackages` parameter of the ``MockServices`` `makeTestDatabaseAndMockServices()` helper method. .. note:: Tests using the `DriverDSL` will automatically register your custom schemas if they are in the same project structure as the driver call. diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 0b9944948d..a525a8b413 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -1,33 +1,42 @@ API: Vault Query ================ -Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). +.. contents:: + +Overview +-------- +Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and +libraries for accessing RDBMS backed transactional stores (including the Vault). Corda provides a number of flexible query mechanisms for accessing the Vault: - Vault Query API -- using a JDBC session (as described in :ref:`Persistence `) -- custom JPA_/JPQL_ queries -- custom 3rd party Data Access frameworks such as `Spring Data `_ +- Using a JDBC session (as described in :ref:`Persistence `) +- Custom JPA_/JPQL_ queries +- Custom 3rd party Data Access frameworks such as `Spring Data `_ -The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows: +The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the +``VaultService`` for use directly by flows: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultService.kt :language: kotlin :start-after: DOCSTART VaultQueryAPI :end-before: DOCEND VaultQueryAPI + :dedent: 4 -and via ``CordaRPCOps`` for use by RPC client applications: +And via ``CordaRPCOps`` for use by RPC client applications: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultQueryByAPI :end-before: DOCEND VaultQueryByAPI + :dedent: 4 .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultTrackByAPI :end-before: DOCEND VaultTrackByAPI + :dedent: 4 Helper methods are also provided with default values for arguments: @@ -35,51 +44,84 @@ Helper methods are also provided with default values for arguments: :language: kotlin :start-after: DOCSTART VaultQueryAPIHelpers :end-before: DOCEND VaultQueryAPIHelpers + :dedent: 4 .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultTrackAPIHelpers :end-before: DOCEND VaultTrackAPIHelpers + :dedent: 4 -The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of filter criteria. +The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of +filter criteria: - Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``) - Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``) .. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL) -Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. -Defaults are defined for Paging (pageNumber = 1, pageSize = 200) and Sorting (direction = ASC). +Simple pagination (page number and size) and sorting (directional ordering using standard or custom property +attributes) is also specifiable. Defaults are defined for paging (pageNumber = 1, pageSize = 200) and sorting +(direction = ASC). -The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported. +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including +and/or composition and a rich set of operators to include: + +* Binary logical (AND, OR) +* Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL) +* Equality (EQUAL, NOT_EQUAL) +* Likeness (LIKE, NOT_LIKE) +* Nullability (IS_NULL, NOT_NULL) +* Collection based (IN, NOT_IN) +* Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) There are four implementations of this interface which can be chained together to define advanced filters. -1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). +1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, + CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). - .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft locked states). + .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft + locked states). -2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). +2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core + ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a + specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable + attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). - .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. + .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common + state attributes to the **vault_fungible_states** table. -3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s). +3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` + and ``DealState`` contract state interfaces, used to represent entities that continuously supersede themselves, all + of which share the same ``linearId`` (e.g. trade entity states such as the ``IRSState`` defined in the SIMM + valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s). - .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. + .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those + interfaces common state attributes to the **vault_linear_states** table. -4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL. +4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined + by a custom contract state that implements its own schema as described in the :doc:`Persistence ` + documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe + ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The + ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator + types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, + max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple + construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in + ``QueryCriteriaUtils`` for a complete specification of the DSL. - .. note:: custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to - :doc:`Persistence ` for mechanisms of registering custom schemas for different testing purposes. + .. note:: Custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to + :doc:`Persistence ` for mechanisms of registering custom schemas for different testing + purposes. All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators. All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes: 1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states. - When chaining several criterias using AND / OR, the last value of this attribute will override any previous. -2. Contract state types (``>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all state types). - When chaining several criteria using ``and`` and ``or`` operators, all specified contract state types are combined into a single set. + When chaining several criterias using AND / OR, the last value of this attribute will override any previous +2. Contract state types (``>``), which will contain at minimum one type (by default this + will be ``ContractState`` which resolves to all state types). When chaining several criteria using ``and`` and + ``or`` operators, all specified contract state types are combined into a single set An example of a custom query is illustrated here: @@ -87,20 +129,30 @@ An example of a custom query is illustrated here: :language: kotlin :start-after: DOCSTART VaultQueryExample20 :end-before: DOCEND VaultQueryExample20 + :dedent: 12 -.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. +.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types + ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root + ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See + ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java. -.. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States). +.. note:: When specifying the ``ContractType`` as a parameterised type to the ``QueryCriteria`` in Kotlin, queries now + include all concrete implementations of that type if this is an interface. Previously, it was only possible to query + on concrete types (or the universe of all ``ContractState``). -The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based :doc:`Persistence ` framework adopted by Corda. +The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based +:doc:`Persistence ` framework adopted by Corda. .. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/ -.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. +.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, + read-only access to underlying Corda tables. -.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute types. +.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that + Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute + types. An example of a custom query in Java is illustrated here: @@ -108,17 +160,24 @@ An example of a custom query in Java is illustrated here: :language: java :start-after: DOCSTART VaultJavaQueryExample3 :end-before: DOCEND VaultJavaQueryExample3 + :dedent: 16 -.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not resolve to an X500Name via the IdentityService, no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. +.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, + where an anonymous party does not resolve to an X500 name via the ``IdentityService``, no query results will ever be + produced. For performance reasons, queries do not use ``PublicKey`` as search criteria. Pagination ---------- -The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 results). -Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer from worrying about pagination where -result sets are expected to be constrained to 200 or fewer entries. Where large result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to define a ``PageSpecification`` to correctly process results with efficient memory utilistion. A fail-fast mode is in place to alert API users to the need for pagination where a single query returns more than 200 results and no ``PageSpecification`` -has been supplied. +The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 +results). Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer +from worrying about pagination where result sets are expected to be constrained to 200 or fewer entries. Where large +result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to +define a ``PageSpecification`` to correctly process results with efficient memory utilisation. A fail-fast mode is in +place to alert API users to the need for pagination where a single query returns more than 200 results and no +``PageSpecification`` has been supplied. -.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme caution as results returned may exceed your JVM's memory footprint. +.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme + caution as results returned may exceed your JVM's memory footprint. Example usage ------------- @@ -126,7 +185,7 @@ Example usage Kotlin ^^^^^^ -**General snapshot queries using** ``VaultQueryCriteria`` +**General snapshot queries using** ``VaultQueryCriteria``: Query for all unconsumed states (simplest query possible): @@ -134,6 +193,7 @@ Query for all unconsumed states (simplest query possible): :language: kotlin :start-after: DOCSTART VaultQueryExample1 :end-before: DOCEND VaultQueryExample1 + :dedent: 12 Query for unconsumed states for some state references: @@ -141,6 +201,7 @@ Query for unconsumed states for some state references: :language: kotlin :start-after: DOCSTART VaultQueryExample2 :end-before: DOCEND VaultQueryExample2 + :dedent: 12 Query for unconsumed states for several contract state types: @@ -148,6 +209,7 @@ Query for unconsumed states for several contract state types: :language: kotlin :start-after: DOCSTART VaultQueryExample3 :end-before: DOCEND VaultQueryExample3 + :dedent: 12 Query for unconsumed states for a given notary: @@ -155,6 +217,7 @@ Query for unconsumed states for a given notary: :language: kotlin :start-after: DOCSTART VaultQueryExample4 :end-before: DOCEND VaultQueryExample4 + :dedent: 12 Query for unconsumed states for a given set of participants: @@ -162,6 +225,7 @@ Query for unconsumed states for a given set of participants: :language: kotlin :start-after: DOCSTART VaultQueryExample5 :end-before: DOCEND VaultQueryExample5 + :dedent: 12 Query for unconsumed states recorded between two time intervals: @@ -169,6 +233,7 @@ Query for unconsumed states recorded between two time intervals: :language: kotlin :start-after: DOCSTART VaultQueryExample6 :end-before: DOCEND VaultQueryExample6 + :dedent: 12 .. note:: This example illustrates usage of a ``Between`` ``ColumnPredicate``. @@ -178,17 +243,21 @@ Query for all states with pagination specification (10 results per page): :language: kotlin :start-after: DOCSTART VaultQueryExample7 :end-before: DOCEND VaultQueryExample7 + :dedent: 12 -.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as demonstrated in the following example. +.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as + demonstrated in the following example. -Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further pages available: +Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further +pages available: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExamplePaging :end-before: DOCEND VaultQueryExamplePaging + :dedent: 8 -**LinearState and DealState queries using** ``LinearStateQueryCriteria`` +**LinearState and DealState queries using** ``LinearStateQueryCriteria``: Query for unconsumed linear states for given linear ids: @@ -196,6 +265,7 @@ Query for unconsumed linear states for given linear ids: :language: kotlin :start-after: DOCSTART VaultQueryExample8 :end-before: DOCEND VaultQueryExample8 + :dedent: 12 Query for all linear states associated with a linear id: @@ -203,6 +273,7 @@ Query for all linear states associated with a linear id: :language: kotlin :start-after: DOCSTART VaultQueryExample9 :end-before: DOCEND VaultQueryExample9 + :dedent: 12 Query for unconsumed deal states with deals references: @@ -210,6 +281,7 @@ Query for unconsumed deal states with deals references: :language: kotlin :start-after: DOCSTART VaultQueryExample10 :end-before: DOCEND VaultQueryExample10 + :dedent: 12 Query for unconsumed deal states with deals parties: @@ -217,8 +289,9 @@ Query for unconsumed deal states with deals parties: :language: kotlin :start-after: DOCSTART VaultQueryExample11 :end-before: DOCEND VaultQueryExample11 + :dedent: 12 -**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria`` +**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria``: Query for fungible assets for a given currency: @@ -226,6 +299,7 @@ Query for fungible assets for a given currency: :language: kotlin :start-after: DOCSTART VaultQueryExample12 :end-before: DOCEND VaultQueryExample12 + :dedent: 12 Query for fungible assets for a minimum quantity: @@ -233,19 +307,21 @@ Query for fungible assets for a minimum quantity: :language: kotlin :start-after: DOCSTART VaultQueryExample13 :end-before: DOCEND VaultQueryExample13 + :dedent: 12 .. note:: This example uses the builder DSL. -Query for fungible assets for a specifc issuer party: +Query for fungible assets for a specific issuer party: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExample14 :end-before: DOCEND VaultQueryExample14 + :dedent: 12 -**Aggregate Function queries using** ``VaultCustomQueryCriteria`` +**Aggregate Function queries using** ``VaultCustomQueryCriteria``: -.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page. +.. note:: Query results for aggregate functions are contained in the ``otherResults`` attribute of a results Page. Aggregations on cash using various functions: @@ -253,8 +329,9 @@ Aggregations on cash using various functions: :language: kotlin :start-after: DOCSTART VaultQueryExample21 :end-before: DOCEND VaultQueryExample21 + :dedent: 12 -.. note:: `otherResults` will contain 5 items, one per calculated aggregate function. +.. note:: ``otherResults`` will contain 5 items, one per calculated aggregate function. Aggregations on cash grouped by currency for various functions: @@ -262,8 +339,10 @@ Aggregations on cash grouped by currency for various functions: :language: kotlin :start-after: DOCSTART VaultQueryExample22 :end-before: DOCEND VaultQueryExample22 + :dedent: 12 -.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result). +.. note:: ``otherResults`` will contain 24 items, one result per calculated aggregate function per currency (the + grouping attribute - currency in this case - is returned per aggregate result). Sum aggregation on cash grouped by issuer party and currency and sorted by sum: @@ -271,10 +350,15 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum: :language: kotlin :start-after: DOCSTART VaultQueryExample23 :end-before: DOCEND VaultQueryExample23 + :dedent: 12 -.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). +.. note:: ``otherResults`` will contain 12 items sorted from largest summed cash amount to smallest, one result per + calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). -**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. +Dynamic queries (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an +additional ``QueryResults`` return type in the form of an ``Observable``. Refer to +`ReactiveX Observable `_ for a detailed understanding and usage of +this type. Track unconsumed cash states: @@ -282,6 +366,7 @@ Track unconsumed cash states: :language: kotlin :start-after: DOCSTART VaultQueryExample15 :end-before: DOCEND VaultQueryExample15 + :dedent: 20 Track unconsumed linear states: @@ -289,8 +374,9 @@ Track unconsumed linear states: :language: kotlin :start-after: DOCSTART VaultQueryExample16 :end-before: DOCEND VaultQueryExample16 + :dedent: 20 -.. note:: This will return both Deal and Linear states. +.. note:: This will return both ``DealState`` and ``LinearState`` states. Track unconsumed deal states: @@ -298,8 +384,9 @@ Track unconsumed deal states: :language: kotlin :start-after: DOCSTART VaultQueryExample17 :end-before: DOCEND VaultQueryExample17 + :dedent: 20 -.. note:: This will return only Deal states. +.. note:: This will return only ``DealState`` states. Java examples ^^^^^^^^^^^^^ @@ -310,6 +397,7 @@ Query for all unconsumed linear states: :language: java :start-after: DOCSTART VaultJavaQueryExample0 :end-before: DOCEND VaultJavaQueryExample0 + :dedent: 12 Query for all consumed cash states: @@ -317,6 +405,7 @@ Query for all consumed cash states: :language: java :start-after: DOCSTART VaultJavaQueryExample1 :end-before: DOCEND VaultJavaQueryExample1 + :dedent: 12 Query for consumed deal states or linear ids, specify a paging specification and sort by unique identifier: @@ -324,8 +413,9 @@ Query for consumed deal states or linear ids, specify a paging specification and :language: java :start-after: DOCSTART VaultJavaQueryExample2 :end-before: DOCEND VaultJavaQueryExample2 + :dedent: 12 -**Aggregate Function queries using** ``VaultCustomQueryCriteria`` +**Aggregate Function queries using** ``VaultCustomQueryCriteria``: Aggregations on cash using various functions: @@ -333,6 +423,7 @@ Aggregations on cash using various functions: :language: java :start-after: DOCSTART VaultJavaQueryExample21 :end-before: DOCEND VaultJavaQueryExample21 + :dedent: 16 Aggregations on cash grouped by currency for various functions: @@ -340,6 +431,7 @@ Aggregations on cash grouped by currency for various functions: :language: java :start-after: DOCSTART VaultJavaQueryExample22 :end-before: DOCEND VaultJavaQueryExample22 + :dedent: 16 Sum aggregation on cash grouped by issuer party and currency and sorted by sum: @@ -347,6 +439,7 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum: :language: java :start-after: DOCSTART VaultJavaQueryExample23 :end-before: DOCEND VaultJavaQueryExample23 + :dedent: 16 Track unconsumed cash states: @@ -354,13 +447,16 @@ Track unconsumed cash states: :language: java :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 + :dedent: 12 -Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique identifier): +Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique +identifier): .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java :language: java :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 + :dedent: 12 Troubleshooting --------------- @@ -373,24 +469,27 @@ If the results your were expecting do not match actual returned query results we Behavioural notes ----------------- -1. **TrackBy** updates do not take into account the full criteria specification due to different and more restrictive syntax - in `observables `_ filtering (vs full SQL-92 JDBC filtering as used in snapshot views). - Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` (UNCONSUMED, CONSUMED, ALL) only. -2. **QueryBy** and **TrackBy snapshot views** using pagination may return different result sets as each paging request is a - separate SQL query on the underlying database, and it is entirely conceivable that state modifications are taking - place in between and/or in parallel to paging requests. - When using pagination, always check the value of the ``totalStatesAvailable`` (from the ``Vault.Page`` result) and - adjust further paging requests appropriately. +1. ``TrackBy`` updates do not take into account the full criteria specification due to different and more restrictive + syntax in `observables `_ filtering (vs full SQL-92 JDBC filtering as used + in snapshot views). Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` + (UNCONSUMED, CONSUMED, ALL) only +2. ``QueryBy`` and ``TrackBy`` snapshot views using pagination may return different result sets as each paging request + is a separate SQL query on the underlying database, and it is entirely conceivable that state modifications are + taking place in between and/or in parallel to paging requests. When using pagination, always check the value of the + ``totalStatesAvailable`` (from the ``Vault.Page`` result) and adjust further paging requests appropriately. Other use case scenarios ------------------------ -For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of the box. Namely, implementations of JPQL (JPA Query Language) such as **Hibernate** for advanced SQL access, and **Spring Data** for advanced pagination and ordering constructs. +For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is +recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of +the box. Namely, implementations of JPQL (JPA Query Language) such as Hibernate for advanced SQL access, and +Spring Data for advanced pagination and ordering constructs. The Corda Tutorials provide examples satisfying these additional Use Cases: - 1. Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State - 2. Template / Tutorial CorDapp service query extension executing Named Queries via JPQL_ + 1. Example CorDapp service using Vault API Custom Query to access attributes of IOU State + 2. Example CorDapp service query extension executing Named Queries via JPQL_ 3. `Advanced pagination `_ queries using Spring Data JPA_ .. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql diff --git a/docs/source/azure-vm.rst b/docs/source/azure-vm.rst index bc5ae24fa4..7f94f0fdcb 100644 --- a/docs/source/azure-vm.rst +++ b/docs/source/azure-vm.rst @@ -97,7 +97,7 @@ Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages t * **Loading the Yo! CorDapp onto your nodes** -The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory. +The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their cordapps directory. Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command: @@ -105,14 +105,14 @@ For Corda nodes running release M10 .. sourcecode:: shell - cd /opt/corda/plugins + cd /opt/corda/cordapps wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar For Corda nodes running release M11 .. sourcecode:: shell - cd /opt/corda/plugins + cd /opt/corda/cordapps wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal: diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 7f1e76f915..e043e0c398 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,14 +6,19 @@ from the previous milestone release. UNRELEASED ---------- +* ``OpaqueBytes.bytes`` now returns a clone of its underlying ``ByteArray``, and has been redeclared as ``final``. + This is a minor change to the public API, but is required to ensure that classes like ``SecureHash`` are immutable. + * ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions. +* Renamed "plugins" directory on nodes to "cordapps" + * The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in the standard CorDapp format. -* ``Cordform`` and node identity generation - * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: +* ``Cordform`` and node identity generation: + * Removed the parameter ``NetworkMap`` from Cordform. Now at the end of the deployment the following happens: 1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory. 2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder. @@ -21,7 +26,7 @@ UNRELEASED * ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. -* Enums now respsect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't +* Enums now respect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is thrown. @@ -36,6 +41,28 @@ UNRELEASED * Cordformation node building DSL can have an additional parameter `configFile` with the path to a properties file to be appended to node.conf. +* ``FlowLogic`` now has a static method called ``sleep`` which can be used in certain circumstances to help with resolving + contention over states in flows. This should be used in place of any other sleep primitive since these are not compatible + with flows and their use will be prevented at some point in the future. Pay attention to the warnings and limitations + described in the documentation for this method. This helps resolve a bug in ``Cash`` coin selection. + A new static property `currentTopLevel` returns the top most `FlowLogic` instance, or null if not in a flow. + +* ``CordaService`` annotated classes should be upgraded to take a constructor parameter of type ``AppServiceHub`` which + allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability + service classes with only ``ServiceHub`` constructors will still work. + +* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the + time-window is open-ended. + +* A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command`` + signers. + +* ``PartialMerkleTree`` is equipped with a ``leafIndex`` function that returns the index of a hash (leaf) in the + partial Merkle tree structure. + +* A new function ``checkCommandVisibility(publicKey: PublicKey)`` has been added to ``FilteredTransaction`` to check + if every command that a signer should receive (e.g. an Oracle) is indeed visible. + .. _changelog_v1: Release 1.0 diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst index 57863be4b7..8b5dd94053 100644 --- a/docs/source/corda-api.rst +++ b/docs/source/corda-api.rst @@ -74,7 +74,7 @@ The following modules are available but we do not commit to their stability or c * **net.corda.samples.demos.bankofcorda**: simulates the role of an asset issuing authority (eg. central bank for cash) * **net.corda.samples.demos.irs**: demonstrates an Interest Rate Swap agreement between two banks * **net.corda.samples.demos.notary**: a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft or BFT SMaRt) notary -* **net.corda.samples.demos.simmvaluation**: See our [main documentation site](https://docs.corda.net/initial-margin-agreement.html) regarding the SIMM valuation and agreement on a distributed ledger +* **net.corda.samples.demos.simmvaluation**: A demo of SIMM valuation and agreement on a distributed ledger * **net.corda.samples.demos.trader**: demonstrates four nodes, a notary, an issuer of cash (Bank of Corda), and two parties trading with each other, exchanging cash for a commercial paper * **net.corda.node.smoke.test.utils**: test utilities for smoke testing * **net.corda.node.test.common**: common test functionality @@ -90,4 +90,4 @@ The following modules are available but we do not commit to their stability or c .. warning:: Code inside any package in the ``net.corda`` namespace which contains ``.internal`` or in ``net.corda.node`` for internal use only. Future releases will reject any CorDapps that use types from these packages. -.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow. \ No newline at end of file +.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 8511b2d486..2b2627f56e 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -94,7 +94,7 @@ path to the node's base directory. .. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field is present the web server will start. -:notary: Optional config object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt +:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. :validating: Boolean to determine whether the notary is a validating or non-validating one. @@ -108,11 +108,15 @@ path to the node's base directory. members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be bootstrapped. - :bftSMaRt: If part of a distributed BFT SMaRt cluster specify this config object, with the following settings: + :bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings: - :replicaId: + :replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id. - :clusterAddresses: + :clusterAddresses: List of all BFT-SMaRt cluster member addresses. + + :custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`. + + Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. :networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is a config object with the details of the network map service: diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 016d741bf2..e20065838f 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -78,11 +78,11 @@ For further information about managing dependencies, see Installing CorDapps ------------------- -At runtime, nodes will load any plugins present in their ``plugins`` folder. Therefore in order to install a cordapp to -a node the cordapp JAR must be added to the ``/plugins/`` folder, where ``node_dir`` is the folder in which the +At runtime, nodes will load any CorDapp JARs present in their ``cordapps`` folder. Therefore in order to install a CorDapp to +a node the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which the node's JAR and configuration files are stored). -The ``deployNodes`` gradle task, if correctly configured, will automatically place your cordapp JAR as well as any +The ``deployNodes`` gradle task, if correctly configured, will automatically place your CorDapp JAR as well as any dependent cordapp JARs specified into the directory automatically. Example diff --git a/docs/source/demobench.rst b/docs/source/demobench.rst index 5382547301..374f12234a 100644 --- a/docs/source/demobench.rst +++ b/docs/source/demobench.rst @@ -37,13 +37,13 @@ Profiles notary/ node.conf - plugins/ + cordapps/ banka/ node.conf - plugins/ + cordapps/ bankb/ node.conf - plugins/ + cordapps/ example-cordapp.jar ... @@ -133,7 +133,7 @@ current working directory of the JVM): corda-webserver.jar explorer/ node-explorer.jar - plugins/ + cordapps/ bank-of-corda.jar .. diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 4457b7d38c..74f8bdac65 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -11,13 +11,12 @@ Cordform is the local node deployment system for CorDapps. The nodes generated a debugging, and testing node configurations, but not for production or testnet deployment. Here is an example Gradle task called ``deployNodes`` that uses the Cordform plugin to deploy three nodes, plus a -notary/network map node: +notary node: .. sourcecode:: groovy task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=UK" node { name "O=Controller,OU=corda,L=London,C=UK" notary = [validating : true] @@ -52,9 +51,7 @@ notary/network map node: } } -You can extend ``deployNodes`` to generate any number of nodes you like. The only requirement is that you must specify -one node as running the network map service, by putting their name in the ``networkMap`` field. In our example, the -``Controller`` is set as the network map service. +You can extend ``deployNodes`` to generate any number of nodes you like. .. warning:: When adding nodes, make sure that there are no port clashes! @@ -85,9 +82,14 @@ run all the nodes at once. Each node in the ``nodes`` folder has the following s .. sourcecode:: none . nodeName - ├── corda.jar // The Corda runtime - ├── node.conf // The node's configuration - └── plugins // Any installed CorDapps + ├── corda.jar // The Corda runtime + ├── node.conf // The node's configuration + ├── cordapps // Any installed CorDapps + └── additional-node-infos // Directory containing all the addresses and certificates of the other nodes. + +.. note:: During the build process each node generates a NodeInfo file which is written in its own root directory, +the plug-in proceeds and copies each node NodeInfo to every other node ``additional-node-infos`` directory. +The NodeInfo file contains a node hostname and port, legal name and security certificate. .. note:: Outside of development environments, do not store your node directories in the build folder. diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index a3ea77f4d8..6cad970f10 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -1,7 +1,6 @@ apply plugin: 'kotlin' apply plugin: 'application' apply plugin: 'net.corda.plugins.cordformation' -apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.quasar-utils' repositories { @@ -73,7 +72,6 @@ task integrationTest(type: Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Notary Service,OU=corda,L=London,C=GB" node { name "O=Notary Service,OU=corda,L=London,C=GB" notary = [validating : true] diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java new file mode 100644 index 0000000000..3023396e81 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java @@ -0,0 +1,46 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.identity.Party; +import net.corda.core.transactions.LedgerTransaction; + +import java.security.PublicKey; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; + +public class IOUContract implements Contract { + // Our Create command. + public static class Create implements CommandData { + } + + @Override + public void verify(LedgerTransaction tx) { + final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = tx.outputsOfType(IOUState.class).get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.", out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // Constraints on the signers. + final List signers = command.getSigners(); + check.using("There must only be one signer.", signers.size() == 1); + check.using("The signer must be the lender.", signers.contains(lender.getOwningKey())); + + return null; + }); + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java new file mode 100644 index 0000000000..5b2d5d47f4 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java @@ -0,0 +1,68 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndContract; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; + +@InitiatingFlow +@StartableByRPC +public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + + // We create a transaction builder. + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey()); + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Finalising the transaction. + subFlow(new FinalityFlow(signedTx)); + + return null; + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java new file mode 100644 index 0000000000..977457fd29 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java @@ -0,0 +1,39 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; + +import java.util.List; + +public class IOUState implements ContractState { + private final int value; + private final Party lender; + private final Party borrower; + + public IOUState(int value, Party lender, Party borrower) { + this.value = value; + this.lender = lender; + this.borrower = borrower; + } + + public int getValue() { + return value; + } + + public Party getLender() { + return lender; + } + + public Party getBorrower() { + return borrower; + } + + @Override + public List getParticipants() { + return ImmutableList.of(lender, borrower); + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java new file mode 100644 index 0000000000..bcf8dded07 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java @@ -0,0 +1,50 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.identity.Party; +import net.corda.core.transactions.LedgerTransaction; + +import java.security.PublicKey; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; +// DOCEND 01 + +public class IOUContract implements Contract { + // Our Create command. + public static class Create implements CommandData { + } + + @Override + public void verify(LedgerTransaction tx) { + final CommandWithParties command = requireSingleCommand(tx.getCommands(), net.corda.docs.java.tutorial.helloworld.IOUContract.Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = tx.outputsOfType(IOUState.class).get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.", out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // DOCSTART 02 + // Constraints on the signers. + final List signers = command.getSigners(); + check.using("There must be two signers.", signers.size() == 2); + check.using("The borrower and lender must be signers.", signers.containsAll( + ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + // DOCEND 02 + + return null; + }); + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java new file mode 100644 index 0000000000..4e0533d323 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java @@ -0,0 +1,82 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndContract; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; + +import java.security.PublicKey; +import java.util.List; +// DOCEND 01 + +@InitiatingFlow +@StartableByRPC +public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + + // We create a transaction builder. + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // DOCSTART 02 + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); + Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Creating a session with the other party. + FlowSession otherpartySession = initiateFlow(otherParty); + + // Obtaining the counterparty's signature. + SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( + signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); + + // Finalising the transaction. + subFlow(new FinalityFlow(fullySignedTx)); + + return null; + // DOCEND 02 + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java new file mode 100644 index 0000000000..ac1f312ab6 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java @@ -0,0 +1,47 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.contracts.ContractState; +import net.corda.core.flows.*; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.utilities.ProgressTracker; +import net.corda.docs.java.tutorial.helloworld.IOUFlow; +import net.corda.docs.java.tutorial.helloworld.IOUState; + +import static net.corda.core.contracts.ContractsDSL.requireThat; + +@InitiatedBy(IOUFlow.class) +public class IOUFlowResponder extends FlowLogic { + private final FlowSession otherPartySession; + + public IOUFlowResponder(FlowSession otherPartySession) { + this.otherPartySession = otherPartySession; + } + + @Suspendable + @Override + public Void call() throws FlowException { + class SignTxFlow extends SignTransactionFlow { + private SignTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { + super(otherPartySession, progressTracker); + } + + @Override + protected void checkTransaction(SignedTransaction stx) { + requireThat(require -> { + ContractState output = stx.getTx().getOutputs().get(0).getData(); + require.using("This must be an IOU transaction.", output instanceof IOUState); + IOUState iou = (IOUState) output; + require.using("The IOU's value can't be too high.", iou.getValue() < 100); + return null; + }); + } + } + + subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); + + return null; + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java new file mode 100644 index 0000000000..4cc6f9f76c --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java @@ -0,0 +1,37 @@ +package net.corda.docs.java.tutorial.twoparty; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; + +import java.util.List; + +public class IOUState implements ContractState { + private final int value; + private final Party lender; + private final Party borrower; + + public IOUState(int value, Party lender, Party borrower) { + this.value = value; + this.lender = lender; + this.borrower = borrower; + } + + public int getValue() { + return value; + } + + public Party getLender() { + return lender; + } + + public Party getBorrower() { + return borrower; + } + + @Override + public List getParticipants() { + return ImmutableList.of(lender, borrower); + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt index 47a5ca00a0..1ae88a6015 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -23,7 +23,7 @@ import java.util.* object CustomVaultQuery { @CordaService - class Service(val services: ServiceHub) : SingletonSerializeAsToken() { + class Service(val services: AppServiceHub) : SingletonSerializeAsToken() { private companion object { val log = loggerFor() } @@ -49,7 +49,7 @@ object CustomVaultQuery { val session = services.jdbcSession() val prepStatement = session.prepareStatement(nativeQuery) val rs = prepStatement.executeQuery() - var topUpLimits: MutableList> = mutableListOf() + val topUpLimits: MutableList> = mutableListOf() while (rs.next()) { val currencyStr = rs.getString(1) val amount = rs.getLong(2) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt new file mode 100644 index 0000000000..39f00d60ea --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt @@ -0,0 +1,33 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.LedgerTransaction + +class IOUContract : Contract { + // Our Create command. + class Create : CommandData + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // Constraints on the signers. + "There must only be one signer." using (command.signers.toSet().size == 1) + "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) + } + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt new file mode 100644 index 0000000000..7bb068cba5 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt @@ -0,0 +1,52 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndContract +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import kotlin.reflect.jvm.jvmName + +@InitiatingFlow +@StartableByRPC +class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), ourIdentity.owningKey) + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Finalising the transaction. + subFlow(FinalityFlow(signedTx)) + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt new file mode 100644 index 0000000000..447265a2ae --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt @@ -0,0 +1,12 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import net.corda.core.contracts.ContractState +import net.corda.core.identity.Party + +class IOUState(val value: Int, + val lender: Party, + val borrower: Party) : ContractState { + override val participants get() = listOf(lender, borrower) +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt new file mode 100644 index 0000000000..25b5b3f6a1 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt @@ -0,0 +1,36 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.LedgerTransaction +// DOCEND 01 + +class IOUContract : Contract { + // Our Create command. + class Create : CommandData + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // DOCSTART 02 + // Constraints on the signers. + "There must be two signers." using (command.signers.toSet().size == 2) + "The borrower and lender must be signers." using (command.signers.containsAll(listOf( + out.borrower.owningKey, out.lender.owningKey))) + // DOCEND 02 + } + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt new file mode 100644 index 0000000000..0d8ac221ad --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt @@ -0,0 +1,57 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndContract +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import kotlin.reflect.jvm.jvmName +// DOCEND 01 + +@InitiatingFlow +@StartableByRPC +class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // DOCSTART 02 + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Creating a session with the other party. + val otherpartySession = initiateFlow(otherParty) + + // Obtaining the counterparty's signature. + val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker())) + + // Finalising the transaction. + subFlow(FinalityFlow(fullySignedTx)) + // DOCEND 02 + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt new file mode 100644 index 0000000000..b8007cc2ec --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt @@ -0,0 +1,30 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.requireThat +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.SignTransactionFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.docs.tutorial.helloworld.IOUFlow +import net.corda.docs.tutorial.helloworld.IOUState + +@InitiatedBy(IOUFlow::class) +class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + val output = stx.tx.outputs.single().data + "This must be an IOU transaction." using (output is IOUState) + val iou = output as IOUState + "The IOU's value can't be too high." using (iou.value < 100) + } + } + + subFlow(signTransactionFlow) + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt new file mode 100644 index 0000000000..a690625d65 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt @@ -0,0 +1,10 @@ +package net.corda.docs.tutorial.twoparty + +import net.corda.core.contracts.ContractState +import net.corda.core.identity.Party + +class IOUState(val value: Int, + val lender: Party, + val borrower: Party) : ContractState { + override val participants get() = listOf(lender, borrower) +} \ No newline at end of file diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index d313217f40..509cb65312 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -2,6 +2,7 @@ package net.corda.docs import net.corda.core.contracts.Amount import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.* @@ -26,23 +27,18 @@ class CustomVaultQueryTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() - nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java) - nodeA.internals.installCordaService(CustomVaultQuery.Service::class.java) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) + nodeA.installCordaService(CustomVaultQuery.Service::class.java) notary = nodeA.services.getDefaultNotary() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index be28ddbec0..7a5193c0b0 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -1,6 +1,7 @@ package net.corda.docs import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -24,13 +25,10 @@ class FxTransactionBuildTutorialTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) notary = nodeA.services.getDefaultNotary() } @@ -38,7 +36,6 @@ class FxTransactionBuildTutorialTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index 6b9f0d279d..833f4c140d 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -3,17 +3,15 @@ package net.corda.docs import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.toFuture import net.corda.core.utilities.getOrThrow -import net.corda.node.internal.StartedNode -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.chooseIdentity +import net.corda.node.services.api.StartedNodeServices +import net.corda.testing.* import net.corda.testing.node.MockNetwork -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -21,8 +19,10 @@ import kotlin.test.assertEquals class WorkflowTransactionBuildTutorialTest { lateinit var mockNet: MockNetwork - lateinit var nodeA: StartedNode - lateinit var nodeB: StartedNode + lateinit var aliceServices: StartedNodeServices + lateinit var bobServices: StartedNodeServices + lateinit var alice: Party + lateinit var bob: Party // Helper method to locate the latest Vault version of a LinearState private inline fun ServiceHub.latest(ref: UniqueIdentifier): StateAndRef { @@ -32,67 +32,70 @@ class WorkflowTransactionBuildTutorialTest { @Before fun setup() { - setCordappPackages("net.corda.docs") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs")) + // While we don't use the notary, we need there to be one on the network mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) - nodeA = mockNet.createPartyNode() - nodeB = mockNet.createPartyNode() - nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) + aliceServices = aliceNode.services + bobServices = bobNode.services + alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `Run workflow to completion`() { // Setup a vault subscriber to wait for successful upload of the proposal to NodeB - val nodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture() + val nodeBVaultUpdate = bobServices.vaultService.updates.toFuture() // Kick of the proposal flow - val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.chooseIdentity())) + val flow1 = aliceServices.startFlow(SubmitTradeApprovalFlow("1234", bob)) // Wait for the flow to finish val proposalRef = flow1.resultFuture.getOrThrow() val proposalLinearId = proposalRef.state.data.linearId // Wait for NodeB to include it's copy in the vault nodeBVaultUpdate.get() // Fetch the latest copy of the state from both nodes - val latestFromA = nodeA.database.transaction { - nodeA.services.latest(proposalLinearId) + val latestFromA = aliceServices.database.transaction { + aliceServices.latest(proposalLinearId) } - val latestFromB = nodeB.database.transaction { - nodeB.services.latest(proposalLinearId) + val latestFromB = bobServices.database.transaction { + bobServices.latest(proposalLinearId) } // Confirm the state as as expected assertEquals(WorkflowState.NEW, proposalRef.state.data.state) assertEquals("1234", proposalRef.state.data.tradeId) - assertEquals(nodeA.info.chooseIdentity(), proposalRef.state.data.source) - assertEquals(nodeB.info.chooseIdentity(), proposalRef.state.data.counterparty) + assertEquals(alice, proposalRef.state.data.source) + assertEquals(bob, proposalRef.state.data.counterparty) assertEquals(proposalRef, latestFromA) assertEquals(proposalRef, latestFromB) // Setup a vault subscriber to pause until the final update is in NodeA and NodeB - val nodeAVaultUpdate = nodeA.services.vaultService.updates.toFuture() - val secondNodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture() + val nodeAVaultUpdate = aliceServices.vaultService.updates.toFuture() + val secondNodeBVaultUpdate = bobServices.vaultService.updates.toFuture() // Run the manual completion flow from NodeB - val flow2 = nodeB.services.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED)) + val flow2 = bobServices.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED)) // wait for the flow to end val completedRef = flow2.resultFuture.getOrThrow() // wait for the vault updates to stabilise nodeAVaultUpdate.get() secondNodeBVaultUpdate.get() // Fetch the latest copies from the vault - val finalFromA = nodeA.database.transaction { - nodeA.services.latest(proposalLinearId) + val finalFromA = aliceServices.database.transaction { + aliceServices.latest(proposalLinearId) } - val finalFromB = nodeB.database.transaction { - nodeB.services.latest(proposalLinearId) + val finalFromB = bobServices.database.transaction { + bobServices.latest(proposalLinearId) } // Confirm the state is as expected assertEquals(WorkflowState.APPROVED, completedRef.state.data.state) assertEquals("1234", completedRef.state.data.tradeId) - assertEquals(nodeA.info.chooseIdentity(), completedRef.state.data.source) - assertEquals(nodeB.info.chooseIdentity(), completedRef.state.data.counterparty) + assertEquals(alice, completedRef.state.data.source) + assertEquals(bob, completedRef.state.data.counterparty) assertEquals(completedRef, finalFromA) assertEquals(completedRef, finalFromB) } diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 7e367ff7eb..05ceaace01 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -78,80 +78,15 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.contracts.* - - ... - - class IOUContract : Contract { - // Our Create command. - class Create : CommandData - - override fun verify(tx: LedgerTransaction) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "There should be one output state of type IOUState." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single().data as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) - - // Constraints on the signers. - "There must only be one signer." using (command.signers.toSet().size == 1) - "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) - } - } - } - - .. code-block:: java - - package com.template.contract; - - import com.template.state.IOUState; - import net.corda.core.contracts.CommandWithParties; - import net.corda.core.contracts.CommandData; - import net.corda.core.contracts.Contract; - import net.corda.core.transactions.LedgerTransaction; - import net.corda.core.identity.Party; - - import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; - import static net.corda.core.contracts.ContractsDSL.requireThat; - - public class IOUContract implements Contract { - // Our Create command. - public static class Create implements CommandData {} - - @Override - public void verify(LedgerTransaction tx) { - final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0).getData(); - final Party lender = out.getLender(); - final Party borrower = out.getBorrower(); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The lender and the borrower cannot be the same entity.", lender != borrower); - - // Constraints on the signers. - check.using("There must only be one signer.", command.getSigners().size() == 1); - check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); - - return null; - }); - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 351888fd0a..bedcf6842a 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -33,128 +33,20 @@ FlowLogic Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding ``FlowLogic.call``. -We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Overwrite both the existing flows in the template -with the following: +We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the template, and +replace them with the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.utilities.ProgressTracker - import net.corda.core.transactions.TransactionBuilder - import net.corda.core.flows.* - - ... - - @InitiatingFlow - @StartableByRPC - class IOUFlow(val iouValue: Int, - val otherParty: Party) : FlowLogic() { - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - override val progressTracker = ProgressTracker() - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - override fun call() { - // We retrieve the notary identity from the network map. - val notary = serviceHub.networkMapCache.notaryIdentities[0] - - // We create a transaction builder - val txBuilder = TransactionBuilder(notary = notary) - - // We create the transaction components. - val outputState = IOUState(iouValue, ourIdentity, otherParty) - val outputContract = IOUContract::class.jvmName - val outputContractAndState = StateAndContract(outputState, outputContract) - val cmd = Command(IOUContract.Create(), ourIdentity.owningKey) - - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd) - - // Verifying the transaction. - txBuilder.verify(serviceHub) - - // Signing the transaction. - val signedTx = serviceHub.signInitialTransaction(txBuilder) - - // Finalising the transaction. - subFlow(FinalityFlow(signedTx)) - } - } - - .. code-block:: java - - package com.template.flow; - - import co.paralleluniverse.fibers.Suspendable; - import com.template.contract.IOUContract; - import com.template.state.IOUState; - import net.corda.core.contracts.Command; - import net.corda.core.contracts.StateAndContract; - import net.corda.core.flows.*; - import net.corda.core.identity.Party; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.transactions.TransactionBuilder; - import net.corda.core.utilities.ProgressTracker; - - @InitiatingFlow - @StartableByRPC - public class IOUFlow extends FlowLogic { - private final Integer iouValue; - private final Party otherParty; - - /** - * The progress tracker provides checkpoints indicating the progress of the flow to observers. - */ - private final ProgressTracker progressTracker = new ProgressTracker(); - - public IOUFlow(Integer iouValue, Party otherParty) { - this.iouValue = iouValue; - this.otherParty = otherParty; - } - - @Override - public ProgressTracker getProgressTracker() { - return progressTracker; - } - - /** - * The flow logic is encapsulated within the call() method. - */ - @Suspendable - @Override - public Void call() throws FlowException { - // We retrieve the notary identity from the network map. - final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); - - // We create a transaction builder. - final TransactionBuilder txBuilder = new TransactionBuilder(); - txBuilder.setNotary(notary); - - // We create the transaction components. - IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); - String outputContract = IOUContract.class.getName(); - StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); - Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey()); - - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd); - - // Verifying the transaction. - txBuilder.verify(getServiceHub()); - - // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Finalising the transaction. - subFlow(new FinalityFlow(signedTx)); - - return null; - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateFlow.java`` to ``IOUFlow.java``. diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst index 9760d91a6d..25bb4e189d 100644 --- a/docs/source/hello-world-introduction.rst +++ b/docs/source/hello-world-introduction.rst @@ -5,7 +5,7 @@ By this point, :doc:`your dev environment should be set up `, yo :doc:`your first CorDapp `, and you're familiar with Corda's :doc:`key concepts `. What comes next? -If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a plugin that is +If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a JAR that is installed on one or more Corda nodes, and gives them the ability to conduct some new process - anything from issuing a debt instrument to making a restaurant booking. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index ae1592fa82..377fd90e05 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -17,39 +17,36 @@ Kotlin) file. We won't be using it, and it will cause build errors unless we rem Deploying our CorDapp --------------------- Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the -``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB: +``task deployNodes`` section. This section defines three nodes - the Controller, PartyA, and PartyB: -.. container:: codeset +.. code:: bash - .. code-block:: kotlin - - task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - directory "./build/nodes" - networkMap "O=Controller,L=London,C=GB" - node { - name "O=Controller,L=London,C=GB" - notary = [validating : true] - p2pPort 10002 - rpcPort 10003 - cordapps = ["net.corda:corda-finance:$corda_release_version"] - } - node { - name "O=PartyA,L=London,C=GB" - p2pPort 10005 - rpcPort 10006 - webPort 10007 - cordapps = ["net.corda:corda-finance:$corda_release_version"] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } - node { - name "O=PartyB,L=New York,C=US" - p2pPort 10008 - rpcPort 10009 - webPort 10010 - cordapps = ["net.corda:corda-finance:$corda_release_version"] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + directory "./build/nodes" + node { + name "O=Controller,L=London,C=GB" + advertisedServices = ["corda.notary.validating"] + p2pPort 10002 + rpcPort 10003 + cordapps = ["net.corda:corda-finance:$corda_release_version"] } + node { + name "O=PartyA,L=London,C=GB" + p2pPort 10005 + rpcPort 10006 + webPort 10007 + cordapps = ["net.corda:corda-finance:$corda_release_version"] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + node { + name "O=PartyB,L=New York,C=US" + p2pPort 10008 + rpcPort 10009 + webPort 10010 + cordapps = ["net.corda:corda-finance:$corda_release_version"] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + } We have three standard nodes, plus a special Controller node that is running the network map service, and is also advertising a validating notary service. Feel free to add additional node definitions here to expand the size of the @@ -62,7 +59,7 @@ We can run this ``deployNodes`` task using Gradle. For each node definition, Gra We can do that now by running the following commands from the root of the project: -.. code:: python +.. code:: bash // On Windows gradlew clean deployNodes @@ -75,19 +72,18 @@ Running the nodes Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see the three node folders. Each node folder has the following structure: - .. code:: python + .. code:: bash . |____corda.jar // The runnable node |____corda-webserver.jar // The node's webserver - |____dependencies |____node.conf // The node's configuration file - |____plugins - |____java/kotlin-source-0.1.jar // Our IOU CorDapp + |____cordapps + |____java/kotlin-source-0.1.jar // Our IOU CorDapp Let's start the nodes by running the following commands from the root of the project: -.. code:: python +.. code:: bash // On Windows build/nodes/runnodes.bat @@ -136,7 +132,7 @@ will display a list of the available commands. We can examine the contents of a The vaults of PartyA and PartyB should both display the following output: -.. code:: python +.. code:: bash states: - state: @@ -192,11 +188,9 @@ There are a number of improvements we could make to this CorDapp: * We could add an API, to make it easier to interact with the CorDapp We will explore some of these improvements in future tutorials. But you should now be ready to develop your own -CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an -API and web front-end, and a set of example CorDapps in `the main Corda repo `_, under -``samples``. An explanation of how to run these samples :doc:`here `. +CorDapps. You can find a list of sample CorDapps `here `_. -As you write CorDapps, you can learn more about the API available :doc:`here `. +As you write CorDapps, you can learn more about the Corda API :doc:`here `. If you get stuck at any point, please reach out on `Slack `_, `Discourse `_, or `Stack Overflow `_. diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index dbcbe7c4b7..a933cc920b 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -63,53 +63,15 @@ define an ``IOUState``: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - class IOUState(val value: Int, - val lender: Party, - val borrower: Party) : ContractState { - override val participants get() = listOf(lender, borrower) - } - - .. code-block:: java - - package com.template.state; - - import com.google.common.collect.ImmutableList; - import net.corda.core.contracts.ContractState; - import net.corda.core.identity.AbstractParty; - import net.corda.core.identity.Party; - - import java.util.List; - - public class IOUState implements ContractState { - private final int value; - private final Party lender; - private final Party borrower; - - public IOUState(int value, Party lender, Party borrower) { - this.value = value; - this.lender = lender; - this.borrower = borrower; - } - - public int getValue() { - return value; - } - - public Party getLender() { - return lender; - } - - public Party getBorrower() { - return borrower; - } - - @Override - public List getParticipants() { - return ImmutableList.of(lender, borrower); - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateState.java`` to ``IOUState.java``. diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 5a6e3920a1..d9358d60eb 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -42,18 +42,23 @@ implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin, .. code-block:: java // 1. The state - src/main/java/com/template/state/TemplateState.java + src/main/java/com/template/TemplateState.java // 2. The contract - src/main/java/com/template/contract/TemplateContract.java + src/main/java/com/template/TemplateContract.java // 3. The flow - src/main/java/com/template/flow/TemplateFlow.java + src/main/java/com/template/TemplateFlow.java .. code-block:: kotlin src/main/kotlin/com/template/App.kt +To prevent build errors later on, you should delete the following file: + +* Java: ``src/test/java/com/template/FlowTests.java`` +* Kotlin: ``src/test/kotlin/com/template/FlowTests.kt`` + Progress so far --------------- We now have a template that we can build upon to define our IOU CorDapp. diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst index 39840924fd..35db806193 100644 --- a/docs/source/key-concepts-contract-constraints.rst +++ b/docs/source/key-concepts-contract-constraints.rst @@ -95,9 +95,9 @@ to specify JAR URLs in the case that the CorDapp(s) involved in testing already MockNetwork/MockNode ******************** -The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to make a call -to ``setCordappPackages`` before the MockNetwork/Node are created and then ``unsetCordappPackages`` after the test -has finished. These calls will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files +The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the +``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java) +when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the ``CordappLoader``. An example of this usage would be: @@ -108,17 +108,7 @@ within those packages will be zipped into a JAR and added to the attachment stor @Before void setup() { - // The ordering of the two below lines is important - if the MockNetwork is created before the nodes and network - // are created the CorDapps will not be loaded into the MockNodes correctly. - setCordappPackages(Arrays.asList("com.domain.cordapp")) - network = new MockNetwork() - } - - @After - void teardown() { - // This must be called at the end otherwise the global state set by setCordappPackages may leak into future - // tests in the same test runner environment. - unsetCordappPackages() + network = new MockNetwork(new MockNetworkParameters().setCordappPackages(Arrays.asList("com.domain.cordapp"))) } ... // Your tests go here @@ -146,4 +136,4 @@ The driver takes a parameter called ``extraCordappPackagesToScan`` which is a li Full Nodes ********** -When testing against full nodes simply place your CorDapp into the plugins directory of the node. +When testing against full nodes simply place your CorDapp into the cordapps directory of the node. diff --git a/docs/source/key-concepts-node.rst b/docs/source/key-concepts-node.rst index 537d0aebc4..624f5308af 100644 --- a/docs/source/key-concepts-node.rst +++ b/docs/source/key-concepts-node.rst @@ -36,7 +36,7 @@ The core elements of the architecture are: * A network interface for interacting with other nodes * An RPC interface for interacting with the node's owner * A service hub for allowing the node's flows to call upon the node's other services -* A plugin registry for extending the node by installing CorDapps +* A cordapp interface and provider for extending the node by installing CorDapps Persistence layer ----------------- @@ -68,11 +68,11 @@ updates. The key services provided are: * Information about the node itself * The current time, as tracked by the node -The plugin registry -------------------- -The plugin registry is where new CorDapps are installed to extend the behavior of the node. +The CorDapp provider +-------------------- +The CorDapp provider is where new CorDapps are installed to extend the behavior of the node. -The node also has several plugins installed by default to handle common tasks such as: +The node also has several CorDapps installed by default to handle common tasks such as: * Retrieving transactions and attachments from counterparties * Upgrading contracts diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 3ab92255b6..b63dbfc942 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -209,10 +209,8 @@ NodeAttachmentService The ``NodeAttachmentService`` provides an implementation of the ``AttachmentStorage`` interface exposed on the ``ServiceHub`` allowing transactions to add documents, copies of the contract code and binary -data to transactions. The data is persisted to the local file system -inside the attachments subfolder of the node workspace. The service is -also interfaced to by the web server, which allows files to be uploaded -via an HTTP post request. +data to transactions. The service is also interfaced to by the web server, +which allows files to be uploaded via an HTTP post request. Flow framework and event scheduling services -------------------------------------------- @@ -320,7 +318,7 @@ does this by tracking update notifications from the ``TransactionStorage`` service and processing relevant updates to delete consumed states and insert new states. The resulting update is then persisted to the database. The ``VaultService`` then exposes query and -event notification APIs to flows and CorDapp plugins to allow them +event notification APIs to flows and CorDapp services to allow them to respond to updates, or query for states meeting various conditions to begin the formation of new transactions consuming them. The equivalent services are also forwarded to RPC clients, so that they may show diff --git a/docs/source/resources/node-architecture.png b/docs/source/resources/node-architecture.png index cfc8b22cf7..7004cda149 100644 Binary files a/docs/source/resources/node-architecture.png and b/docs/source/resources/node-architecture.png differ diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index b87378bd29..4373e93a3e 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -10,7 +10,7 @@ already installed. You run each node by navigating to ```` in a termin java -jar corda.jar -.. warning:: If your working directory is not ```` your plugins and configuration will not be used. +.. warning:: If your working directory is not ```` your cordapps and configuration will not be used. The configuration file and workspace paths can be overridden on the command line. For example: diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index f9e78737d1..11ebb794c2 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -35,7 +35,7 @@ To run from the command line in Unix: 2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes 3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively. 4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -45,7 +45,7 @@ To run from the command line in Windows: 2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes 3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -112,8 +112,11 @@ Notary demo This demo shows a party getting transactions notarised by either a single-node or a distributed notary service. All versions of the demo start two counterparty nodes. One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit them for notarisation. -The `Raft `_ version of the demo will start three distributed notary nodes. -The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. + +* The `Raft `_ version of the demo will start three distributed notary nodes. +* The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. +* The Single version of the demo will start a single-node validating notary service. +* The Custom version of the demo will load and start a custom single-node notary service that is defined the demo CorDapp. The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary, every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement. @@ -122,9 +125,9 @@ You will notice that successive transactions get signed by different members of To run the Raft version of the demo from the command line in Unix: -1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). +1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create node directories for all versions of the demo, + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests @@ -133,8 +136,8 @@ To run the Raft version of the demo from the command line in Unix: To run from the command line in Windows: 1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests @@ -142,6 +145,7 @@ To run from the command line in Windows: To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``. +For the custom notary service use ``nodesCustom`. Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 92d542dd71..b9eb1b7e78 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -63,7 +63,7 @@ The long term goal is to migrate the current serialization format for everything #. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states. #. Increased security from deserialized objects being constructed through supported constructors rather than having - data poked directy into their fields without an opportunity to validate consistency or intercept attempts to manipulate + data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate supposed invariants. Documentation on that format, and how JVM classes are translated to AMQP, will be linked here when it is available. @@ -259,9 +259,10 @@ Kotlin Objects `````````````` #. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties - and deserialize back to the singleton instance. - -Currently, the same is not true of Java singletons, and they will deserialize to new instances of the class. + and deserialize back to the singleton instance. Currently, the same is not true of Java singletons, + and they will deserialize to new instances of the class. + #. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like: + ``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration. The Carpenter ````````````` diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 077133b378..2e59e2f0ef 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -11,31 +11,37 @@ Remember that each state references a contract. The contract imposes constraints If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid ledger update. -We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. This will -only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final two lines of -the ``requireThat`` block as follows: +We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. + +In ``IOUContract.java``/``IOUContract.kt``, change the imports block to the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - // Constraints on the signers. - "There must be two signers." using (command.signers.toSet().size == 2) - "The borrower and lender must be signers." using (command.signers.containsAll(listOf( - out.borrower.owningKey, out.lender.owningKey))) + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - .. code-block:: java +And update the final block of constraints in the ``requireThat`` block as follows: - ... +.. container:: codeset - import com.google.common.collect.ImmutableList; + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + :language: kotlin + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 12 - ... - - // Constraints on the signers. - check.using("There must be two signers.", command.getSigners().size() == 2); - check.using("The borrower and lender must be signers.", command.getSigners().containsAll( - ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java + :language: java + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 12 Progress so far --------------- diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index b19630eb32..4d5866b2c8 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -21,76 +21,35 @@ by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to We also need to add the borrower's public key to the transaction's command, making the borrower one of the required signers on the transaction. -In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: +In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - // We create the transaction components. - val outputState = IOUState(iouValue, ourIdentity, otherParty) - val outputContract = IOUContract::class.jvmName - val outputContractAndState = StateAndContract(outputState, outputContract) - val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) +And update ``IOUFlow.call`` by changing the code following the creation of the ``TransactionBuilder`` as follows: - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd) +.. container:: codeset - // Verifying the transaction. - txBuilder.verify(serviceHub) + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + :language: kotlin + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 - // Signing the transaction. - val signedTx = serviceHub.signInitialTransaction(txBuilder) - - // Creating a session with the other party. - val otherpartySession = initiateFlow(otherParty) - - // Obtaining the counterparty's signature. - val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker())) - - // Finalising the transaction. - subFlow(FinalityFlow(fullySignedTx)) - - .. code-block:: java - - ... - - import com.google.common.collect.ImmutableList; - import java.security.PublicKey; - import java.util.Collections; - import java.util.List; - - ... - - // We create the transaction components. - IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); - String outputContract = IOUContract.class.getName(); - StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); - List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); - Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); - - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd); - - // Verifying the transaction. - txBuilder.verify(getServiceHub()); - - // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Creating a session with the other party. - FlowSession otherpartySession = initiateFlow(otherParty); - - // Obtaining the counterparty's signature. - SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( - signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); - - // Finalising the transaction. - subFlow(new FinalityFlow(signedTx)); - - return null; + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java + :language: java + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command. @@ -116,81 +75,15 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.transactions.SignedTransaction - - ... - - @InitiatedBy(IOUFlow::class) - class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { - @Suspendable - override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { - override fun checkTransaction(stx: SignedTransaction) = requireThat { - val output = stx.tx.outputs.single().data - "This must be an IOU transaction." using (output is IOUState) - val iou = output as IOUState - "The IOU's value can't be too high." using (iou.value < 100) - } - } - - subFlow(signTransactionFlow) - } - } - - .. code-block:: java - - package com.template.flow; - - import co.paralleluniverse.fibers.Suspendable; - import com.template.state.IOUState; - import net.corda.core.contracts.ContractState; - import net.corda.core.flows.FlowException; - import net.corda.core.flows.FlowLogic; - import net.corda.core.flows.FlowSession; - import net.corda.core.flows.InitiatedBy; - import net.corda.core.flows.SignTransactionFlow; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.utilities.ProgressTracker; - - import static net.corda.core.contracts.ContractsDSL.requireThat; - - @InitiatedBy(IOUFlow.class) - public class IOUFlowResponder extends FlowLogic { - private final FlowSession otherPartySession; - - public IOUFlowResponder(FlowSession otherPartySession) { - this.otherPartySession = otherPartySession; - } - - @Suspendable - @Override - public Void call() throws FlowException { - class SignTxFlow extends SignTransactionFlow { - private signTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { - super(otherPartySession, progressTracker); - } - - @Override - protected void checkTransaction(SignedTransaction stx) { - requireThat(require -> { - ContractState output = stx.getTx().getOutputs().get(0).getData(); - require.using("This must be an IOU transaction.", output instanceof IOUState); - IOUState iou = (IOUState) output; - require.using("The IOU's value can't be too high.", iou.getValue() < 100); - return null; - }); - } - } - - subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); - - return null; - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``. diff --git a/docs/source/tut-two-party-introduction.rst b/docs/source/tut-two-party-introduction.rst index 95ad302e2c..66f4f14acc 100644 --- a/docs/source/tut-two-party-introduction.rst +++ b/docs/source/tut-two-party-introduction.rst @@ -20,4 +20,4 @@ IOU onto the ledger. We'll need to make two changes: signature (as well as the lender's) to become valid ledger updates * The ``IOUFlow`` will need to be updated to allow for the gathering of the borrower's signature -We'll start by updating the contract. +We'll start by updating the contract. \ No newline at end of file diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index ed203a66e5..7073a67af3 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -17,7 +17,7 @@ if: We will deploy the CorDapp on 4 test nodes: -* **Controller**, which hosts the network map service and a validating notary service +* **Controller**, which hosts a validating notary service * **PartyA** * **PartyB** * **PartyC** @@ -210,9 +210,9 @@ Building the example CorDapp . nodeName ├── corda.jar ├── node.conf - └── plugins + └── cordapps - ``corda.jar`` is the Corda runtime, ``plugins`` contains our node's CorDapps, and the node's configuration is + ``corda.jar`` is the Corda runtime, ``cordapps`` contains our node's CorDapps, and the node's configuration is given by ``node.conf`` Running the example CorDapp @@ -276,7 +276,7 @@ IntelliJ The node driver defined in ``/src/test/kotlin/com/example/Main.kt`` allows you to specify how many nodes you would like to run and the configuration settings for each node. For the example CorDapp, the driver starts up four nodes - and adds an RPC user for all but the "Controller" node (which serves as the notary and network map service): + and adds an RPC user for all but the "Controller" node (which serves as the notary): .. sourcecode:: kotlin @@ -489,9 +489,6 @@ You must now edit the configuration file for each node, including the controller and make the following changes: * Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``) -* Change the network map service's address to the IP address of the machine where the controller node is running - (e.g. ``networkMapService { address="10.18.0.166:10002" legalName="O=Controller,L=London,C=GB" ``). The controller - will not have the ``networkMapService`` configuration entry After starting each node, the nodes will be able to see one another and agree IOUs among themselves. diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index 01e5da1b71..28d5dc1158 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -1,17 +1,17 @@ .. highlight:: kotlin -Writing a custom notary service -=============================== +Writing a custom notary service (experimental) +============================================== -.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently, +.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently, customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to implement a custom database connector (or use a separate database for the notary), and use a custom configuration file. Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it -with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement -is that the class provide a constructor with a single parameter of type ``ServiceHub``. +with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary +service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 1 :end-before: END 1 @@ -20,7 +20,16 @@ The next step is to write a notary service flow. You are free to copy and modify as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the ``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 2 :end-before: END 2 + +To enable the service, add the following to the node configuration: + +.. parsed-literal:: + + notary : { + validating : true # Set to false if your service is non-validating + custom : true + } \ No newline at end of file diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst index c0b1a5af14..f14be4385f 100644 --- a/docs/source/tutorials-index.rst +++ b/docs/source/tutorials-index.rst @@ -16,6 +16,7 @@ Tutorials flow-testing running-a-notary oracles + tutorial-custom-notary tutorial-tear-offs tutorial-attachments event-scheduling \ No newline at end of file diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst index 3f6444d497..1dcb0d72b4 100644 --- a/docs/source/upgrade-notes.rst +++ b/docs/source/upgrade-notes.rst @@ -25,6 +25,19 @@ versions you are currently using are still in force. We also strongly recommend cross referencing with the :doc:`changelog` to confirm changes. +UNRELEASED +---------- + +Testing +^^^^^^^ + +* The registration mechanism for CorDapps in ``MockNetwork`` unit tests has changed. + + It is now done via the ``cordappPackages`` constructor parameter of MockNetwork. + This takes a list of `String` values which should be the + package names of the CorDapps containing the contract verification code you wish to load. + The ``unsetCordappPackages`` method is now redundant and has been removed. + :ref:`Milestone 14 ` ------------ @@ -220,6 +233,8 @@ Miscellaneous apps would not typically select random, unknown counterparties from the network map based on self-declared capabilities. We will introduce a replacement for this functionality, business networks, in a future release. + For now, your should retrieve the service by legal name using ``NetworkMapCache.getNodeByLegalName``. + Gotchas ^^^^^^^ diff --git a/experimental/kryo-hook/build.gradle b/experimental/kryo-hook/build.gradle new file mode 100644 index 0000000000..cf52f3c9bb --- /dev/null +++ b/experimental/kryo-hook/build.gradle @@ -0,0 +1,53 @@ +buildscript { + // For sharing constants between builds + Properties constants = new Properties() + file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } + + ext.kotlin_version = constants.getProperty("kotlinVersion") + ext.javaassist_version = "3.12.1.GA" + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +repositories { + mavenLocal() + mavenCentral() + jcenter() +} + +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' +apply plugin: 'idea' + +description 'A javaagent to allow hooking into Kryo' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile "javassist:javassist:$javaassist_version" + compile "com.esotericsoftware:kryo:4.0.0" + compile "co.paralleluniverse:quasar-core:$quasar_version:jdk8" +} + +jar { + archiveName = "${project.name}.jar" + manifest { + attributes( + 'Premain-Class': 'net.corda.kryohook.KryoHookAgent', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true', + 'Can-Set-Native-Method-Prefix': 'true', + 'Implementation-Title': "KryoHook", + 'Implementation-Version': rootProject.version + ) + } + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt new file mode 100644 index 0000000000..5afbd0e4ff --- /dev/null +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt @@ -0,0 +1,167 @@ +package net.corda.kryohook + +import co.paralleluniverse.strands.Strand +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Output +import javassist.ClassPool +import javassist.CtClass +import java.io.ByteArrayInputStream +import java.lang.StringBuilder +import java.lang.instrument.ClassFileTransformer +import java.lang.instrument.Instrumentation +import java.security.ProtectionDomain +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +class KryoHookAgent { + companion object { + @JvmStatic + fun premain(argumentsString: String?, instrumentation: Instrumentation) { + Runtime.getRuntime().addShutdownHook(Thread { + val statsTrees = KryoHook.events.values.flatMap { + readTrees(it, 0).second + } + val builder = StringBuilder() + statsTrees.forEach { + prettyStatsTree(0, it, builder) + } + print(builder.toString()) + }) + instrumentation.addTransformer(KryoHook) + } + } +} + +fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) { + when (statsTree) { + is StatsTree.Object -> { + builder.append(kotlin.CharArray(indent) { ' ' }) + builder.append(statsTree.className) + builder.append(" ") + builder.append(statsTree.size) + builder.append("\n") + for (child in statsTree.children) { + prettyStatsTree(indent + 2, child, builder) + } + } + } +} + +/** + * The hook simply records the write() entries and exits together with the output offset at the time of the call. + * This is recorded in a StrandID -> List map. + * + * Later we "parse" these lists into a tree. + */ +object KryoHook : ClassFileTransformer { + val classPool = ClassPool.getDefault() + + val hookClassName = javaClass.name + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray + ): ByteArray? { + if (className.startsWith("java") || className.startsWith("javassist") || className.startsWith("kotlin")) { + return null + } + return try { + val clazz = classPool.makeClass(ByteArrayInputStream(classfileBuffer)) + instrumentClass(clazz)?.toBytecode() + } catch (throwable: Throwable) { + println("SOMETHING WENT WRONG") + throwable.printStackTrace(System.out) + null + } + } + + private fun instrumentClass(clazz: CtClass): CtClass? { + for (method in clazz.declaredBehaviors) { + if (method.name == "write") { + val parameterTypeNames = method.parameterTypes.map { it.name } + if (parameterTypeNames == listOf("com.esotericsoftware.kryo.Kryo", "com.esotericsoftware.kryo.io.Output", "java.lang.Object")) { + if (method.isEmpty) continue + println("Instrumenting ${clazz.name}") + method.insertBefore("$hookClassName.${this::writeEnter.name}($1, $2, $3);") + method.insertAfter("$hookClassName.${this::writeExit.name}($1, $2, $3);") + return clazz + } + } + } + return null + } + + // StrandID -> StatsEvent map + val events = ConcurrentHashMap>() + + @JvmStatic + fun writeEnter(kryo: Kryo, output: Output, obj: Any) { + events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add( + StatsEvent.Enter(obj.javaClass.name, output.total()) + ) + } + @JvmStatic + fun writeExit(kryo: Kryo, output: Output, obj: Any) { + events.get(Strand.currentStrand().id)!!.add( + StatsEvent.Exit(obj.javaClass.name, output.total()) + ) + } +} + +/** + * TODO we could add events on entries/exits to field serializers to get more info on what's being serialised. + */ +sealed class StatsEvent { + data class Enter(val className: String, val offset: Long) : StatsEvent() + data class Exit(val className: String, val offset: Long) : StatsEvent() +} + +/** + * TODO add Field constructor. + */ +sealed class StatsTree { + data class Object( + val className: String, + val size: Long, + val children: List + ) : StatsTree() +} + +fun readTree(events: List, index: Int): Pair { + val event = events[index] + when (event) { + is StatsEvent.Enter -> { + val (nextIndex, children) = readTrees(events, index + 1) + val exit = events[nextIndex] as StatsEvent.Exit + require(event.className == exit.className) + return Pair(nextIndex + 1, StatsTree.Object(event.className, exit.offset - event.offset, children)) + } + is StatsEvent.Exit -> { + throw IllegalStateException("Wasn't expecting Exit") + } + } +} + +fun readTrees(events: List, index: Int): Pair> { + val trees = ArrayList() + var i = index + while (true) { + val event = events.getOrNull(i) + when (event) { + is StatsEvent.Enter -> { + val (nextIndex, tree) = readTree(events, i) + trees.add(tree) + i = nextIndex + } + is StatsEvent.Exit -> { + return Pair(i, trees) + } + null -> { + return Pair(i, trees) + } + } + } +} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md new file mode 100644 index 0000000000..ec7899a290 --- /dev/null +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md @@ -0,0 +1,23 @@ +What is this +------------ + +This is a javaagent that hooks into Kryo serializers to record a breakdown of how many bytes objects take in the output. + +The dump is quite ugly now, but the in-memory representation is a simple tree so we could put some nice visualisation on +top if we want. + +How do I run it +--------------- + +Build the agent: +``` +./gradlew experimental:kryo-hook:jar +``` + +Add this JVM flag to what you're running: + +``` +-javaagent:/experimental/kryo-hook/build/libs/kryo-hook.jar +``` + +The agent will dump the output when the JVM shuts down. diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt index 95c7e983fd..bedc953058 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt @@ -5,16 +5,18 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before +import net.corda.testing.EnforceVerifyOrFail +import net.corda.testing.TransactionDSL +import net.corda.testing.TransactionDSLInterpreter import org.junit.Ignore import org.junit.Test import java.time.Instant import java.time.LocalDate +fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + net.corda.testing.transaction(cordappPackages = listOf("net.corda.finance.contracts.universal"), dsl = script) +} + class Cap { val TEST_TX_TIME_1: Instant get() = Instant.parse("2017-09-02T12:00:00.00Z") @@ -167,16 +169,6 @@ class Cap { } } - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt index 8793dd65b9..390ea97154 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt @@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -53,17 +48,6 @@ class Caplet { val stateFixed = UniversalContract.State(listOf(DUMMY_NOTARY), contractFixed) val stateFinal = UniversalContract.State(listOf(DUMMY_NOTARY), contractFinal) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt index ec78fc1b0c..7117b22b74 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -50,17 +45,6 @@ class FXFwdTimeOption val inState = UniversalContract.State(listOf(DUMMY_NOTARY), initialContract) val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract1) val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract2) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `issue - signature`() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt index 26189d2424..67983a6c0e 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -41,17 +36,6 @@ class FXSwap { val outStateBad3 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad3) val inState = UniversalContract.State(listOf(DUMMY_NOTARY), contract) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `issue - signature`() { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt index ae55271902..9239146e8c 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt @@ -4,11 +4,6 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -132,17 +127,6 @@ class IRS { val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterExecutionFirst) val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY), paymentFirst) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt index 40d4b7bb94..3a86f41cfa 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt @@ -2,11 +2,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.Frequency import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test import java.time.Instant import kotlin.test.assertEquals @@ -122,16 +117,6 @@ class RollOutTests { next() } - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `arrangement equality transfer`() { assertEquals(contract_transfer1, contract_transfer2) diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt index 590e7a47ac..c0b464af33 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt @@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -59,17 +54,6 @@ class Swaption { } val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY), contractInitial) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt index 3d79d0df1b..8692518724 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test import java.time.Instant import kotlin.test.assertEquals @@ -43,17 +38,6 @@ class ZeroCouponBond { val outStateWrong = UniversalContract.State(listOf(DUMMY_NOTARY), transferWrong) val outStateMove = UniversalContract.State(listOf(DUMMY_NOTARY), contractMove) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun basic() { assertEquals(Zero(), Zero()) diff --git a/finance/build.gradle b/finance/build.gradle index c642969785..662e7c9aa5 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -32,6 +32,9 @@ dependencies { testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" + + // AssertJ: for fluent assertions for testing + testCompile "org.assertj:assertj-core:$assertj_version" } configurations { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 55dffe1d36..d8ead23d9d 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -19,79 +19,21 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String -import net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl +import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero import java.math.BigInteger import java.security.PublicKey -import java.sql.DatabaseMetaData import java.util.* -import java.util.concurrent.atomic.AtomicReference ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Cash // -/** - * Pluggable interface to allow for different cash selection provider implementations - * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. - * Custom implementations must implement this interface and declare their implementation in - * META-INF/services/net.corda.contracts.asset.CashSelection - */ -interface CashSelection { - companion object { - val instance = AtomicReference() - - fun getInstance(metadata: () -> java.sql.DatabaseMetaData): CashSelection { - return instance.get() ?: { - val _metadata = metadata() - val cashSelectionAlgos = ServiceLoader.load(CashSelection::class.java).toList() - val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } - cashSelectionAlgo?.let { - instance.set(cashSelectionAlgo) - cashSelectionAlgo - } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + - "\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection") - }.invoke() - } - } - - /** - * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services - * this method determines whether the loaded implementation is compatible and usable with the currently - * loaded JDBC driver. - * Note: the first loaded implementation to pass this check will be used at run-time. - */ - fun isCompatible(metadata: DatabaseMetaData): Boolean - - /** - * Query to gather Cash states that are available - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @Suspendable - fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set = emptySet(), - notary: Party? = null, - lockId: UUID, - withIssuerRefs: Set = emptySet()): List> -} - /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -384,7 +326,7 @@ class Cash : OnLedgerAsset() { // Retrieve unspent and unlocked cash states that meet our spending criteria. val totalAmount = payments.map { it.amount }.sumOrThrow() - val cashSelection = CashSelection.getInstance({ services.jdbcSession().metaData }) + val cashSelection = AbstractCashSelection.getInstance({ services.jdbcSession().metaData }) val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId) val revocationEnabled = false // Revocation is currently unsupported // Generate a new identity that change will be sent to for confidentiality purposes. This means that a diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt new file mode 100644 index 0000000000..fdc3d0d5a7 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt @@ -0,0 +1,168 @@ +package net.corda.finance.contracts.asset.cash.selection + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.* +import net.corda.finance.contracts.asset.Cash +import java.sql.* +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Pluggable interface to allow for different cash selection provider implementations + * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. + * Custom implementations must implement this interface and declare their implementation in + * META-INF/services/net.corda.contracts.asset.CashSelection + */ +abstract class AbstractCashSelection { + companion object { + val instance = AtomicReference() + + fun getInstance(metadata: () -> java.sql.DatabaseMetaData): AbstractCashSelection { + return instance.get() ?: { + val _metadata = metadata() + val cashSelectionAlgos = ServiceLoader.load(AbstractCashSelection::class.java).toList() + val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } + cashSelectionAlgo?.let { + instance.set(cashSelectionAlgo) + cashSelectionAlgo + } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + + "\nPlease specify an implementation in META-INF/services/${AbstractCashSelection::class.java}") + }.invoke() + } + + val log = loggerFor() + } + + // coin selection retry loop counter, sleep (msecs) and lock for selecting states + // TODO: make parameters configurable when we get CorDapp configuration. + private val MAX_RETRIES = 8 + private val RETRY_SLEEP = 100 + private val RETRY_CAP = 2000 + private val spendLock: ReentrantLock = ReentrantLock() + + /** + * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services + * this method determines whether the loaded implementation is compatible and usable with the currently + * loaded JDBC driver. + * Note: the first loaded implementation to pass this check will be used at run-time. + */ + abstract fun isCompatible(metadata: DatabaseMetaData): Boolean + + /** + * A vendor specific query(ies) to gather Cash states that are available. + * @param statement The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param onlyFromIssuerParties Optional issuer parties to match against. + * @param withIssuerRefs Optional issuer references to match against. + * @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + abstract fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet + + override abstract fun toString() : String + + /** + * Query to gather Cash states that are available and retry if they are temporarily unavailable. + * @param services The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, + * otherwise the set of eligible states wil be filtered to only include those from these issuers. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param withIssuerRefs If not empty the specific set of issuer references to match against. + * @return The matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + @Suspendable + fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> { + val stateAndRefs = mutableListOf>() + + for (retryCount in 1..MAX_RETRIES) { + if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { + log.warn("Coin selection failed on attempt $retryCount") + // TODO: revisit the back off strategy for contended spending. + if (retryCount != MAX_RETRIES) { + stateAndRefs.clear() + val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt() + FlowLogic.sleep(durationMillis.millis) + } else { + log.warn("Insufficient spendable states identified for $amount") + } + } else { + break + } + } + return stateAndRefs + } + + private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): Boolean { + spendLock.withLock { + val connection = services.jdbcSession() + try { + // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) + // the softLockReserve update will detect whether we try to lock states locked by others + val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) + stateAndRefs.clear() + + var totalPennies = 0L + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val stateRef = StateRef(txHash, index) + val state = rs.getBlob(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) + val pennies = rs.getLong(4) + totalPennies = rs.getLong(5) + val rowLockId = rs.getString(6) + stateAndRefs.add(StateAndRef(state, stateRef)) + log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } + } + + if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + return true + } + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + // retry as more states may become available + } catch (e: SQLException) { + log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] + $e. + """) + } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine + log.warn(e.message) + // retry only if there are locked states that may become available again (or consumed with change) + } + } + return false + } +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt index a447827205..5894eff73f 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -1,28 +1,15 @@ package net.corda.finance.contracts.asset.cash.selection -import co.paralleluniverse.fibers.Suspendable -import co.paralleluniverse.strands.Strand import net.corda.core.contracts.Amount -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.node.services.StatesNotAvailableException -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize import net.corda.core.utilities.* -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import java.sql.Connection import java.sql.DatabaseMetaData -import java.sql.SQLException +import java.sql.ResultSet import java.util.* -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock -class CashSelectionH2Impl : CashSelection { +class CashSelectionH2Impl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "H2 JDBC Driver" @@ -33,118 +20,48 @@ class CashSelectionH2Impl : CashSelection { return metadata.driverName == JDBC_DRIVER_NAME } - // coin selection retry loop counter, sleep (msecs) and lock for selecting states - private val MAX_RETRIES = 5 - private val RETRY_SLEEP = 100 - private val spendLock: ReentrantLock = ReentrantLock() + override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" - /** - * An optimised query to gather Cash states that are available and retry if they are temporarily unavailable. - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @Suspendable - override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { - val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) - val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) + // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: + // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the + // running total of such an accumulator + // 2) H2 uses session variables to perform this accumulator function: + // http://www.h2database.com/html/functions.html#set + // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet { + connection.createStatement().execute("CALL SET(@t, 0);") - val stateAndRefs = mutableListOf>() - - // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: - // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the - // running total of such an accumulator - // 2) H2 uses session variables to perform this accumulator function: - // http://www.h2database.com/html/functions.html#set - // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) - - for (retryCount in 1..MAX_RETRIES) { - - spendLock.withLock { - val statement = services.jdbcSession().createStatement() - try { - statement.execute("CALL SET(@t, CAST(0 AS BIGINT));") - - // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) - // the softLockReserve update will detect whether we try to lock states locked by others - val selectJoin = """ + val selectJoin = """ SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 - AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} - AND (vs.lock_id = '$lockId' OR vs.lock_id is null) + AND ccs.ccy_code = ? and @t < ? + AND (vs.lock_id = ? OR vs.lock_id is null) """ + - (if (notary != null) - " AND vs.notary_name = '${notary.name}'" else "") + - (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + - (if (withIssuerRefs.isNotEmpty()) - " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") + (if (notary != null) + " AND vs.notary_name = ?" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN (?)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN (?)" else "") - // Retrieve spendable state refs - val rs = statement.executeQuery(selectJoin) - stateAndRefs.clear() - log.debug(selectJoin) - var totalPennies = 0L - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val stateRef = StateRef(txHash, index) - val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) - val pennies = rs.getLong(4) - totalPennies = rs.getLong(5) - val rowLockId = rs.getString(6) - stateAndRefs.add(StateAndRef(state, stateRef)) - log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } - } + // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) + val psSelectJoin = connection.prepareStatement(selectJoin) + var pIndex = 0 + psSelectJoin.setString(++pIndex, amount.token.currencyCode) + psSelectJoin.setLong(++pIndex, amount.quantity) + psSelectJoin.setString(++pIndex, lockId.toString()) + if (notary != null) + psSelectJoin.setString(++pIndex, notary.name.toString()) + if (onlyFromIssuerParties.isNotEmpty()) + psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) + if (withIssuerRefs.isNotEmpty()) + psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) + log.debug { psSelectJoin.toString() } - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // With the current single threaded state machine available states are guaranteed to lock. - // TODO However, we will have to revisit these methods in the future multi-threaded. - services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return stateAndRefs - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") - // retry as more states may become available - } catch (e: SQLException) { - log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] - $e. - """) - } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine - stateAndRefs.clear() - log.warn(e.message) - // retry only if there are locked states that may become available again (or consumed with change) - } finally { - statement.close() - } - } - - log.warn("Coin selection failed on attempt $retryCount") - // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { - Strand.sleep(RETRY_SLEEP * retryCount.toLong()) - } - } - - log.warn("Insufficient spendable states identified for $amount") - return stateAndRefs + return psSelectJoin.executeQuery() } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt index 1a0eac0169..853ba23d07 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt @@ -1,17 +1,15 @@ package net.corda.finance.contracts.asset.cash.selection import net.corda.core.contracts.Amount -import net.corda.core.contracts.StateAndRef import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub import net.corda.core.utilities.OpaqueBytes -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import java.sql.Connection import java.sql.DatabaseMetaData +import java.sql.ResultSet import java.util.* -class CashSelectionMySQLImpl : CashSelection { +class CashSelectionMySQLImpl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "MySQL JDBC Driver" @@ -21,12 +19,9 @@ class CashSelectionMySQLImpl : CashSelection { return metadata.driverName == JDBC_DRIVER_NAME } - override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { + override fun executeQuery(statement: Connection, amount: Amount, lockId: UUID, notary: Party?, issuerKeysStr: Set, issuerRefsStr: Set): ResultSet { TODO("MySQL cash selection not implemented") } + + override fun toString() = "${this::class.java} for ${CashSelectionH2Impl.JDBC_DRIVER_NAME}" } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index c978ded6f0..23b05a1e8f 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.issuedBy import java.util.* @@ -46,7 +46,7 @@ class CashExitFlow(private val amount: Amount, progressTracker.currentStep = GENERATING_TX val builder = TransactionBuilder(notary = null) val issuer = ourIdentity.ref(issuerRef) - val exitStates = CashSelection + val exitStates = AbstractCashSelection .getInstance { serviceHub.jdbcSession().metaData } .unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) val signers = try { diff --git a/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection b/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection similarity index 100% rename from finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection rename to finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 6175bca1b7..d1d6751aa1 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -231,7 +231,6 @@ class CommercialPaperTestsGeneric { // @Test @Ignore fun `issue move and then redeem`() { - setCordappPackages("net.corda.finance.contracts") initialiseTestSerialization() val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY)) val databaseAlice = aliceDatabaseAndServices.first diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt new file mode 100644 index 0000000000..e0cb3297b5 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt @@ -0,0 +1,42 @@ +package net.corda.finance.contracts.asset + +import net.corda.core.internal.packageName +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashException +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.schemas.CashSchemaV1 +import net.corda.testing.chooseIdentity +import net.corda.testing.node.MockNetwork +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test + + +class CashSelectionH2Test { + + @Test + fun `check does not hold connection over retries`() { + val mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) + try { + val notaryNode = mockNet.createNotaryNode() + val bankA = mockNet.createNode(configOverrides = { existingConfig -> + // Tweak connections to be minimal to make this easier (1 results in a hung node during start up, so use 2 connections). + existingConfig.dataSourceProperties.setProperty("maximumPoolSize", "2") + existingConfig + }) + + mockNet.startNodes() + + // Start more cash spends than we have connections. If spend leaks a connection on retry, we will run out of connections. + val flow1 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + val flow2 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + val flow3 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + + assertThatThrownBy { flow1.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + assertThatThrownBy { flow2.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + assertThatThrownBy { flow3.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + } finally { + mockNet.stopNodes() + } + } +} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 537f347bd6..df14ca6843 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -3,9 +3,7 @@ package net.corda.finance.contracts.asset import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party +import net.corda.core.identity.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy @@ -32,25 +30,25 @@ import java.util.* import kotlin.test.* class CashTests : TestDependencyInjectionBase() { - val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) - val defaultIssuer = MEGA_CORP.ref(defaultRef) - val inState = Cash.State( + private val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) + private val defaultIssuer = MEGA_CORP.ref(defaultRef) + private val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, owner = AnonymousParty(ALICE_PUBKEY) ) // Input state held by the issuer - val issuerInState = inState.copy(owner = defaultIssuer.party) - val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) + private val issuerInState = inState.copy(owner = defaultIssuer.party) + private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) - fun Cash.State.editDepositRef(ref: Byte) = copy( + private fun Cash.State.editDepositRef(ref: Byte) = copy( amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) - lateinit var miniCorpServices: MockServices - lateinit var megaCorpServices: MockServices + private lateinit var miniCorpServices: MockServices + private lateinit var megaCorpServices: MockServices val vault: VaultService get() = miniCorpServices.vaultService lateinit var database: CordaPersistence - lateinit var vaultStatesUnconsumed: List> + private lateinit var vaultStatesUnconsumed: List> @Before fun setUp() { @@ -475,19 +473,20 @@ class CashTests : TestDependencyInjectionBase() { // // Spend tx generation - val OUR_KEY: KeyPair by lazy { generateKeyPair() } - val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) + private val OUR_KEY: KeyPair by lazy { generateKeyPair() } + private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) + private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public) - val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) - val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) + private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) + private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) - fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = + private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) - val WALLET = listOf( + private val WALLET = listOf( makeCash(100.DOLLARS, MEGA_CORP), makeCash(400.DOLLARS, MEGA_CORP), makeCash(80.DOLLARS, MINI_CORP), @@ -507,7 +506,7 @@ class CashTests : TestDependencyInjectionBase() { private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - Cash.generateSpend(miniCorpServices, tx, amount, dest) + Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest) } return tx.toWireTransaction(miniCorpServices) } @@ -541,7 +540,7 @@ class CashTests : TestDependencyInjectionBase() { assertEquals(1, expectedInputs.size) val inputState = expectedInputs.single() val actualChange = wtx.outputs.single().data as Cash.State - val expectedChangeAmount = (inputState.state.data as Cash.State).amount.quantity - 50.DOLLARS.quantity + val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner) assertEquals(expectedChange, wtx.getOutput(0)) } @@ -588,9 +587,9 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateExitWithEmptyVault() { initialiseTestSerialization() - assertFailsWith { + assertFailsWith { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList()) + Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1) } } @@ -615,7 +614,7 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) + Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -631,13 +630,13 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val vaultState = vaultStatesUnconsumed.elementAt(0) val changeAmount = 90.DOLLARS `issued by` defaultIssuer - val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).filter { state -> + val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state -> if (state is Cash.State) { state.amount == changeAmount } else { false } - }.single() + } val changeOwner = (likelyChangeState as Cash.State).owner assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size) assertEquals(vaultState.ref, wtx.inputs[0]) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index d5aa6d8cdc..c2d683a74b 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -593,7 +593,6 @@ class ObligationTests { } // Try defaulting an obligation that is now in the past - unsetCordappPackages() ledger { transaction("Settlement") { attachments(Obligation.PROGRAM_ID) diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index c8c92fad91..94b3f83658 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -28,8 +28,7 @@ class CashExitFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) notaryNode = mockNet.createNotaryNode() bankOfCordaNode = mockNet.createPartyNode(BOC.name) notary = notaryNode.services.getDefaultNotary() @@ -45,7 +44,6 @@ class CashExitFlowTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt index 3aeb782a1d..911a014d5f 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -10,11 +10,9 @@ import net.corda.node.internal.StartedNode import net.corda.testing.chooseIdentity import net.corda.testing.getDefaultNotary import net.corda.testing.BOC -import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -30,8 +28,7 @@ class CashIssueFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) notaryNode = mockNet.createNotaryNode() bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index a9b1352646..0c5b1e5efa 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -31,8 +31,7 @@ class CashPaymentFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) notaryNode = mockNet.createNotaryNode() bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt index ce65e18e5f..e307a7b162 100644 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt @@ -29,7 +29,7 @@ class CordappPlugin : Plugin { private fun configureCordappJar(project: Project) { // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead val task = project.task("configureCordappFatJar") - val jarTask = project.tasks.single { it.name == "jar" } as Jar + val jarTask = project.tasks.getByName("jar") as Jar task.doLast { jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply { exclude("META-INF/*.SF") @@ -71,6 +71,4 @@ class CordappPlugin : Plugin { } return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet() } - - private fun Project.configuration(name: String): Configuration = configurations.single { it.name == name } } diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt index 33e552ca7d..d7200ba9f8 100644 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt @@ -1,7 +1,17 @@ package net.corda.plugins import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.artifacts.Configuration +import org.gradle.api.plugins.ExtraPropertiesExtension + +/** + * Mimics the "project.ext" functionality in groovy which provides a direct + * accessor to the "ext" extention (See: ExtraPropertiesExtension) + */ +@Suppress("UNCHECKED_CAST") +fun Project.ext(name: String): T = (extensions.findByName("ext") as ExtraPropertiesExtension).get(name) as T +fun Project.configuration(name: String): Configuration = configurations.single { it.name == name } class Utils { companion object { @@ -14,4 +24,5 @@ class Utils { } } } + } \ No newline at end of file diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java index 51d44def93..047d7e6289 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -7,11 +7,9 @@ import java.util.function.Consumer; public abstract class CordformDefinition { public final Path driverDirectory; public final ArrayList> nodeConfigurers = new ArrayList<>(); - public final String networkMapNodeName; - public CordformDefinition(Path driverDirectory, String networkMapNodeName) { + public CordformDefinition(Path driverDirectory) { this.driverDirectory = driverDirectory; - this.networkMapNodeName = networkMapNodeName; } public void addNode(Consumer configurer) { diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 828647d434..94eb8f3b7b 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -8,7 +8,6 @@ buildscript { } } -apply plugin: 'groovy' apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' @@ -34,8 +33,8 @@ sourceSets { dependencies { compile gradleApi() - compile localGroovy() compile project(":cordapp") + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy deleted file mode 100644 index 7bfd9e0f0d..0000000000 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy +++ /dev/null @@ -1,183 +0,0 @@ -package net.corda.plugins - -import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME -import net.corda.cordform.CordformContext -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformNode -import org.apache.tools.ant.filters.FixCrLfFilter -import org.bouncycastle.asn1.x500.X500Name -import org.gradle.api.DefaultTask -import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.api.tasks.TaskAction -import java.nio.file.Path -import java.nio.file.Paths - -/** - * Creates nodes based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -class Cordform extends DefaultTask { - /** - * Optionally the name of a CordformDefinition subclass to which all configuration will be delegated. - */ - String definitionClass - protected def directory = Paths.get("build", "nodes") - private def nodes = new ArrayList() - protected String networkMapNodeName - - /** - * Set the directory to install nodes into. - * - * @param directory The directory the nodes will be installed into. - * @return - */ - void directory(String directory) { - this.directory = Paths.get(directory) - } - - /** - * Set the network map node. - * - * @warning Ensure the node name is one of the configured nodes. - * @param nodeName The name of the node that will host the network map. - */ - void networkMap(String nodeName) { - networkMapNodeName = nodeName - } - - /** - * Add a node configuration. - * - * @param configureClosure A node configuration that will be deployed. - */ - void node(Closure configureClosure) { - nodes << (Node) project.configure(new Node(project), configureClosure) - } - - /** - * Returns a node by name. - * - * @param name The name of the node as specified in the node configuration DSL. - * @return A node instance. - */ - private Node getNodeByName(String name) { - for (Node node : nodes) { - if (node.name == name) { - return node - } - } - - return null - } - - /** - * Installs the run script into the nodes directory. - */ - private void installRunScript() { - project.copy { - from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar") - fileMode 0755 - into "${directory}/" - } - - project.copy { - from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes") - // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. - filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf")) - fileMode 0755 - into "${directory}/" - } - - project.copy { - from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat") - into "${directory}/" - } - } - - /** - * The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. - */ - private CordformDefinition loadCordformDefinition() { - def plugin = project.convention.getPlugin(JavaPluginConvention.class) - def classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath - URL[] urls = classpath.files.collect { it.toURI().toURL() } - (CordformDefinition) new URLClassLoader(urls, CordformDefinition.classLoader).loadClass(definitionClass).newInstance() - } - - /** - * This task action will create and install the nodes based on the node configurations added. - */ - @TaskAction - void build() { - String networkMapNodeName = initializeConfigurationAndGetNetworkMapNodeName() - installRunScript() - finalizeConfiguration(networkMapNodeName) - } - - private initializeConfigurationAndGetNetworkMapNodeName() { - if (null != definitionClass) { - def cd = loadCordformDefinition() - cd.nodeConfigurers.each { nc -> - node { Node it -> - nc.accept it - it.rootDir directory - } - } - cd.setup new CordformContext() { - Path baseDirectory(String nodeName) { - project.projectDir.toPath().resolve(getNodeByName(nodeName).nodeDir.toPath()) - } - } - return cd.networkMapNodeName.toString() - } else { - nodes.each { - it.rootDir directory - } - return this.networkMapNodeName - } - } - - private finalizeConfiguration(String networkMapNodeName) { - Node networkMapNode = getNodeByName(networkMapNodeName) - if (networkMapNode == null) { - nodes.each { - it.build() - } - generateNodeInfos() - logger.info("Starting without networkMapNode, this an experimental feature") - } else { - nodes.each { - if (it != networkMapNode) { - it.networkMapAddress(networkMapNode.getP2PAddress(), networkMapNodeName) - } - it.build() - } - } - } - - Path fullNodePath(Node node) { - return project.projectDir.toPath().resolve(node.nodeDir.toPath()) - } - - private generateNodeInfos() { - nodes.each { Node node -> - def process = new ProcessBuilder("java", "-jar", Node.NODEJAR_NAME, "--just-generate-node-info") - .directory(fullNodePath(node).toFile()) - .redirectErrorStream(true) - .start() - .waitFor() - } - for (source in nodes) { - for (destination in nodes) { - if (source.nodeDir != destination.nodeDir) { - project.copy { - from fullNodePath(source).toString() - include 'nodeInfo-*' - into fullNodePath(destination).resolve(Node.NODE_INFO_DIRECTORY).toString() - } - } - } - } - } -} diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy deleted file mode 100644 index eeb4443801..0000000000 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration - -/** - * The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation, - * testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner. - */ -class Cordformation implements Plugin { - /** - * Gets a resource file from this plugin's JAR file. - * - * @param project The project environment this plugin executes in. - * @param filePathInJar The file in the JAR, relative to root, you wish to access. - * @return A file handle to the file in the JAR. - */ - protected static File getPluginFile(Project project, String filePathInJar) { - return project.rootProject.resources.text.fromArchiveEntry(project.rootProject.buildscript.configurations.classpath.find { - it.name.contains('cordformation') - }, filePathInJar).asFile() - } - - void apply(Project project) { - Utils.createCompileConfiguration("cordapp", project) - } -} diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy deleted file mode 100644 index 5049ee260f..0000000000 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ /dev/null @@ -1,269 +0,0 @@ -package net.corda.plugins - -import com.typesafe.config.* -import net.corda.cordform.CordformNode -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.RDN -import org.bouncycastle.asn1.x500.style.BCStyle -import org.gradle.api.Project -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path - -/** - * Represents a node that will be installed. - */ -class Node extends CordformNode { - static final String NODEJAR_NAME = 'corda.jar' - static final String WEBJAR_NAME = 'corda-webserver.jar' - - /** - * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven - * dependency name, eg: com.example:product-name:0.1 - * - * @note Your app will be installed by default and does not need to be included here. - */ - protected List cordapps = [] - - protected File nodeDir - private Project project - - /** - * Sets whether this node will use HTTPS communication. - * - * @param isHttps True if this node uses HTTPS communication. - */ - void https(Boolean isHttps) { - config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps)) - } - - /** - * Sets the H2 port for this node - */ - void h2Port(Integer h2Port) { - config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port)) - } - - void useTestClock(Boolean useTestClock) { - config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) - } - - /** - * Set the HTTP web server port for this node. - * - * @param webPort The web port number for this node. - */ - void webPort(Integer webPort) { - config = config.withValue("webAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort".toString())) - } - - /** - * Set the network map address for this node. - * - * @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the - * Cordform task instead. - * @param networkMapAddress Network map node address. - * @param networkMapLegalName Network map node legal name. - */ - void networkMapAddress(String networkMapAddress, String networkMapLegalName) { - def networkMapService = new HashMap() - networkMapService.put("address", networkMapAddress) - networkMapService.put("legalName", networkMapLegalName) - config = config.withValue("networkMapService", ConfigValueFactory.fromMap(networkMapService)) - } - - /** - * Set the SSHD port for this node. - * - * @param sshdPort The SSHD port. - */ - void sshdPort(Integer sshdPort) { - config = config.withValue("sshdAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort".toString())) - } - - Node(Project project) { - this.project = project - } - - protected void rootDir(Path rootDir) { - def dirName = name - try { - X500Name x500Name = new X500Name(name) - RDN[] o = x500Name.getRDNs(BCStyle.O) - if (o.length > 0) { - dirName = o.getAt(0).getFirst().getValue().toString() - } - } catch(IllegalArgumentException ignore) {} - nodeDir = new File(rootDir.toFile(), dirName.replaceAll("\\s","")) - } - - protected void build() { - configureProperties() - installCordaJar() - if (config.hasPath("webAddress")) { - installWebserverJar() - } - installBuiltPlugin() - installCordapps() - installConfig() - appendOptionalConfig() - } - - /** - * Get the artemis address for this node. - * - * @return This node's P2P address. - */ - String getP2PAddress() { - return config.getString("p2pAddress") - } - - private void configureProperties() { - config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers)) - if (notary) { - config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) - } - if (extraConfig) { - config = config.withFallback(ConfigFactory.parseMap(extraConfig)) - } - } - - /** - * Installs the corda fat JAR to the node directory. - */ - private void installCordaJar() { - def cordaJar = verifyAndGetCordaJar() - project.copy { - from cordaJar - into nodeDir - rename cordaJar.name, NODEJAR_NAME - fileMode 0755 - } - } - - /** - * Installs the corda webserver JAR to the node directory - */ - private void installWebserverJar() { - def webJar = verifyAndGetWebserverJar() - project.copy { - from webJar - into nodeDir - rename webJar.name, WEBJAR_NAME - } - } - - /** - * Installs this project's cordapp to this directory. - */ - private void installBuiltPlugin() { - def pluginsDir = new File(nodeDir, "plugins") - project.copy { - from project.jar - into pluginsDir - } - } - - /** - * Installs other cordapps to this node's plugins directory. - */ - private void installCordapps() { - def pluginsDir = new File(nodeDir, "plugins") - def cordapps = getCordappList() - project.copy { - from cordapps - into pluginsDir - } - } - - /** - * Installs the configuration file to this node's directory and detokenises it. - */ - private void installConfig() { - def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList() - - // Need to write a temporary file first to use the project.copy, which resolves directories correctly. - def tmpDir = new File(project.buildDir, "tmp") - def tmpConfFile = new File(tmpDir, 'node.conf') - Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8) - - project.copy { - from tmpConfFile - into nodeDir - } - } - - /** - * Appends installed config file with properties from an optional file. - */ - private void appendOptionalConfig() { - final configFileProperty = "configFile" - File optionalConfig - if (project.findProperty(configFileProperty)) { //provided by -PconfigFile command line property when running Gradle task - optionalConfig = new File(project.findProperty(configFileProperty)) - } else if (config.hasPath(configFileProperty)) { - optionalConfig = new File(config.getString(configFileProperty)) - } - if (optionalConfig) { - if (!optionalConfig.exists()) { - println "$configFileProperty '$optionalConfig' not found" - } else { - def confFile = new File(project.buildDir.getPath() + "/../" + nodeDir, 'node.conf') - optionalConfig.withInputStream { - input -> confFile << input - } - } - } - } - - /** - * Find the corda JAR amongst the dependencies. - * - * @return A file representing the Corda JAR. - */ - private File verifyAndGetCordaJar() { - def maybeCordaJAR = project.configurations.runtime.filter { - it.toString().contains("corda-${project.corda_release_version}.jar") || it.toString().contains("corda-enterprise-${project.corda_release_version}.jar") - } - if (maybeCordaJAR.size() == 0) { - throw new RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-${project.corda_release_version}.jar\"") - } else { - def cordaJar = maybeCordaJAR.getSingleFile() - assert(cordaJar.isFile()) - return cordaJar - } - } - - /** - * Find the corda JAR amongst the dependencies - * - * @return A file representing the Corda webserver JAR - */ - private File verifyAndGetWebserverJar() { - def maybeJar = project.configurations.runtime.filter { - it.toString().contains("corda-webserver-${project.corda_release_version}.jar") - } - if (maybeJar.size() == 0) { - throw new RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-${project.corda_release_version}.jar\"") - } else { - def jar = maybeJar.getSingleFile() - assert(jar.isFile()) - return jar - } - } - - /** - * Gets a list of cordapps based on what dependent cordapps were specified. - * - * @return List of this node's cordapps. - */ - private Collection getCordappList() { - // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string - List cordapps = this.cordapps.collect { it.toString() } - return project.configurations.cordapp.files { - cordapps.contains(it.group + ":" + it.name + ":" + it.version) - } - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt new file mode 100644 index 0000000000..5193703c59 --- /dev/null +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt @@ -0,0 +1,198 @@ +package net.corda.plugins + +import groovy.lang.Closure +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode +import org.apache.tools.ant.filters.FixCrLfFilter +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + +/** + * Creates nodes based on the configuration of this task in the gradle configuration DSL. + * + * See documentation for examples. + */ +@Suppress("unused") +open class Cordform : DefaultTask() { + /** + * Optionally the name of a CordformDefinition subclass to which all configuration will be delegated. + */ + @Suppress("MemberVisibilityCanPrivate") + var definitionClass: String? = null + private var directory = Paths.get("build", "nodes") + private val nodes = mutableListOf() + + /** + * Set the directory to install nodes into. + * + * @param directory The directory the nodes will be installed into. + */ + fun directory(directory: String) { + this.directory = Paths.get(directory) + } + + /** + * Add a node configuration. + * + * @param configureClosure A node configuration that will be deployed. + */ + @Suppress("MemberVisibilityCanPrivate") + fun node(configureClosure: Closure) { + nodes += project.configure(Node(project), configureClosure) as Node + } + + /** + * Add a node configuration + * + * @param configureFunc A node configuration that will be deployed + */ + @Suppress("MemberVisibilityCanPrivate") + fun node(configureFunc: Node.() -> Any?): Node { + val node = Node(project).apply { configureFunc() } + nodes += node + return node + } + + /** + * Returns a node by name. + * + * @param name The name of the node as specified in the node configuration DSL. + * @return A node instance. + */ + private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name } + + /** + * Installs the run script into the nodes directory. + */ + private fun installRunScript() { + project.copy { + it.apply { + from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar")) + fileMode = Cordformation.executableFileMode + into("$directory/") + } + } + + project.copy { + it.apply { + from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes")) + // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. + filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java) + fileMode = Cordformation.executableFileMode + into("$directory/") + } + } + + project.copy { + it.apply { + from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat")) + into("$directory/") + } + } + } + + /** + * The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. + */ + private fun loadCordformDefinition(): CordformDefinition { + val plugin = project.convention.getPlugin(JavaPluginConvention::class.java) + val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath + val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray() + return URLClassLoader(urls, CordformDefinition::class.java.classLoader) + .loadClass(definitionClass) + .asSubclass(CordformDefinition::class.java) + .newInstance() + } + + /** + * This task action will create and install the nodes based on the node configurations added. + */ + @Suppress("unused") + @TaskAction + fun build() { + project.logger.info("Running Cordform task") + initializeConfiguration() + installRunScript() + nodes.forEach(Node::build) + generateAndInstallNodeInfos() + } + + private fun initializeConfiguration() { + if (definitionClass != null) { + val cd = loadCordformDefinition() + cd.nodeConfigurers.forEach { + val node = node { } + it.accept(node) + node.rootDir(directory) + } + cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) } + } else { + nodes.forEach { + it.rootDir(directory) + } + } + } + + private fun fullNodePath(node: Node): Path = project.projectDir.toPath().resolve(node.nodeDir.toPath()) + + private fun generateAndInstallNodeInfos() { + generateNodeInfos() + installNodeInfos() + } + + private fun generateNodeInfos() { + project.logger.info("Generating node infos") + val generateTimeoutSeconds = 60L + val processes = nodes.map { node -> + project.logger.info("Generating node info for ${fullNodePath(node)}") + val logDir = File(fullNodePath(node).toFile(), "logs") + logDir.mkdirs() // Directory may not exist at this point + Pair(node, ProcessBuilder("java", "-jar", Node.nodeJarName, "--just-generate-node-info") + .directory(fullNodePath(node).toFile()) + .redirectErrorStream(true) + // InheritIO causes hangs on windows due the gradle buffer also not being flushed. + // Must redirect to output or logger (node log is still written, this is just startup banner) + .redirectOutput(File(logDir, "generate-info-log.txt")) + .start()) + } + try { + processes.parallelStream().forEach { (node, process) -> + if (!process.waitFor(generateTimeoutSeconds, TimeUnit.SECONDS)) { + throw GradleException("Node took longer $generateTimeoutSeconds seconds than too to generate node info - see node log at ${fullNodePath(node)}/logs") + } else if (process.exitValue() != 0) { + throw GradleException("Node exited with ${process.exitValue()} when generating node infos - see node log at ${fullNodePath(node)}/logs") + } + } + } finally { + // This will be a no-op on success - abort remaining on failure + processes.forEach { + it.second.destroyForcibly() + } + } + } + + private fun installNodeInfos() { + project.logger.info("Installing node infos") + for (source in nodes) { + for (destination in nodes) { + if (source.nodeDir != destination.nodeDir) { + project.copy { + it.apply { + from(fullNodePath(source).toString()) + include("nodeInfo-*") + into(fullNodePath(destination).resolve(CordformNode.NODE_INFO_DIRECTORY).toString()) + } + } + } + } + } + } +} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt new file mode 100644 index 0000000000..b0e09ad2da --- /dev/null +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -0,0 +1,33 @@ +package net.corda.plugins + +import org.gradle.api.Plugin +import org.gradle.api.Project +import java.io.File + +/** + * The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation, + * testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner. + */ +class Cordformation : Plugin { + internal companion object { + /** + * Gets a resource file from this plugin's JAR file. + * + * @param project The project environment this plugin executes in. + * @param filePathInJar The file in the JAR, relative to root, you wish to access. + * @return A file handle to the file in the JAR. + */ + fun getPluginFile(project: Project, filePathInJar: String): File { + val archive: File? = project.rootProject.buildscript.configurations.single { it.name == "classpath" }.find { + it.name.contains("cordformation") + } + return project.rootProject.resources.text.fromArchiveEntry(archive, filePathInJar).asFile() + } + + val executableFileMode = "0755".toInt(8) + } + + override fun apply(project: Project) { + Utils.createCompileConfiguration("cordapp", project) + } +} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt new file mode 100644 index 0000000000..738a68c6d7 --- /dev/null +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -0,0 +1,284 @@ +package net.corda.plugins + +import com.typesafe.config.* +import net.corda.cordform.CordformNode +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.style.BCStyle +import org.gradle.api.Project +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path + +/** + * Represents a node that will be installed. + */ +class Node(private val project: Project) : CordformNode() { + companion object { + @JvmStatic + val nodeJarName = "corda.jar" + @JvmStatic + val webJarName = "corda-webserver.jar" + private val configFileProperty = "configFile" + } + + /** + * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven + * dependency name, eg: com.example:product-name:0.1 + * + * @note Your app will be installed by default and does not need to be included here. + * @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it + */ + var cordapps = mutableListOf() + + private val releaseVersion = project.rootProject.ext("corda_release_version") + internal lateinit var nodeDir: File + + /** + * Sets whether this node will use HTTPS communication. + * + * @param isHttps True if this node uses HTTPS communication. + */ + fun https(isHttps: Boolean) { + config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps)) + } + + /** + * Sets the H2 port for this node + */ + fun h2Port(h2Port: Int) { + config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port)) + } + + fun useTestClock(useTestClock: Boolean) { + config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) + } + + /** + * Set the HTTP web server port for this node. + * + * @param webPort The web port number for this node. + */ + fun webPort(webPort: Int) { + config = config.withValue("webAddress", + ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort")) + } + + /** + * Set the network map address for this node. + * + * @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the + * Cordform task instead. + * @param networkMapAddress Network map node address. + * @param networkMapLegalName Network map node legal name. + */ + fun networkMapAddress(networkMapAddress: String, networkMapLegalName: String) { + val networkMapService = mutableMapOf() + networkMapService.put("address", networkMapAddress) + networkMapService.put("legalName", networkMapLegalName) + config = config.withValue("networkMapService", ConfigValueFactory.fromMap(networkMapService)) + } + + /** + * Set the SSHD port for this node. + * + * @param sshdPort The SSHD port. + */ + fun sshdPort(sshdPort: Int) { + config = config.withValue("sshdAddress", + ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort")) + } + + internal fun build() { + configureProperties() + installCordaJar() + if (config.hasPath("webAddress")) { + installWebserverJar() + } + installBuiltCordapp() + installCordapps() + installConfig() + appendOptionalConfig() + } + + /** + * Get the artemis address for this node. + * + * @return This node's P2P address. + */ + fun getP2PAddress(): String { + return config.getString("p2pAddress") + } + + internal fun rootDir(rootDir: Path) { + if(name == null) { + project.logger.error("Node has a null name - cannot create node") + throw IllegalStateException("Node has a null name - cannot create node") + } + + val dirName = try { + X500Name(name).getRDNs(BCStyle.O).first().first.value.toString() + } catch(_ : IllegalArgumentException) { + // Can't parse as an X500 name, use the full string + name + } + nodeDir = File(rootDir.toFile(), dirName.replace("\\s", "")) + } + + private fun configureProperties() { + config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers)) + if (notary != null) { + config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) + } + if (extraConfig != null) { + config = config.withFallback(ConfigFactory.parseMap(extraConfig)) + } + } + + /** + * Installs the corda fat JAR to the node directory. + */ + private fun installCordaJar() { + val cordaJar = verifyAndGetCordaJar() + project.copy { + it.apply { + from(cordaJar) + into(nodeDir) + rename(cordaJar.name, nodeJarName) + fileMode = Cordformation.executableFileMode + } + } + } + + /** + * Installs the corda webserver JAR to the node directory + */ + private fun installWebserverJar() { + val webJar = verifyAndGetWebserverJar() + project.copy { + it.apply { + from(webJar) + into(nodeDir) + rename(webJar.name, webJarName) + } + } + } + + /** + * Installs this project's cordapp to this directory. + */ + private fun installBuiltCordapp() { + val cordappsDir = File(nodeDir, "cordapps") + project.copy { + it.apply { + from(project.tasks.getByName("jar")) + into(cordappsDir) + } + } + } + + /** + * Installs other cordapps to this node's cordapps directory. + */ + private fun installCordapps() { + val cordappsDir = File(nodeDir, "cordapps") + val cordapps = getCordappList() + project.copy { + it.apply { + from(cordapps) + into(cordappsDir) + } + } + } + + /** + * Installs the configuration file to this node's directory and detokenises it. + */ + private fun installConfig() { + val options = ConfigRenderOptions.defaults().setOriginComments(false).setComments(false).setFormatted(false).setJson(false) + val configFileText = config.root().render(options).split("\n").toList() + + // Need to write a temporary file first to use the project.copy, which resolves directories correctly. + val tmpDir = File(project.buildDir, "tmp") + val tmpConfFile = File(tmpDir, "node.conf") + Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8) + + project.copy { + it.apply { + from(tmpConfFile) + into(nodeDir) + } + } + } + + /** + * Appends installed config file with properties from an optional file. + */ + private fun appendOptionalConfig() { + val optionalConfig: File? = when { + project.findProperty(configFileProperty) != null -> //provided by -PconfigFile command line property when running Gradle task + File(project.findProperty(configFileProperty) as String) + config.hasPath(configFileProperty) -> File(config.getString(configFileProperty)) + else -> null + } + + if (optionalConfig != null) { + if (!optionalConfig.exists()) { + project.logger.error("$configFileProperty '$optionalConfig' not found") + } else { + val confFile = File(project.buildDir.path + "/../" + nodeDir, "node.conf") + confFile.appendBytes(optionalConfig.readBytes()) + } + } + } + + /** + * Find the corda JAR amongst the dependencies. + * + * @return A file representing the Corda JAR. + */ + private fun verifyAndGetCordaJar(): File { + val maybeCordaJAR = project.configuration("runtime").filter { + it.toString().contains("corda-$releaseVersion.jar") || it.toString().contains("corda-enterprise-$releaseVersion.jar") + } + if (maybeCordaJAR.isEmpty) { + throw RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-$releaseVersion.jar\"") + } else { + val cordaJar = maybeCordaJAR.singleFile + assert(cordaJar.isFile) + return cordaJar + } + } + + /** + * Find the corda JAR amongst the dependencies + * + * @return A file representing the Corda webserver JAR + */ + private fun verifyAndGetWebserverJar(): File { + val maybeJar = project.configuration("runtime").filter { + it.toString().contains("corda-webserver-$releaseVersion.jar") + } + if (maybeJar.isEmpty) { + throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"") + } else { + val jar = maybeJar.singleFile + assert(jar.isFile) + return jar + } + } + + /** + * Gets a list of cordapps based on what dependent cordapps were specified. + * + * @return List of this node's cordapps. + */ + private fun getCordappList(): Collection { + // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string + @Suppress("RemoveRedundantCallsOfConversionMethods") + val cordapps: List = cordapps.map { it.toString() } + return project.configuration("cordapp").files { + cordapps.contains(it.group + ":" + it.name + ":" + it.version) + } + } +} diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy index ee978bdbb8..97029028e3 100644 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy @@ -15,6 +15,13 @@ class ProjectPublishExtension { task.setPublishName(name) } + /** + * Get the publishing name for this project. + */ + String name() { + return task.getPublishName() + } + /** * True when we do not want to publish default Java components */ diff --git a/node-api/build.gradle b/node-api/build.gradle index be014cdc80..a9384bd4ab 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -18,6 +18,8 @@ dependencies { // TODO: Remove this dependency and the code that requires it compile "commons-fileupload:commons-fileupload:$fileupload_version" + compile "net.corda.plugins:cordform-common:$gradle_plugins_version" + // TypeSafe Config: for simple and human friendly config files. compile "com.typesafe:config:$typesafe_config_version" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/NodeInfoFilesCopier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/NodeInfoFilesCopier.kt new file mode 100644 index 0000000000..aadc49f4c3 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/NodeInfoFilesCopier.kt @@ -0,0 +1,165 @@ +package net.corda.nodeapi + +import net.corda.cordform.CordformNode +import net.corda.core.internal.ThreadBox +import net.corda.core.internal.createDirectories +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list +import net.corda.core.utilities.loggerFor +import rx.Observable +import rx.Scheduler +import rx.Subscription +import rx.schedulers.Schedulers +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime +import java.util.concurrent.TimeUnit + +/** + * Utility class which copies nodeInfo files across a set of running nodes. + * + * This class will create paths that it needs to poll and to where it needs to copy files in case those + * don't exist yet. + */ +class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()) : AutoCloseable { + + companion object { + private val log = loggerFor() + const val NODE_INFO_FILE_NAME_PREFIX = "nodeInfo-" + } + + private val nodeDataMapBox = ThreadBox(mutableMapOf()) + /** + * Whether the NodeInfoFilesCopier is closed. When the NodeInfoFilesCopier is closed it will stop polling the + * filesystem and all the public methods except [#close] will throw. + */ + private var closed = false + private val subscription: Subscription + + init { + this.subscription = Observable.interval(5, TimeUnit.SECONDS, scheduler) + .subscribe { poll() } + } + + /** + * @param nodeDir a path to be watched for NodeInfos + * Add a path of a node which is about to be started. + * Its nodeInfo file will be copied to other nodes' additional-node-infos directory, and conversely, + * other nodes' nodeInfo files will be copied to this node additional-node-infos directory. + */ + fun addConfig(nodeDir: Path) { + require(!closed) { "NodeInfoFilesCopier is already closed" } + nodeDataMapBox.locked { + val newNodeFile = NodeData(nodeDir) + put(nodeDir, newNodeFile) + + for (previouslySeenFile in allPreviouslySeenFiles()) { + atomicCopy(previouslySeenFile, newNodeFile.additionalNodeInfoDirectory.resolve(previouslySeenFile.fileName)) + } + log.info("Now watching: $nodeDir") + } + } + + /** + * @param nodeConfig the configuration to be removed. + * Remove the configuration of a node which is about to be stopped or already stopped. + * No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this + * one. + */ + fun removeConfig(nodeDir: Path) { + require(!closed) { "NodeInfoFilesCopier is already closed" } + nodeDataMapBox.locked { + remove(nodeDir) ?: return + log.info("Stopped watching: $nodeDir") + } + } + + fun reset() { + require(!closed) { "NodeInfoFilesCopier is already closed" } + nodeDataMapBox.locked { + clear() + } + } + + /** + * Stops polling the filesystem. + * This function can be called as many times as one wants. + */ + override fun close() { + if (!closed) { + closed = true + subscription.unsubscribe() + } + } + + private fun allPreviouslySeenFiles() = nodeDataMapBox.alreadyLocked { values.flatMap { it.previouslySeenFiles.keys } } + + private fun poll() { + nodeDataMapBox.locked { + for (nodeData in values) { + nodeData.nodeDir.list { paths -> + paths.filter { it.isRegularFile() } + .filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) } + .forEach { path -> processPath(nodeData, path) } + } + } + } + } + + // Takes a path under nodeData config dir and decides whether the file represented by that path needs to + // be copied. + private fun processPath(nodeData: NodeData, path: Path) { + nodeDataMapBox.alreadyLocked { + val newTimestamp = Files.readAttributes(path, BasicFileAttributes::class.java).lastModifiedTime() + val previousTimestamp = nodeData.previouslySeenFiles.put(path, newTimestamp) ?: FileTime.fromMillis(-1) + if (newTimestamp > previousTimestamp) { + for (destination in this.values.filter { it.nodeDir != nodeData.nodeDir }.map { it.additionalNodeInfoDirectory }) { + val fullDestinationPath = destination.resolve(path.fileName) + atomicCopy(path, fullDestinationPath) + } + } + } + } + + private fun atomicCopy(source: Path, destination: Path) { + val tempDestination = try { + Files.createTempFile(destination.parent, "", null) + } catch (exception: IOException) { + log.warn("Couldn't create a temporary file to copy $source", exception) + throw exception + } + try { + // First copy the file to a temporary file within the appropriate directory. + Files.copy(source, tempDestination, COPY_ATTRIBUTES, REPLACE_EXISTING) + } catch (exception: IOException) { + log.warn("Couldn't copy $source to $tempDestination.", exception) + Files.delete(tempDestination) + throw exception + } + try { + // Then rename it to the desired name. This way the file 'appears' on the filesystem as an atomic operation. + Files.move(tempDestination, destination, REPLACE_EXISTING) + } catch (exception: IOException) { + log.warn("Couldn't move $tempDestination to $destination.", exception) + Files.delete(tempDestination) + throw exception + } + } + + /** + * Convenience holder for all the paths and files relative to a single node. + */ + private class NodeData(val nodeDir: Path) { + val additionalNodeInfoDirectory: Path = nodeDir.resolve(CordformNode.NODE_INFO_DIRECTORY) + // Map from Path to its lastModifiedTime. + val previouslySeenFiles = mutableMapOf() + + init { + additionalNodeInfoDirectory.createDirectories() + } + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/VerifierApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/VerifierApi.kt index eee653b30f..ca9f03cc07 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/VerifierApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/VerifierApi.kt @@ -1,8 +1,8 @@ package net.corda.nodeapi -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize +import net.corda.core.serialization.* import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.sequence import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ClientMessage import org.apache.activemq.artemis.reader.MessageUtil @@ -20,12 +20,15 @@ object VerifierApi { val responseAddress: SimpleString ) { companion object { - fun fromClientMessage(message: ClientMessage): VerificationRequest { - return VerificationRequest( + fun fromClientMessage(message: ClientMessage): ObjectWithCompatibleContext { + val bytes = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) } + val bytesSequence = bytes.sequence() + val (transaction, context) = bytesSequence.deserializeWithCompatibleContext() + val request = VerificationRequest( message.getLongProperty(VERIFICATION_ID_FIELD_NAME), - ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }.deserialize(), - MessageUtil.getJMSReplyTo(message) - ) + transaction, + MessageUtil.getJMSReplyTo(message)) + return ObjectWithCompatibleContext(request, context) } } @@ -49,11 +52,11 @@ object VerifierApi { } } - fun writeToClientMessage(message: ClientMessage) { + fun writeToClientMessage(message: ClientMessage, context: SerializationContext) { message.putLongProperty(VERIFICATION_ID_FIELD_NAME, verificationId) if (exception != null) { - message.putBytesProperty(RESULT_EXCEPTION_FIELD_NAME, exception.serialize().bytes) + message.putBytesProperty(RESULT_EXCEPTION_FIELD_NAME, exception.serialize(context = context).bytes) } } } -} +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 04c43122ec..97c50dcd67 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -57,12 +57,13 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl private fun checkClass(type: Class<*>): Registration? { // If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking. if (!whitelistEnabled) return null - // Allow primitives, abstracts and interfaces - if (type.isPrimitive || type == Any::class.java || isAbstract(type.modifiers) || type == String::class.java) return null // If array, recurse on element type if (type.isArray) return checkClass(type.componentType) // Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry. if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) return checkClass(type.superclass) + // Allow primitives, abstracts and interfaces. Note that we can also create abstract Enum types, + // but we don't want to whitelist those here. + if (type.isPrimitive || type == Any::class.java || type == String::class.java || (!type.isEnum && isAbstract(type.modifiers))) return null // It's safe to have the Class already, since Kryo loads it with initialisation off. // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted. // Thus, blacklisting precedes annotation checking. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/GeneratedAttachment.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/GeneratedAttachment.kt index e42f18ef4b..82601202e6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/GeneratedAttachment.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/GeneratedAttachment.kt @@ -3,6 +3,6 @@ package net.corda.nodeapi.internal.serialization import net.corda.core.crypto.sha256 import net.corda.core.internal.AbstractAttachment -class GeneratedAttachment(bytes: ByteArray) : AbstractAttachment({ bytes }) { +class GeneratedAttachment(val bytes: ByteArray) : AbstractAttachment({ bytes }) { override val id = bytes.sha256() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index 1e7c257a1d..a6be235f88 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -19,6 +19,7 @@ import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes import net.corda.nodeapi.internal.AttachmentsClassLoader +import org.slf4j.LoggerFactory import java.io.ByteArrayOutputStream import java.io.NotSerializableException import java.util.* @@ -37,7 +38,7 @@ object NotSupportedSerializationScheme : SerializationScheme { override fun serialize(obj: T, context: SerializationContext): SerializedBytes = doThrow() } -data class SerializationContextImpl(override val preferredSerializationVersion: ByteSequence, +data class SerializationContextImpl(override val preferredSerializationVersion: VersionHeader, override val deserializationClassLoader: ClassLoader, override val whitelist: ClassWhitelist, override val properties: Map, @@ -88,36 +89,54 @@ data class SerializationContextImpl(override val preferredSerializationVersion: }) } - override fun withPreferredSerializationVersion(versionHeader: ByteSequence) = copy(preferredSerializationVersion = versionHeader) + override fun withPreferredSerializationVersion(versionHeader: VersionHeader) = copy(preferredSerializationVersion = versionHeader) } private const val HEADER_SIZE: Int = 8 +fun ByteSequence.obtainHeaderSignature(): VersionHeader = take(HEADER_SIZE).copy() + open class SerializationFactoryImpl : SerializationFactory() { private val creator: List = Exception().stackTrace.asList() private val registeredSchemes: MutableCollection = Collections.synchronizedCollection(mutableListOf()) + private val logger = LoggerFactory.getLogger(javaClass) + // TODO: This is read-mostly. Probably a faster implementation to be found. private val schemes: ConcurrentHashMap, SerializationScheme> = ConcurrentHashMap() - private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): SerializationScheme { + private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): Pair { // truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays - return schemes.computeIfAbsent(byteSequence.take(HEADER_SIZE).copy() to target) { + val lookupKey = byteSequence.obtainHeaderSignature() to target + val scheme = schemes.computeIfAbsent(lookupKey) { registeredSchemes .filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) } .forEach { return@computeIfAbsent it } + logger.warn("Cannot find serialization scheme for: $lookupKey, registeredSchemes are: $registeredSchemes") NotSupportedSerializationScheme } + return scheme to lookupKey.first } @Throws(NotSerializableException::class) override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { - return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) } } + return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).first.deserialize(byteSequence, clazz, context) } } + } + + @Throws(NotSerializableException::class) + override fun deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): ObjectWithCompatibleContext { + return asCurrent { + withCurrentContext(context) { + val (scheme, versionHeader) = schemeFor(byteSequence, context.useCase) + val deserializedObject = scheme.deserialize(byteSequence, clazz, context) + ObjectWithCompatibleContext(deserializedObject, context.withPreferredSerializationVersion(versionHeader)) + } + } } override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) } } + return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).first.serialize(obj, context) } } } fun registerScheme(scheme: SerializationScheme) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt new file mode 100644 index 0000000000..562717e641 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt @@ -0,0 +1,139 @@ +package net.corda.nodeapi + +import net.corda.cordform.CordformNode +import net.corda.testing.eventually +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.schedulers.TestScheduler +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.streams.toList +import kotlin.test.assertEquals + +/** + * tests for [NodeInfoFilesCopier] + */ +class NodeInfoFilesCopierTest { + + @Rule @JvmField var folder = TemporaryFolder() + private val rootPath get() = folder.root.toPath() + private val scheduler = TestScheduler() + companion object { + private const val ORGANIZATION = "Organization" + private const val NODE_1_PATH = "node1" + private const val NODE_2_PATH = "node2" + + private val content = "blah".toByteArray(Charsets.UTF_8) + private val GOOD_NODE_INFO_NAME = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}test" + private val GOOD_NODE_INFO_NAME_2 = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}anotherNode" + private val BAD_NODE_INFO_NAME = "something" + } + + private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase()) + + private val node1RootPath by lazy { nodeDir(NODE_1_PATH) } + private val node2RootPath by lazy { nodeDir(NODE_2_PATH) } + private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) } + private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) } + + lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier + + @Before + fun setUp() { + nodeInfoFilesCopier = NodeInfoFilesCopier(scheduler) + } + + @Test + fun `files created before a node is started are copied to that node`() { + // Configure the first node. + nodeInfoFilesCopier.addConfig(node1RootPath) + // Ensure directories are created. + advanceTime() + + // Create 2 files, a nodeInfo and another file in node1 folder. + Files.write(node1RootPath.resolve(GOOD_NODE_INFO_NAME), content) + Files.write(node1RootPath.resolve(BAD_NODE_INFO_NAME), content) + + // Configure the second node. + nodeInfoFilesCopier.addConfig(node2RootPath) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied. + checkDirectoryContainsSingleFile(node2AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `polling of running nodes`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1RootPath) + nodeInfoFilesCopier.addConfig(node2RootPath) + advanceTime() + + // Create 2 files, one of which to be copied, in a node root path. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content) + Files.write(node2RootPath.resolve(BAD_NODE_INFO_NAME), content) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied to the other node. + checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `remove nodes`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1RootPath) + nodeInfoFilesCopier.addConfig(node2RootPath) + advanceTime() + + // Create a file, in node 2 root path. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content) + advanceTime() + + // Remove node 2 + nodeInfoFilesCopier.removeConfig(node2RootPath) + + // Create another file in node 2 directory. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied to the other node. + checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `clear`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1RootPath) + nodeInfoFilesCopier.addConfig(node2RootPath) + advanceTime() + + nodeInfoFilesCopier.reset() + + advanceTime() + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content) + + // Give some time to the filesystem to report the change. + Thread.sleep(100) + assertEquals(0, Files.list(node1AdditionalNodeInfoPath).toList().size) + } + + private fun advanceTime() { + scheduler.advanceTimeBy(1, TimeUnit.HOURS) + } + + private fun checkDirectoryContainsSingleFile(path: Path, filename: String) { + assertEquals(1, Files.list(path).toList().size) + val onlyFileName = Files.list(path).toList().first().fileName.toString() + assertEquals(filename, onlyFileName) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index ce95ac21d8..3e72b1d0d4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -9,7 +9,6 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.testing.* import net.corda.testing.node.MockServices -import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -48,11 +47,6 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase() serviceHub = MockServices(cordappPackages = listOf("net.corda.nodeapi.internal")) } - @After - fun `clear packages`() { - unsetCordappPackages() - } - @Test fun `test serialization of WireTransaction with statically loaded contract`() { val tx = AttachmentDummyContract().generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index bf4138379d..5fd6b46f21 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -55,8 +55,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { class DummyServiceHub : MockServices() { override val cordappProvider: CordappProviderImpl - = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH))).start(attachments) - + = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments) private val cordapp get() = cordappProvider.cordapps.first() val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!! val appContext get() = cordappProvider.getAppContext(cordapp) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index 74a3d574ae..cdd54295a6 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -10,7 +10,6 @@ import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* -import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.AttachmentsClassLoader import net.corda.nodeapi.internal.AttachmentsClassLoaderTests import net.corda.testing.node.MockAttachmentStorage @@ -34,6 +33,23 @@ enum class Foo { abstract val value: Int } +enum class BadFood { + Mud { + override val value = -1 + }; + + abstract val value: Int +} + +@CordaSerializable +enum class Simple { + Easy +} + +enum class BadSimple { + Nasty +} + @CordaSerializable open class Element @@ -91,32 +107,41 @@ class CordaClassResolverTests { val emptyMapClass = mapOf().javaClass } - val factory: SerializationFactory = object : SerializationFactory() { - override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { - TODO("not implemented") - } - - override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - TODO("not implemented") - } - } - private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P) @Test fun `Annotation on enum works for specialised entries`() { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) } + @Test(expected = KryoException::class) + fun `Unannotated specialised enum does not work`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(BadFood.Mud::class.java) + } + + @Test + fun `Annotation on simple enum works`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(Simple.Easy::class.java) + } + + @Test(expected = KryoException::class) + fun `Unannotated simple enum does not work`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(BadSimple.Nasty::class.java) + } + @Test fun `Annotation on array element works`() { val values = arrayOf(Element()) CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) } + @Test(expected = KryoException::class) + fun `Unannotated array elements do not work`() { + val values = arrayOf(NotSerializable()) + CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) + } + @Test fun `Annotation not needed on abstract class`() { CordaClassResolver(emptyWhitelistContext).getRegistration(AbstractClass::class.java) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index ede793b2be..d9d0f58e0c 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -11,11 +11,12 @@ import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayOutputStream import java.io.NotSerializableException -import java.nio.charset.StandardCharsets.* +import java.nio.charset.StandardCharsets.US_ASCII import java.util.* class ListsSerializationTest : TestDependencyInjectionBase() { @@ -40,16 +41,19 @@ class ListsSerializationTest : TestDependencyInjectionBase() { @Test fun `check list can be serialized as part of SessionData`() { run { - val sessionData = SessionData(123, listOf(1)) + val sessionData = SessionData(123, listOf(1).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(listOf(1), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, listOf(1, 2)) + val sessionData = SessionData(123, listOf(1, 2).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(listOf(1, 2), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, emptyList()) + val sessionData = SessionData(123, emptyList().serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(emptyList(), sessionData.payload.deserialize()) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt index 9788885420..4e9f598eab 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt @@ -3,17 +3,19 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions +import org.bouncycastle.asn1.x500.X500Name import org.junit.Assert.assertArrayEquals import org.junit.Test -import org.bouncycastle.asn1.x500.X500Name import java.io.ByteArrayOutputStream import java.util.* +import kotlin.test.assertEquals class MapsSerializationTest : TestDependencyInjectionBase() { private companion object { @@ -33,8 +35,9 @@ class MapsSerializationTest : TestDependencyInjectionBase() { @Test fun `check list can be serialized as part of SessionData`() { - val sessionData = SessionData(123, smallMap) + val sessionData = SessionData(123, smallMap.serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(smallMap, sessionData.payload.deserialize()) } @CordaSerializable diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt index 4a652a7521..210a0cd800 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt @@ -2,11 +2,13 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.kryoSpecific -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayOutputStream import java.util.* @@ -26,16 +28,19 @@ class SetsSerializationTest : TestDependencyInjectionBase() { @Test fun `check set can be serialized as part of SessionData`() { run { - val sessionData = SessionData(123, setOf(1)) + val sessionData = SessionData(123, setOf(1).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(setOf(1), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, setOf(1, 2)) + val sessionData = SessionData(123, setOf(1, 2).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(setOf(1, 2), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, emptySet()) + val sessionData = SessionData(123, emptySet().serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(emptySet(), sessionData.payload.deserialize()) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 4b1011ad49..31136f0723 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -534,8 +534,22 @@ class SerializationOutputTests { } } - val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" + @Test + fun `test custom object`() { + serdes(FooContract) + } + @Test + @Ignore("Cannot serialize due to known Kotlin/serialization limitation") + fun `test custom anonymous object`() { + val anonymous: Contract = object : Contract { + override fun verify(tx: LedgerTransaction) { + } + } + serdes(anonymous) + } + + private val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" class FooState : ContractState { override val participants: List = emptyList() } diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 525790ea22..f17465ac47 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -14,7 +14,6 @@ import net.corda.nodeapi.internal.ServiceType import net.corda.testing.ALICE import net.corda.testing.ProjectStructure.projectRootDir import net.corda.testing.driver.ListenProcessDeathException -import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -59,7 +58,7 @@ class BootTests { @Test fun `node quits on failure to register with network map`() { val tooManyAdvertisedServices = (1..100).map { ServiceInfo(ServiceType.notary.getSubType("$it")) }.toSet() - driver(networkMapStartStrategy = NetworkMapStartStrategy.Nominated(ALICE.name)) { + driver { val future = startNode(providedName = ALICE.name) assertFailsWith(ListenProcessDeathException::class) { future.getOrThrow() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt index 2f29d8df6d..a9f2586013 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt @@ -1,7 +1,6 @@ package net.corda.node import com.google.common.base.Stopwatch -import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.driver.driver import org.junit.Ignore import org.junit.Test @@ -14,8 +13,7 @@ class NodeStartupPerformanceTests { // Measure the startup time of nodes. Note that this includes an RPC roundtrip, which causes e.g. Kryo initialisation. @Test fun `single node startup time`() { - driver(networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false)) { - startDedicatedNetworkMapService().get() + driver { val times = ArrayList() for (i in 1..10) { val time = Stopwatch.createStarted().apply { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 3c15d0aec5..9d4abec634 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -37,7 +37,7 @@ import kotlin.test.assertFailsWith class AttachmentLoadingTests : TestDependencyInjectionBase() { private class Services : MockServices() { - private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR))).start(attachments) + private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments) private val cordapp get() = provider.cordapps.first() val attachmentId get() = provider.getCordappAttachmentId(cordapp)!! val appContext get() = provider.getAppContext(cordapp) @@ -69,7 +69,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { // Copy the app jar to the first node. The second won't have it. - val path = (baseDirectory(nodeName.toString()) / "plugins").createDirectories() / "isolated.jar" + val path = (baseDirectory(nodeName.toString()) / "cordapps").createDirectories() / "isolated.jar" logger.info("Installing isolated jar to $path") isolatedJAR.openStream().buffered().use { input -> Files.newOutputStream(path).buffered().use { output -> diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 0b5d72d858..3ba78ff7bd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -16,26 +16,14 @@ import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.NodeBasedTest -import org.junit.After -import org.junit.Before import org.junit.Test import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class RaftNotaryServiceTests : NodeBasedTest() { +class RaftNotaryServiceTests : NodeBasedTest(listOf("net.corda.testing.contracts")) { private val notaryName = CordaX500Name(RaftValidatingNotaryService.id, "RAFT Notary Service", "London", "GB") - @Before - fun setup() { - setCordappPackages("net.corda.testing.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `detect double spend`() { val (bankA) = listOf( diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 0f102a5e79..d307a02705 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -8,6 +8,7 @@ import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.nodeapi.NodeInfoFilesCopier import net.corda.testing.ALICE import net.corda.testing.ALICE_KEY import net.corda.testing.DEV_TRUST_ROOT @@ -42,7 +43,6 @@ class NodeInfoWatcherTest : NodeBasedTest() { lateinit var nodeInfoWatcher: NodeInfoWatcher companion object { - val nodeInfoFileRegex = Regex("nodeInfo\\-.*") val nodeInfo = NodeInfo(listOf(), listOf(getTestPartyAndCertificate(ALICE)), 0, 0) } @@ -56,12 +56,14 @@ class NodeInfoWatcherTest : NodeBasedTest() { @Test fun `save a NodeInfo`() { - assertEquals(0, folder.root.list().size) + assertEquals(0, + folder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size) NodeInfoWatcher.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) - assertEquals(1, folder.root.list().size) - val fileName = folder.root.list()[0] - assertTrue(fileName.matches(nodeInfoFileRegex)) + val nodeInfoFiles = folder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) } + assertEquals(1, nodeInfoFiles.size) + val fileName = nodeInfoFiles.first() + assertTrue(fileName.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX)) val file = (folder.root.path / fileName).toFile() // Just check that something is written, another tests verifies that the written value can be read back. assertThat(contentOf(file)).isNotEmpty() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index fae4c25c51..00671b35a9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -46,7 +46,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { @Test fun `get nodes by owning key and by name, no network map service`() { val alice = startNodesWithPort(listOf(ALICE), noNetworkMap = true)[0] - val netCache = alice.services.networkMapCache as PersistentNetworkMapCache + val netCache = alice.services.networkMapCache alice.database.transaction { val res = netCache.getNodeByLegalIdentity(alice.info.chooseIdentity()) assertEquals(alice.info, res) @@ -58,7 +58,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { @Test fun `get nodes by address no network map service`() { val alice = startNodesWithPort(listOf(ALICE), noNetworkMap = true)[0] - val netCache = alice.services.networkMapCache as PersistentNetworkMapCache + val netCache = alice.services.networkMapCache alice.database.transaction { val res = netCache.getNodeByAddress(alice.info.addresses[0]) assertEquals(alice.info, res) diff --git a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt index 0e8f062c9c..9a71e5c60f 100644 --- a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt @@ -7,6 +7,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState @@ -21,37 +22,56 @@ import net.corda.node.services.FlowPermissions import net.corda.nodeapi.User import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity +import net.corda.testing.driver.DriverDSLExposedInterface +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver +import org.junit.Assume import org.junit.Test import java.lang.management.ManagementFactory import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Table import kotlin.test.assertEquals +import kotlin.test.assertNotNull class NodeStatePersistenceTests { @Test fun `persistent state survives node restart`() { + // Temporary disable this test when executed on Windows. It is known to be sporadically failing. + // More investigation is needed to establish why. + Assume.assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) + val user = User("mark", "dadada", setOf(FlowPermissions.startFlowPermission())) val message = Message("Hello world!") driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) { - startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() - var nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() - val nodeName = nodeHandle.nodeInfo.chooseIdentity().name - nodeHandle.rpcClientToNode().start(user.username, user.password).use { - it.proxy.startFlow(::SendMessageFlow, message).returnValue.getOrThrow() - } - nodeHandle.stop().getOrThrow() + val (nodeName, notaryNodeHandle) = { + val notaryNodeHandle = startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() + val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() + ensureAcquainted(notaryNodeHandle, nodeHandle) + val nodeName = nodeHandle.nodeInfo.chooseIdentity().name + nodeHandle.rpcClientToNode().start(user.username, user.password).use { + it.proxy.startFlow(::SendMessageFlow, message).returnValue.getOrThrow() + } + nodeHandle.stop().getOrThrow() + nodeName to notaryNodeHandle + }() - nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() + val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() + ensureAcquainted(notaryNodeHandle, nodeHandle) nodeHandle.rpcClientToNode().start(user.username, user.password).use { val page = it.proxy.vaultQuery(MessageState::class.java) - val retrievedMessage = page.states.singleOrNull()?.state?.data?.message + val stateAndRef = page.states.singleOrNull() + assertNotNull(stateAndRef) + val retrievedMessage = stateAndRef!!.state.data.message assertEquals(message, retrievedMessage) } } } + + private fun DriverDSLExposedInterface.ensureAcquainted(one: NodeHandle, another: NodeHandle) { + listOf(one.pollUntilKnowsAbout(another), another.pollUntilKnowsAbout(one)).transpose().getOrThrow() + } } fun isQuasarAgentSpecified(): Boolean { @@ -95,7 +115,7 @@ object MessageSchemaV1 : MappedSchema( ) : PersistentState() } -val MESSAGE_CONTRACT_PROGRAM_ID = "net.corda.test.node.MessageContract" +const val MESSAGE_CONTRACT_PROGRAM_ID = "net.corda.test.node.MessageContract" open class MessageContract : Contract { override fun verify(tx: LedgerTransaction) { diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index 83e76ae2ba..fa39580fa7 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -24,26 +24,27 @@ public class CordaCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - return (T) augmentClasspath((List) cp, "plugins"); + + (new File("cordapps")).mkdir(); + augmentClasspath((List) cp, "cordapps"); + augmentClasspath((List) cp, "plugins"); + return cp; } return super.attribute(attr); } // TODO: Make directory configurable via the capsule manifest. // TODO: Add working directory variable to capsules string replacement variables. - private List augmentClasspath(List classpath, String dirName) { + private void augmentClasspath(List classpath, String dirName) { File dir = new File(dirName); - if (!dir.exists()) { - dir.mkdir(); - } - - File[] files = dir.listFiles(); - for (File file : files) { - if (file.isFile() && isJAR(file)) { - classpath.add(file.toPath().toAbsolutePath()); + if (dir.exists()) { + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isFile() && isJAR(file)) { + classpath.add(file.toPath().toAbsolutePath()); + } } } - return classpath; } @Override diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 0d92fe6df5..dee4123fd0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -7,7 +7,6 @@ import net.corda.confidential.SwapIdentitiesFlow import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture -import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.* import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -19,17 +18,12 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.RPCOps -import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.* import net.corda.core.messaging.* import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.node.services.* -import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken @@ -40,6 +34,7 @@ import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler @@ -82,6 +77,7 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.sql.Connection import java.time.Clock +import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS @@ -102,6 +98,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair abstract class AbstractNode(config: NodeConfiguration, val platformClock: Clock, protected val versionInfo: VersionInfo, + protected val cordappLoader: CordappLoader, @VisibleForTesting val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { open val configuration = config.apply { require(minimumPlatformVersion <= versionInfo.platformVersion) { @@ -111,7 +108,7 @@ abstract class AbstractNode(config: NodeConfiguration, private class StartedNodeImpl( override val internals: N, - override val services: ServiceHubInternalImpl, + services: ServiceHubInternalImpl, override val info: NodeInfo, override val checkpointStorage: CheckpointStorage, override val smm: StateMachineManager, @@ -119,8 +116,11 @@ abstract class AbstractNode(config: NodeConfiguration, override val inNodeNetworkMapService: NetworkMapService, override val network: MessagingService, override val database: CordaPersistence, - override val rpcOps: CordaRPCOps) : StartedNode - + override val rpcOps: CordaRPCOps, + flowStarter: FlowStarter, + internal val schedulerService: NodeSchedulerService) : StartedNode { + override val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter {} + } // TODO: Persist this, as well as whether the node is registered. /** * Sequence number of changes sent to the network map service, when registering/de-registering this node. @@ -141,6 +141,7 @@ abstract class AbstractNode(config: NodeConfiguration, protected val services: ServiceHubInternal get() = _services private lateinit var _services: ServiceHubInternalImpl protected lateinit var legalIdentity: PartyAndCertificate + private lateinit var allIdentities: List protected lateinit var info: NodeInfo protected var myNotaryIdentity: PartyAndCertificate? = null protected lateinit var checkpointStorage: CheckpointStorage @@ -150,9 +151,6 @@ abstract class AbstractNode(config: NodeConfiguration, protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() protected lateinit var database: CordaPersistence - lateinit var cordappProvider: CordappProviderImpl - protected val cordappLoader by lazy { makeCordappLoader() } - protected val _nodeReadyFuture = openFuture() /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ @@ -165,7 +163,7 @@ abstract class AbstractNode(config: NodeConfiguration, } open val serializationWhitelists: List by lazy { - cordappProvider.cordapps.flatMap { it.serializationWhitelists } + cordappLoader.cordapps.flatMap { it.serializationWhitelists } } /** Set to non-null once [start] has been successfully called. */ @@ -173,8 +171,8 @@ abstract class AbstractNode(config: NodeConfiguration, @Volatile private var _started: StartedNode? = null /** The implementation of the [CordaRPCOps] interface used by this node. */ - open fun makeRPCOps(): CordaRPCOps { - return CordaRPCOpsImpl(services, smm, database) + open fun makeRPCOps(flowStarter: FlowStarter): CordaRPCOps { + return CordaRPCOpsImpl(services, smm, database, flowStarter) } private fun saveOwnNodeInfo() { @@ -189,13 +187,15 @@ abstract class AbstractNode(config: NodeConfiguration, validateKeystore() } + private fun makeSchemaService() = NodeSchemaService(cordappLoader) open fun generateNodeInfo() { check(started == null) { "Node has already been started" } initCertificate() log.info("Generating nodeInfo ...") - val schemaService = NodeSchemaService() + val schemaService = makeSchemaService() initialiseDatabasePersistence(schemaService) { - makeServices(schemaService) + val transactionStorage = makeTransactionStorage() + makeServices(schemaService, transactionStorage, StateLoaderImpl(transactionStorage)) saveOwnNodeInfo() } } @@ -204,10 +204,12 @@ abstract class AbstractNode(config: NodeConfiguration, check(started == null) { "Node has already been started" } initCertificate() log.info("Node starting up ...") - val schemaService = NodeSchemaService() + val schemaService = makeSchemaService() // Do all of this in a database transaction so anything that might need a connection has one. val startedImpl = initialiseDatabasePersistence(schemaService) { - val tokenizableServices = makeServices(schemaService) + val transactionStorage = makeTransactionStorage() + val stateLoader = StateLoaderImpl(transactionStorage) + val tokenizableServices = makeServices(schemaService, transactionStorage, stateLoader) saveOwnNodeInfo() smm = StateMachineManager(services, checkpointStorage, @@ -215,9 +217,10 @@ abstract class AbstractNode(config: NodeConfiguration, database, busyNodeLatch, cordappLoader.appClassLoader) - + val flowStarter = FlowStarterImpl(serverThread, smm) + val schedulerService = NodeSchedulerService(platformClock, this@AbstractNode.database, flowStarter, stateLoader, unfinishedSchedules = busyNodeLatch, serverThread = serverThread) smm.tokenizableServices.addAll(tokenizableServices) - + smm.tokenizableServices.add(schedulerService) if (serverThread is ExecutorService) { runOnStop += { // We wait here, even though any in-flight messages should have been drained away because the @@ -226,21 +229,17 @@ abstract class AbstractNode(config: NodeConfiguration, MoreExecutors.shutdownAndAwaitTermination(serverThread as ExecutorService, 50, SECONDS) } } - - makeVaultObservers() - - val rpcOps = makeRPCOps() + makeVaultObservers(schedulerService) + val rpcOps = makeRPCOps(flowStarter) startMessagingService(rpcOps) installCoreFlows() - - installCordaServices() + installCordaServices(flowStarter) registerCordappFlows() - _services.rpcFlows += cordappProvider.cordapps.flatMap { it.rpcFlows } - registerCustomSchemas(cordappProvider.cordapps.flatMap { it.customSchemas }.toSet()) + _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader runOnStop += network::stop - StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, inNodeNetworkMapService, network, database, rpcOps) + StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, inNodeNetworkMapService, network, database, rpcOps, flowStarter, schedulerService) } // If we successfully loaded network data from database, we set this future to Unit. _nodeReadyFuture.captureLater(registerWithNetworkMapIfConfigured()) @@ -249,7 +248,7 @@ abstract class AbstractNode(config: NodeConfiguration, smm.start() // Shut down the SMM so no Fibers are scheduled. runOnStop += { smm.stop(acceptableLiveFiberCountOnStop()) } - services.schedulerService.start() + schedulerService.start() } _started = this } @@ -257,10 +256,11 @@ abstract class AbstractNode(config: NodeConfiguration, private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause) - private fun installCordaServices() { - cordappProvider.cordapps.flatMap { it.services }.forEach { + private fun installCordaServices(flowStarter: FlowStarter) { + val loadedServices = cordappLoader.cordapps.flatMap { it.services } + filterServicesToInstall(loadedServices).forEach { try { - installCordaService(it) + installCordaService(flowStarter, it) } catch (e: NoSuchMethodException) { log.error("${it.name}, as a Corda service, must have a constructor with a single parameter of type " + ServiceHub::class.java.name) @@ -272,10 +272,28 @@ abstract class AbstractNode(config: NodeConfiguration, } } + private fun filterServicesToInstall(loadedServices: List>): List> { + val customNotaryServiceList = loadedServices.filter { isNotaryService(it) } + if (customNotaryServiceList.isNotEmpty()) { + if (configuration.notary?.custom == true) { + require(customNotaryServiceList.size == 1) { + "Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}" + } + } else return loadedServices - customNotaryServiceList + } + return loadedServices + } + + /** + * If the [serviceClass] is a notary service, it will only be enable if the "custom" flag is set in + * the notary configuration. + */ + private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass) + /** * This customizes the ServiceHub for each CordaService that is initiating flows */ - private class AppServiceHubImpl(val serviceHub: ServiceHubInternal) : AppServiceHub, ServiceHub by serviceHub { + private class AppServiceHubImpl(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter) : AppServiceHub, ServiceHub by serviceHub { lateinit var serviceInstance: T override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { val stateMachine = startFlowChecked(flow) @@ -295,41 +313,32 @@ abstract class AbstractNode(config: NodeConfiguration, val logicType = flow.javaClass require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" } val currentUser = FlowInitiator.Service(serviceInstance.javaClass.name) - return serviceHub.startFlow(flow, currentUser) + return flowStarter.startFlow(flow, currentUser) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AppServiceHubImpl<*>) return false - - if (serviceHub != other.serviceHub) return false - if (serviceInstance != other.serviceInstance) return false - - return true + return serviceHub == other.serviceHub + && flowStarter == other.flowStarter + && serviceInstance == other.serviceInstance } - override fun hashCode(): Int { - var result = serviceHub.hashCode() - result = 31 * result + serviceInstance.hashCode() - return result - } + override fun hashCode() = Objects.hash(serviceHub, flowStarter, serviceInstance) } - /** - * Use this method to install your Corda services in your tests. This is automatically done by the node when it - * starts up for all classes it finds which are annotated with [CordaService]. - */ - fun installCordaService(serviceClass: Class): T { + internal fun installCordaService(flowStarter: FlowStarter, serviceClass: Class): T { serviceClass.requireAnnotation() val service = try { - if (NotaryService::class.java.isAssignableFrom(serviceClass)) { + val serviceContext = AppServiceHubImpl(services, flowStarter) + if (isNotaryService(serviceClass)) { check(myNotaryIdentity != null) { "Trying to install a notary service but no notary identity specified" } - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } - constructor.newInstance(services, myNotaryIdentity!!.owningKey) + val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } + serviceContext.serviceInstance = constructor.newInstance(serviceContext, myNotaryIdentity!!.owningKey) + serviceContext.serviceInstance } else { try { val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } - val serviceContext = AppServiceHubImpl(services) serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext) serviceContext.serviceInstance } catch (ex: NoSuchMethodException) { @@ -357,7 +366,7 @@ abstract class AbstractNode(config: NodeConfiguration, } private fun registerCordappFlows() { - cordappProvider.cordapps.flatMap { it.initiatedFlows } + cordappLoader.cordapps.flatMap { it.initiatedFlows } .forEach { try { registerInitiatedFlowInternal(it, track = false) @@ -452,19 +461,22 @@ abstract class AbstractNode(config: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(schemaService: SchemaService): MutableList { + private fun makeServices(schemaService: SchemaService, transactionStorage: WritableTransactionStorage, stateLoader: StateLoader): MutableList { checkpointStorage = DBCheckpointStorage() - cordappProvider = CordappProviderImpl(cordappLoader) - val transactionStorage = makeTransactionStorage() - _services = ServiceHubInternalImpl(schemaService, transactionStorage, StateLoaderImpl(transactionStorage)) - attachments = NodeAttachmentService(services.monitoringService.metrics) - cordappProvider.start(attachments) + val metrics = MetricRegistry() + attachments = NodeAttachmentService(metrics) + val cordappProvider = CordappProviderImpl(cordappLoader, attachments) + _services = ServiceHubInternalImpl(schemaService, transactionStorage, stateLoader, MonitoringService(metrics), cordappProvider) legalIdentity = obtainIdentity(notaryConfig = null) + // TODO We keep only notary identity as additional legalIdentity if we run it on a node . Multiple identities need more design thinking. + myNotaryIdentity = getNotaryIdentity() + allIdentities = listOf(legalIdentity, myNotaryIdentity).filterNotNull() network = makeMessagingService(legalIdentity) - info = makeInfo(legalIdentity) + val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. + info = NodeInfo(addresses, allIdentities, versionInfo.platformVersion, platformClock.instant().toEpochMilli()) val networkMapCache = services.networkMapCache val tokenizableServices = mutableListOf(attachments, network, services.vaultService, - services.keyManagementService, services.identityService, platformClock, services.schedulerService, + services.keyManagementService, services.identityService, platformClock, services.auditService, services.monitoringService, networkMapCache, services.schemaService, services.transactionVerifierService, services.validatedTransactions, services.contractUpgradeService, services, cordappProvider, this) @@ -472,34 +484,12 @@ abstract class AbstractNode(config: NodeConfiguration, return tokenizableServices } - private fun makeCordappLoader(): CordappLoader { - val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages") - return if (CordappLoader.testPackages.isNotEmpty()) { - check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, CordappLoader.testPackages) - } else if (scanPackages != null) { - check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, scanPackages.split(",")) - } else { - CordappLoader.createDefault(configuration.baseDirectory) - } - } - protected open fun makeTransactionStorage(): WritableTransactionStorage = DBTransactionStorage() - private fun makeVaultObservers() { - VaultSoftLockManager(services.vaultService, smm) - ScheduledActivityObserver(services) - HibernateObserver(services.vaultService.rawUpdates, services.database.hibernateConfig) - } - - private fun makeInfo(legalIdentity: PartyAndCertificate): NodeInfo { - // TODO We keep only notary identity as additional legalIdentity if we run it on a node . Multiple identities need more design thinking. - myNotaryIdentity = getNotaryIdentity() - val allIdentitiesList = mutableListOf(legalIdentity) - myNotaryIdentity?.let { allIdentitiesList.add(it) } - val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. - return NodeInfo(addresses, allIdentitiesList, versionInfo.platformVersion, platformClock.instant().toEpochMilli()) + private fun makeVaultObservers(schedulerService: SchedulerService) { + VaultSoftLockManager.install(services.vaultService, smm) + ScheduledActivityObserver.install(services.vaultService, schedulerService) + HibernateObserver.install(services.vaultService.rawUpdates, database.hibernateConfig) } /** @@ -537,7 +527,7 @@ abstract class AbstractNode(config: NodeConfiguration, protected open fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T): T { val props = configuration.dataSourceProperties if (props.isNotEmpty()) { - this.database = configureDatabase(props, configuration.database, schemaService, { _services.identityService }) + this.database = configureDatabase(props, configuration.database, { _services.identityService }, schemaService) // Now log the vendor string as this will also cause a connection to be tested eagerly. database.transaction { log.info("Connected to ${database.dataSource.connection.metaData.databaseProductName} database.") @@ -551,8 +541,16 @@ abstract class AbstractNode(config: NodeConfiguration, } } + private fun setupInNodeNetworkMapService(networkMapCache: NetworkMapCacheInternal) { + inNodeNetworkMapService = + if (configuration.networkMapService == null && !configuration.noNetworkMapServiceMode) + makeNetworkMapService(network, networkMapCache) + else + NullNetworkMapService + } + private fun makeNetworkServices(network: MessagingService, networkMapCache: NetworkMapCacheInternal, tokenizableServices: MutableList) { - inNodeNetworkMapService = if (configuration.networkMapService == null) makeNetworkMapService(network, networkMapCache) else NullNetworkMapService + setupInNodeNetworkMapService(networkMapCache) configuration.notary?.let { val notaryService = makeCoreNotaryService(it) tokenizableServices.add(notaryService) @@ -611,7 +609,7 @@ abstract class AbstractNode(config: NodeConfiguration, /** This is overriden by the mock node implementation to enable operation without any network map service */ protected open fun noNetworkMapConfigured(): CordaFuture { - if (services.networkMapCache.loadDBSuccess) { + if (services.networkMapCache.loadDBSuccess || configuration.noNetworkMapServiceMode) { return doneFuture(Unit) } else { // TODO: There should be a consistent approach to configuration error exceptions. @@ -662,17 +660,7 @@ abstract class AbstractNode(config: NodeConfiguration, val caCertificates: Array = listOf(legalIdentity.certificate, clientCa?.certificate?.cert) .filterNotNull() .toTypedArray() - val service = PersistentIdentityService(info.legalIdentitiesAndCerts, trustRoot = trustRoot, caCertificates = *caCertificates) - services.networkMapCache.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { service.verifyAndRegisterIdentity(it) } } - services.networkMapCache.changed.subscribe { mapChange -> - // TODO how should we handle network map removal - if (mapChange is MapChange.Added) { - mapChange.node.legalIdentitiesAndCerts.forEach { - service.verifyAndRegisterIdentity(it) - } - } - } - return service + return PersistentIdentityService(allIdentities, trustRoot = trustRoot, caCertificates = *caCertificates) } protected abstract fun makeTransactionVerifierService(): TransactionVerifierService @@ -702,7 +690,9 @@ abstract class AbstractNode(config: NodeConfiguration, // Node's main identity Pair("identity", myLegalName) } else { - val notaryId = notaryConfig.run { NotaryService.constructId(validating, raft != null, bftSMaRt != null) } + val notaryId = notaryConfig.run { + NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom) + } if (notaryConfig.bftSMaRt == null && notaryConfig.raft == null) { // Node's notary identity Pair(notaryId, myLegalName.copy(commonName = notaryId)) @@ -754,26 +744,29 @@ abstract class AbstractNode(config: NodeConfiguration, } protected open fun generateKeyPair() = cryptoGenerateKeyPair() + protected open fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader): VaultServiceInternal { + return NodeVaultService(platformClock, keyManagementService, stateLoader, database.hibernateConfig) + } private inner class ServiceHubInternalImpl( override val schemaService: SchemaService, override val validatedTransactions: WritableTransactionStorage, - private val stateLoader: StateLoader + private val stateLoader: StateLoader, + override val monitoringService: MonitoringService, + override val cordappProvider: CordappProviderInternal ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by stateLoader { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() - override val monitoringService = MonitoringService(MetricRegistry()) override val transactionVerifierService by lazy { makeTransactionVerifierService() } - override val networkMapCache by lazy { PersistentNetworkMapCache(this) } - override val vaultService by lazy { NodeVaultService(platformClock, keyManagementService, stateLoader, this@AbstractNode.database.hibernateConfig) } + override val networkMapCache by lazy { NetworkMapCacheImpl(PersistentNetworkMapCache(this@AbstractNode.database, this@AbstractNode.configuration), identityService) } + override val vaultService by lazy { makeVaultService(keyManagementService, stateLoader) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. override val keyManagementService by lazy { makeKeyManagementService(identityService) } - override val schedulerService by lazy { NodeSchedulerService(this, unfinishedSchedules = busyNodeLatch, serverThread = serverThread) } override val identityService by lazy { val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) @@ -788,17 +781,11 @@ abstract class AbstractNode(config: NodeConfiguration, override val myInfo: NodeInfo get() = info override val database: CordaPersistence get() = this@AbstractNode.database override val configuration: NodeConfiguration get() = this@AbstractNode.configuration - override val cordappProvider: CordappProvider = this@AbstractNode.cordappProvider - override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") } - override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator, ourIdentity: Party?): FlowStateMachineImpl { - return serverThread.fetchFrom { smm.add(logic, flowInitiator, ourIdentity) } - } - override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { return flowFactories[initiatingFlowClass] } @@ -811,9 +798,10 @@ abstract class AbstractNode(config: NodeConfiguration, override fun jdbcSession(): Connection = database.createSession() } - - fun registerCustomSchemas(schemas: Set) { - database.hibernateConfig.schemaService.registerCustomSchemas(schemas) - } - +} + +internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager) : FlowStarter { + override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator, ourIdentity: Party?): FlowStateMachineImpl { + return serverThread.fetchFrom { smm.add(logic, flowInitiator, ourIdentity) } + } } diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index ee685466d3..af827b9e39 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -19,6 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.transactions.SignedTransaction import net.corda.node.services.FlowPermissions.Companion.startFlowPermission +import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.getRpcContext import net.corda.node.services.messaging.requirePermission @@ -37,7 +38,8 @@ import java.time.Instant class CordaRPCOpsImpl( private val services: ServiceHubInternal, private val smm: StateMachineManager, - private val database: CordaPersistence + private val database: CordaPersistence, + private val flowStarter: FlowStarter ) : CordaRPCOps { override fun networkMapSnapshot(): List { val (snapshot, updates) = networkMapFeed() @@ -150,7 +152,7 @@ class CordaRPCOpsImpl( rpcContext.requirePermission(startFlowPermission(logicType)) val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username) // TODO RPC flows should have mapping user -> identity that should be resolved automatically on starting flow. - return services.invokeFlowAsync(logicType, currentUser, *args) + return flowStarter.invokeFlowAsync(logicType, currentUser, *args) } override fun attachmentExists(id: SecureHash): Boolean { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 84b126980a..e76f226adc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -15,6 +15,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.* import net.corda.node.VersionInfo +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService @@ -22,6 +23,7 @@ import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.SchemaService import net.corda.node.services.config.FullNodeConfiguration +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectRequestProperty import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty @@ -60,10 +62,11 @@ import kotlin.system.exitProcess * * @param configuration This is typically loaded from a TypeSafe HOCON configuration file. */ -open class Node(override val configuration: FullNodeConfiguration, +open class Node(configuration: FullNodeConfiguration, versionInfo: VersionInfo, - val initialiseSerialization: Boolean = true -) : AbstractNode(configuration, createClock(configuration), versionInfo) { + val initialiseSerialization: Boolean = true, + cordappLoader: CordappLoader = makeCordappLoader(configuration) +) : AbstractNode(configuration, createClock(configuration), versionInfo, cordappLoader) { companion object { private val logger = loggerFor() var renderBasicInfoToConsole = true @@ -86,9 +89,17 @@ open class Node(override val configuration: FullNodeConfiguration, } private val sameVmNodeCounter = AtomicInteger() + val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" + val scanPackagesSeparator = "," + private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader { + return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages -> + CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator)) + } ?: CordappLoader.createDefault(configuration.baseDirectory) + } } override val log: Logger get() = logger + override val configuration get() = super.configuration as FullNodeConfiguration // Necessary to avoid init order NPE. override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress) override fun makeTransactionVerifierService() = (network as NodeMessagingClient).verifierService diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 67a06aa2e9..bc95502ee9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -80,6 +80,7 @@ open class NodeStartup(val args: Array) { exitProcess(1) } + logger.info("Node exiting successfully") exitProcess(0) } @@ -95,7 +96,7 @@ open class NodeStartup(val args: Array) { return } val startedNode = node.start() - Node.printBasicNodeInfo("Loaded CorDapps", startedNode.internals.cordappProvider.cordapps.joinToString { it.name }) + Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name }) startedNode.internals.nodeReadyFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 val name = startedNode.info.legalIdentitiesAndCerts.first().name.organisation diff --git a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt index e0320d9c62..2d0645cef4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt @@ -7,9 +7,11 @@ import net.corda.core.flows.FlowLogic import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.StateLoader +import net.corda.core.node.services.CordaService import net.corda.core.node.services.TransactionStorage +import net.corda.core.serialization.SerializeAsToken import net.corda.node.services.api.CheckpointStorage -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService @@ -18,7 +20,7 @@ import net.corda.node.utilities.CordaPersistence interface StartedNode { val internals: N - val services: ServiceHubInternal + val services: StartedNodeServices val info: NodeInfo val checkpointStorage: CheckpointStorage val smm: StateMachineManager @@ -29,6 +31,11 @@ interface StartedNode { val rpcOps: CordaRPCOps fun dispose() = internals.stop() fun > registerInitiatedFlow(initiatedFlowClass: Class) = internals.registerInitiatedFlow(initiatedFlowClass) + /** + * Use this method to install your Corda services in your tests. This is automatically done by the node when it + * starts up for all classes it finds which are annotated with [CordaService]. + */ + fun installCordaService(serviceClass: Class) = internals.installCordaService(services, serviceClass) } class StateLoaderImpl(private val validatedTransactions: TransactionStorage) : StateLoader { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 493b965ba6..5154c592b1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -14,6 +14,7 @@ import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.serialization.DefaultWhitelist import java.io.File import java.io.FileOutputStream @@ -37,10 +38,10 @@ import kotlin.streams.toList * * @property cordappJarPaths The classpath of cordapp JARs */ -class CordappLoader private constructor(private val cordappJarPaths: List) { +class CordappLoader private constructor(private val cordappJarPaths: List) { val cordapps: List by lazy { loadCordapps() + coreCordapp } - internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) + internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader) init { if (cordappJarPaths.isEmpty()) { @@ -53,25 +54,31 @@ class CordappLoader private constructor(private val cordappJarPaths: List) companion object { private val logger = loggerFor() + /** + * Default cordapp dir name + */ + val CORDAPPS_DIR_NAME = "cordapps" + /** * Creates a default CordappLoader intended to be used in non-dev or non-test environments. * - * @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory + * @param baseDir The directory that this node is running in. Will use this to resolve the cordapps directory * for classpath scanning. */ - fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir))) + fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getCordappsPath(baseDir))) /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath - * and plugins directory. This is intended mostly for use by the driver. + * and cordapps directory. This is intended mostly for use by the driver. * * @param baseDir See [createDefault.baseDir] * @param testPackages See [createWithTestPackages.testPackages] */ @VisibleForTesting - @JvmOverloads - fun createDefaultWithTestPackages(baseDir: Path, testPackages: List = CordappLoader.testPackages) - = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)) + testPackages.flatMap(this::createScanPackage)) + fun createDefaultWithTestPackages(configuration: NodeConfiguration, testPackages: List): CordappLoader { + check(configuration.devMode) { "Package scanning can only occur in dev mode" } + return CordappLoader(getCordappsInDirectory(getCordappsPath(configuration.baseDirectory)) + testPackages.flatMap(this::createScanPackage)) + } /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath. @@ -81,8 +88,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) * CorDapps. */ @VisibleForTesting - @JvmOverloads - fun createWithTestPackages(testPackages: List = CordappLoader.testPackages) + fun createWithTestPackages(testPackages: List) = CordappLoader(testPackages.flatMap(this::createScanPackage)) /** @@ -91,20 +97,22 @@ class CordappLoader private constructor(private val cordappJarPaths: List) * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection */ @VisibleForTesting - fun createDevMode(scanJars: List) = CordappLoader(scanJars) + fun createDevMode(scanJars: List) = CordappLoader(scanJars.map { RestrictedURL(it, null) }) - private fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins" + private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME - private fun createScanPackage(scanPackage: String): List { + private fun createScanPackage(scanPackage: String): List { val resource = scanPackage.replace('.', '/') return this::class.java.classLoader.getResources(resource) .asSequence() .map { path -> if (path.protocol == "jar") { - (path.openConnection() as JarURLConnection).jarFileURL.toURI() + // When running tests from gradle this may be a corda module jar, so restrict to scanPackage: + RestrictedURL((path.openConnection() as JarURLConnection).jarFileURL, scanPackage) } else { - createDevCordappJar(scanPackage, path, resource) - }.toURL() + // No need to restrict as createDevCordappJar has already done that: + RestrictedURL(createDevCordappJar(scanPackage, path, resource).toURL(), null) + } } .toList() } @@ -137,21 +145,16 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return generatedCordapps[path]!! } - private fun getCordappsInDirectory(pluginsDir: Path): List { - return if (!pluginsDir.exists()) { - emptyList() + private fun getCordappsInDirectory(cordappsDir: Path): List { + return if (!cordappsDir.exists()) { + emptyList() } else { - pluginsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() + cordappsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() } } } - /** - * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only. - */ - @VisibleForTesting - var testPackages: List = emptyList() private val generatedCordapps = mutableMapOf() /** A list of the core RPC flows present in Corda */ @@ -186,15 +189,15 @@ class CordappLoader private constructor(private val cordappJarPaths: List) findServices(scanResult), findPlugins(it), findCustomSchemas(scanResult), - it) + it.url) } } - private fun findServices(scanResult: ScanResult): List> { + private fun findServices(scanResult: RestrictedScanResult): List> { return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) } - private fun findInitiatedFlows(scanResult: ScanResult): List>> { + private fun findInitiatedFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) // First group by the initiating flow class in case there are multiple mappings .groupBy { it.requireAnnotation().value.java } @@ -213,35 +216,35 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) } - private fun findRPCFlows(scanResult: ScanResult): List>> { + private fun findRPCFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } } - private fun findServiceFlows(scanResult: ScanResult): List>> { + private fun findServiceFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class) } - private fun findSchedulableFlows(scanResult: ScanResult): List>> { + private fun findSchedulableFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) } - private fun findContractClassNames(scanResult: ScanResult): List { - return (scanResult.getNamesOfClassesImplementing(Contract::class.java) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class.java)).distinct() + private fun findContractClassNames(scanResult: RestrictedScanResult): List { + return (scanResult.getNamesOfClassesImplementing(Contract::class) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class)).distinct() } - private fun findPlugins(cordappJarPath: URL): List { - return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath), appClassLoader)).toList().filter { - cordappJarPath == it.javaClass.protectionDomain.codeSource.location + private fun findPlugins(cordappJarPath: RestrictedURL): List { + return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath.url), appClassLoader)).toList().filter { + it.javaClass.protectionDomain.codeSource.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. } - private fun findCustomSchemas(scanResult: ScanResult): Set { + private fun findCustomSchemas(scanResult: RestrictedScanResult): Set { return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet() } - private fun scanCordapp(cordappJarPath: URL): ScanResult { + private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { logger.info("Scanning CorDapp in $cordappJarPath") - return FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath).scan() + return RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix) } private class FlowTypeHierarchyComparator(val initiatingFlow: Class>) : Comparator>> { @@ -268,16 +271,30 @@ class CordappLoader private constructor(private val cordappJarPaths: List) } } - private fun ScanResult.getClassesWithSuperclass(type: KClass): List { - return getNamesOfSubclassesOf(type.java) - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } - .map { it.kotlin.objectOrNewInstance() } + /** @param rootPackageName only this package and subpackages may be extracted from [url], or null to allow all packages. */ + private class RestrictedURL(val url: URL, rootPackageName: String?) { + val qualifiedNamePrefix = rootPackageName?.let { it + '.' } ?: "" } - private fun ScanResult.getClassesWithAnnotation(type: KClass, annotation: KClass): List> { - return getNamesOfClassesWithAnnotation(annotation.java) - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } + private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) { + fun getNamesOfClassesImplementing(type: KClass<*>): List { + return scanResult.getNamesOfClassesImplementing(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + } + + fun getClassesWithSuperclass(type: KClass): List { + return scanResult.getNamesOfSubclassesOf(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + .map { it.kotlin.objectOrNewInstance() } + } + + fun getClassesWithAnnotation(type: KClass, annotation: KClass): List> { + return scanResult.getNamesOfClassesWithAnnotation(annotation.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index 967c332fd3..ba305283f2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -6,15 +6,14 @@ import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext -import net.corda.core.cordapp.CordappProvider import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.SingletonSerializeAsToken -import java.net.URLClassLoader +import java.net.URL /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. */ -open class CordappProviderImpl(private val cordappLoader: CordappLoader) : SingletonSerializeAsToken(), CordappProvider { +open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { override fun getAppContext(): CordappContext { // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> @@ -34,28 +33,19 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl /** * Current known CorDapps loaded on this node */ - val cordapps get() = cordappLoader.cordapps - private lateinit var cordappAttachments: HashBiMap - - /** - * Should only be called once from the initialisation routine of the node or tests - */ - fun start(attachmentStorage: AttachmentStorage): CordappProviderImpl { - cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage)) - return this - } - + override val cordapps get() = cordappLoader.cordapps + private val cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage)) /** * Gets the attachment ID of this CorDapp. Only CorDapps with contracts have an attachment ID * * @param cordapp The cordapp to get the attachment ID * @return An attachment ID if it exists, otherwise nothing */ - fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp) + fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp.jarPath) - private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { - val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() } - val attachmentIds = cordappsWithAttachments.map { it.jarPath.openStream().use { attachmentStorage.importAttachment(it) } } + private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { + val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath } + val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importAttachment(it) } } return attachmentIds.zip(cordappsWithAttachments).toMap() } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt new file mode 100644 index 0000000000..a29d8bab25 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt @@ -0,0 +1,8 @@ +package net.corda.node.internal.cordapp + +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappProvider + +interface CordappProviderInternal : CordappProvider { + val cordapps: List +} diff --git a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt index 7428a2e90a..4a4d815708 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt @@ -30,11 +30,5 @@ interface SchemaService { * or via custom logic in this service. */ fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState - - /** - * Registration mechanism to add custom contract schemas that extend the [MappedSchema] class. - */ - fun registerCustomSchemas(customSchemas: Set) - } //DOCEND SchemaService diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 65bd38d843..de7b8eb2eb 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -16,18 +16,21 @@ import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache +import net.corda.core.node.services.NetworkMapCacheBase import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.CordaPersistence -interface NetworkMapCacheInternal : NetworkMapCache { +interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBaseInternal +interface NetworkMapCacheBaseInternal : NetworkMapCacheBase { /** * Deregister from updates from the given map service. * @param network the network messaging service. @@ -83,13 +86,12 @@ interface ServiceHubInternal : ServiceHub { val monitoringService: MonitoringService val schemaService: SchemaService override val networkMapCache: NetworkMapCacheInternal - val schedulerService: SchedulerService val auditService: AuditService val rpcFlows: List>> val networkService: MessagingService val database: CordaPersistence val configuration: NodeConfiguration - + override val cordappProvider: CordappProviderInternal override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { require(txs.any()) { "No transactions passed in for recording" } val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } @@ -108,6 +110,10 @@ interface ServiceHubInternal : ServiceHub { } } + fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? +} + +interface FlowStarter { /** * Starts an already constructed flow. Note that you must be on the server thread to call this method. [FlowInitiator] * defaults to [FlowInitiator.RPC] with username "Only For Testing". @@ -137,10 +143,9 @@ interface ServiceHubInternal : ServiceHub { val logic: FlowLogic = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef)) return startFlow(logic, flowInitiator, ourIdentity = null) } - - fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? } +interface StartedNodeServices : ServiceHubInternal, FlowStarter /** * Thread-safe storage of transactions. */ diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index a72465ccee..6d150a3ab3 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -20,6 +20,7 @@ interface NodeConfiguration : NodeSSLConfiguration { * service. */ val networkMapService: NetworkMapInfo? + val noNetworkMapServiceMode: Boolean val minimumPlatformVersion: Int val emailAddress: String val exportJMXto: String @@ -36,9 +37,15 @@ interface NodeConfiguration : NodeSSLConfiguration { val additionalNodeInfoPollingFrequencyMsec: Long } -data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null) { +data class NotaryConfig(val validating: Boolean, + val raft: RaftConfig? = null, + val bftSMaRt: BFTSMaRtConfiguration? = null, + val custom: Boolean = false +) { init { - require(raft == null || bftSMaRt == null) { "raft and bftSMaRt configs cannot be specified together" } + require(raft == null || bftSMaRt == null || !custom) { + "raft, bftSMaRt, and custom configs cannot be specified together" + } } } @@ -46,9 +53,10 @@ data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: /** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ data class BFTSMaRtConfiguration constructor(val replicaId: Int, - val clusterAddresses: List, - val debug: Boolean = false, - val exposeRaces: Boolean = false) { + val clusterAddresses: List, + val debug: Boolean = false, + val exposeRaces: Boolean = false +) { init { require(replicaId >= 0) { "replicaId cannot be negative" } } @@ -71,6 +79,7 @@ data class FullNodeConfiguration( override val database: Properties?, override val certificateSigningService: URL, override val networkMapService: NetworkMapInfo?, + override val noNetworkMapServiceMode: Boolean = false, override val minimumPlatformVersion: Int = 1, override val rpcUsers: List, override val verifierType: VerifierType, diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 26a8322b80..a0c208a53e 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -14,15 +14,17 @@ import net.corda.core.flows.FlowLogic import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.until +import net.corda.core.node.StateLoader import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import net.corda.node.internal.MutableClock +import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.SchedulerService -import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.AffinityExecutor +import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.NODE_DATABASE_PREFIX import net.corda.node.utilities.PersistentMap import org.apache.activemq.artemis.utils.ReusableLatch @@ -47,12 +49,14 @@ import javax.persistence.Entity * in the nodes, maybe we can consider multiple activities and whether the activities have been completed or not, * but that starts to sound a lot like off-ledger state. * - * @param services Core node services. * @param schedulerTimerExecutor The executor the scheduler blocks on waiting for the clock to advance to the next * activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on. */ @ThreadSafe -class NodeSchedulerService(private val services: ServiceHubInternal, +class NodeSchedulerService(private val clock: Clock, + private val database: CordaPersistence, + private val flowStarter: FlowStarter, + private val stateLoader: StateLoader, private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val unfinishedSchedules: ReusableLatch = ReusableLatch(), private val serverThread: AffinityExecutor) @@ -108,8 +112,8 @@ class NodeSchedulerService(private val services: ServiceHubInternal, toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, fromPersistentEntity = { //TODO null check will become obsolete after making DB/JPA columns not nullable - var txId = it.output.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") - var index = it.output.index ?: throw IllegalStateException("DB returned null SecureHash index") + val txId = it.output.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") + val index = it.output.index ?: throw IllegalStateException("DB returned null SecureHash index") Pair(StateRef(SecureHash.parse(txId), index), ScheduledStateRef(StateRef(SecureHash.parse(txId), index), it.scheduledAt)) }, @@ -172,7 +176,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, mutex.locked { val previousState = scheduledStates[action.ref] scheduledStates[action.ref] = action - var previousEarliest = scheduledStatesQueue.peek() + val previousEarliest = scheduledStatesQueue.peek() scheduledStatesQueue.remove(previousState) scheduledStatesQueue.add(action) if (previousState == null) { @@ -223,7 +227,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, log.trace { "Scheduling as next $scheduledState" } // This will block the scheduler single thread until the scheduled time (returns false) OR // the Future is cancelled due to rescheduling (returns true). - if (!awaitWithDeadline(services.clock, scheduledState.scheduledAt, ourRescheduledFuture)) { + if (!awaitWithDeadline(clock, scheduledState.scheduledAt, ourRescheduledFuture)) { log.trace { "Invoking as next $scheduledState" } onTimeReached(scheduledState) } else { @@ -237,11 +241,11 @@ class NodeSchedulerService(private val services: ServiceHubInternal, serverThread.execute { var flowName: String? = "(unknown)" try { - services.database.transaction { + database.transaction { val scheduledFlow = getScheduledFlow(scheduledState) if (scheduledFlow != null) { flowName = scheduledFlow.javaClass.name - val future = services.startFlow(scheduledFlow, FlowInitiator.Scheduled(scheduledState)).resultFuture + val future = flowStarter.startFlow(scheduledFlow, FlowInitiator.Scheduled(scheduledState)).resultFuture future.then { unfinishedSchedules.countDown() } @@ -265,9 +269,9 @@ class NodeSchedulerService(private val services: ServiceHubInternal, unfinishedSchedules.countDown() scheduledStates.remove(scheduledState.ref) scheduledStatesQueue.remove(scheduledState) - } else if (scheduledActivity.scheduledAt.isAfter(services.clock.instant())) { + } else if (scheduledActivity.scheduledAt.isAfter(clock.instant())) { log.info("Scheduled state $scheduledState has rescheduled to ${scheduledActivity.scheduledAt}.") - var newState = ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) + val newState = ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) scheduledStates[scheduledState.ref] = newState scheduledStatesQueue.remove(scheduledState) scheduledStatesQueue.add(newState) @@ -286,7 +290,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, } private fun getScheduledActivity(scheduledState: ScheduledStateRef): ScheduledActivity? { - val txState = services.loadState(scheduledState.ref) + val txState = stateLoader.loadState(scheduledState.ref) val state = txState.data as SchedulableState return try { // This can throw as running contract code. diff --git a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt index 3509fa2d34..3020a9e528 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt @@ -4,18 +4,28 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.SchedulableState import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateAndRef -import net.corda.node.services.api.ServiceHubInternal +import net.corda.core.node.services.VaultService +import net.corda.node.services.api.SchedulerService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl /** * This observes the vault and schedules and unschedules activities appropriately based on state production and * consumption. */ -class ScheduledActivityObserver(val services: ServiceHubInternal) { - init { - services.vaultService.rawUpdates.subscribe { (consumed, produced) -> - consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) } - produced.forEach { scheduleStateActivity(it) } +class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService) { + companion object { + @JvmStatic + fun install(vaultService: VaultService, schedulerService: SchedulerService) { + val observer = ScheduledActivityObserver(schedulerService) + vaultService.rawUpdates.subscribe { (consumed, produced) -> + consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) } + produced.forEach { observer.scheduleStateActivity(it) } + } + } + + // TODO: Beware we are calling dynamically loaded contract code inside here. + private inline fun sandbox(code: () -> T?): T? { + return code() } } @@ -23,12 +33,7 @@ class ScheduledActivityObserver(val services: ServiceHubInternal) { val producedState = produced.state.data if (producedState is SchedulableState) { val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return - services.schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt)) + schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt)) } } - - // TODO: Beware we are calling dynamically loaded contract code inside here. - private inline fun sandbox(code: () -> T?): T? { - return code() - } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index b05dc10c02..a708166239 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -213,6 +213,7 @@ data class TopicSession(val topic: String, val sessionID: Long = MessagingServic * These IDs and timestamps should not be assumed to be globally unique, although due to the nanosecond precision of * the timestamp field they probably will be, even if an implementation just uses a hash prefix as the message id. */ +@CordaSerializable interface Message { val topicSession: TopicSession val data: ByteArray diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 02ee5500ec..5a9cd04730 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -133,6 +133,11 @@ class NodeMessagingClient(override val config: NodeConfiguration, persistentEntityClass = RetryMessage::class.java ) } + + private class NodeClientMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID) : Message { + override val debugTimestamp: Instant = Instant.now() + override fun toString() = "$topicSession#${String(data)}" + } } private class InnerState { @@ -599,13 +604,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, override fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID): Message { // TODO: We could write an object that proxies directly to an underlying MQ message here and avoid copying. - return object : Message { - override val topicSession: TopicSession = topicSession - override val data: ByteArray = data - override val debugTimestamp: Instant = Instant.now() - override val uniqueMessageId: UUID = uuid - override fun toString() = "$topicSession#${String(data)}" - } + return NodeClientMessage(topicSession, data, uuid) } private fun createOutOfProcessVerifierService(): TransactionVerifierService { diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt index aefd79c2d1..4bd9c72cb4 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -9,9 +9,11 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds +import net.corda.nodeapi.NodeInfoFilesCopier import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers +import java.io.IOException import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.streams.toList @@ -32,7 +34,8 @@ class NodeInfoWatcher(private val nodePath: Path, private val scheduler: Scheduler = Schedulers.io()) { private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY - private val pollFrequencyMsec: Long + private val pollFrequencyMsec: Long = maxOf(pollFrequencyMsec, 5.seconds.toMillis()) + private val successfullyProcessedFiles = mutableSetOf() companion object { private val logger = loggerFor() @@ -53,7 +56,8 @@ class NodeInfoWatcher(private val nodePath: Path, val serializedBytes = nodeInfo.serialize() val regSig = keyManager.sign(serializedBytes.bytes, nodeInfo.legalIdentities.first().owningKey) val signedData = SignedData(serializedBytes, regSig) - signedData.serialize().open().copyTo(path / "nodeInfo-${serializedBytes.hash}") + signedData.serialize().open().copyTo( + path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${serializedBytes.hash}") } catch (e: Exception) { logger.warn("Couldn't write node info to file", e) } @@ -61,7 +65,13 @@ class NodeInfoWatcher(private val nodePath: Path, } init { - this.pollFrequencyMsec = maxOf(pollFrequencyMsec, 5.seconds.toMillis()) + if (!nodeInfoDirectory.isDirectory()) { + try { + nodeInfoDirectory.createDirectories() + } catch (e: IOException) { + logger.info("Failed to create $nodeInfoDirectory", e) + } + } } /** @@ -71,8 +81,7 @@ class NodeInfoWatcher(private val nodePath: Path, * We simply list the directory content every 5 seconds, the Java implementation of WatchService has been proven to * be unreliable on MacOs and given the fairly simple use case we have, this simple implementation should do. * - * @return an [Observable] returning [NodeInfo]s, there is no guarantee that the same value isn't returned more - * than once. + * @return an [Observable] returning [NodeInfo]s, at most one [NodeInfo] is returned for each processed file. */ fun nodeInfoUpdates(): Observable { return Observable.interval(pollFrequencyMsec, TimeUnit.MILLISECONDS, scheduler) @@ -87,16 +96,20 @@ class NodeInfoWatcher(private val nodePath: Path, */ private fun loadFromDirectory(): List { if (!nodeInfoDirectory.isDirectory()) { - logger.info("$nodeInfoDirectory isn't a Directory, not loading NodeInfo from files") return emptyList() } val result = nodeInfoDirectory.list { paths -> - paths.filter { it.isRegularFile() } - .map { processFile(it) } + paths.filter { it !in successfullyProcessedFiles } + .filter { it.isRegularFile() } + .map { path -> + processFile(path)?.apply { successfullyProcessedFiles.add(path) } + } .toList() .filterNotNull() } - logger.info("Successfully read ${result.size} NodeInfo files.") + if (result.isNotEmpty()) { + logger.info("Successfully read ${result.size} NodeInfo files from disk.") + } return result } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 1f24e27526..c6caa567da 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -12,6 +12,7 @@ import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NotaryService import net.corda.core.node.services.PartyInfo @@ -24,14 +25,15 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase58String import net.corda.node.services.api.NetworkCacheException import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.NetworkMapCacheBaseInternal +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.createMessage import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.NetworkMapService.FetchMapResponse import net.corda.node.services.network.NetworkMapService.SubscribeResponse +import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import org.hibernate.Session @@ -39,20 +41,36 @@ import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey import java.security.SignatureException -import java.time.Duration import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.collections.HashMap +class NetworkMapCacheImpl(networkMapCacheBase: NetworkMapCacheBaseInternal, private val identityService: IdentityService) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal { + init { + networkMapCacheBase.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { identityService.verifyAndRegisterIdentity(it) } } + networkMapCacheBase.changed.subscribe { mapChange -> + // TODO how should we handle network map removal + if (mapChange is MapChange.Added) { + mapChange.node.legalIdentitiesAndCerts.forEach { + identityService.verifyAndRegisterIdentity(it) + } + } + } + } + + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { + val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party) + return wellKnownParty?.let { + getNodesByLegalIdentityKey(it.owningKey).firstOrNull() + } + } +} + /** * Extremely simple in-memory cache of the network map. - * - * @param serviceHub an optional service hub from which we'll take the identity service. We take a service hub rather - * than the identity service directly, as this avoids problems with service start sequence (network map cache - * and identity services depend on each other). Should always be provided except for unit test cases. */ @ThreadSafe -open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) : SingletonSerializeAsToken(), NetworkMapCacheInternal { +open class PersistentNetworkMapCache(private val database: CordaPersistence, configuration: NodeConfiguration) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { companion object { val logger = loggerFor() } @@ -88,12 +106,12 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) .sortedBy { it.name.toString() } } - private val nodeInfoSerializer = NodeInfoWatcher(serviceHub.configuration.baseDirectory, - serviceHub.configuration.additionalNodeInfoPollingFrequencyMsec) + private val nodeInfoSerializer = NodeInfoWatcher(configuration.baseDirectory, + configuration.additionalNodeInfoPollingFrequencyMsec) init { loadFromFiles() - serviceHub.database.transaction { loadFromDB() } + database.transaction { loadFromDB(session) } } private fun loadFromFiles() { @@ -102,7 +120,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun getPartyInfo(party: Party): PartyInfo? { - val nodes = serviceHub.database.transaction { queryByIdentityKey(party.owningKey) } + val nodes = database.transaction { queryByIdentityKey(session, party.owningKey) } if (nodes.size == 1 && nodes[0].isLegalIdentity(party)) { return PartyInfo.SingleNode(party, nodes[0].addresses) } @@ -117,20 +135,13 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = getNodesByLegalName(name).firstOrNull() - override fun getNodesByLegalName(name: CordaX500Name): List = serviceHub.database.transaction { queryByLegalName(name) } + override fun getNodesByLegalName(name: CordaX500Name): List = database.transaction { queryByLegalName(session, name) } override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = - serviceHub.database.transaction { queryByIdentityKey(identityKey) } + database.transaction { queryByIdentityKey(session, identityKey) } - override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { - val wellKnownParty = serviceHub.identityService.wellKnownPartyFromAnonymous(party) - return wellKnownParty?.let { - getNodesByLegalIdentityKey(it.owningKey).firstOrNull() - } - } + override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) } - override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(address) } - - override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = serviceHub.database.transaction { queryIdentityByLegalName(name) } + override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = database.transaction { queryIdentityByLegalName(session, name) } override fun track(): DataFeed, MapChange> { synchronized(_changed) { @@ -182,13 +193,13 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) val previousNode = registeredNodes.put(node.legalIdentities.first().owningKey, node) // TODO hack... we left the first one as special one if (previousNode == null) { logger.info("No previous node found") - serviceHub.database.transaction { + database.transaction { updateInfoDB(node) changePublisher.onNext(MapChange.Added(node)) } } else if (previousNode != node) { logger.info("Previous node was found as: $previousNode") - serviceHub.database.transaction { + database.transaction { updateInfoDB(node) changePublisher.onNext(MapChange.Modified(node, previousNode)) } @@ -203,8 +214,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) logger.info("Removing node with info: $node") synchronized(_changed) { registeredNodes.remove(node.legalIdentities.first().owningKey) - serviceHub.database.transaction { - removeInfoDB(node) + database.transaction { + removeInfoDB(session, node) changePublisher.onNext(MapChange.Removed(node)) } } @@ -238,10 +249,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override val allNodes: List - get () = serviceHub.database.transaction { - createSession { - getAllInfos(it).map { it.toNodeInfo() } - } + get() = database.transaction { + getAllInfos(session).map { it.toNodeInfo() } } private fun processRegistration(reg: NodeRegistration) { @@ -259,10 +268,6 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) // Changes related to NetworkMap redesign // TODO It will be properly merged into network map cache after services removal. - private inline fun createSession(block: (Session) -> T): T { - return DatabaseTransactionManager.current().session.let { block(it) } - } - private fun getAllInfos(session: Session): List { val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java) criteria.select(criteria.from(NodeInfoSchemaV1.PersistentNodeInfo::class.java)) @@ -272,31 +277,29 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) /** * Load NetworkMap data from the database if present. Node can start without having NetworkMapService configured. */ - private fun loadFromDB() { + private fun loadFromDB(session: Session) { logger.info("Loading network map from database...") - createSession { - val result = getAllInfos(it) - for (nodeInfo in result) { - try { - logger.info("Loaded node info: $nodeInfo") - val node = nodeInfo.toNodeInfo() - addNode(node) - _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. - } catch (e: Exception) { - logger.warn("Exception parsing network map from the database.", e) - } - } - if (loadDBSuccess) { - _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + val result = getAllInfos(session) + for (nodeInfo in result) { + try { + logger.info("Loaded node info: $nodeInfo") + val node = nodeInfo.toNodeInfo() + addNode(node) + _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. + } catch (e: Exception) { + logger.warn("Exception parsing network map from the database.", e) } } + if (loadDBSuccess) { + _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + } } private fun updateInfoDB(nodeInfo: NodeInfo) { // TODO Temporary workaround to force isolated transaction (otherwise it causes race conditions when processing // network map registration on network map node) - serviceHub.database.dataSource.connection.use { - val session = serviceHub.database.entityManagerFactory.withOptions().connection(it.apply { + database.dataSource.connection.use { + val session = database.entityManagerFactory.withOptions().connection(it.apply { transactionIsolation = 1 }).openSession() session.use { @@ -313,11 +316,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } } - private fun removeInfoDB(nodeInfo: NodeInfo) { - createSession { - val info = findByIdentityKey(it, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single() - it.remove(info) - } + private fun removeInfoDB(session: Session, nodeInfo: NodeInfo) { + val info = findByIdentityKey(session, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single() + session.remove(info) } private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { @@ -328,48 +329,40 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) return query.resultList } - private fun queryByIdentityKey(identityKey: PublicKey): List { - createSession { - val result = findByIdentityKey(it, identityKey) - return result.map { it.toNodeInfo() } - } + private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List { + val result = findByIdentityKey(session, identityKey) + return result.map { it.toNodeInfo() } } - private fun queryIdentityByLegalName(name: CordaX500Name): PartyAndCertificate? { - createSession { - val query = it.createQuery( - // We do the JOIN here to restrict results to those present in the network map - "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", - NodeInfoSchemaV1.DBPartyAndCertificate::class.java) - query.setParameter("name", name.toString()) - val candidates = query.resultList.map { it.toLegalIdentityAndCert() } - // The map is restricted to holding a single identity for any X.500 name, so firstOrNull() is correct here. - return candidates.firstOrNull() - } + private fun queryIdentityByLegalName(session: Session, name: CordaX500Name): PartyAndCertificate? { + val query = session.createQuery( + // We do the JOIN here to restrict results to those present in the network map + "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.DBPartyAndCertificate::class.java) + query.setParameter("name", name.toString()) + val candidates = query.resultList.map { it.toLegalIdentityAndCert() } + // The map is restricted to holding a single identity for any X.500 name, so firstOrNull() is correct here. + return candidates.firstOrNull() } - private fun queryByLegalName(name: CordaX500Name): List { - createSession { - val query = it.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", - NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("name", name.toString()) - val result = query.resultList - return result.map { it.toNodeInfo() } - } + private fun queryByLegalName(session: Session, name: CordaX500Name): List { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("name", name.toString()) + val result = query.resultList + return result.map { it.toNodeInfo() } } - private fun queryByAddress(hostAndPort: NetworkHostAndPort): NodeInfo? { - createSession { - val query = it.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", - NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("host", hostAndPort.host) - query.setParameter("port", hostAndPort.port) - val result = query.resultList - return if (result.isEmpty()) null - else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") - } + private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("host", hostAndPort.host) + query.setParameter("port", hostAndPort.port) + val result = query.resultList + return if (result.isEmpty()) null + else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") } /** Object Relational Mapping support. */ @@ -387,11 +380,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun clearNetworkMapCache() { - serviceHub.database.transaction { - createSession { - val result = getAllInfos(it) - for (nodeInfo in result) it.remove(nodeInfo) - } + database.transaction { + val result = getAllInfos(session) + for (nodeInfo in result) session.remove(nodeInfo) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index a82642adf7..b9b5f0bbdc 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -3,8 +3,8 @@ package net.corda.node.services.persistence import net.corda.core.serialization.SerializedBytes import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.currentDBSession import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -28,15 +28,14 @@ class DBCheckpointStorage : CheckpointStorage { ) override fun addCheckpoint(checkpoint: Checkpoint) { - val session = DatabaseTransactionManager.current().session - session.save(DBCheckpoint().apply { + currentDBSession().save(DBCheckpoint().apply { checkpointId = checkpoint.id.toString() this.checkpoint = checkpoint.serializedFiber.bytes }) } override fun removeCheckpoint(checkpoint: Checkpoint) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java) val root = delete.from(DBCheckpoint::class.java) @@ -45,7 +44,7 @@ class DBCheckpointStorage : CheckpointStorage { } override fun forEach(block: (Checkpoint) -> Boolean) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(DBCheckpoint::class.java) val root = criteriaQuery.from(DBCheckpoint::class.java) criteriaQuery.select(root) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt index bf6efb6c8c..8fb0023b3d 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt @@ -33,25 +33,13 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab private val sessionFactories = ConcurrentHashMap, SessionFactory>() private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?: "") - - init { - logger.info("Init HibernateConfiguration for schemas: ${schemaService.schemaOptions.keys}") - sessionFactoryForRegisteredSchemas() + val sessionFactoryForRegisteredSchemas = schemaService.schemaOptions.keys.let { + logger.info("Init HibernateConfiguration for schemas: $it") + sessionFactoryForSchemas(it) } - fun sessionFactoryForRegisteredSchemas(): SessionFactory { - return sessionFactoryForSchemas(*schemaService.schemaOptions.keys.toTypedArray()) - } - - fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { - return sessionFactoryForSchemas(schema) - } - - //vararg to set conversions left to preserve method signature for now - fun sessionFactoryForSchemas(vararg schemas: MappedSchema): SessionFactory { - val schemaSet: Set = schemas.toSet() - return sessionFactories.computeIfAbsent(schemaSet, { makeSessionFactoryForSchemas(schemaSet) }) - } + /** @param key must be immutable, not just read-only. */ + fun sessionFactoryForSchemas(key: Set) = sessionFactories.computeIfAbsent(key, { makeSessionFactoryForSchemas(key) }) private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { logger.info("Creating session factory for schemas: $schemas") diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index c864521d7b..c95fe2bf94 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -13,8 +13,8 @@ import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.currentDBSession import java.io.* import java.nio.file.Paths import java.util.jar.JarInputStream @@ -50,7 +50,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single private val attachmentCount = metrics.counter("Attachments") init { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java))) @@ -140,7 +140,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } override fun openAttachment(id: SecureHash): Attachment? { - val attachment = DatabaseTransactionManager.current().session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) attachment?.let { return AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad) } @@ -161,7 +161,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single checkIsAValidJAR(ByteArrayInputStream(bytes)) val id = SecureHash.SHA256(hs.hash().asBytes()) - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index 9afc740949..2babcc1d70 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -3,6 +3,7 @@ package net.corda.node.services.schema import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.internal.VisibleForTesting import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef @@ -17,14 +18,15 @@ import rx.Observable * A vault observer that extracts Object Relational Mappings for contract states that support it, and persists them with Hibernate. */ // TODO: Manage version evolution of the schemas via additional tooling. -class HibernateObserver(vaultUpdates: Observable>, val config: HibernateConfiguration) { - +class HibernateObserver private constructor(private val config: HibernateConfiguration) { companion object { - val logger = loggerFor() - } - - init { - vaultUpdates.subscribe { persist(it.produced) } + private val log = loggerFor() + @JvmStatic + fun install(vaultUpdates: Observable>, config: HibernateConfiguration): HibernateObserver { + val observer = HibernateObserver(config) + vaultUpdates.subscribe { observer.persist(it.produced) } + return observer + } } private fun persist(produced: Set>) { @@ -33,12 +35,13 @@ class HibernateObserver(vaultUpdates: Observable>, v private fun persistState(stateAndRef: StateAndRef) { val state = stateAndRef.state.data - logger.debug { "Asked to persist state ${stateAndRef.ref}" } + log.debug { "Asked to persist state ${stateAndRef.ref}" } config.schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) } } - fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { - val sessionFactory = config.sessionFactoryForSchema(schema) + @VisibleForTesting + internal fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { + val sessionFactory = config.sessionFactoryForSchemas(setOf(schema)) val session = sessionFactory.withOptions(). connection(DatabaseTransactionManager.current().connection). flushMode(FlushMode.MANUAL). diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 0599616644..8436f9459b 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -9,6 +9,7 @@ import net.corda.core.schemas.NodeInfoSchemaV1 import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.SchemaService import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.identity.PersistentIdentityService @@ -27,13 +28,13 @@ import net.corda.node.services.vault.VaultSchemaV1 /** * Most basic implementation of [SchemaService]. - * + * @param cordappLoader if not null, custom schemas will be extracted from its cordapps. * TODO: support loading schema options from node configuration. * TODO: support configuring what schemas are to be selected for persistence. * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(cordappLoader: CordappLoader?) : SchemaService, SingletonSerializeAsToken() { // Entities for compulsory services object NodeServices @@ -67,9 +68,12 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), Pair(NodeServicesV1, SchemaService.SchemaOptions())) - override var schemaOptions: Map = requiredSchemas.plus(customSchemas.map { mappedSchema -> - Pair(mappedSchema, SchemaService.SchemaOptions()) - }) + override val schemaOptions: Map = if (cordappLoader == null) { + requiredSchemas + } else { + val customSchemas = cordappLoader.cordapps.flatMap { it.customSchemas }.toSet() + requiredSchemas.plus(customSchemas.map { mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) }) + } // Currently returns all schemas supported by the state, with no filtering or enrichment. override fun selectSchemas(state: ContractState): Iterable { @@ -92,10 +96,4 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) return (state as QueryableState).generateMappedObject(schema) } - - override fun registerCustomSchemas(_customSchemas: Set) { - schemaOptions = schemaOptions.plus(_customSchemas.map { mappedSchema -> - Pair(mappedSchema, SchemaService.SchemaOptions()) - }) - } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt index cd56d786ad..bd29525072 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash +import java.time.Instant interface FlowIORequest { // This is used to identify where we suspended, in case of message mismatch errors and other things where we @@ -112,4 +113,9 @@ data class WaitForLedgerCommit(val hash: SecureHash, val fiber: FlowStateMachine override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = message is ErrorSessionEnd } +data class Sleep(val until: Instant, val fiber: FlowStateMachineImpl<*>) : FlowIORequest { + @Transient + override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() +} + class StackSnapshot : Throwable("This is a stack trace to help identify the source of the underlying problem") diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index c61ef66ead..a3df2461c8 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -12,13 +12,11 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.abbreviate +import net.corda.core.internal.* import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.staticField -import net.corda.core.internal.uncheckedCast +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent @@ -32,13 +30,15 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.nio.file.Paths import java.sql.SQLException +import java.time.Duration +import java.time.Instant import java.util.* import java.util.concurrent.TimeUnit class FlowPermissionException(message: String) : FlowException(message) class FlowStateMachineImpl(override val id: StateMachineRunId, - val logic: FlowLogic, + override val logic: FlowLogic, scheduler: FiberScheduler, override val flowInitiator: FlowInitiator, // Store the Party rather than the full cert path with PartyAndCertificate @@ -52,23 +52,6 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, * Return the current [FlowStateMachineImpl] or null if executing outside of one. */ fun currentStateMachine(): FlowStateMachineImpl<*>? = Strand.currentStrand() as? FlowStateMachineImpl<*> - - /** - * Provide a mechanism to sleep within a Strand without locking any transactional state - */ - // TODO: inlined due to an intermittent Quasar error (to be fully investigated) - @Suppress("NOTHING_TO_INLINE") - @Suspendable - inline fun sleep(millis: Long) { - if (currentStateMachine() != null) { - val db = DatabaseTransactionManager.dataSource - DatabaseTransactionManager.current().commit() - DatabaseTransactionManager.current().close() - Strand.sleep(millis) - DatabaseTransactionManager.dataSource = db - DatabaseTransactionManager.newTransaction() - } else Strand.sleep(millis) - } } // These fields shouldn't be serialised, so they are marked @Transient. @@ -85,12 +68,10 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, * is not necessary. */ override val logger: Logger = LoggerFactory.getLogger("net.corda.flow.$id") - - @Transient private var _resultFuture: OpenFuture? = openFuture() + @Transient private var resultFutureTransient: OpenFuture? = openFuture() + private val _resultFuture get() = resultFutureTransient ?: openFuture().also { resultFutureTransient = it } /** This future will complete when the call method returns. */ - override val resultFuture: CordaFuture - get() = _resultFuture ?: openFuture().also { _resultFuture = it } - + override val resultFuture: CordaFuture get() = _resultFuture // This state IS serialised, as we need it to know what the fiber is waiting for. internal val openSessions = HashMap, Party>, FlowSessionInternal>() internal var waitingForResponse: WaitingRequest? = null @@ -132,7 +113,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, recordDuration(startTime) // This is to prevent actionOnEnd being called twice if it throws an exception actionOnEnd(Try.Success(result), false) - _resultFuture?.set(result) + _resultFuture.set(result) logic.progressTracker?.currentStep = ProgressTracker.DONE logger.debug { "Flow finished with result ${result.toString().abbreviate(300)}" } } @@ -145,7 +126,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun processException(exception: Throwable, propagated: Boolean) { actionOnEnd(Try.Failure(exception), propagated) - _resultFuture?.setException(exception) + _resultFuture.setException(exception) logic.progressTracker?.endWithError(exception) } @@ -259,6 +240,13 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, throw IllegalStateException("We were resumed after waiting for $hash but it wasn't found in our local storage") } + // Provide a mechanism to sleep within a Strand without locking any transactional state. + // This checkpoints, since we cannot undo any database writes up to this point. + @Suspendable + override fun sleepUntil(until: Instant) { + suspend(Sleep(until, this)) + } + // TODO Dummy implementation of access to application specific permission controls and audit logging override fun checkFlowPermission(permissionName: String, extraAuditData: Map) { val permissionGranted = true // TODO define permission control service on ServiceHubInternal and actually check authorization. @@ -339,7 +327,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, is FlowSessionState.Initiated -> sessionState.peerSessionId else -> throw IllegalStateException("We've somehow held onto a non-initiated session: $session") } - return SessionData(peerSessionId, payload) + return SessionData(peerSessionId, payload.serialize(context = SerializationDefaults.P2P_CONTEXT)) } @Suspendable @@ -401,7 +389,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, session.state = FlowSessionState.Initiating(state.otherParty) session.retryable = retryable val (version, initiatingFlowClass) = session.flow.javaClass.flowVersionAndInitiatingClass - val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, firstPayload) + val payloadBytes = firstPayload?.serialize(context = SerializationDefaults.P2P_CONTEXT) + val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, payloadBytes) sendInternal(session, sessionInit) if (waitForConfirmation) { session.waitForConfirmation() @@ -494,6 +483,10 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } + if (exceptionDuringSuspend == null && ioRequest is Sleep) { + // Sleep on the fiber. This will not sleep if it's in the past. + Strand.sleep(Duration.between(Instant.now(), ioRequest.until).toNanos(), TimeUnit.NANOSECONDS) + } createTransaction() // TODO Now that we're throwing outside of the suspend the FlowLogic can catch it. We need Quasar to terminate // the fiber when exceptions occur inside a suspend. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt index fc103e6dca..c321d3768a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt @@ -5,7 +5,10 @@ import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.UntrustworthyData +import java.io.IOException @CordaSerializable interface SessionMessage @@ -25,7 +28,7 @@ data class SessionInit(val initiatorSessionId: Long, val initiatingFlowClass: String, val flowVersion: Int, val appName: String, - val firstPayload: Any?) : SessionMessage + val firstPayload: SerializedBytes?) : SessionMessage data class SessionConfirm(override val initiatorSessionId: Long, val initiatedSessionId: Long, @@ -34,7 +37,7 @@ data class SessionConfirm(override val initiatorSessionId: Long, data class SessionReject(override val initiatorSessionId: Long, val errorMessage: String) : SessionInitResponse -data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage +data class SessionData(override val recipientSessionId: Long, val payload: SerializedBytes) : ExistingSessionMessage data class NormalSessionEnd(override val recipientSessionId: Long) : SessionEnd @@ -42,8 +45,15 @@ data class ErrorSessionEnd(override val recipientSessionId: Long, val errorRespo data class ReceivedSessionMessage(val sender: Party, val message: M) -fun ReceivedSessionMessage.checkPayloadIs(type: Class): UntrustworthyData { - return type.castIfPossible(message.payload)?.let { UntrustworthyData(it) } ?: +fun ReceivedSessionMessage.checkPayloadIs(type: Class): UntrustworthyData { + val payloadData: T = try { + val serializer = SerializationDefaults.SERIALIZATION_FACTORY + serializer.deserialize(message.payload, type, SerializationDefaults.P2P_CONTEXT) + } catch (ex: Exception) { + throw IOException("Payload invalid", ex) + } + return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) } ?: throw UnexpectedFlowEndException("We were expecting a ${type.name} from $sender but we instead got a " + - "${message.payload.javaClass.name} (${message.payload})") + "${payloadData.javaClass.name} (${payloadData})") + } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 42fd677f65..74697821e9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -15,11 +15,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.castIfPossible -import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.* import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SerializationDefaults.CHECKPOINT_CONTEXT import net.corda.core.serialization.SerializationDefaults.SERIALIZATION_FACTORY @@ -290,7 +286,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } private fun onSessionMessage(message: ReceivedMessage) { - val sessionMessage = message.data.deserialize() + val sessionMessage = try { + message.data.deserialize() + } catch (ex: Exception) { + logger.error("Received corrupt SessionMessage data from ${message.peer}") + return + } val sender = serviceHub.networkMapCache.getPeerByLegalName(message.peer) if (sender != null) { when (sessionMessage) { @@ -382,12 +383,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, updateCheckpoint(fiber) session to initiatedFlowFactory } catch (e: SessionRejectException) { - // TODO: Handle this more gracefully - try { - logger.warn("${e.logMessage}: $sessionInit") - } catch (e: Throwable) { - logger.warn("Problematic session init message during logging", e) - } + logger.warn("${e.logMessage}: $sessionInit") sendSessionReject(e.rejectMessage) return } catch (e: Exception) { @@ -584,6 +580,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, when (ioRequest) { is SendRequest -> processSendRequest(ioRequest) is WaitForLedgerCommit -> processWaitForCommitRequest(ioRequest) + is Sleep -> processSleepRequest(ioRequest) } } @@ -621,6 +618,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } + private fun processSleepRequest(ioRequest: Sleep) { + // Resume the fiber now we have checkpointed, so we can sleep on the Fiber. + resumeFiber(ioRequest.fiber) + } + private fun sendSessionMessage(party: Party, message: SessionMessage, fiber: FlowStateMachineImpl<*>? = null, retryId: Long? = null) { val partyInfo = serviceHub.networkMapCache.getPartyInfo(party) ?: throw IllegalArgumentException("Don't know about party $party") diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 3b5236d97a..934cc2cb83 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -29,6 +29,7 @@ import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit +import net.corda.node.utilities.currentDBSession import net.corda.node.utilities.wrapWithDatabaseTransaction import org.hibernate.Session import rx.Observable @@ -38,6 +39,15 @@ import java.time.Clock import java.time.Instant import java.util.* import javax.persistence.Tuple +import javax.persistence.criteria.CriteriaBuilder +import javax.persistence.criteria.CriteriaUpdate +import javax.persistence.criteria.Predicate +import javax.persistence.criteria.Root + +private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.(CriteriaUpdate<*>) -> Any?) = createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java).let { update -> + update.from(VaultSchemaV1.VaultStates::class.java).run { configure(update) } + session.createQuery(update).executeUpdate() +} /** * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when @@ -49,7 +59,7 @@ import javax.persistence.Tuple * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ -class NodeVaultService(private val clock: Clock, private val keyManagementService: KeyManagementService, private val stateLoader: StateLoader, private val hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultServiceInternal { +class NodeVaultService(private val clock: Clock, private val keyManagementService: KeyManagementService, private val stateLoader: StateLoader, hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultServiceInternal { private companion object { val log = loggerFor() @@ -73,7 +83,7 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic val consumedStateRefs = update.consumed.map { it.ref } log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." } - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() producedStateRefsMap.forEach { stateAndRef -> val state = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, @@ -189,7 +199,7 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic private fun loadStates(refs: Collection): HashSet> { val states = HashSet>() if (refs.isNotEmpty()) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) @@ -223,11 +233,11 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { val txnNoteEntity = VaultSchemaV1.VaultTxnNote(txnId.toString(), noteText) - DatabaseTransactionManager.current().session.save(txnNoteEntity) + currentDBSession().save(txnNoteEntity) } override fun getTransactionNotes(txnId: SecureHash): Iterable { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultTxnNote::class.java) val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultTxnNote::class.java) @@ -241,37 +251,36 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet) { val softLockTimestamp = clock.instant() try { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.or(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name).isNull, - criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) - val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) - val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array) -> Any?) = criteriaBuilder.executeUpdate(session) { update -> + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + configure(update, arrayOf(stateRefsPredicate)) + } + + val updatedRows = execute { update, commonPredicates -> + val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.or(get(VaultSchemaV1.VaultStates::lockId.name).isNull, + criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.where(stateStatusPredication, lockIdPredicate, *commonPredicates) + } if (updatedRows > 0 && updatedRows == stateRefs.size) { - log.trace("Reserving soft lock states for $lockId: $stateRefs") + log.trace { "Reserving soft lock states for $lockId: $stateRefs" } FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true } else { // revert partial soft locks - val criteriaRevertUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStatesRevert = criteriaRevertUpdate.from(VaultSchemaV1.VaultStates::class.java) - val lockIdPredicateRevert = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - val lockUpdateTime = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - val persistentStateRefsRevert = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKeyRevert = vaultStatesRevert.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicateRevert = criteriaBuilder.and(compositeKeyRevert.`in`(persistentStateRefsRevert)) - criteriaRevertUpdate.set(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaRevertUpdate.where(lockUpdateTime, lockIdPredicateRevert, stateRefsPredicateRevert) - val revertUpdatedRows = session.createQuery(criteriaRevertUpdate).executeUpdate() + val revertUpdatedRows = execute { update, commonPredicates -> + val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + val lockUpdateTime = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.where(lockUpdateTime, lockIdPredicate, *commonPredicates) + } if (revertUpdatedRows > 0) { - log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId") + log.trace { "Reverting $revertUpdatedRows partially soft locked states for $lockId" } } throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available") } @@ -286,35 +295,32 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { val softLockTimestamp = clock.instant() - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder + fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array) -> Any?) = criteriaBuilder.executeUpdate(session) { update -> + val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + configure(update, arrayOf(stateStatusPredication, lockIdPredicate)) + } if (stateRefs == null) { - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate) - val update = session.createQuery(criteriaUpdate).executeUpdate() + val update = execute { update, commonPredicates -> + update.where(*commonPredicates) + } if (update > 0) { - log.trace("Releasing $update soft locked states for $lockId") + log.trace { "Releasing $update soft locked states for $lockId" } } } else { try { - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) - val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + val updatedRows = execute { update, commonPredicates -> + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + update.where(*commonPredicates, stateRefsPredicate) + } if (updatedRows > 0) { - log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs") + log.trace { "Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs" } } } catch (e: Exception) { log.error("""soft lock update error attempting to release states for $lockId and $stateRefs") @@ -371,9 +377,8 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic return keysToCheck.any { it in myKeys } } - private var sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - private var criteriaBuilder = sessionFactory.criteriaBuilder - + private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas + private val criteriaBuilder = sessionFactory.criteriaBuilder /** * Maintain a list of contract state interfaces to concrete types stored in the vault * for usage in generic queries of type queryBy or queryBy> @@ -400,11 +405,6 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic @Throws(VaultQueryException::class) override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { log.info("Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting") - - // refresh to include any schemas registered after initial VQ service initialisation - sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - criteriaBuilder = sessionFactory.criteriaBuilder - // calculate total results where a page specification has been defined var totalStates = -1L if (!paging.isDefault) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 0c8ba52a4e..909d35925c 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -3,13 +3,12 @@ package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes -import org.hibernate.annotations.Generated -import org.hibernate.annotations.GenerationTime import java.io.Serializable import java.time.Instant import java.util.* @@ -32,7 +31,7 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio class VaultStates( /** refers to the X500Name of the notary a state is attached to */ @Column(name = "notary_name") - var notary: AbstractParty, + var notary: Party, /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ @Column(name = "contract_state_class_name") diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt index 2d063a1468..8bf233589b 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt @@ -1,8 +1,8 @@ package net.corda.node.services.vault +import net.corda.core.contracts.FungibleAsset import net.corda.core.contracts.StateRef import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StateMachineRunId import net.corda.core.node.services.VaultService import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.loggerFor @@ -12,50 +12,50 @@ import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import java.util.* -class VaultSoftLockManager(val vault: VaultService, smm: StateMachineManager) { - - private companion object { - val log = loggerFor() - } - - init { - smm.changes.subscribe { change -> - if (change is StateMachineManager.Change.Removed && (FlowStateMachineImpl.currentStateMachine())?.hasSoftLockedStates == true) { - log.trace { "Remove flow name ${change.logic.javaClass} with id $change.id" } - unregisterSoftLocks(change.logic.runId, change.logic) +class VaultSoftLockManager private constructor(private val vault: VaultService) { + companion object { + private val log = loggerFor() + @JvmStatic + fun install(vault: VaultService, smm: StateMachineManager) { + val manager = VaultSoftLockManager(vault) + smm.changes.subscribe { change -> + if (change is StateMachineManager.Change.Removed) { + val logic = change.logic + // Don't run potentially expensive query if the flow didn't lock any states: + if ((logic.stateMachine as FlowStateMachineImpl<*>).hasSoftLockedStates) { + manager.unregisterSoftLocks(logic.runId.uuid, logic) + } + } } - } - - // Discussion - // - // The intent of the following approach is to support what might be a common pattern in a flow: - // 1. Create state - // 2. Do something with state - // without possibility of another flow intercepting the state between 1 and 2, - // since we cannot lock the state before it exists. e.g. Issue and then Move some Cash. - // - // The downside is we could have a long running flow that holds a lock for a long period of time. - // However, the lock can be programmatically released, like any other soft lock, - // should we want a long running flow that creates a visible state mid way through. - - vault.rawUpdates.subscribe { (_, produced, flowId) -> - flowId?.let { - if (produced.isNotEmpty()) { - registerSoftLocks(flowId, (produced.map { it.ref }).toNonEmptySet()) + // Discussion + // + // The intent of the following approach is to support what might be a common pattern in a flow: + // 1. Create state + // 2. Do something with state + // without possibility of another flow intercepting the state between 1 and 2, + // since we cannot lock the state before it exists. e.g. Issue and then Move some Cash. + // + // The downside is we could have a long running flow that holds a lock for a long period of time. + // However, the lock can be programmatically released, like any other soft lock, + // should we want a long running flow that creates a visible state mid way through. + vault.rawUpdates.subscribe { (_, produced, flowId) -> + if (flowId != null) { + val fungible = produced.filter { it.state.data is FungibleAsset<*> } + if (fungible.isNotEmpty()) { + manager.registerSoftLocks(flowId, fungible.map { it.ref }.toNonEmptySet()) + } } } } } private fun registerSoftLocks(flowId: UUID, stateRefs: NonEmptySet) { - log.trace("Reserving soft locks for flow id $flowId and states $stateRefs") + log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" } vault.softLockReserve(flowId, stateRefs) } - private fun unregisterSoftLocks(id: StateMachineRunId, logic: FlowLogic<*>) { - val flowClassName = logic.javaClass.simpleName - log.trace("Releasing soft locks for flow $flowClassName with flow id ${id.uuid}") - vault.softLockRelease(id.uuid) - + private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) { + log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" } + vault.softLockRelease(flowId) } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index a671fdb1dd..75c4f85a5c 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -7,23 +7,20 @@ import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.google.common.io.Closeables +import net.corda.client.jackson.JacksonSupport +import net.corda.client.jackson.StringToMethodCallParser +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.UniqueIdentifier import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic -import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.* import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.write -import net.corda.core.internal.* import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineUpdate import net.corda.core.utilities.loggerFor -import net.corda.client.jackson.JacksonSupport -import net.corda.client.jackson.StringToMethodCallParser -import net.corda.core.CordaException import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT @@ -200,7 +197,7 @@ object InteractiveShell { } private fun createOutputMapper(factory: JsonFactory): ObjectMapper { - return JacksonSupport.createNonRpcMapper(factory).apply({ + return JacksonSupport.createNonRpcMapper(factory).apply { // Register serializers for stateful objects from libraries that are special to the RPC system and don't // make sense to print out to the screen. For classes we own, annotations can be used instead. val rpcModule = SimpleModule() @@ -210,7 +207,7 @@ object InteractiveShell { disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) enable(SerializationFeature.INDENT_OUTPUT) - }) + } } // TODO: This should become the default renderer rather than something used specifically by commands. @@ -397,7 +394,7 @@ object InteractiveShell { } private fun printAndFollowRPCResponse(response: Any?, toStream: PrintWriter): CordaFuture? { - val printerFun = { obj: Any? -> yamlMapper.writeValueAsString(obj) } + val printerFun = yamlMapper::writeValueAsString toStream.println(printerFun(response)) toStream.flush() return maybeFollow(response, printerFun, toStream) @@ -443,13 +440,9 @@ object InteractiveShell { val observable: Observable<*> = when (response) { is Observable<*> -> response - is Pair<*, *> -> when { - response.first is Observable<*> -> response.first as Observable<*> - response.second is Observable<*> -> response.second as Observable<*> - else -> null - } - else -> null - } ?: return null + is DataFeed<*, *> -> response.updates + else -> return null + } val subscriber = PrintingSubscriber(printerFun, toStream) uncheckedCast(observable).subscribe(subscriber) @@ -500,8 +493,8 @@ object InteractiveShell { gen.writeString("") } else { val path = Paths.get(toPath) - path.write { value.copyTo(it) } - gen.writeString("") + value.copyTo(path) + gen.writeString("") } } finally { try { diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index 62508f0e2b..a90f77d171 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -40,10 +40,11 @@ class AppendOnlyPersistentMap( * Returns all key/value pairs from the underlying storage. */ fun allPersisted(): Sequence> { - val criteriaQuery = DatabaseTransactionManager.current().session.criteriaBuilder.createQuery(persistentEntityClass) + val session = currentDBSession() + val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) val root = criteriaQuery.from(persistentEntityClass) criteriaQuery.select(root) - val query = DatabaseTransactionManager.current().session.createQuery(criteriaQuery) + val query = session.createQuery(criteriaQuery) val result = query.resultList return result.map { x -> fromPersistentEntity(x) }.asSequence() } @@ -87,7 +88,7 @@ class AppendOnlyPersistentMap( */ operator fun set(key: K, value: V) = set(key, value, logWarning = false) { k, v -> - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + currentDBSession().save(toPersistentEntity(k, v)) null } @@ -98,9 +99,10 @@ class AppendOnlyPersistentMap( */ fun addWithDuplicatesAllowed(key: K, value: V, logWarning: Boolean = true): Boolean = set(key, value, logWarning) { k, v -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second @@ -114,7 +116,7 @@ class AppendOnlyPersistentMap( } private fun loadValue(key: K): V? { - val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -125,7 +127,7 @@ class AppendOnlyPersistentMap( * WARNING!! The method is not thread safe. */ fun clear() { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val deleteQuery = session.criteriaBuilder.createCriteriaDelete(persistentEntityClass) deleteQuery.from(persistentEntityClass) session.createQuery(deleteQuery).executeUpdate() diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index 0bf9885945..77055c6073 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -6,8 +6,6 @@ import net.corda.core.node.services.IdentityService import net.corda.node.services.api.SchemaService import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService -import org.hibernate.SessionFactory - import rx.Observable import rx.Subscriber import rx.subjects.UnicastSubject @@ -32,12 +30,7 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi HibernateConfiguration(schemaService, databaseProperties, createIdentityService) } } - - val entityManagerFactory: SessionFactory by lazy { - transaction { - hibernateConfig.sessionFactoryForRegisteredSchemas() - } - } + val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas companion object { fun connect(dataSource: HikariDataSource, schemaService: SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { @@ -47,10 +40,21 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi } } - fun createTransaction(): DatabaseTransaction { + /** + * Creates an instance of [DatabaseTransaction], with the given isolation level. + * @param isolationLevel isolation level for the transaction. If not specified the default (i.e. provided at the creation time) is used. + */ + fun createTransaction(isolationLevel: Int): DatabaseTransaction { // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. DatabaseTransactionManager.dataSource = this - return DatabaseTransactionManager.currentOrNew(transactionIsolationLevel) + return DatabaseTransactionManager.currentOrNew(isolationLevel) + } + + /** + * Creates an instance of [DatabaseTransaction], with the transaction isolation level specified at the creation time. + */ + fun createTransaction(): DatabaseTransaction { + return createTransaction(transactionIsolationLevel) } fun createSession(): Connection { @@ -60,9 +64,22 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.") } - fun transaction(statement: DatabaseTransaction.() -> T): T { + /** + * Executes given statement in the scope of transaction, with the given isolation level. + * @param isolationLevel isolation level for the transaction. + * @param statement to be executed in the scope of this transaction. + */ + fun transaction(isolationLevel: Int, statement: DatabaseTransaction.() -> T): T { DatabaseTransactionManager.dataSource = this - return transaction(transactionIsolationLevel, 3, statement) + return transaction(isolationLevel, 3, statement) + } + + /** + * Executes given statement in the scope of transaction with the transaction level specified at the creation time. + * @param statement to be executed in the scope of this transaction. + */ + fun transaction(statement: DatabaseTransaction.() -> T): T { + return transaction(transactionIsolationLevel, statement) } private fun transaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T { @@ -103,7 +120,7 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi } } -fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, schemaService: SchemaService = NodeSchemaService(), createIdentityService: () -> IdentityService): CordaPersistence { +fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, createIdentityService: () -> IdentityService, schemaService: SchemaService = NodeSchemaService(null)): CordaPersistence { val config = HikariConfig(dataSourceProperties) val dataSource = HikariDataSource(config) val persistence = CordaPersistence.connect(dataSource, schemaService, createIdentityService, databaseProperties ?: Properties()) diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt index 9016112f7b..6c810a7005 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt @@ -62,6 +62,7 @@ class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal() diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt index c24ce3c229..11ace024ef 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -28,7 +28,7 @@ class PersistentMap( removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass) ).apply { //preload to allow all() to take data only from the cache (cache is unbound) - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) @@ -38,7 +38,7 @@ class PersistentMap( override fun onRemoval(notification: RemovalNotification?) { when (notification?.cause) { RemovalCause.EXPLICIT -> { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val elem = session.find(persistentEntityClass, toPersistentEntityKey(notification.key)) if (elem != null) { session.remove(elem) @@ -101,7 +101,7 @@ class PersistentMap( set(key, value, logWarning = false, store = { k: K, v: V -> - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + currentDBSession().save(toPersistentEntity(k, v)) null }, replace = { _: K, _: V -> Unit } @@ -115,9 +115,10 @@ class PersistentMap( fun addWithDuplicatesAllowed(key: K, value: V) = set(key, value, store = { k, v -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second @@ -145,18 +146,19 @@ class PersistentMap( } private fun merge(key: K, value: V): V? { - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(key)) return if (existingEntry != null) { - DatabaseTransactionManager.current().session.merge(toPersistentEntity(key, value)) + session.merge(toPersistentEntity(key, value)) fromPersistentEntity(existingEntry).second } else { - DatabaseTransactionManager.current().session.save(toPersistentEntity(key, value)) + session.save(toPersistentEntity(key, value)) null } } private fun loadValue(key: K): V? { - val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -256,7 +258,7 @@ class PersistentMap( } fun load() { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) cache.getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt index dbb62d3bac..82c48bf82e 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.list import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap +import net.corda.node.internal.cordapp.CordappLoader import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess @@ -39,12 +40,12 @@ class CordappSmokeTest { @Test fun `FlowContent appName returns the filename of the CorDapp jar`() { - val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + val cordappsDir = (factory.baseDirectory(aliceConfig) / CordappLoader.CORDAPPS_DIR_NAME).createDirectories() // Find the jar file for the smoke tests of this module val selfCordapp = Paths.get("build", "libs").list { it.filter { "-smokeTests" in it.toString() }.toList().single() } - selfCordapp.copyToDirectory(pluginsDir) + selfCordapp.copyToDirectory(cordappsDir) factory.create(aliceConfig).use { alice -> alice.connect().use { connectionToAlice -> @@ -59,8 +60,8 @@ class CordappSmokeTest { } @Test - fun `empty plugins directory`() { - (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + fun `empty cordapps directory`() { + (factory.baseDirectory(aliceConfig) / CordappLoader.CORDAPPS_DIR_NAME).createDirectories() factory.create(aliceConfig).close() } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 97dab2aaa7..2ad7697d20 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -8,7 +8,6 @@ import net.corda.core.messaging.*; import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.*; -import net.corda.core.schemas.*; import net.corda.core.utilities.*; import net.corda.finance.contracts.*; import net.corda.finance.contracts.asset.*; @@ -43,15 +42,14 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Before public void setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset"); + List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName()); ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); - Set requiredSchemas = Collections.singleton(CashSchemaV1.INSTANCE); IdentityService identitySvc = makeTestIdentityService(); @SuppressWarnings("unchecked") - Pair databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc, Collections.EMPTY_LIST); - issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); + Pair databaseAndServices = makeTestDatabaseAndMockServices(keys, () -> identitySvc, cordappPackages); + issuerServices = new MockServices(cordappPackages, getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); vaultService = services.getVaultService(); @@ -60,7 +58,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @After public void cleanUp() throws IOException { database.close(); - unsetCordappPackages(); } /** diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index e03fd6972c..ec69a89fe5 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -62,12 +62,10 @@ class CordaRPCOpsImplTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) aliceNode = mockNet.createNode() notaryNode = mockNet.createNotaryNode(validating = false) - rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) + rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database, aliceNode.services) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -81,7 +79,6 @@ class CordaRPCOpsImplTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test @@ -100,7 +97,6 @@ class CordaRPCOpsImplTest { } // Tell the monitoring service node to issue some cash - val recipient = aliceNode.info.chooseIdentity() val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notary) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index f694eec66c..54117732cf 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -26,7 +26,7 @@ import kotlin.test.assertEquals class InteractiveShellTest { @Before fun setup() { - InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), ::makeTestIdentityService) } @After @@ -52,7 +52,7 @@ class InteractiveShellTest { private fun check(input: String, expected: String) { var output: DummyFSM? = null InteractiveShell.runFlowFromString({ DummyFSM(it as FlowA).apply { output = this } }, input, FlowA::class.java, om) - assertEquals(expected, output!!.logic.a, input) + assertEquals(expected, output!!.flowA.a, input) } @Test @@ -83,5 +83,5 @@ class InteractiveShellTest { @Test fun party() = check("party: \"${MEGA_CORP.name}\"", MEGA_CORP.name.toString()) - class DummyFSM(val logic: FlowA) : FlowStateMachine by mock() + class DummyFSM(val flowA: FlowA) : FlowStateMachine by mock() } diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt index 1a08a04e76..6135863388 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -15,8 +15,6 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.node.internal.cordapp.DummyRPCFlow import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -74,9 +72,7 @@ class TestCordaService2(val appServiceHub: AppServiceHub): SingletonSerializeAsT } @CordaService -class LegacyCordaService(val simpleServiceHub: ServiceHub): SingletonSerializeAsToken() { - -} +class LegacyCordaService(@Suppress("UNUSED_PARAMETER") simpleServiceHub: ServiceHub) : SingletonSerializeAsToken() class CordaServiceTest { lateinit var mockNet: MockNetwork @@ -85,8 +81,7 @@ class CordaServiceTest { @Before fun start() { - setCordappPackages("net.corda.node.internal","net.corda.finance") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.node.internal","net.corda.finance")) notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = true) nodeA = mockNet.createNode() mockNet.startNodes() @@ -95,7 +90,6 @@ class CordaServiceTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 877037c387..9f7833f928 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -1,5 +1,6 @@ package net.corda.node.internal.cordapp +import com.nhaarman.mockito_kotlin.mock import net.corda.core.node.services.AttachmentStorage import net.corda.testing.node.MockAttachmentStorage import org.junit.Assert @@ -22,9 +23,7 @@ class CordappProviderImplTests { @Test fun `isolated jar is loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) - - provider.start(attachmentStore) + val provider = CordappProviderImpl(loader, attachmentStore) val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first()) Assert.assertNotNull(maybeAttachmentId) @@ -34,17 +33,14 @@ class CordappProviderImplTests { @Test fun `empty jar is not loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(emptyJAR)) - val provider = CordappProviderImpl(loader) - - provider.start(attachmentStore) - + val provider = CordappProviderImpl(loader, attachmentStore) Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) } @Test fun `test that we find a cordapp class that is loaded into the store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) + val provider = CordappProviderImpl(loader, mock()) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.cordapps.first() @@ -57,10 +53,8 @@ class CordappProviderImplTests { @Test fun `test that we find an attachment for a cordapp contrat class`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) + val provider = CordappProviderImpl(loader, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" - - provider.start(attachmentStore) val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val actual = provider.getContractAttachmentID(className) diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 1d19ed5833..148ca7bbb4 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -31,7 +31,6 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.ownedBy import net.corda.finance.flows.TwoPartyTradeFlow.Buyer import net.corda.finance.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.StartedNode @@ -45,6 +44,7 @@ import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import net.corda.testing.node.pumpReceive import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -73,6 +73,7 @@ import kotlin.test.assertTrue @RunWith(Parameterized::class) class TwoPartyTradeFlowTests(val anonymous: Boolean) { companion object { + private val cordappPackages = listOf("net.corda.finance.contracts") @JvmStatic @Parameterized.Parameters fun data(): Collection { @@ -84,7 +85,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Before fun before() { - setCordappPackages("net.corda.finance.contracts") LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap") } @@ -92,7 +92,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { fun after() { mockNet.stopNodes() LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap") - unsetCordappPackages() } @Test @@ -100,16 +99,17 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - mockNet = MockNetwork(false, true) - - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(threadPerNode = true, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(ALICE.name) - val bobNode = mockNet.createPartyNode(BOB.name) - val bankNode = mockNet.createPartyNode(BOC.name) + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) + val alice = aliceNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() val notary = notaryNode.services.getDefaultNotary() - val cashIssuer = bankNode.info.chooseIdentity().ref(1) - val cpIssuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + val cashIssuer = bank.ref(1) + val cpIssuer = bank.ref(1, 2, 3) aliceNode.internals.disableDBCloseOnStop() bobNode.internals.disableDBCloseOnStop() @@ -120,8 +120,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, cpIssuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second + fillUpForSeller(false, cpIssuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), null, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -149,14 +149,15 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { - mockNet = MockNetwork(false, true) - - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(threadPerNode = true, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(ALICE.name) - val bobNode = mockNet.createPartyNode(BOB.name) - val bankNode = mockNet.createPartyNode(BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1) + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) + val alice = aliceNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1) val notary = aliceNode.services.getDefaultNotary() aliceNode.internals.disableDBCloseOnStop() @@ -168,8 +169,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), null, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -204,36 +205,30 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `shutdown and restore`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(ALICE.name) - var bobNode = mockNet.createPartyNode(BOB.name) - val bankNode = mockNet.createPartyNode(BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) - - aliceNode.database.transaction { - aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.chooseIdentityAndCert()) - } - bobNode.database.transaction { - bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.chooseIdentityAndCert()) - } + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + var bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) aliceNode.internals.disableDBCloseOnStop() bobNode.internals.disableDBCloseOnStop() val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle - val networkMapAddress = notaryNode.network.myAddress - mockNet.runNetwork() // Clear network map registration messages - val notary = aliceNode.services.getDefaultNotary() + + val notary = notaryNode.services.getDefaultNotary() + val alice = aliceNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary, issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), null, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) val aliceFuture = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult @@ -278,7 +273,7 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { return MockNetwork.MockNode(config, network, networkMapAddr, bobAddr.id, notaryIdentity, entropyRoot) } - }, BOB.name) + }, BOB_NAME) // Find the future representing the result of this state machine again. val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second @@ -331,16 +326,18 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `check dependencies of sale asset are resolved`() { - mockNet = MockNetwork(false) - + mockNet = MockNetwork(cordappPackages = cordappPackages) val notaryNode = mockNet.createNotaryNode() - val aliceNode = makeNodeWithTracking(ALICE.name) - val bobNode = makeNodeWithTracking(BOB.name) - val bankNode = makeNodeWithTracking(BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + val aliceNode = makeNodeWithTracking(ALICE_NAME) + val bobNode = makeNodeWithTracking(BOB_NAME) + val bankNode = makeNodeWithTracking(BOC_NAME) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() + val alice = aliceNode.info.singleIdentity() + val bob = bobNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) ledger(aliceNode.services, initialiseSerialization = false) { @@ -356,12 +353,12 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { } val bobsFakeCash = bobNode.database.transaction { - fillUpForBuyer(false, issuer, AnonymousParty(bobNode.info.chooseIdentity().owningKey), notary) + fillUpForBuyer(false, issuer, AnonymousParty(bob.owningKey), notary) }.second val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode) val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), attachmentID, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second } val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -436,17 +433,18 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `track works`() { - mockNet = MockNetwork(false) - + mockNet = MockNetwork(cordappPackages = cordappPackages) val notaryNode = mockNet.createNotaryNode() - val aliceNode = makeNodeWithTracking(ALICE.name) - val bobNode = makeNodeWithTracking(BOB.name) - val bankNode = makeNodeWithTracking(BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + val aliceNode = makeNodeWithTracking(ALICE_NAME) + val bobNode = makeNodeWithTracking(BOB_NAME) + val bankNode = makeNodeWithTracking(BOC_NAME) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() + val alice: Party = aliceNode.info.singleIdentity() + val bank: Party = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. @@ -467,8 +465,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode) val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), attachmentID, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -517,16 +515,16 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `dependency with error on buyer side`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { runWithError(true, false, "at least one cash input") } } @Test fun `dependency with error on seller side`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { runWithError(false, true, "Issuances have a time-window") } } @@ -593,22 +591,23 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { expectedMessageSubstring: String ) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(ALICE.name) - val bobNode = mockNet.createPartyNode(BOB.name) - val bankNode = mockNet.createPartyNode(BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() + val alice = aliceNode.info.singleIdentity() + val bob = bobNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { - fillUpForBuyer(bobError, issuer, bobNode.info.chooseIdentity(), - notary).second + fillUpForBuyer(bobError, issuer, bob, notary).second } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(aliceError, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` issuer, null, notary).second + fillUpForSeller(aliceError, issuer, alice,1200.DOLLARS `issued by` issuer, null, notary).second } insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bankNode) diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index a758655ff8..58aea5fac8 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -7,11 +7,13 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -35,22 +37,20 @@ class NotaryChangeTests { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) oldNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) clientNodeA = mockNet.createNode() clientNodeB = mockNet.createNode() - newNotaryNode = mockNet.createNotaryNode() + newNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name.copy(organisation = "Dummy Notary 2")) mockNet.runNetwork() // Clear network map registration messages oldNotaryNode.internals.ensureRegistered() - newNotaryParty = newNotaryNode.info.legalIdentities[1] - oldNotaryParty = oldNotaryNode.info.legalIdentities[1] + oldNotaryParty = newNotaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! + newNotaryParty = newNotaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME.copy(organisation = "Dummy Notary 2"))!! } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test @@ -213,10 +213,10 @@ fun issueMultiPartyState(nodeA: StartedNode<*>, nodeB: StartedNode<*>, notaryNod return stx.tx.outRef(0) } -fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) +fun issueInvalidState(services: ServiceHub, identity: Party, notary: Party): StateAndRef { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) tx.setTimeWindow(Instant.now(), 30.seconds) - val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(stx) + val stx = services.signInitialTransaction(tx) + services.recordTransactions(stx) return stx.tx.outRef(0) } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index ad05ba1ad2..2040fb562b 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -12,6 +12,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.days +import net.corda.node.internal.FlowStarterImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.services.api.VaultServiceInternal @@ -72,14 +73,13 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") initialiseTestSerialization() countDown = CountDownLatch(1) smmHasRemovedAllFlows = CountDownLatch(1) calls = 0 val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() - database = configureDatabase(dataSourceProps, databaseProperties, createIdentityService = ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, databaseProperties, ::makeTestIdentityService) val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) val kms = MockKeyManagementService(identityService, ALICE_KEY) @@ -98,11 +98,11 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { network = mockMessagingService), TestReference { override val vaultService: VaultServiceInternal = NodeVaultService(testClock, kms, stateLoader, database.hibernateConfig) override val testReference = this@NodeSchedulerServiceTest - override val cordappProvider: CordappProviderImpl = CordappProviderImpl(CordappLoader.createWithTestPackages()).start(attachments) + override val cordappProvider = CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts")), attachments) } smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) - scheduler = NodeSchedulerService(services, schedulerGatedExecutor, serverThread = smmExecutor) val mockSMM = StateMachineManager(services, DBCheckpointStorage(), smmExecutor, database) + scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM), services.stateLoader, schedulerGatedExecutor, serverThread = smmExecutor) mockSMM.changes.subscribe { change -> if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) { smmHasRemovedAllFlows.countDown() @@ -124,7 +124,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { smmExecutor.awaitTermination(60, TimeUnit.SECONDS) database.close() resetTestSerialization() - unsetCordappPackages() } class TestState(val flowLogicRef: FlowLogicRef, val instant: Instant, val myIdentity: Party) : LinearState, SchedulableState { diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 013eea5d74..be2fc12b96 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -91,8 +91,7 @@ class ScheduledFlowTests { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) val a = mockNet.createUnstartedNode() val b = mockNet.createUnstartedNode() @@ -107,7 +106,6 @@ class ScheduledFlowTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 6a1adee86a..b4504546bc 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -1,6 +1,7 @@ package net.corda.node.services.messaging import com.codahale.metrics.MetricRegistry +import com.nhaarman.mockito_kotlin.mock import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.generateKeyPair import net.corda.core.internal.concurrent.doneFuture @@ -12,10 +13,10 @@ import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.testing.MockServiceHubInternal import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -57,7 +58,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { var messagingClient: NodeMessagingClient? = null var messagingServer: ArtemisMessagingServer? = null - lateinit var networkMapCache: PersistentNetworkMapCache + lateinit var networkMapCache: NetworkMapCacheImpl val rpcOps = object : RPCOps { override val protocolVersion: Int get() = throw UnsupportedOperationException() @@ -71,9 +72,9 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { baseDirectory = baseDirectory, myLegalName = ALICE.name) LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) networkMapRegistrationFuture = doneFuture(Unit) - networkMapCache = PersistentNetworkMapCache(serviceHub = object : MockServiceHubInternal(database, config) {}) + networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, config), mock()) } @After diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index 10aa2c62ac..8d4f2d65a4 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -83,7 +83,7 @@ class NetworkMapCacheTest { val aliceNode = mockNet.createPartyNode(ALICE.name) val notaryLegalIdentity = notaryNode.info.chooseIdentity() val alice = aliceNode.info.chooseIdentity() - val notaryCache = notaryNode.services.networkMapCache as PersistentNetworkMapCache + val notaryCache = notaryNode.services.networkMapCache mockNet.runNetwork() notaryNode.database.transaction { assertThat(notaryCache.getNodeByLegalIdentity(alice) != null) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index aaa8e49db1..f71f266388 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -33,7 +33,7 @@ class DBCheckpointStorageTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) newCheckpointStorage() } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 3a3148c0db..160ea87bf3 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -38,14 +38,14 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), NodeSchemaService(), ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), ::makeTestIdentityService) database.transaction { services = object : MockServices(BOB_KEY) { override val vaultService: VaultServiceInternal get() { val vaultService = NodeVaultService(clock, keyManagementService, stateLoader, database.hibernateConfig) - hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) + hibernatePersister = HibernateObserver.install(vaultService.rawUpdates, database.hibernateConfig) return vaultService } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index e554ea59be..0e1cdc1047 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -9,6 +9,7 @@ import net.corda.core.utilities.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize @@ -25,7 +26,6 @@ import net.corda.finance.schemas.SampleCashSchemaV2 import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.finance.utils.sumCash import net.corda.node.services.schema.HibernateObserver -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -73,14 +73,14 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) + val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) val dataSourceProps = makeTestDataSourceProperties() val defaultDatabaseProperties = makeTestDatabaseProperties() - database = configureDatabase(dataSourceProps, defaultDatabaseProperties, NodeSchemaService(), ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, defaultDatabaseProperties, ::makeTestIdentityService) database.transaction { hibernateConfig = database.hibernateConfig - services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { + services = object : MockServices(cordappPackages, BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { override val vaultService = makeVaultService(database.hibernateConfig) override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { @@ -95,17 +95,16 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { hibernatePersister = services.hibernatePersister } setUpDb() - - val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) - sessionFactory = hibernateConfig.sessionFactoryForSchemas(*customSchemas.toTypedArray()) + sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) entityManager = sessionFactory.createEntityManager() criteriaBuilder = sessionFactory.criteriaBuilder } + private fun sessionFactoryForSchemas(vararg schemas: MappedSchema) = hibernateConfig.sessionFactoryForSchemas(schemas.toSet()) + @After fun cleanUp() { database.close() - unsetCordappPackages() } private fun setUpDb() { @@ -537,8 +536,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestLinearStates(2) } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -569,8 +567,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestLinearStates(2) } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -636,8 +633,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) } } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -765,8 +761,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -818,8 +813,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 46e954ecde..a8a352cbf1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -11,7 +11,6 @@ import net.corda.core.internal.write import net.corda.core.internal.writeLines import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase import net.corda.testing.LogHelper import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -41,7 +40,7 @@ class NodeAttachmentStorageTest { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProperties = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), ::makeTestIdentityService) fs = Jimfs.newFileSystem(Configuration.unix()) } @@ -98,19 +97,18 @@ class NodeAttachmentStorageTest { @Test fun `corrupt entry throws exception`() { val testJar = makeTestJar() - val id = - database.transaction { - val storage = NodeAttachmentService(MetricRegistry()) - val id = testJar.read { storage.importAttachment(it) } + val id = database.transaction { + val storage = NodeAttachmentService(MetricRegistry()) + val id = testJar.read { storage.importAttachment(it) } - // Corrupt the file in the store. - val bytes = testJar.readAll() - val corruptBytes = "arggghhhh".toByteArray() - System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) - val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) - DatabaseTransactionManager.current().session.merge(corruptAttachment) - id - } + // Corrupt the file in the store. + val bytes = testJar.readAll() + val corruptBytes = "arggghhhh".toByteArray() + System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) + val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) + session.merge(corruptAttachment) + id + } database.transaction { val storage = NodeAttachmentService(MetricRegistry()) val e = assertFailsWith { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 44fc3cb47b..30d877ab93 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -57,8 +57,6 @@ class HibernateObserverTests { val testSchema = TestSchema val rawUpdatesPublisher = PublishSubject.create>() val schemaService = object : SchemaService { - override fun registerCustomSchemas(customSchemas: Set) {} - override val schemaOptions: Map = emptyMap() override fun selectSchemas(state: ContractState): Iterable = setOf(testSchema) @@ -70,9 +68,8 @@ class HibernateObserverTests { return parent } } - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), schemaService, createIdentityService = ::makeTestIdentityService) - @Suppress("UNUSED_VARIABLE") - val observer = HibernateObserver(rawUpdatesPublisher, database.hibernateConfig) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService, schemaService) + HibernateObserver.install(rawUpdatesPublisher, database.hibernateConfig) database.transaction { rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))) val parentRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index 73bb252af5..be168e8b98 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -3,11 +3,13 @@ package net.corda.node.services.schema import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.packageName import net.corda.core.messaging.startFlow import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.ServiceHubInternal +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.MockNetwork import net.corda.testing.schemas.DummyLinearStateSchemaV1 @@ -15,6 +17,7 @@ import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType import org.junit.Test import javax.persistence.* +import kotlin.test.assertEquals import kotlin.test.assertTrue class NodeSchemaServiceTest { @@ -23,11 +26,9 @@ class NodeSchemaServiceTest { */ @Test fun `registering custom schemas for testing with MockNode`() { - val mockNet = MockNetwork() + val mockNet = MockNetwork(cordappPackages = listOf(DummyLinearStateSchemaV1::class.packageName)) val mockNode = mockNet.createNode() mockNet.runNetwork() - - mockNode.internals.registerCustomSchemas(setOf(DummyLinearStateSchemaV1)) val schemaService = mockNode.services.schemaService assertTrue(schemaService.schemaOptions.containsKey(DummyLinearStateSchemaV1)) @@ -50,6 +51,16 @@ class NodeSchemaServiceTest { } } + @Test + fun `custom schemas are loaded eagerly`() { + val expected = setOf("PARENTS", "CHILDREN") + assertEquals>(expected, driver { + (startNode(startInSameProcess = true).getOrThrow() as NodeHandle.InProcess).node.database.transaction { + session.createNativeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").list() + } + }.toMutableSet().apply { retainAll(expected) }) + } + @StartableByRPC class MappedSchemasFlow : FlowLogic>() { @Suspendable diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 00b66b30b9..e65592f70b 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -58,58 +58,61 @@ class FlowFrameworkTests { private lateinit var mockNet: MockNetwork private val receivedSessionMessages = ArrayList() - private lateinit var node1: StartedNode - private lateinit var node2: StartedNode - private lateinit var notary: StartedNode + private lateinit var aliceNode: StartedNode + private lateinit var bobNode: StartedNode private lateinit var notaryIdentity: Party + private lateinit var alice: Party + private lateinit var bob: Party @Before fun start() { - setCordappPackages("net.corda.finance.contracts", "net.corda.testing.contracts") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - node1 = mockNet.createNode() - node2 = mockNet.createNode() + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts")) + aliceNode = mockNet.createNode(legalName = ALICE_NAME) + bobNode = mockNet.createNode(legalName = BOB_NAME) mockNet.runNetwork() - node1.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() // We intentionally create our own notary and ignore the one provided by the network // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. - notary = mockNet.createNotaryNode() + val notary = mockNet.createNotaryNode() receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } mockNet.runNetwork() - notaryIdentity = notary.services.myInfo.legalIdentities[1] + + // Extract identities + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + notaryIdentity = notary.services.getDefaultNotary() } @After fun cleanUp() { mockNet.stopNodes() receivedSessionMessages.clear() - unsetCordappPackages() } @Test fun `newly added flow is preserved on restart`() { - node1.services.startFlow(NoOpFlow(nonTerminating = true)) - node1.internals.acceptableLiveFiberCountOnStop = 1 - val restoredFlow = node1.restartAndGetRestoredFlow() + aliceNode.services.startFlow(NoOpFlow(nonTerminating = true)) + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 + val restoredFlow = aliceNode.restartAndGetRestoredFlow() assertThat(restoredFlow.flowStarted).isTrue() } @Test fun `flow can lazily use the serviceHub in its constructor`() { val flow = LazyServiceHubAccessFlow() - node1.services.startFlow(flow) + aliceNode.services.startFlow(flow) assertThat(flow.lazyTime).isNotNull() } @Test fun `exception while fiber suspended`() { - node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } - val flow = ReceiveFlow(node2.info.chooseIdentity()) - val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl + bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + val flow = ReceiveFlow(bob) + val fiber = aliceNode.services.startFlow(flow) as FlowStateMachineImpl // Before the flow runs change the suspend action to throw an exception val exceptionDuringSuspend = Exception("Thrown during suspend") fiber.actionOnSuspend = { @@ -119,31 +122,31 @@ class FlowFrameworkTests { assertThatThrownBy { fiber.resultFuture.getOrThrow() }.isSameAs(exceptionDuringSuspend) - assertThat(node1.smm.allStateMachines).isEmpty() + assertThat(aliceNode.smm.allStateMachines).isEmpty() // Make sure the fiber does actually terminate assertThat(fiber.isTerminated).isTrue() } @Test fun `flow restarted just after receiving payload`() { - node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - node1.services.startFlow(SendFlow("Hello", node2.info.chooseIdentity())) + bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + aliceNode.services.startFlow(SendFlow("Hello", bob)) // We push through just enough messages to get only the payload sent - node2.pumpReceive() - node2.internals.disableDBCloseOnStop() - node2.internals.acceptableLiveFiberCountOnStop = 1 - node2.dispose() + bobNode.pumpReceive() + bobNode.internals.disableDBCloseOnStop() + bobNode.internals.acceptableLiveFiberCountOnStop = 1 + bobNode.dispose() mockNet.runNetwork() - val restoredFlow = node2.restartAndGetRestoredFlow() + val restoredFlow = bobNode.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @Test fun `flow added before network map does run after init`() { - val node3 = mockNet.createNode() //create vanilla node + val charlieNode = mockNet.createNode() //create vanilla node val flow = NoOpFlow() - node3.services.startFlow(flow) + charlieNode.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow assertEquals(true, flow.flowStarted) // Now we should have run the flow @@ -151,40 +154,40 @@ class FlowFrameworkTests { @Test fun `flow added before network map will be init checkpointed`() { - var node3 = mockNet.createNode() //create vanilla node + var charlieNode = mockNet.createNode() //create vanilla node val flow = NoOpFlow() - node3.services.startFlow(flow) + charlieNode.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet - node3.internals.disableDBCloseOnStop() - node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. - node3.dispose() + charlieNode.internals.disableDBCloseOnStop() + charlieNode.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. + charlieNode.dispose() - node3 = mockNet.createNode(node3.internals.id) - val restoredFlow = node3.getSingleFlow().first + charlieNode = mockNet.createNode(charlieNode.internals.id) + val restoredFlow = charlieNode.getSingleFlow().first assertEquals(false, restoredFlow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow - node3.smm.executor.flush() + charlieNode.smm.executor.flush() assertEquals(true, restoredFlow.flowStarted) // Now we should have run the flow and hopefully cleared the init checkpoint - node3.internals.disableDBCloseOnStop() - node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. - node3.dispose() + charlieNode.internals.disableDBCloseOnStop() + charlieNode.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. + charlieNode.dispose() // Now it is completed the flow should leave no Checkpoint. - node3 = mockNet.createNode(node3.internals.id) + charlieNode = mockNet.createNode(charlieNode.internals.id) mockNet.runNetwork() // Allow network map messages to flow - node3.smm.executor.flush() - assertTrue(node3.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) + charlieNode.smm.executor.flush() + assertTrue(charlieNode.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) } @Test fun `flow loaded from checkpoint will respond to messages from before start`() { - node1.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } - node2.services.startFlow(ReceiveFlow(node1.info.chooseIdentity()).nonTerminating()) // Prepare checkpointed receive flow + aliceNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow // Make sure the add() has finished initial processing. - node2.smm.executor.flush() - node2.internals.disableDBCloseOnStop() - node2.dispose() // kill receiver - val restoredFlow = node2.restartAndGetRestoredFlow() + bobNode.smm.executor.flush() + bobNode.internals.disableDBCloseOnStop() + bobNode.dispose() // kill receiver + val restoredFlow = bobNode.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @@ -196,26 +199,27 @@ class FlowFrameworkTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val node3 = mockNet.createNode() - val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) + val secondFlow = charlieNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() // Kick off first send and receive - node2.services.startFlow(PingPongFlow(node3.info.chooseIdentity(), payload)) - node2.database.transaction { - assertEquals(1, node2.checkpointStorage.checkpoints().size) + bobNode.services.startFlow(PingPongFlow(charlie, payload)) + bobNode.database.transaction { + assertEquals(1, bobNode.checkpointStorage.checkpoints().size) } // Make sure the add() has finished initial processing. - node2.smm.executor.flush() - node2.internals.disableDBCloseOnStop() + bobNode.smm.executor.flush() + bobNode.internals.disableDBCloseOnStop() // Restart node and thus reload the checkpoint and resend the message with same UUID - node2.dispose() - node2.database.transaction { - assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint - node2.services.networkMapCache.clearNetworkMapCache() + bobNode.dispose() + bobNode.database.transaction { + assertEquals(1, bobNode.checkpointStorage.checkpoints().size) // confirm checkpoint + bobNode.services.networkMapCache.clearNetworkMapCache() } - val node2b = mockNet.createNode(node2.internals.id) - node2.internals.manuallyCloseDB() + val node2b = mockNet.createNode(bobNode.internals.id) + bobNode.internals.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. mockNet.runNetwork() @@ -230,8 +234,8 @@ class FlowFrameworkTests { node2b.database.transaction { assertEquals(0, node2b.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") } - node3.database.transaction { - assertEquals(0, node3.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") + charlieNode.database.transaction { + assertEquals(0, charlieNode.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") } assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3") assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3") @@ -241,87 +245,89 @@ class FlowFrameworkTests { @Test fun `sending to multiple parties`() { - val node3 = mockNet.createNode() + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() - node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - node3.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + val charlie = charlieNode.info.singleIdentity() + bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + charlieNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } val payload = "Hello World" - node1.services.startFlow(SendFlow(payload, node2.info.chooseIdentity(), node3.info.chooseIdentity())) + aliceNode.services.startFlow(SendFlow(payload, bob, charlie)) mockNet.runNetwork() - val node2Flow = node2.getSingleFlow().first - val node3Flow = node3.getSingleFlow().first - assertThat(node2Flow.receivedPayloads[0]).isEqualTo(payload) - assertThat(node3Flow.receivedPayloads[0]).isEqualTo(payload) + val bobFlow = bobNode.getSingleFlow().first + val charlieFlow = charlieNode.getSingleFlow().first + assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload) + assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload) - assertSessionTransfers(node2, - node1 sent sessionInit(SendFlow::class, payload = payload) to node2, - node2 sent sessionConfirm() to node1, - node1 sent normalEnd to node2 + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + aliceNode sent normalEnd to bobNode //There's no session end from the other flows as they're manually suspended ) - assertSessionTransfers(node3, - node1 sent sessionInit(SendFlow::class, payload = payload) to node3, - node3 sent sessionConfirm() to node1, - node1 sent normalEnd to node3 + assertSessionTransfers(charlieNode, + aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode, + charlieNode sent sessionConfirm() to aliceNode, + aliceNode sent normalEnd to charlieNode //There's no session end from the other flows as they're manually suspended ) - node2.internals.acceptableLiveFiberCountOnStop = 1 - node3.internals.acceptableLiveFiberCountOnStop = 1 + bobNode.internals.acceptableLiveFiberCountOnStop = 1 + charlieNode.internals.acceptableLiveFiberCountOnStop = 1 } @Test fun `receiving from multiple parties`() { - val node3 = mockNet.createNode() + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() - val node2Payload = "Test 1" - val node3Payload = "Test 2" - node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(node2Payload, it) } - node3.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(node3Payload, it) } - val multiReceiveFlow = ReceiveFlow(node2.info.chooseIdentity(), node3.info.chooseIdentity()).nonTerminating() - node1.services.startFlow(multiReceiveFlow) - node1.internals.acceptableLiveFiberCountOnStop = 1 + val charlie = charlieNode.info.singleIdentity() + val bobPayload = "Test 1" + val charliePayload = "Test 2" + bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } + charlieNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } + val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating() + aliceNode.services.startFlow(multiReceiveFlow) + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 mockNet.runNetwork() - assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(node2Payload) - assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(node3Payload) + assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload) + assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload) - assertSessionTransfers(node2, - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent sessionData(node2Payload) to node1, - node2 sent normalEnd to node1 + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData(bobPayload) to aliceNode, + bobNode sent normalEnd to aliceNode ) - assertSessionTransfers(node3, - node1 sent sessionInit(ReceiveFlow::class) to node3, - node3 sent sessionConfirm() to node1, - node3 sent sessionData(node3Payload) to node1, - node3 sent normalEnd to node1 + assertSessionTransfers(charlieNode, + aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode, + charlieNode sent sessionConfirm() to aliceNode, + charlieNode sent sessionData(charliePayload) to aliceNode, + charlieNode sent normalEnd to aliceNode ) } @Test fun `both sides do a send as their first IO request`() { - node2.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } - node1.services.startFlow(PingPongFlow(node2.info.chooseIdentity(), 10L)) + bobNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } + aliceNode.services.startFlow(PingPongFlow(bob, 10L)) mockNet.runNetwork() assertSessionTransfers( - node1 sent sessionInit(PingPongFlow::class, payload = 10L) to node2, - node2 sent sessionConfirm() to node1, - node2 sent sessionData(20L) to node1, - node1 sent sessionData(11L) to node2, - node2 sent sessionData(21L) to node1, - node1 sent normalEnd to node2, - node2 sent normalEnd to node1 + aliceNode sent sessionInit(PingPongFlow::class, payload = 10L) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData(20L) to aliceNode, + aliceNode sent sessionData(11L) to bobNode, + bobNode sent sessionData(21L) to aliceNode, + aliceNode sent normalEnd to bobNode, + bobNode sent normalEnd to aliceNode ) } @Test fun `other side ends before doing expected send`() { - node2.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } - val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } + val resultFuture = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { resultFuture.getOrThrow() @@ -330,11 +336,11 @@ class FlowFrameworkTests { @Test fun `receiving unexpected session end before entering sendAndReceive`() { - node2.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } + bobNode.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } val sessionEndReceived = Semaphore(0) receivedSessionMessagesObservable().filter { it.message is SessionEnd }.subscribe { sessionEndReceived.release() } - val resultFuture = node1.services.startFlow( - WaitForOtherSideEndBeforeSendAndReceive(node2.info.chooseIdentity(), sessionEndReceived)).resultFuture + val resultFuture = aliceNode.services.startFlow( + WaitForOtherSideEndBeforeSendAndReceive(bob, sessionEndReceived)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { resultFuture.getOrThrow() @@ -357,14 +363,14 @@ class FlowFrameworkTests { @Test fun `non-FlowException thrown on other side`() { - val erroringFlowFuture = node2.registerFlowFactory(ReceiveFlow::class) { + val erroringFlowFuture = bobNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } } val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps } - val receiveFlow = ReceiveFlow(node2.info.chooseIdentity()) + val receiveFlow = ReceiveFlow(bob) val receiveFlowSteps = receiveFlow.progressSteps - val receiveFlowResult = node1.services.startFlow(receiveFlow).resultFuture + val receiveFlowResult = aliceNode.services.startFlow(receiveFlow).resultFuture mockNet.runNetwork() @@ -383,20 +389,20 @@ class FlowFrameworkTests { ) assertSessionTransfers( - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent erroredEnd() to node1 + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent erroredEnd() to aliceNode ) } @Test fun `FlowException thrown on other side`() { - val erroringFlow = node2.registerFlowFactory(ReceiveFlow::class) { + val erroringFlow = bobNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps } - val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())) as FlowStateMachineImpl + val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) as FlowStateMachineImpl mockNet.runNetwork() @@ -404,8 +410,8 @@ class FlowFrameworkTests { .isThrownBy { receivingFiber.resultFuture.getOrThrow() } .withMessage("Nothing useful") .withStackTraceContaining(ReceiveFlow::class.java.name) // Make sure the stack trace is that of the receiving flow - node2.database.transaction { - assertThat(node2.checkpointStorage.checkpoints()).isEmpty() + bobNode.database.transaction { + assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty() } assertThat(receivingFiber.isTerminated).isTrue() @@ -416,9 +422,9 @@ class FlowFrameworkTests { ) assertSessionTransfers( - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent erroredEnd(erroringFlow.get().exceptionThrown) to node1 + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent erroredEnd(erroringFlow.get().exceptionThrown) to aliceNode ) // Make sure the original stack trace isn't sent down the wire assertThat((receivedSessionMessages.last().message as ErrorSessionEnd).errorResponse!!.stackTrace).isEmpty() @@ -426,12 +432,13 @@ class FlowFrameworkTests { @Test fun `FlowException propagated in invocation chain`() { - val node3 = mockNet.createNode() + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() - node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } - node2.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(node3.info.chooseIdentity()) } - val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())) + charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } + bobNode.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } + val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) mockNet.runNetwork() assertThatExceptionOfType(MyFlowException::class.java) .isThrownBy { receivingFiber.resultFuture.getOrThrow() } @@ -440,34 +447,35 @@ class FlowFrameworkTests { @Test fun `FlowException thrown and there is a 3rd unrelated party flow`() { - val node3 = mockNet.createNode() + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() - // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move - // onto node 3 which will throw the exception - val node2Fiber = node2 + // Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move + // onto Charlie which will throw the exception + val node2Fiber = bobNode .registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } .map { it.stateMachine } - node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } + charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } - val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity(), node3.info.chooseIdentity())) as FlowStateMachineImpl + val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl mockNet.runNetwork() - // Node 1 will terminate with the error it received from node 3 but it won't propagate that to node 2 (as it's + // Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's // not relevant to it) but it will end its session with it assertThatExceptionOfType(MyFlowException::class.java).isThrownBy { - node1Fiber.resultFuture.getOrThrow() + aliceFiber.resultFuture.getOrThrow() } - val node2ResultFuture = node2Fiber.getOrThrow().resultFuture + val bobResultFuture = node2Fiber.getOrThrow().resultFuture assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { - node2ResultFuture.getOrThrow() + bobResultFuture.getOrThrow() } - assertSessionTransfers(node2, - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent sessionData("Hello") to node1, - node1 sent erroredEnd() to node2 + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData("Hello") to aliceNode, + aliceNode sent erroredEnd() to bobNode ) } @@ -501,16 +509,16 @@ class FlowFrameworkTests { } } - node2.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } - val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } + val resultFuture = aliceNode.services.startFlow(RetryOnExceptionFlow(bob)).resultFuture mockNet.runNetwork() assertThat(resultFuture.getOrThrow()).isEqualTo("Hello") } @Test fun `serialisation issue in counterparty`() { - node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } - val result = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } + val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { result.getOrThrow() @@ -519,10 +527,10 @@ class FlowFrameworkTests { @Test fun `FlowException has non-serialisable object`() { - node2.registerFlowFactory(ReceiveFlow::class) { + bobNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) } } - val result = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture + val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(FlowException::class.java).isThrownBy { result.getOrThrow() @@ -533,13 +541,13 @@ class FlowFrameworkTests { fun `wait for transaction`() { val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) - .addCommand(dummyCommand(node1.info.chooseIdentity().owningKey)) - val stx = node1.services.signInitialTransaction(ptx) + .addCommand(dummyCommand(alice.owningKey)) + val stx = aliceNode.services.signInitialTransaction(ptx) - val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) { + val committerFiber = aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) }.map { it.stateMachine } - val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.chooseIdentity())).resultFuture + val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture mockNet.runNetwork() assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow()) } @@ -549,12 +557,12 @@ class FlowFrameworkTests { val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) .addCommand(dummyCommand()) - val stx = node1.services.signInitialTransaction(ptx) + val stx = aliceNode.services.signInitialTransaction(ptx) - node1.registerFlowFactory(WaitingFlows.Waiter::class) { + aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) { throw Exception("Error") } } - val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.chooseIdentity())).resultFuture + val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { waiter.getOrThrow() @@ -565,13 +573,13 @@ class FlowFrameworkTests { fun `verify vault query service is tokenizable by force checkpointing within a flow`() { val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) - .addCommand(dummyCommand(node1.info.chooseIdentity().owningKey)) - val stx = node1.services.signInitialTransaction(ptx) + .addCommand(dummyCommand(alice.owningKey)) + val stx = aliceNode.services.signInitialTransaction(ptx) - node1.registerFlowFactory(VaultQueryFlow::class) { + aliceNode.registerFlowFactory(VaultQueryFlow::class) { WaitingFlows.Committer(it) } - val result = node2.services.startFlow(VaultQueryFlow(stx, node1.info.chooseIdentity())).resultFuture + val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEmpty() @@ -579,15 +587,15 @@ class FlowFrameworkTests { @Test fun `customised client flow`() { - val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } - node1.services.startFlow(CustomSendFlow("Hello", node2.info.chooseIdentity())).resultFuture + val receiveFlowFuture = bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } + aliceNode.services.startFlow(CustomSendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello") } @Test fun `customised client flow which has annotated @InitiatingFlow again`() { - val result = node1.services.startFlow(IncorrectCustomSendFlow("Hello", node2.info.chooseIdentity())).resultFuture + val result = aliceNode.services.startFlow(IncorrectCustomSendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { result.getOrThrow() @@ -596,12 +604,12 @@ class FlowFrameworkTests { @Test fun `upgraded initiating flow`() { - node2.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } - val result = node1.services.startFlow(UpgradedFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } + val result = aliceNode.services.startFlow(UpgradedFlow(bob)).resultFuture mockNet.runNetwork() assertThat(receivedSessionMessages).startsWith( - node1 sent sessionInit(UpgradedFlow::class, flowVersion = 2) to node2, - node2 sent sessionConfirm(flowVersion = 1) to node1 + aliceNode sent sessionInit(UpgradedFlow::class, flowVersion = 2) to bobNode, + bobNode sent sessionConfirm(flowVersion = 1) to aliceNode ) val (receivedPayload, node2FlowVersion) = result.getOrThrow() assertThat(receivedPayload).isEqualTo("Old initiated") @@ -610,20 +618,20 @@ class FlowFrameworkTests { @Test fun `upgraded initiated flow`() { - node2.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } - val initiatingFlow = SendFlow("Old initiating", node2.info.chooseIdentity()) - val flowInfo = node1.services.startFlow(initiatingFlow).resultFuture + bobNode.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } + val initiatingFlow = SendFlow("Old initiating", bob) + val flowInfo = aliceNode.services.startFlow(initiatingFlow).resultFuture mockNet.runNetwork() assertThat(receivedSessionMessages).startsWith( - node1 sent sessionInit(SendFlow::class, flowVersion = 1, payload = "Old initiating") to node2, - node2 sent sessionConfirm(flowVersion = 2) to node1 + aliceNode sent sessionInit(SendFlow::class, flowVersion = 1, payload = "Old initiating") to bobNode, + bobNode sent sessionConfirm(flowVersion = 2) to aliceNode ) assertThat(flowInfo.get().flowVersion).isEqualTo(2) } @Test fun `unregistered flow`() { - val future = node1.services.startFlow(SendFlow("Hello", node2.info.chooseIdentity())).resultFuture + val future = aliceNode.services.startFlow(SendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java) .isThrownBy { future.getOrThrow() } @@ -632,7 +640,7 @@ class FlowFrameworkTests { @Test fun `unknown class in session init`() { - node1.sendSessionMessage(SessionInit(random63BitValue(), "not.a.real.Class", 1, "version", null), node2) + aliceNode.sendSessionMessage(SessionInit(random63BitValue(), "not.a.real.Class", 1, "version", null), bob) mockNet.runNetwork() assertThat(receivedSessionMessages).hasSize(2) // Only the session-init and session-reject are expected val reject = receivedSessionMessages.last().message as SessionReject @@ -641,7 +649,7 @@ class FlowFrameworkTests { @Test fun `non-flow class in session init`() { - node1.sendSessionMessage(SessionInit(random63BitValue(), String::class.java.name, 1, "version", null), node2) + aliceNode.sendSessionMessage(SessionInit(random63BitValue(), String::class.java.name, 1, "version", null), bob) mockNet.runNetwork() assertThat(receivedSessionMessages).hasSize(2) // Only the session-init and session-reject are expected val reject = receivedSessionMessages.last().message as SessionReject @@ -650,23 +658,23 @@ class FlowFrameworkTests { @Test fun `single inlined sub-flow`() { - node2.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } - val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.chooseIdentity(), "Hello")).resultFuture + bobNode.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } + val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") } @Test fun `double inlined sub-flow`() { - node2.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } - val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.chooseIdentity(), "Hello")).resultFuture + bobNode.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } + val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") } @Test fun `double initiateFlow throws`() { - val future = node1.services.startFlow(DoubleInitiatingFlow()).resultFuture + val future = aliceNode.services.startFlow(DoubleInitiatingFlow()).resultFuture mockNet.runNetwork() assertThatExceptionOfType(IllegalStateException::class.java) .isThrownBy { future.getOrThrow() } @@ -677,8 +685,8 @@ class FlowFrameworkTests { private class DoubleInitiatingFlow : FlowLogic() { @Suspendable override fun call() { - initiateFlow(serviceHub.myInfo.chooseIdentity()) - initiateFlow(serviceHub.myInfo.chooseIdentity()) + initiateFlow(ourIdentity) + initiateFlow(ourIdentity) } } @@ -712,17 +720,17 @@ class FlowFrameworkTests { } private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): SessionInit { - return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload) + return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload?.serialize()) } private fun sessionConfirm(flowVersion: Int = 1) = SessionConfirm(0, 0, flowVersion, "") - private fun sessionData(payload: Any) = SessionData(0, payload) + private fun sessionData(payload: Any) = SessionData(0, payload.serialize()) private val normalEnd = NormalSessionEnd(0) private fun erroredEnd(errorResponse: FlowException? = null) = ErrorSessionEnd(0, errorResponse) - private fun StartedNode<*>.sendSessionMessage(message: SessionMessage, destination: StartedNode<*>) { + private fun StartedNode<*>.sendSessionMessage(message: SessionMessage, destination: Party) { services.networkService.apply { - val address = getAddressOfParty(PartyInfo.SingleNode(destination.info.chooseIdentity(), emptyList())) + val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList())) send(createMessage(StateMachineManager.sessionTopic, message.serialize().bytes), address) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index bd4c3358f3..ce79064a79 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -86,7 +86,7 @@ class DistributedImmutableMapTests : TestDependencyInjectionBase() { private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), ::makeTestIdentityService) databases.add(database) val stateMachineFactory = { DistributedImmutableMap(database, RaftUniquenessProvider.Companion::createMap) } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 579f653018..29ade9ec4f 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -8,11 +8,12 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.node.internal.StartedNode +import net.corda.node.services.api.StartedNodeServices import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -27,36 +28,37 @@ import kotlin.test.assertFailsWith class NotaryServiceTests { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode - lateinit var clientNode: StartedNode + lateinit var notaryServices: StartedNodeServices + lateinit var aliceServices: StartedNodeServices lateinit var notary: Party + lateinit var alice: Party @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() - notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = false) - clientNode = mockNet.createNode() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + val notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = false) + aliceServices = mockNet.createNode(legalName = ALICE_NAME).services mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() - notary = clientNode.services.getDefaultNotary() + notaryServices = notaryNode.services + notary = notaryServices.getDefaultNotary() + alice = aliceServices.myInfo.singleIdentity() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `should sign a unique transaction with a valid time-window`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) + .addCommand(dummyCommand(alice.owningKey)) .setTimeWindow(Instant.now(), 30.seconds) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -67,11 +69,11 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction without a time-window`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -82,12 +84,12 @@ class NotaryServiceTests { @Test fun `should report error for transaction with an invalid time-window`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) + .addCommand(dummyCommand(alice.owningKey)) .setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -99,17 +101,17 @@ class NotaryServiceTests { @Test fun `should sign identical transaction multiple times (signing is idempotent)`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val firstAttempt = NotaryFlow.Client(stx) val secondAttempt = NotaryFlow.Client(stx) - val f1 = clientNode.services.startFlow(firstAttempt) - val f2 = clientNode.services.startFlow(secondAttempt) + val f1 = aliceServices.startFlow(firstAttempt) + val f2 = aliceServices.startFlow(secondAttempt) mockNet.runNetwork() @@ -118,25 +120,25 @@ class NotaryServiceTests { @Test fun `should report conflict when inputs are reused across transactions`() { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val stx = run { val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val stx2 = run { val tx = TransactionBuilder(notary) .addInputState(inputState) - .addInputState(issueState(clientNode)) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addInputState(issueState(aliceServices, alice)) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val firstSpend = NotaryFlow.Client(stx) val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction. - clientNode.services.startFlow(firstSpend) - val future = clientNode.services.startFlow(secondSpend) + aliceServices.startFlow(firstSpend) + val future = aliceServices.startFlow(secondSpend) mockNet.runNetwork() @@ -148,16 +150,16 @@ class NotaryServiceTests { private fun runNotaryClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) - val future = clientNode.services.startFlow(flow).resultFuture + val future = aliceServices.startFlow(flow).resultFuture mockNet.runNetwork() return future } - fun issueState(node: StartedNode<*>): StateAndRef<*> { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey) - node.services.recordTransactions(stx) + fun issueState(services: ServiceHub, identity: Party): StateAndRef<*> { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) + val signedByNode = services.signInitialTransaction(tx) + val stx = notaryServices.addSignature(signedByNode, notary.owningKey) + services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 491931ab2a..088a14e18f 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -23,7 +23,7 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) } @After diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 87410fecb8..6f3dd1ac66 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -9,10 +9,11 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub 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.node.services.api.StartedNodeServices import net.corda.node.services.issueInvalidState import net.corda.testing.* import net.corda.testing.contracts.DummyContract @@ -27,35 +28,37 @@ import kotlin.test.assertFailsWith class ValidatingNotaryServiceTests { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode - lateinit var clientNode: StartedNode + lateinit var notaryServices: StartedNodeServices + lateinit var aliceServices: StartedNodeServices lateinit var notary: Party + lateinit var alice: Party @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() - notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) - clientNode = mockNet.createNode() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + val notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + val aliceNode = mockNet.createNode(legalName = ALICE_NAME) mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() - notary = clientNode.services.getDefaultNotary() + notaryServices = notaryNode.services + aliceServices = aliceNode.services + notary = notaryServices.getDefaultNotary() + alice = aliceNode.info.singleIdentity() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `should report error for invalid transaction dependency`() { val stx = run { - val inputState = issueInvalidState(clientNode, notary) + val inputState = issueInvalidState(aliceServices, alice, notary) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val future = runClient(stx) @@ -69,11 +72,11 @@ class ValidatingNotaryServiceTests { fun `should report error for missing signatures`() { val expectedMissingKey = MEGA_CORP_KEY.public val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val command = Command(DummyContract.Commands.Move(), expectedMissingKey) val tx = TransactionBuilder(notary).withItems(inputState, command) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val ex = assertFailsWith(NotaryException::class) { @@ -89,16 +92,16 @@ class ValidatingNotaryServiceTests { private fun runClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) - val future = clientNode.services.startFlow(flow).resultFuture + val future = aliceServices.startFlow(flow).resultFuture mockNet.runNetwork() return future } - fun issueState(node: StartedNode<*>): StateAndRef<*> { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey) - node.services.recordTransactions(stx) + fun issueState(serviceHub: ServiceHub, identity: Party): StateAndRef<*> { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) + val signedByNode = serviceHub.signInitialTransaction(tx) + val stx = notaryServices.addSignature(signedByNode, notary.owningKey) + serviceHub.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 5250e7f500..46802e8a7f 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.* @@ -45,27 +46,29 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeVaultServiceTest : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + } + lateinit var services: MockServices - lateinit var issuerServices: MockServices + private lateinit var issuerServices: MockServices val vaultService get() = services.vaultService as NodeVaultService lateinit var database: CordaPersistence @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts.asset") LogHelper.setLevel(NodeVaultService::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY), - customSchemas = setOf(CashSchemaV1)) + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOC_KEY) + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOC_KEY) } @After fun tearDown() { database.close() LogHelper.reset(NodeVaultService::class) - unsetCordappPackages() } @Suspendable @@ -440,8 +443,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun addNoteToTransaction() { - val megaCorpServices = MockServices(MEGA_CORP_KEY) - + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP_KEY) database.transaction { val freshKey = services.myInfo.chooseIdentity().owningKey diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 9e484833d6..8d34babb97 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* @@ -45,7 +46,9 @@ import java.time.temporal.ChronoUnit import java.util.* class VaultQueryTests : TestDependencyInjectionBase() { - + private val cordappPackages = setOf( + "net.corda.testing.contracts", "net.corda.finance.contracts", + CashSchemaV1::class.packageName, CommercialPaperSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName).toMutableList() private lateinit var services: MockServices private lateinit var notaryServices: MockServices private val vaultService: VaultService get() = services.vaultService @@ -59,23 +62,20 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts") - // register additional identities identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY) identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), createIdentityService = { identitySvc }, - customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1)) + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) } @After fun tearDown() { database.close() - unsetCordappPackages() } /** @@ -84,8 +84,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Ignore @Test fun createPersistentTestDb() { - val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = { identitySvc }) - + val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), { identitySvc }) setUpDb(database, 5000) database.close() @@ -1490,18 +1489,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { // GBP issuer val gbpCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1001)) val gbpCashIssuer = Party(CordaX500Name(organisation = "British Pounds Cash Issuer", locality = "London", country = "GB"), gbpCashIssuerKey.public).ref(1) - val gbpCashIssuerServices = MockServices(gbpCashIssuerKey) + val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerKey) // USD issuer val usdCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1002)) val usdCashIssuer = Party(CordaX500Name(organisation = "US Dollars Cash Issuer", locality = "New York", country = "US"), usdCashIssuerKey.public).ref(1) - val usdCashIssuerServices = MockServices(usdCashIssuerKey) + val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerKey) // CHF issuer val chfCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1003)) val chfCashIssuer = Party(CordaX500Name(organisation = "Swiss Francs Cash Issuer", locality = "Zurich", country = "CH"), chfCashIssuerKey.public).ref(1) - val chfCashIssuerServices = MockServices(chfCashIssuerKey) - + val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerKey) database.transaction { - services.fillWithSomeTestCash(100.POUNDS, gbpCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (gbpCashIssuer)) services.fillWithSomeTestCash(100.DOLLARS, usdCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (usdCashIssuer)) services.fillWithSomeTestCash(100.SWISS_FRANCS, chfCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (chfCashIssuer)) @@ -1754,6 +1751,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `query attempting to use unregistered schema`() { + tearDown() + cordappPackages -= SampleCashSchemaV3::class.packageName + setUp() database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt new file mode 100644 index 0000000000..d0d7562234 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -0,0 +1,161 @@ +package net.corda.node.services.vault + +import co.paralleluniverse.fibers.Suspendable +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions +import net.corda.core.contracts.* +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.AbstractParty +import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.packageName +import net.corda.core.internal.uncheckedCast +import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.StateLoader +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria.SoftLockingCondition +import net.corda.core.node.services.vault.QueryCriteria.SoftLockingType.LOCKED_ONLY +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.services.api.VaultServiceInternal +import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.chooseIdentity +import net.corda.testing.node.MockNetwork +import org.junit.After +import org.junit.Test +import java.math.BigInteger +import java.security.KeyPair +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.reflect.jvm.jvmName +import kotlin.test.assertEquals + +private class NodePair(private val mockNet: MockNetwork) { + private class ServerLogic(private val session: FlowSession, private val running: AtomicBoolean) : FlowLogic() { + @Suspendable + override fun call() { + running.set(true) + session.receive().unwrap { assertEquals("ping", it) } + session.send("pong") + } + } + + @InitiatingFlow + abstract class AbstractClientLogic(nodePair: NodePair) : FlowLogic() { + protected val server = nodePair.server.info.chooseIdentity() + protected abstract fun callImpl(): T + @Suspendable + override fun call() = callImpl().also { + initiateFlow(server).sendAndReceive("ping").unwrap { assertEquals("pong", it) } + } + } + + private val serverRunning = AtomicBoolean() + val server = mockNet.createNode() + var client = mockNet.createNode().apply { + internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. + } + private set + + fun communicate(clientLogic: AbstractClientLogic, rebootClient: Boolean): FlowStateMachine { + server.internals.internalRegisterFlowFactory(AbstractClientLogic::class.java, InitiatedFlowFactory.Core { ServerLogic(it, serverRunning) }, ServerLogic::class.java, false) + client.services.startFlow(clientLogic) + while (!serverRunning.get()) mockNet.runNetwork(1) + if (rebootClient) { + client.dispose() + client = mockNet.createNode(client.internals.id) + } + return uncheckedCast(client.smm.allStateMachines.single().stateMachine) + } +} + +class VaultSoftLockManagerTest { + private val mockVault: VaultServiceInternal = mock() + private val mockNet = MockNetwork(cordappPackages = listOf(ContractImpl::class.packageName), defaultFactory = object : MockNetwork.Factory { + override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { + override fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader): VaultServiceInternal { + val realVault = super.makeVaultService(keyManagementService, stateLoader) + return object : VaultServiceInternal by realVault { + override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { + mockVault.softLockRelease(lockId, stateRefs) // No need to also call the real one for these tests. + } + } + } + } + } + }) + private val nodePair = NodePair(mockNet) + @After + fun tearDown() { + mockNet.stopNodes() + } + + private object CommandDataImpl : CommandData + private class ClientLogic(nodePair: NodePair, private val state: ContractState) : NodePair.AbstractClientLogic>(nodePair) { + override fun callImpl() = run { + subFlow(FinalityFlow(serviceHub.signInitialTransaction(TransactionBuilder(notary = ourIdentity).apply { + addOutputState(state, ContractImpl::class.jvmName) + addCommand(CommandDataImpl, ourIdentity.owningKey) + }))) + serviceHub.vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(LOCKED_ONLY))).states.map { + it.state.data + } + } + } + + private abstract class SingleParticipantState(nodePair: NodePair) : ContractState { + override val participants = listOf(nodePair.client.info.chooseIdentity()) + } + + private class PlainOldState(nodePair: NodePair) : SingleParticipantState(nodePair) + private class FungibleAssetImpl(nodePair: NodePair) : SingleParticipantState(nodePair), FungibleAsset { + override val owner get() = participants[0] + override fun withNewOwner(newOwner: AbstractParty) = throw UnsupportedOperationException() + override val amount get() = Amount(1, Issued(PartyAndReference(owner, OpaqueBytes.of(1)), Unit)) + override val exitKeys get() = throw UnsupportedOperationException() + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty) = throw UnsupportedOperationException() + override fun equals(other: Any?) = other is FungibleAssetImpl && participants == other.participants + override fun hashCode() = participants.hashCode() + } + + class ContractImpl : Contract { + override fun verify(tx: LedgerTransaction) {} + } + + private fun run(expectSoftLock: Boolean, state: ContractState, checkpoint: Boolean) { + val fsm = nodePair.communicate(ClientLogic(nodePair, state), checkpoint) + mockNet.runNetwork() + if (expectSoftLock) { + assertEquals(listOf(state), fsm.resultFuture.getOrThrow()) + verify(mockVault).softLockRelease(fsm.id.uuid, null) + } else { + assertEquals(emptyList(), fsm.resultFuture.getOrThrow()) + // In this case we don't want softLockRelease called so that we avoid its expensive query, even after restore from checkpoint. + } + verifyNoMoreInteractions(mockVault) + } + + @Test + fun `plain old state is not soft locked`() = run(false, PlainOldState(nodePair), false) + + @Test + fun `plain old state is not soft locked with checkpoint`() = run(false, PlainOldState(nodePair), true) + + @Test + fun `fungible asset is soft locked`() = run(true, FungibleAssetImpl(nodePair), false) + + @Test + fun `fungible asset is soft locked with checkpoint`() = run(true, FungibleAssetImpl(nodePair), true) +} diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 60e853d023..feb6530062 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,9 +1,10 @@ -package net.corda.node.services.vaultService +package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.LinearState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AnonymousParty +import net.corda.core.internal.packageName import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy @@ -34,6 +35,10 @@ import kotlin.test.assertEquals // TODO: Move this to the cash contract tests once mock services are further split up. class VaultWithCashTest : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + } + lateinit var services: MockServices lateinit var issuerServices: MockServices val vaultService: VaultService get() = services.vaultService @@ -42,22 +47,19 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - LogHelper.setLevel(VaultWithCashTest::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY), - customSchemas = setOf(CashSchemaV1)) + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) - notaryServices = MockServices(DUMMY_NOTARY_KEY) + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, DUMMY_NOTARY_KEY) } @After fun tearDown() { LogHelper.reset(VaultWithCashTest::class) database.close() - unsetCordappPackages() } @Test @@ -81,7 +83,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Test fun `issue and spend total correctly and irrelevant ignored`() { - val megaCorpServices = MockServices(MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP_KEY) val freshKey = services.keyManagementService.freshKey() val usefulTX = diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index ffac359f49..e7b9ca1623 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -21,7 +21,7 @@ class ObservablesTests { val toBeClosed = mutableListOf() fun createDatabase(): CordaPersistence { - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) toBeClosed += database return database } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index c331834529..b977d45ffb 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -38,7 +38,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow"]]] directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index da04426168..d95d203fc4 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -50,14 +50,12 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. - // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 rpcPort 10003 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] } node { name "O=BankOfCorda,L=London,C=GB" @@ -65,7 +63,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10005 rpcPort 10006 webPort 10007 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = [ ['username' : "bankUser", 'password' : "test", @@ -81,7 +79,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10008 rpcPort 10009 webPort 10010 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = [ ['username' : "bigCorpUser", 'password' : "test", diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index d379e3b98a..147d6a65e0 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -41,12 +41,14 @@ class BankOfCordaRPCClientTest { // Register for Big Corporation Vault updates val vaultUpdatesBigCorp = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates + val bigCorporation = bigCorpProxy.wellKnownPartyFromX500Name(BIGCORP_LEGAL_NAME)!! + // Kick-off actual Issuer Flow val anonymous = true val notary = bocProxy.notaryIdentities().first() bocProxy.startFlow(::CashIssueAndPaymentFlow, 1000.DOLLARS, BIG_CORP_PARTY_REF, - nodeBigCorporation.nodeInfo.chooseIdentity(), + bigCorporation, anonymous, notary).returnValue.getOrThrow() diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index ac2e890f09..81dbf12166 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -69,7 +69,7 @@ private class BankOfCordaDriver { val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf( startFlowPermission())) - startNotaryNode(DUMMY_NOTARY.name, validating = false) + startNotaryNode(DUMMY_NOTARY.name, validating = true) val bankOfCorda = startNode( providedName = BOC.name, rpcUsers = listOf(bankUser)) diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 2341793e1f..bcb20c214a 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -51,14 +51,13 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 rpcPort 10003 webPort 10004 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] useTestClock true } node { @@ -66,7 +65,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10005 rpcPort 10006 webPort 10007 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] useTestClock true } node { @@ -74,7 +73,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10008 rpcPort 10009 webPort 10010 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] useTestClock true } } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index a28d299d24..bd694bd41b 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -2,11 +2,10 @@ package net.corda.irs.api import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command -import net.corda.core.crypto.MerkleTreeException import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.* import net.corda.core.internal.ThreadBox -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.FilteredTransaction @@ -78,7 +77,7 @@ object NodeInterestRates { @ThreadSafe // DOCSTART 3 @CordaService - class Oracle(private val services: ServiceHub) : SingletonSerializeAsToken() { + class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() { private val mutex = ThreadBox(InnerState()) init { @@ -146,7 +145,7 @@ object NodeInterestRates { } require(ftx.checkWithFun(::check)) - + ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey) // It all checks out, so we can return a signature. // // Note that we will happily sign an invalid transaction, as we are only being presented with a filtered diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 38fc4e0b56..3953c876b0 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,7 +14,6 @@ import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.ownedBy import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -24,6 +23,7 @@ import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties import net.corda.testing.node.MockServices.Companion.makeTestIdentityService +import net.corda.testing.node.createMockCordaService import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -62,17 +62,16 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts") - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) database.transaction { - oracle = NodeInterestRates.Oracle(services).apply { knownFixes = TEST_DATA } + oracle = createMockCordaService(services, NodeInterestRates::Oracle) + oracle.knownFixes = TEST_DATA } } @After fun tearDown() { database.close() - unsetCordappPackages() } @Test @@ -201,13 +200,13 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `network tearoff`() { - val mockNet = MockNetwork(initialiseSerialization = false) + val mockNet = MockNetwork(initialiseSerialization = false, cordappPackages = listOf("net.corda.finance.contracts")) val n1 = mockNet.createNotaryNode() val oracleNode = mockNet.createNode().apply { internals.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) internals.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) database.transaction { - internals.installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA + installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA } } val tx = makePartialTX() diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 6c929c47b7..19c5750d65 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -12,7 +12,6 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.statemachine.StateMachineManager import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork @@ -141,7 +140,10 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } } - val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) + val mockNet = MockNetwork( + networkSendManuallyPumped = networkSendManuallyPumped, + threadPerNode = runAsync, + cordappPackages = listOf("net.corda.irs.contract", "net.corda.finance.contract")) // This one must come first. val networkMap = mockNet.startNetworkMapNode(nodeFactory = NetworkMapNodeFactory) val notary = mockNet.createNotaryNode(validating = false, nodeFactory = NotaryNodeFactory) @@ -255,7 +257,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val networkInitialisationFinished = allOf(*mockNet.nodes.map { it.nodeReadyFuture.toCompletableFuture() }.toTypedArray()) fun start(): Future { - setCordappPackages("net.corda.irs.contract", "net.corda.finance.contract") mockNet.startNodes() // Wait for all the nodes to have finished registering with the network map service. return networkInitialisationFinished.thenCompose { startMainSimulation() } @@ -271,9 +272,3 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, mockNet.stopNodes() } } - -/** - * Helper function for verifying that a service info contains the given type of advertised service. For non-simulation cases - * this is a configuration matter rather than implementation. - */ -fun Iterable.containsType(type: ServiceType) = any { it.type == type } \ No newline at end of file diff --git a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index 4bd32364a4..6da9d13091 100644 --- a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -2,25 +2,10 @@ package net.corda.netmap.simulation import net.corda.core.utilities.getOrThrow import net.corda.testing.LogHelper -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test class IRSSimulationTest { // TODO: These tests should be a lot more complete. - - @Before - fun setup() { - setCordappPackages("net.corda.irs.contract") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `runs to completion`() { LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests. diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index a13ab70ad8..4d3f97f065 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -46,13 +46,18 @@ publishing { } } -task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT']) +task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) task deployNodesSingle(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesSingle" definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } +task deployNodesCustom(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesCustom" + definitionClass = 'net.corda.notarydemo.CustomNotaryCordform' +} + task deployNodesRaft(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesRaft" definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 477adecd20..b3e295d1cb 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -13,16 +13,20 @@ import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB -import net.corda.testing.internal.demorun.* +import net.corda.testing.internal.demorun.name +import net.corda.testing.internal.demorun.node +import net.corda.testing.internal.demorun.notary +import net.corda.testing.internal.demorun.rpcUsers +import net.corda.testing.internal.demorun.runNodes -fun main(args: Array) = BFTNotaryCordform.runNodes() +fun main(args: Array) = BFTNotaryCordform().runNodes() private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { +class BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt index 1bab32c9a9..7ab6fff629 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt @@ -3,7 +3,7 @@ package net.corda.notarydemo import net.corda.testing.internal.demorun.clean fun main(args: Array) { - listOf(SingleNotaryCordform, RaftNotaryCordform, BFTNotaryCordform).forEach { + listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).forEach { it.clean() } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt new file mode 100644 index 0000000000..3cd43d7a0e --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -0,0 +1,36 @@ +package net.corda.notarydemo + +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.core.internal.div +import net.corda.node.services.config.NotaryConfig +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.internal.demorun.* + +fun main(args: Array) = CustomNotaryCordform().runNodes() + +class CustomNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { + init { + node { + name(ALICE.name) + p2pPort(10002) + rpcPort(10003) + rpcUsers(notaryDemoUser) + } + node { + name(BOB.name) + p2pPort(10005) + rpcPort(10006) + } + node { + name(DUMMY_NOTARY.name) + p2pPort(10009) + rpcPort(10010) + notary(NotaryConfig(validating = true, custom = true)) + } + } + + override fun setup(context: CordformContext) {} +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt similarity index 68% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 8d1c0d0274..55adae9ee2 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -1,21 +1,27 @@ -package net.corda.docs +package net.corda.notarydemo import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException +/** + * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. + * + * Note that at present only a single-node notary service can be customised. + */ // START 1 @CordaService -class MyCustomValidatingNotaryService(override val services: ServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() @@ -26,6 +32,7 @@ class MyCustomValidatingNotaryService(override val services: ServiceHub, overrid } // END 1 +@Suppress("UNUSED_PARAMETER") // START 2 class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { /** @@ -38,11 +45,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) - null - else - stx.tx.timeWindow - val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + var timeWindow: TimeWindow? = null + val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(serviceHub) + } else { + val wtx = stx.tx + customVerify(wtx.toLedgerTransaction(serviceHub)) + timeWindow = wtx.timeWindow + stx + } checkSignatures(transactionWithSignatures) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { @@ -54,6 +65,10 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } + private fun customVerify(transaction: LedgerTransaction) { + // Add custom verification logic + } + private fun checkSignatures(tx: TransactionWithSignatures) { try { tx.verifySignaturesExcept(service.notaryIdentityKey) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 1d78c51266..ff128c6edf 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -14,7 +14,7 @@ import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.internal.demorun.* -fun main(args: Array) = RaftNotaryCordform.runNodes() +fun main(args: Array) = RaftNotaryCordform().runNodes() internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } @@ -22,7 +22,7 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { +class RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index beec66a8fc..8e349494b8 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -11,15 +11,19 @@ import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.internal.demorun.* +import net.corda.testing.internal.demorun.name +import net.corda.testing.internal.demorun.node +import net.corda.testing.internal.demorun.notary +import net.corda.testing.internal.demorun.rpcUsers +import net.corda.testing.internal.demorun.runNodes -fun main(args: Array) = SingleNotaryCordform.runNodes() +fun main(args: Array) = SingleNotaryCordform().runNodes() val notaryDemoUser = User("demou", "demop", setOf(startFlowPermission(), startFlowPermission())) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object SingleNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", DUMMY_NOTARY.name.toString()) { +class SingleNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { init { node { name(ALICE.name) diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 502b2bdee8..832e2eb283 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -64,19 +64,18 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] } node { name "O=Bank A,L=London,C=GB" p2pPort 10004 webPort 10005 rpcPort 10006 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers } node { @@ -84,7 +83,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10007 webPort 10008 rpcPort 10009 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers } node { @@ -92,7 +91,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10010 webPort 10011 rpcPort 10012 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers } } diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index e113f235b7..87b9fa7839 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -11,8 +11,6 @@ import net.corda.vega.api.PortfolioApiUtils import net.corda.vega.api.SwapDataModel import net.corda.vega.api.SwapDataView import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before import org.junit.Test import java.math.BigDecimal import java.time.LocalDate @@ -26,19 +24,9 @@ class SimmValuationTest : IntegrationTestCategory { val testTradeId = "trade1" } - @Before - fun setup() { - setCordappPackages("net.corda.vega.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `runs SIMM valuation demo`() { - driver(isDebug = true) { + driver(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.vega.contracts")) { startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() val nodeAFuture = startNode(providedName = nodeALegalName) val nodeBFuture = startNode(providedName = nodeBLegalName) diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 8d224d11ab..392ecd60ac 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -51,33 +51,31 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into TraderDemoClientApi so if you change it here, change it there too. - // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] } node { name "O=Bank A,L=London,C=GB" p2pPort 10005 rpcPort 10006 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers } node { name "O=Bank B,L=New York,C=US" p2pPort 10008 rpcPort 10009 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers } node { name "O=BankOfCorda,L=New York,C=US" p2pPort 10011 rpcPort 10012 - cordapps = ["net.corda:finance:$corda_release_version"] + cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers } } diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 0efa101ec7..61580c8632 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -1,6 +1,7 @@ package net.corda.traderdemo import net.corda.client.rpc.CordaRPCClient +import net.corda.core.internal.packageName import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.finance.DOLLARS @@ -17,23 +18,12 @@ import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before import org.junit.Test import java.util.concurrent.Executors -class TraderDemoTest : NodeBasedTest() { - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.asset", "net.corda.finance.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - +class TraderDemoTest : NodeBasedTest(listOf( + "net.corda.finance.contracts.asset", "net.corda.finance.contracts", + CashSchemaV1::class.packageName, CommercialPaperSchemaV1::class.packageName)) { @Test fun `runs trader demo`() { val demoUser = User("demo", "demo", setOf(startFlowPermission())) @@ -48,9 +38,6 @@ class TraderDemoTest : NodeBasedTest() { val (nodeA, nodeB, bankNode) = listOf(nodeAFuture, nodeBFuture, bankNodeFuture, notaryFuture).map { it.getOrThrow() } nodeA.internals.registerInitiatedFlow(BuyerFlow::class.java) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1, CommercialPaperSchemaV1)) - val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val client = CordaRPCClient(it.internals.configuration.rpcAddress!!) client.start(demoUser.username, demoUser.password).proxy diff --git a/settings.gradle b/settings.gradle index a3b8270486..ec0d5adcc6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ include 'webserver:webcapsule' include 'experimental' include 'experimental:sandbox' include 'experimental:quasar-hook' +include 'experimental:kryo-hook' include 'verifier' include 'test-common' include 'test-utils' diff --git a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt index c4ca2077cf..62fdc58934 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt @@ -1,10 +1,7 @@ package net.corda.node.testing import com.codahale.metrics.MetricRegistry -import net.corda.core.cordapp.CordappProvider -import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.StateLoader import net.corda.core.node.services.* @@ -13,12 +10,12 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StateLoaderImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowStateMachineImpl +import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.CordaPersistence @@ -44,13 +41,11 @@ open class MockServiceHubInternal( override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage(), override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage(), val mapCache: NetworkMapCacheInternal? = null, - val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), - val schemas: SchemaService? = NodeSchemaService(), val customContractUpgradeService: ContractUpgradeService? = null, val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2), - override val cordappProvider: CordappProvider = CordappProviderImpl(CordappLoader.createDefault(Paths.get("."))).start(attachments), - protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions) + override val cordappProvider: CordappProviderInternal = CordappProviderImpl(CordappLoader.createDefault(Paths.get(".")), attachments), + val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions) ) : ServiceHubInternal, StateLoader by stateLoader { override val transactionVerifierService: TransactionVerifierService get() = customTransactionVerifierService ?: throw UnsupportedOperationException() @@ -65,9 +60,7 @@ open class MockServiceHubInternal( override val networkService: MessagingService get() = network ?: throw UnsupportedOperationException() override val networkMapCache: NetworkMapCacheInternal - get() = mapCache ?: MockNetworkMapCache(this) - override val schedulerService: SchedulerService - get() = scheduler ?: throw UnsupportedOperationException() + get() = mapCache ?: NetworkMapCacheImpl(MockNetworkMapCache(database, configuration), identityService) override val clock: Clock get() = overrideClock ?: throw UnsupportedOperationException() override val myInfo: NodeInfo @@ -75,18 +68,12 @@ open class MockServiceHubInternal( override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() - override val schemaService: SchemaService - get() = schemas ?: throw UnsupportedOperationException() + override val schemaService get() = throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() lateinit var smm: StateMachineManager override fun cordaService(type: Class): T = throw UnsupportedOperationException() - - override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator, ourIdentity: Party?): FlowStateMachineImpl { - return smm.executor.fetchFrom { smm.add(logic, flowInitiator, ourIdentity) } - } - override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? = null override fun jdbcSession(): Connection = database.createSession() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index 7fe3ee5c09..551a8075b8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -46,8 +46,9 @@ fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), initialiseSerialization: Boolean = true, + cordappPackages: List = emptyList(), dsl: TransactionDSL.() -> EnforceVerifyOrFail -) = ledger(initialiseSerialization = initialiseSerialization) { +) = ledger(services = MockServices(cordappPackages), initialiseSerialization = initialiseSerialization) { dsl(TransactionDSL(TestTransactionDSLInterpreter(this.interpreter, transactionBuilder))) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt index c5156c16e2..4de8f86442 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -228,7 +228,6 @@ fun rpcDriver( systemProperties: Map = emptyMap(), useTestClock: Boolean = false, initialiseSerialization: Boolean = true, - networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false), startNodesInProcess: Boolean = false, extraCordappPackagesToScan: List = emptyList(), dsl: RPCDriverExposedDSLInterface.() -> A @@ -240,7 +239,6 @@ fun rpcDriver( systemProperties = systemProperties, driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, - networkMapStartStrategy = networkMapStartStrategy, isDebug = isDebug, startNodesInProcess = startNodesInProcess, extraCordappPackagesToScan = extraCordappPackagesToScan diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 9a1ce481ca..d6733b1576 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -8,7 +8,6 @@ import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode -import net.corda.cordform.NodeDefinition import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf @@ -20,13 +19,17 @@ import net.corda.core.internal.div import net.corda.core.internal.times import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.toFuture import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* import net.corda.node.services.network.NetworkMapService import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.nodeapi.NodeInfoFilesCopier import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.config.toConfig @@ -36,6 +39,8 @@ import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import okhttp3.OkHttpClient import okhttp3.Request import org.slf4j.Logger +import rx.Observable +import rx.observables.ConnectableObservable import java.io.File import java.net.* import java.nio.file.Path @@ -66,6 +71,10 @@ import kotlin.concurrent.thread private val log: Logger = loggerFor() +private val DEFAULT_POLL_INTERVAL = 500.millis + +private const val DEFAULT_WARN_COUNT = 120 + /** * This is the interface that's exposed to DSL users. */ @@ -101,13 +110,12 @@ interface DriverDSLExposedInterface : CordformContext { validating: Boolean = true): CordaFuture /** - * Helper function for starting a [node] with custom parameters from Java. + * Helper function for starting a [Node] with custom parameters from Java. * - * @param defaultParameters The default parameters for the driver. - * @param dsl The dsl itself. + * @param parameters The default parameters for the driver. * @return The value returned in the [dsl] closure. */ - fun startNode(parameters: NodeParameters): CordaFuture { + fun startNode(parameters: NodeParameters): CordaFuture { return startNode(defaultParameters = parameters) } @@ -145,14 +153,6 @@ interface DriverDSLExposedInterface : CordformContext { */ fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture - /** - * Starts a network map service node. Note that only a single one should ever be running, so you will probably want - * to set networkMapStartStrategy to Dedicated(false) in your [driver] call. - * @param startInProcess Determines if the node should be started inside this process. If null the Driver-level - * value will be used. - */ - fun startDedicatedNetworkMapService(startInProcess: Boolean? = null, maximumHeapSize: String = "200m"): CordaFuture - fun waitForAllNodesToFinish() /** @@ -164,16 +164,25 @@ interface DriverDSLExposedInterface : CordformContext { * @param check The function being polled. * @return A future that completes with the non-null value [check] has returned. */ - fun pollUntilNonNull(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> A?): CordaFuture + fun pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture /** * Polls the given function until it returns true. * @see pollUntilNonNull */ - fun pollUntilTrue(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> Boolean): CordaFuture { + fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture { return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null } } + /** + * Polls until a given node knows about presence of another node via its own NetworkMap + */ + fun NodeHandle.pollUntilKnowsAbout(another: NodeHandle, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT): CordaFuture { + return pollUntilTrue("${nodeInfo.legalIdentities} knows about ${another.nodeInfo.legalIdentities}", pollInterval, warnCount) { + another.nodeInfo in rpc.networkMapSnapshot() + } + } + val shutdownManager: ShutdownManager } @@ -198,13 +207,15 @@ sealed class NodeHandle { override val configuration: FullNodeConfiguration, override val webAddress: NetworkHostAndPort, val debugPort: Int?, - val process: Process + val process: Process, + private val onStopCallback: () -> Unit ) : NodeHandle() { override fun stop(): CordaFuture { with(process) { destroy() waitFor() } + onStopCallback() return doneFuture(Unit) } } @@ -215,7 +226,8 @@ sealed class NodeHandle { override val configuration: FullNodeConfiguration, override val webAddress: NetworkHostAndPort, val node: StartedNode, - val nodeThread: Thread + val nodeThread: Thread, + private val onStopCallback: () -> Unit ) : NodeHandle() { override fun stop(): CordaFuture { node.dispose() @@ -223,6 +235,7 @@ sealed class NodeHandle { interrupt() join() } + onStopCallback() return doneFuture(Unit) } } @@ -259,9 +272,8 @@ sealed class PortAllocation { } } -/** - * Helper builder for configuring a [Node] from Java. - */ +/** Helper builder for configuring a [Node] from Java. */ +@Suppress("unused") data class NodeParameters( val providedName: CordaX500Name? = null, val rpcUsers: List = emptyList(), @@ -303,7 +315,6 @@ data class NodeParameters( * @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. * @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. * @param useTestClock If true the test clock will be used in Node. - * @param networkMapStartStrategy Determines whether a network map node is started automatically. * @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or * not. Note that this may be overridden in [DriverDSLExposedInterface.startNode]. * @param dsl The dsl itself. @@ -318,7 +329,7 @@ fun driver( systemProperties: Map = defaultParameters.systemProperties, useTestClock: Boolean = defaultParameters.useTestClock, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - networkMapStartStrategy: NetworkMapStartStrategy = defaultParameters.networkMapStartStrategy, + startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, dsl: DriverDSLExposedInterface.() -> A @@ -331,7 +342,6 @@ fun driver( driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, isDebug = isDebug, - networkMapStartStrategy = networkMapStartStrategy, startNodesInProcess = startNodesInProcess, extraCordappPackagesToScan = extraCordappPackagesToScan ), @@ -344,7 +354,7 @@ fun driver( /** * Helper function for starting a [driver] with custom parameters from Java. * - * @param defaultParameters The default parameters for the driver. + * @param parameters The default parameters for the driver. * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ @@ -355,9 +365,8 @@ fun driver( return driver(defaultParameters = parameters, dsl = dsl) } -/** - * Helper builder for configuring a [driver] from Java. - */ +/** Helper builder for configuring a [driver] from Java. */ +@Suppress("unused") data class DriverParameters( val isDebug: Boolean = false, val driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), @@ -366,7 +375,6 @@ data class DriverParameters( val systemProperties: Map = emptyMap(), val useTestClock: Boolean = false, val initialiseSerialization: Boolean = true, - val networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), val startNodesInProcess: Boolean = false, val extraCordappPackagesToScan: List = emptyList() ) { @@ -377,7 +385,6 @@ data class DriverParameters( fun setSystemProperties(systemProperties: Map) = copy(systemProperties = systemProperties) fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock) fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) - fun setNetworkMapStartStrategy(networkMapStartStrategy: NetworkMapStartStrategy) = copy(networkMapStartStrategy = networkMapStartStrategy) fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) } @@ -511,7 +518,6 @@ class ShutdownManager(private val executorService: ExecutorService) { } fun shutdown() { - unsetCordappPackages() val shutdownActionFutures = state.locked { if (isShutdown) { emptyList Unit>>() @@ -593,16 +599,20 @@ class DriverDSL( val driverDirectory: Path, val useTestClock: Boolean, val isDebug: Boolean, - val networkMapStartStrategy: NetworkMapStartStrategy, val startNodesInProcess: Boolean, - val extraCordappPackagesToScan: List + extraCordappPackagesToScan: List ) : DriverDSLInternalInterface { - private val dedicatedNetworkMapAddress = portAllocation.nextHostAndPort() private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! private var _shutdownManager: ShutdownManager? = null override val shutdownManager get() = _shutdownManager!! - private val packagesToScanString = extraCordappPackagesToScan + getCallerPackage() + private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() + // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ + // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. + // Investigate whether we can avoid that. + private val nodeInfoFilesCopier = NodeInfoFilesCopier() + // Map from a nodes legal name to an observable emitting the number of nodes in its network map. + private val countObservables = mutableMapOf>() class State { val processes = ArrayList>() @@ -659,25 +669,6 @@ class DriverDSL( } } - private fun networkMapServiceConfigLookup(networkMapCandidates: List): (CordaX500Name) -> Map? { - return networkMapStartStrategy.run { - when (this) { - is NetworkMapStartStrategy.Dedicated -> { - serviceConfig(dedicatedNetworkMapAddress).let { - { _: CordaX500Name -> it } - } - } - is NetworkMapStartStrategy.Nominated -> { - serviceConfig(networkMapCandidates.single { - it.name == legalName.toString() - }.config.getString("p2pAddress").let(NetworkHostAndPort.Companion::parse)).let { - { nodeName: CordaX500Name -> if (nodeName == legalName) null else it } - } - } - } - } - } - override fun startNode( defaultParameters: NodeParameters, providedName: CordaX500Name?, @@ -692,10 +683,6 @@ class DriverDSL( val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB") - val networkMapServiceConfigLookup = networkMapServiceConfigLookup(listOf(object : NodeDefinition { - override fun getName() = name.toString() - override fun getConfig() = configOf("p2pAddress" to p2pAddress.toString()) - })) val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, @@ -704,10 +691,10 @@ class DriverDSL( "p2pAddress" to p2pAddress.toString(), "rpcAddress" to rpcAddress.toString(), "webAddress" to webAddress.toString(), - "networkMapService" to networkMapServiceConfigLookup(name), "useTestClock" to useTestClock, - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toMap() }, - "verifierType" to verifierType.name + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toConfig().root().unwrapped() }, + "verifierType" to verifierType.name, + "noNetworkMapServiceMode" to true ) + customOverrides ) return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) @@ -723,7 +710,6 @@ class DriverDSL( } override fun startNodes(nodes: List, startInSameProcess: Boolean?, maximumHeapSize: String): List> { - val networkMapServiceConfigLookup = networkMapServiceConfigLookup(nodes) return nodes.map { node -> portAllocation.nextHostAndPort() // rpcAddress val webAddress = portAllocation.nextHostAndPort() @@ -734,8 +720,8 @@ class DriverDSL( baseDirectory = baseDirectory(name), allowMissingConfig = true, configOverrides = node.config + notary + mapOf( - "networkMapService" to networkMapServiceConfigLookup(name), - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers, + "noNetworkMapServiceMode" to true ) ) startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) @@ -821,11 +807,7 @@ class DriverDSL( override fun start() { _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) - // We set this property so that in-process nodes find cordapps. Out-of-process nodes need this passed in when started. - setCordappPackages(*packagesToScanString.toTypedArray()) - if (networkMapStartStrategy.startDedicated) { - startDedicatedNetworkMapService().andForget(log) // Allow it to start concurrently with other nodes. - } + shutdownManager.registerShutdown { nodeInfoFilesCopier.close() } } fun baseDirectory(nodeName: CordaX500Name): Path { @@ -836,30 +818,59 @@ class DriverDSL( override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) - override fun startDedicatedNetworkMapService(startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { - val webAddress = portAllocation.nextHostAndPort() - val rpcAddress = portAllocation.nextHostAndPort() - val networkMapLegalName = networkMapStartStrategy.legalName - val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(networkMapLegalName), - allowMissingConfig = true, - configOverrides = configOf( - "myLegalName" to networkMapLegalName.toString(), - // TODO: remove the webAddress as NMS doesn't need to run a web server. This will cause all - // node port numbers to be shifted, so all demos and docs need to be updated accordingly. - "webAddress" to webAddress.toString(), - "rpcAddress" to rpcAddress.toString(), - "rpcUsers" to defaultRpcUserList, - "p2pAddress" to dedicatedNetworkMapAddress.toString(), - "useTestClock" to useTestClock) - ) - return startNodeInternal(config, webAddress, startInProcess, maximumHeapSize) + /** + * @param initial number of nodes currently in the network map of a running node. + * @param networkMapCacheChangeObservable an observable returning the updates to the node network map. + * @return a [ConnectableObservable] which emits a new [Int] every time the number of registered nodes changes + * the initial value emitted is always [initial] + */ + private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable): + ConnectableObservable { + val count = AtomicInteger(initial) + return networkMapCacheChangeObservable.map { it -> + when (it) { + is NetworkMapCache.MapChange.Added -> count.incrementAndGet() + is NetworkMapCache.MapChange.Removed -> count.decrementAndGet() + is NetworkMapCache.MapChange.Modified -> count.get() + } + }.startWith(initial).replay() + } + + /** + * @param rpc the [CordaRPCOps] of a newly started node. + * @return a [CordaFuture] which resolves when every node started by driver has in its network map a number of nodes + * equal to the number of running nodes. The future will yield the number of connected nodes. + */ + private fun allNodesConnected(rpc: CordaRPCOps): CordaFuture { + val (snapshot, updates) = rpc.networkMapFeed() + val counterObservable = nodeCountObservable(snapshot.size, updates) + countObservables.put(rpc.nodeInfo().legalIdentities.first().name, counterObservable) + /* TODO: this might not always be the exact number of nodes one has to wait for, + * for example in the following sequence + * 1 start 3 nodes in order, A, B, C. + * 2 before the future returned by this function resolves, kill B + * At that point this future won't ever resolve as it will wait for nodes to know 3 other nodes. + */ + val requiredNodes = countObservables.size + + // This is an observable which yield the minimum number of nodes in each node network map. + val smallestSeenNetworkMapSize = Observable.combineLatest(countObservables.values.toList()) { args : Array -> + args.map { it as Int }.min() ?: 0 + } + val future = smallestSeenNetworkMapSize.filter { it >= requiredNodes }.toFuture() + counterObservable.connect() + return future } private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val nodeConfiguration = config.parseAs() + nodeInfoFilesCopier.addConfig(nodeConfiguration.baseDirectory) + val onNodeExit: () -> Unit = { + nodeInfoFilesCopier.removeConfig(nodeConfiguration.baseDirectory) + countObservables.remove(nodeConfiguration.myLegalName) + } if (startInProcess ?: startNodesInProcess) { - val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) + val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config, cordappPackages) shutdownManager.registerShutdown( nodeAndThreadFuture.map { (node, thread) -> { @@ -870,14 +881,14 @@ class DriverDSL( ) return nodeAndThreadFuture.flatMap { (node, thread) -> establishRpc(nodeConfiguration, openFuture()).flatMap { rpc -> - rpc.waitUntilNetworkReady().map { - NodeHandle.InProcess(rpc.nodeInfo(), rpc, nodeConfiguration, webAddress, node, thread) + allNodesConnected(rpc).map { + NodeHandle.InProcess(rpc.nodeInfo(), rpc, nodeConfiguration, webAddress, node, thread, onNodeExit) } } } } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, packagesToScanString.joinToString(","), maximumHeapSize) + val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, cordappPackages, maximumHeapSize) registerProcess(processFuture) return processFuture.flatMap { process -> val processDeathFuture = poll(executorService, "process death") { @@ -885,15 +896,18 @@ class DriverDSL( } establishRpc(nodeConfiguration, processDeathFuture).flatMap { rpc -> // Call waitUntilNetworkReady in background in case RPC is failing over: - val networkMapFuture = executorService.fork { - rpc.waitUntilNetworkReady() - }.flatMap { it } + val forked = executorService.fork { + allNodesConnected(rpc) + } + val networkMapFuture = forked.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { throw ListenProcessDeathException(nodeConfiguration.p2pAddress, process) } processDeathFuture.cancel(false) - NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, nodeConfiguration, webAddress, debugPort, process) + log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: ${webAddress}") + NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, nodeConfiguration, webAddress, debugPort, process, + onNodeExit) } } } @@ -907,7 +921,7 @@ class DriverDSL( } companion object { - private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toMap()) + private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped()) private val names = arrayOf( ALICE.name, @@ -920,14 +934,15 @@ class DriverDSL( private fun startInProcessNode( executorService: ScheduledExecutorService, nodeConf: FullNodeConfiguration, - config: Config + config: Config, + cordappPackages: List ): CordaFuture, Thread>> { return executorService.fork { log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) // TODO pass the version in? - val node = Node(nodeConf, MOCK_VERSION_INFO, initialiseSerialization = false).start() + val node = Node(nodeConf, MOCK_VERSION_INFO, initialiseSerialization = false, cordappLoader = CordappLoader.createDefaultWithTestPackages(nodeConf, cordappPackages)).start() val nodeThread = thread(name = nodeConf.myLegalName.organisation) { node.internals.run() } @@ -942,7 +957,7 @@ class DriverDSL( quasarJarPath: String, debugPort: Int?, overriddenSystemProperties: Map, - packagesToScanString: String, + cordappPackages: List, maximumHeapSize: String ): CordaFuture { val processFuture = executorService.fork { @@ -953,7 +968,7 @@ class DriverDSL( val systemProperties = overriddenSystemProperties + mapOf( "name" to nodeConf.myLegalName, "visualvm.display.name" to "corda-${nodeConf.myLegalName}", - "net.corda.node.cordapp.scan.packages" to packagesToScanString, + Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator), "java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process ) // See experimental/quasar-hook/README.md for how to generate. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt deleted file mode 100644 index 0086884c77..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.corda.testing.driver - -import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.DUMMY_MAP - -sealed class NetworkMapStartStrategy { - internal abstract val startDedicated: Boolean - internal abstract val legalName: CordaX500Name - internal fun serviceConfig(address: NetworkHostAndPort) = mapOf( - "address" to address.toString(), - "legalName" to legalName.toString() - ) - - class Dedicated(startAutomatically: Boolean) : NetworkMapStartStrategy() { - override val startDedicated = startAutomatically - override val legalName = DUMMY_MAP.name - } - - class Nominated(override val legalName: CordaX500Name) : NetworkMapStartStrategy() { - override val startDedicated = false - } -} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt index 2897c3e92f..cf452a9116 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt @@ -4,8 +4,6 @@ package net.corda.testing.internal.demorun import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode -import net.corda.core.identity.CordaX500Name -import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -20,7 +18,6 @@ fun CordformDefinition.clean() { fun CordformDefinition.runNodes() = driver( isDebug = true, driverDirectory = driverDirectory, - networkMapStartStrategy = NetworkMapStartStrategy.Nominated(CordaX500Name.parse(networkMapNodeName)), portAllocation = PortAllocation.Incremental(10001) ) { setup(this) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index 4be46a59f5..a44cf1c4a5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -279,7 +279,6 @@ class InMemoryMessagingNetwork( _sentMessages.onNext(transfer) } - @CordaSerializable private data class InMemoryMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID, @@ -287,7 +286,6 @@ class InMemoryMessagingNetwork( override fun toString() = "$topicSession#${String(data)}" } - @CordaSerializable private data class InMemoryReceivedMessage(override val topicSession: TopicSession, override val data: ByteArray, override val platformVersion: Int, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 1b82ca04c1..889f9cfd3b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -7,9 +7,9 @@ import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.NonEmptySet -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.network.PersistentNetworkMapCache +import net.corda.node.utilities.CordaPersistence import net.corda.testing.getTestPartyAndCertificate import rx.Observable import rx.subjects.PublishSubject @@ -18,7 +18,7 @@ import java.math.BigInteger /** * Network map cache with no backing map service. */ -class MockNetworkMapCache(serviceHub: ServiceHubInternal) : PersistentNetworkMapCache(serviceHub) { +class MockNetworkMapCache(database: CordaPersistence, configuration: NodeConfiguration) : PersistentNetworkMapCache(database, configuration) { private companion object { val BANK_C = getTestPartyAndCertificate(CordaX500Name(organisation = "Bank C", locality = "London", country = "GB"), entropyToKeyPair(BigInteger.valueOf(1000)).public) val BANK_D = getTestPartyAndCertificate(CordaX500Name(organisation = "Bank D", locality = "London", country = "GB"), entropyToKeyPair(BigInteger.valueOf(2000)).public) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 88824e118e..f574d4d9bb 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -18,7 +18,6 @@ import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService -import net.corda.core.node.services.NetworkMapCache import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow @@ -26,6 +25,7 @@ import net.corda.core.utilities.loggerFor import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.SchemaService import net.corda.node.services.config.BFTSMaRtConfiguration @@ -43,12 +43,9 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CertificateAndKeyPair import net.corda.nodeapi.internal.ServiceInfo -import net.corda.testing.DUMMY_KEY_1 -import net.corda.testing.initialiseTestSerialization +import net.corda.testing.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.resetTestSerialization -import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import java.io.Closeable @@ -64,6 +61,23 @@ fun StartedNode.pumpReceive(block: Boolean = false): InMem return (network as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block) } +/** Helper builder for configuring a [MockNetwork] from Java. */ +@Suppress("unused") +data class MockNetworkParameters( + val networkSendManuallyPumped: Boolean = false, + val threadPerNode: Boolean = false, + val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), + val defaultFactory: MockNetwork.Factory<*> = MockNetwork.DefaultFactory, + val initialiseSerialization: Boolean = true, + val cordappPackages: List = emptyList()) { + fun setNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean) = copy(networkSendManuallyPumped = networkSendManuallyPumped) + fun setThreadPerNode(threadPerNode: Boolean) = copy(threadPerNode = threadPerNode) + fun setServicePeerAllocationStrategy(servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy) = copy(servicePeerAllocationStrategy = servicePeerAllocationStrategy) + fun setDefaultFactory(defaultFactory: MockNetwork.Factory<*>) = copy(defaultFactory = defaultFactory) + fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) + fun setCordappPackages(cordappPackages: List) = copy(cordappPackages = cordappPackages) +} + /** * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. * Components that do IO are either swapped out for mocks, or pointed to a [Jimfs] in memory filesystem or an in @@ -77,12 +91,16 @@ fun StartedNode.pumpReceive(block: Boolean = false): InMem * * LogHelper.setLevel("+messages") */ -class MockNetwork(private val networkSendManuallyPumped: Boolean = false, - private val threadPerNode: Boolean = false, - servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = - InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), - private val defaultFactory: Factory<*> = MockNetwork.DefaultFactory, - private val initialiseSerialization: Boolean = true) : Closeable { +class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParameters(), + private val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, + private val threadPerNode: Boolean = defaultParameters.threadPerNode, + servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, + private val defaultFactory: Factory<*> = defaultParameters.defaultFactory, + private val initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, + private val cordappPackages: List = defaultParameters.cordappPackages) : Closeable { + /** Helper constructor for creating a [MockNetwork] with custom parameters from Java. */ + constructor(parameters: MockNetworkParameters) : this(defaultParameters = parameters) + companion object { // TODO In future PR we're removing the concept of network map node so the details of this mock are not important. val MOCK_NET_MAP = Party(CordaX500Name(organisation = "Mock Network Map", locality = "Madrid", country = "ES"), DUMMY_KEY_1.public) @@ -160,7 +178,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val id: Int, internal val notaryIdentity: Pair?, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue())) : - AbstractNode(config, TestClock(), MOCK_VERSION_INFO, mockNet.busyLatch) { + AbstractNode(config, TestClock(), MOCK_VERSION_INFO, CordappLoader.createDefaultWithTestPackages(config, mockNet.cordappPackages), mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() override val serverThread: AffinityExecutor = @@ -188,26 +206,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, .getOrThrow() } - override fun makeIdentityService(trustRoot: X509Certificate, - clientCa: CertificateAndKeyPair?, - legalIdentity: PartyAndCertificate): IdentityService { - val caCertificates: Array = listOf(legalIdentity.certificate, clientCa?.certificate?.cert) - .filterNotNull() - .toTypedArray() - val identityService = PersistentIdentityService(info.legalIdentitiesAndCerts, - trustRoot = trustRoot, caCertificates = *caCertificates) - services.networkMapCache.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { identityService.verifyAndRegisterIdentity(it) } } - services.networkMapCache.changed.subscribe { mapChange -> - // TODO how should we handle network map removal - if (mapChange is NetworkMapCache.MapChange.Added) { - mapChange.node.legalIdentitiesAndCerts.forEach { - identityService.verifyAndRegisterIdentity(it) - } - } - } - return identityService - } - override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { return E2ETestKeyManagementService(identityService, partyKeys + (notaryIdentity?.let { setOf(it.second) } ?: emptySet())) } @@ -391,13 +389,13 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } @JvmOverloads - fun createNotaryNode(legalName: CordaX500Name? = null, validating: Boolean = true): StartedNode { + fun createNotaryNode(legalName: CordaX500Name = DUMMY_NOTARY.name, validating: Boolean = true): StartedNode { return createNode(legalName = legalName, configOverrides = { whenever(it.notary).thenReturn(NotaryConfig(validating)) }) } - fun createNotaryNode(legalName: CordaX500Name? = null, + fun createNotaryNode(legalName: CordaX500Name = DUMMY_NOTARY.name, validating: Boolean = true, nodeFactory: Factory): StartedNode { return createNode(legalName = legalName, nodeFactory = nodeFactory, configOverrides = { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 3d49484fef..fa881e1703 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,15 +1,19 @@ package net.corda.testing.node +import com.google.common.collect.MutableClassToInstanceMap import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* +import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.StateLoader import net.corda.core.node.services.* -import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction @@ -46,7 +50,7 @@ import java.util.* * building chains of transactions and verifying them. It isn't sufficient for testing flows however. */ open class MockServices( - cordappPackages: List, + cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions), vararg val keys: KeyPair @@ -96,24 +100,22 @@ open class MockServices( /** * Makes database and mock services appropriate for unit tests. - * - * @param customSchemas a set of schemas being used by [NodeSchemaService] - * @param keys a lis of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] + * @param keys a list of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] * @param createIdentityService a lambda function returning an instance of [IdentityService]. Defauts to [InMemoryIdentityService]. * * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic - fun makeTestDatabaseAndMockServices(customSchemas: Set = emptySet(), - keys: List = listOf(MEGA_CORP_KEY), + fun makeTestDatabaseAndMockServices(keys: List = listOf(MEGA_CORP_KEY), createIdentityService: () -> IdentityService = { makeTestIdentityService() }, cordappPackages: List = emptyList()): Pair { + val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() val identityServiceRef: IdentityService by lazy { createIdentityService() } - val database = configureDatabase(dataSourceProps, databaseProperties, NodeSchemaService(customSchemas), { identityServiceRef }) + val database = configureDatabase(dataSourceProps, databaseProperties, { identityServiceRef }, NodeSchemaService(cordappLoader)) val mockService = database.transaction { - object : MockServices(cordappPackages, *(keys.toTypedArray())) { + object : MockServices(cordappLoader, *(keys.toTypedArray())) { override val identityService: IdentityService = database.transaction { identityServiceRef } override val vaultService = makeVaultService(database.hibernateConfig) @@ -132,7 +134,8 @@ open class MockServices( } } - constructor(cordappPackages: List, vararg keys: KeyPair) : this(cordappPackages, MockTransactionStorage(), keys = *keys) + private constructor(cordappLoader: CordappLoader, vararg keys: KeyPair) : this(cordappLoader, MockTransactionStorage(), keys = *keys) + constructor(cordappPackages: List, vararg keys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), keys = *keys) constructor(vararg keys: KeyPair) : this(emptyList(), *keys) constructor() : this(generateKeyPair()) @@ -162,18 +165,21 @@ open class MockServices( return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - val mockCordappProvider: MockCordappProvider = MockCordappProvider(CordappLoader.createWithTestPackages(cordappPackages + CordappLoader.testPackages)).start(attachments) as MockCordappProvider - override val cordappProvider: CordappProvider = mockCordappProvider - + val mockCordappProvider = MockCordappProvider(cordappLoader, attachments) + override val cordappProvider: CordappProvider get() = mockCordappProvider lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultServiceInternal { + fun makeVaultService(hibernateConfig: HibernateConfiguration): VaultServiceInternal { val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, stateLoader, hibernateConfig) - hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) + hibernatePersister = HibernateObserver.install(vaultService.rawUpdates, hibernateConfig) return vaultService } - override fun cordaService(type: Class): T = throw IllegalArgumentException("${type.name} not found") + val cordappServices = MutableClassToInstanceMap.create() + override fun cordaService(type: Class): T { + require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } + return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + } override fun jdbcSession(): Connection = throw UnsupportedOperationException() } @@ -244,3 +250,23 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id] } + +fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { + class MockAppServiceHubImpl(val serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : AppServiceHub, ServiceHub by serviceHub { + val serviceInstance: T + + init { + serviceInstance = serviceConstructor(this) + serviceHub.cordappServices.putInstance(serviceInstance.javaClass, serviceInstance) + } + + override fun startFlow(flow: FlowLogic): FlowHandle { + throw UnsupportedOperationException() + } + + override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { + throw UnsupportedOperationException() + } + } + return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 3d4d4e9d63..36dfb43a45 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -9,6 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.Node import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User @@ -31,7 +32,7 @@ import kotlin.concurrent.thread * purposes. Use the driver if you need to run the nodes in separate processes otherwise this class will suffice. */ // TODO Some of the logic here duplicates what's in the driver -abstract class NodeBasedTest : TestDependencyInjectionBase() { +abstract class NodeBasedTest(private val cordappPackages: List = emptyList()) : TestDependencyInjectionBase() { companion object { private val WHITESPACE = "\\s++".toRegex() } @@ -193,7 +194,8 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { val node = Node( parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), - initialiseSerialization = false).start() + initialiseSerialization = false, + cordappLoader = CordappLoader.createDefaultWithTestPackages(parsedConfig, cordappPackages)).start() nodes += node thread(name = legalName.organisation) { node.internals.run() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index cfb1e3a29d..6dad146916 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -14,8 +14,8 @@ import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient +import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.testing.MockServiceHubInternal import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -37,12 +37,12 @@ class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) - val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, NodeSchemaService(), { InMemoryIdentityService(trustRoot = trustRoot) }) + val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, { InMemoryIdentityService(trustRoot = trustRoot) }) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.organisation, 1) // TODO: We should have a dummy service hub rather than change behaviour in tests val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, - MockNetworkMapCache(serviceHub = object : MockServiceHubInternal(database = database, configuration = config) {}), userService) + NetworkMapCacheImpl(MockNetworkMapCache(database, config), identityService), userService) val networkMapRegistrationFuture = openFuture() val network = database.transaction { NodeMessagingClient( diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 4e20194249..71be78c0bd 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -17,7 +17,6 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CertificateAndKeyPair @@ -70,11 +69,11 @@ val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CordaX500Name(organisation = "MiniCorp", locality = "London", country = "GB"), MINI_CORP_PUBKEY) val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party +val BOC_NAME: CordaX500Name = CordaX500Name(organisation = "BankOfCorda", locality = "London", country = "GB") val BOC_KEY: KeyPair by lazy { generateKeyPair() } val BOC_PUBKEY: PublicKey get() = BOC_KEY.public -val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CordaX500Name(organisation = "BankOfCorda", locality = "London", country = "GB"), BOC_PUBKEY) +val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOC_NAME, BOC_PUBKEY) val BOC: Party get() = BOC_IDENTITY.party -val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() } val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public @@ -167,20 +166,14 @@ inline fun T.amqpSpecific(reason: String, function: () -> Unit fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCerts.first() fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party +/** + * Extract a single identity from the node info. Throws an error if the node has multiple identities. + */ +fun NodeInfo.singleIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCerts.single() + +/** + * Extract a single identity from the node info. Throws an error if the node has multiple identities. + */ +fun NodeInfo.singleIdentity(): Party = singleIdentityAndCert().party /** Returns the identity of the first notary found on the network */ fun ServiceHub.getDefaultNotary(): Party = networkMapCache.notaryIdentities.first() - -/** - * Set the package to scan for cordapps - this overrides the default behaviour of scanning the cordapps directory - * @param packageNames A package name that you wish to scan for cordapps - */ -fun setCordappPackages(vararg packageNames: String) { - CordappLoader.testPackages = packageNames.toList() -} - -/** - * Unsets the default overriding behaviour of [setCordappPackages] - */ -fun unsetCordappPackages() { - CordappLoader.testPackages = emptyList() -} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt index ad2e488e91..1fab8508fc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt @@ -1,5 +1,6 @@ package net.corda.testing +import net.corda.core.internal.packageName import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext @@ -25,7 +26,7 @@ object LogHelper { } } - fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.java.`package`.name }.toTypedArray()) + fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.packageName }.toTypedArray()) /** Removes custom configuration for the specified logger names */ fun reset(vararg names: String) { @@ -35,7 +36,7 @@ object LogHelper { loggerContext.updateLoggers(config) } - fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.java.`package`.name }.toTypedArray()) + fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.packageName }.toTypedArray()) /** Updates logging level for the specified Log4j logger name */ private fun setLevel(name: String, level: Level) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 891808e4b5..830316435e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -103,6 +103,10 @@ class TestSerializationFactory : SerializationFactory() { return delegate!!.deserialize(byteSequence, clazz, context) } + override fun deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): ObjectWithCompatibleContext { + return delegate!!.deserializeWithCompatibleContext(byteSequence, clazz, context) + } + override fun serialize(obj: T, context: SerializationContext): SerializedBytes { return delegate!!.serialize(obj, context) } @@ -147,7 +151,7 @@ class TestSerializationContext : SerializationContext { return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withWhitelisted(clazz) } } - override fun withPreferredSerializationVersion(versionHeader: ByteSequence): SerializationContext { + override fun withPreferredSerializationVersion(versionHeader: VersionHeader): SerializationContext { return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withPreferredSerializationVersion(versionHeader) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index a7f7e11157..9e73764f06 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -31,6 +31,7 @@ val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) /** Dummy notary identity for tests and simulations */ val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) val DUMMY_NOTARY: Party get() = Party(CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"), DUMMY_NOTARY_KEY.public) +val DUMMY_NOTARY_SERVICE_NAME: CordaX500Name = DUMMY_NOTARY.name.copy(commonName = "corda.notary.validating") val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) } /** Dummy network map service identity for tests and simulations */ @@ -51,17 +52,20 @@ val DUMMY_BANK_C: Party get() = Party(CordaX500Name(organisation = "Bank C", loc val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) } /** Dummy individual identity for tests and simulations */ val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE) -val ALICE: Party get() = Party(CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"), ALICE_KEY.public) +val ALICE_NAME = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES") +val ALICE: Party get() = Party(ALICE_NAME, ALICE_KEY.public) val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) } /** Dummy individual identity for tests and simulations */ val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB) -val BOB: Party get() = Party(CordaX500Name(organisation = "Bob Plc", locality = "Rome", country = "IT"), BOB_KEY.public) +val BOB_NAME = CordaX500Name(organisation = "Bob Plc", locality = "Rome", country = "IT") +val BOB: Party get() = Party(BOB_NAME, BOB_KEY.public) val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } /** Dummy individual identity for tests and simulations */ val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE) -val CHARLIE: Party get() = Party(CordaX500Name(organisation = "Charlie Ltd", locality = "Athens", country = "GR"), CHARLIE_KEY.public) +val CHARLIE_NAME = CordaX500Name(organisation = "Charlie Ltd", locality = "Athens", country = "GR") +val CHARLIE: Party get() = Party(CHARLIE_NAME, CHARLIE_KEY.public) val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } /** Dummy regulator for tests and simulations */ diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index dae72192a6..5c8736335e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -4,12 +4,13 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.AttachmentStorage import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import java.nio.file.Paths import java.util.* -class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(cordappLoader) { +class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) { val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index c2d1142cc7..c0f21ad796 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -46,6 +46,8 @@ dependencies { // Controls FX: more java FX components http://fxexperience.com/controlsfx/ compile "org.controlsfx:controlsfx:$controlsfx_version" + compile "net.corda.plugins:cordform-common:$gradle_plugins_version" + compile project(':client:rpc') compile project(':finance') @@ -122,12 +124,12 @@ distributions { } from(project(':finance').tasks.jar) { rename 'corda-finance-(.*)', 'corda-finance.jar' - into 'plugins' + into 'cordapps' fileMode = 0444 } from(project(':samples:bank-of-corda-demo').jar) { rename 'bank-of-corda-demo-(.*)', 'bank-of-corda.jar' - into 'plugins' + into 'cordapps' fileMode = 0444 } } @@ -201,7 +203,7 @@ task javapackage(dependsOn: distZip) { fileset(dir: dist_source, type: 'data') { include(name: 'corda/*.jar') - include(name: 'plugins/*.jar') + include(name: 'cordapps/*.jar') include(name: 'explorer/*.jar') } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 165388b2b0..286ad1e4c6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -5,6 +5,7 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.utilities.loggerFor import net.corda.demobench.model.JVMConfig +import net.corda.demobench.model.NodeConfig import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.readErrorLines import tornadofx.* @@ -82,11 +83,11 @@ class Explorer internal constructor(private val explorerController: ExplorerCont // Note: does not copy dependencies because we should soon be making all apps fat jars and dependencies implicit. // // TODO: Remove this code when serialisation has been upgraded. - val pluginsDir = config.explorerDir / "plugins" - pluginsDir.createDirectories() - config.pluginDir.list { + val cordappsDir = config.explorerDir / NodeConfig.cordappDirName + cordappsDir.createDirectories() + config.cordappsDir.list { it.forEachOrdered { path -> - val destPath = pluginsDir / path.fileName.toString() + val destPath = cordappsDir / path.fileName.toString() try { // Try making a symlink to make things faster and use less disk space. Files.createSymbolicLink(destPath, path) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/DemoBenchNodeInfoFilesCopier.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DemoBenchNodeInfoFilesCopier.kt new file mode 100644 index 0000000000..a0818c3532 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DemoBenchNodeInfoFilesCopier.kt @@ -0,0 +1,35 @@ +package net.corda.demobench.model + +import net.corda.nodeapi.NodeInfoFilesCopier +import rx.Scheduler +import rx.schedulers.Schedulers +import tornadofx.* + +/** + * Utility class which copies nodeInfo files across a set of running nodes. + * + * This class will create paths that it needs to poll and to where it needs to copy files in case those + * don't exist yet. + */ +class DemoBenchNodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()): Controller() { + + private val nodeInfoFilesCopier = NodeInfoFilesCopier(scheduler) + + /** + * @param nodeConfig the configuration to be added. + * Add a [NodeConfig] for a node which is about to be started. + * Its nodeInfo file will be copied to other nodes' additional-node-infos directory, and conversely, + * other nodes' nodeInfo files will be copied to this node additional-node-infos directory. + */ + fun addConfig(nodeConfig: NodeConfigWrapper) : Unit = nodeInfoFilesCopier.addConfig(nodeConfig.nodeDir) + + /** + * @param nodeConfig the configuration to be removed. + * Remove the configuration of a node which is about to be stopped or already stopped. + * No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this + * one. + */ + fun removeConfig(nodeConfig: NodeConfigWrapper) : Unit = nodeInfoFilesCopier.removeConfig(nodeConfig.nodeDir) + + fun reset() : Unit = nodeInfoFilesCopier.reset() +} \ No newline at end of file diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt similarity index 56% rename from tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt rename to tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt index 52a388b7a7..de0a2607b5 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt @@ -2,6 +2,6 @@ package net.corda.demobench.model import java.nio.file.Path -interface HasPlugins { - val pluginDir: Path +interface HasCordapps { + val cordappsDir: Path } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index f34ebfef6d..d2dba4a687 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -37,9 +37,9 @@ class InstallFactory : Controller() { * Wraps the configuration information for a Node * which isn't ready to be instantiated yet. */ -class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfigWrapper) : HasPlugins { +class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfigWrapper) : HasCordapps { val key = config.key - override val pluginDir: Path = baseDir / "plugins" + override val cordappsDir: Path = baseDir / "cordapps" fun deleteBaseDir(): Boolean = baseDir.toFile().deleteRecursively() fun installTo(installDir: Path) = config.copy(baseDir = installDir) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index e536b3bfb4..504fdfab79 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -31,6 +31,7 @@ data class NodeConfig( companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) val defaultUser = user("guest") + val cordappDirName = "cordapps" } @Suppress("unused") @@ -56,18 +57,18 @@ data class NotaryService(val validating: Boolean) : ExtraService { } // TODO Think of a better name -data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : HasPlugins { +data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : HasCordapps { val key: String = nodeConfig.myLegalName.organisation.toKey() val nodeDir: Path = baseDir / key val explorerDir: Path = baseDir / "$key-explorer" - override val pluginDir: Path = nodeDir / "plugins" + override val cordappsDir: Path = nodeDir / NodeConfig.cordappDirName var state: NodeState = NodeState.STARTING fun install(cordapps: Collection) { if (cordapps.isEmpty()) return - pluginDir.createDirectories() + cordappsDir.createDirectories() for (cordapp in cordapps) { - cordapp.copyToDirectory(pluginDir, StandardCopyOption.REPLACE_EXISTING) + cordapp.copyToDirectory(cordappsDir, StandardCopyOption.REPLACE_EXISTING) } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 4e7b6a6cc7..2830c969b8 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle import net.corda.core.utilities.NetworkHostAndPort -import net.corda.demobench.plugin.PluginController +import net.corda.demobench.plugin.CordappController import net.corda.demobench.pty.R3Pty import tornadofx.* import java.io.IOException @@ -27,7 +27,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { } private val jvm by inject() - private val pluginController by inject() + private val cordappController by inject() + private val nodeInfoFilesCopier by inject() private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") @@ -86,12 +87,16 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { log.info("Network map provided by: ${nodeConfig.myLegalName}") } + nodeInfoFilesCopier.addConfig(wrapper) + return wrapper } fun dispose(config: NodeConfigWrapper) { config.state = NodeState.DEAD + nodeInfoFilesCopier.removeConfig(config) + if (config.nodeConfig.isNetworkMap) { log.warning("Network map service (Node '${config.nodeConfig.myLegalName}') has exited.") } @@ -112,7 +117,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { config.nodeDir.createDirectories() // Install any built-in plugins into the working directory. - pluginController.populate(config) + cordappController.populate(config) // Write this node's configuration file into its working directory. val confFile = config.nodeDir / "node.conf" @@ -138,6 +143,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Wipe out any knowledge of previous nodes. networkMapConfig = null nodes.clear() + nodeInfoFilesCopier.reset() } /** @@ -147,6 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { if (nodes.putIfAbsent(config.key, config) != null) { return false } + nodeInfoFilesCopier.addConfig(config) updatePort(config.nodeConfig) @@ -164,9 +171,9 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { fun install(config: InstallConfig): NodeConfigWrapper { val installed = config.installTo(baseDir) - pluginController.userPluginsFor(config).forEach { - installed.pluginDir.createDirectories() - val plugin = it.copyToDirectory(installed.pluginDir) + cordappController.useCordappsFor(config).forEach { + installed.cordappsDir.createDirectories() + val plugin = it.copyToDirectory(installed.cordappsDir) log.info("Installed: $plugin") } 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 new file mode 100644 index 0000000000..765824f4e8 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt @@ -0,0 +1,61 @@ +package net.corda.demobench.plugin + +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.exists +import net.corda.demobench.model.HasCordapps +import net.corda.demobench.model.JVMConfig +import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper +import tornadofx.* +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.stream.Stream + +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") + + /** + * Install any built-in cordapps that this node requires. + */ + @Throws(IOException::class) + fun populate(config: NodeConfigWrapper) { + if (!config.cordappsDir.exists()) { + config.cordappsDir.createDirectories() + } + if (finance.exists()) { + 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") + } + } + + /** + * Generates a stream of a node's non-built-in cordapps. + */ + @Throws(IOException::class) + fun useCordappsFor(config: HasCordapps): Stream = walkCordapps(config.cordappsDir) + .filter { !bankOfCorda.endsWith(it.fileName) } + .filter { !finance.endsWith(it.fileName) } + + private fun walkCordapps(cordappsDir: Path): Stream { + return if (Files.isDirectory(cordappsDir)) + Files.walk(cordappsDir, 1).filter(Path::isCordapp) + else + Stream.empty() + } + +} + +fun Path.isCordapp(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar") +fun Path.inCordappsDir(): Boolean = (this.parent != null) && this.parent.endsWith("cordapps/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt deleted file mode 100644 index ac25d72c55..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt +++ /dev/null @@ -1,58 +0,0 @@ -package net.corda.demobench.plugin - -import net.corda.core.internal.copyToDirectory -import net.corda.core.internal.createDirectories -import net.corda.core.internal.exists -import net.corda.demobench.model.HasPlugins -import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeConfigWrapper -import tornadofx.* -import java.io.IOException -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption -import java.util.stream.Stream - -class PluginController : Controller() { - - private val jvm by inject() - private val pluginDir: Path = jvm.applicationDir.resolve("plugins") - private val bankOfCorda: Path = pluginDir.resolve("bank-of-corda.jar") - private val finance: Path = pluginDir.resolve("corda-finance.jar") - - /** - * Install any built-in plugins that this node requires. - */ - @Throws(IOException::class) - fun populate(config: NodeConfigWrapper) { - config.pluginDir.createDirectories() - if (finance.exists()) { - finance.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Finance' plugin") - } - // Nodes cannot issue cash unless they contain the "Bank of Corda" plugin. - if (config.nodeConfig.issuableCurrencies.isNotEmpty() && bankOfCorda.exists()) { - bankOfCorda.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Bank of Corda' plugin") - } - } - - /** - * Generates a stream of a node's non-built-in plugins. - */ - @Throws(IOException::class) - fun userPluginsFor(config: HasPlugins): Stream = walkPlugins(config.pluginDir) - .filter { !bankOfCorda.endsWith(it.fileName) } - .filter { !finance.endsWith(it.fileName) } - - private fun walkPlugins(pluginDir: Path): Stream { - return if (Files.isDirectory(pluginDir)) - Files.walk(pluginDir, 1).filter(Path::isPlugin) - else - Stream.empty() - } - -} - -fun Path.isPlugin(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar") -fun Path.inPluginsDir(): Boolean = (this.parent != null) && this.parent.endsWith("plugins/") 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 c963a1c9ae..9f8a380b38 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 @@ -6,13 +6,10 @@ import javafx.stage.FileChooser import javafx.stage.FileChooser.ExtensionFilter import net.corda.core.internal.createDirectories import net.corda.core.internal.div -import net.corda.demobench.model.InstallConfig -import net.corda.demobench.model.InstallFactory -import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeController -import net.corda.demobench.plugin.PluginController -import net.corda.demobench.plugin.inPluginsDir -import net.corda.demobench.plugin.isPlugin +import net.corda.demobench.model.* +import net.corda.demobench.plugin.CordappController +import net.corda.demobench.plugin.inCordappsDir +import net.corda.demobench.plugin.isCordapp import tornadofx.* import java.io.File import java.io.IOException @@ -31,7 +28,7 @@ class ProfileController : Controller() { private val jvm by inject() private val baseDir: Path = jvm.dataHome private val nodeController by inject() - private val pluginController by inject() + private val cordappController by inject() private val installFactory by inject() private val chooser = FileChooser() @@ -64,11 +61,11 @@ class ProfileController : Controller() { val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8)) log.info("Wrote: $file") - // Write all of the non-built-in plugins. - val pluginDir = Files.createDirectory(nodeDir.resolve("plugins")) - pluginController.userPluginsFor(config).forEach { - val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString())) - log.info("Wrote: $plugin") + // Write all of the non-built-in cordapps. + val cordappDir = Files.createDirectory(nodeDir.resolve(NodeConfig.cordappDirName)) + cordappController.useCordappsFor(config).forEach { + val cordapp = Files.copy(it, cordappDir.resolve(it.fileName.toString())) + log.info("Wrote: $cordapp") } } } @@ -120,16 +117,16 @@ 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.inPluginsDir() && p.isPlugin() && attr.isRegularFile }) } - .forEach { plugin -> - val config = nodeIndex[plugin.getName(0).toString()] ?: return@forEach + .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 try { - val pluginDir = Files.createDirectories(config.pluginDir) - Files.copy(plugin, pluginDir.resolve(plugin.fileName.toString())) - log.info("Loaded: $plugin") + val cordappDir = Files.createDirectories(config.cordappsDir) + Files.copy(cordapp, cordappDir.resolve(cordapp.fileName.toString())) + log.info("Loaded: $cordapp") } catch (e: Exception) { - log.log(Level.SEVERE, "Failed to extract '$plugin': ${e.message}", e) + log.log(Level.SEVERE, "Failed to extract '$cordapp': ${e.message}", e) configs.forEach { c -> c.deleteBaseDir() } throw e } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index e8fb14dff4..0bf02c1ca8 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -5,6 +5,7 @@ import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs diff --git a/tools/explorer/src/main/java/ExplorerCaplet.java b/tools/explorer/src/main/java/ExplorerCaplet.java index 7ab25d09b5..fdeb2821fb 100644 --- a/tools/explorer/src/main/java/ExplorerCaplet.java +++ b/tools/explorer/src/main/java/ExplorerCaplet.java @@ -19,7 +19,7 @@ public class ExplorerCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - List classpath = augmentClasspath((List) cp, "plugins"); + List classpath = augmentClasspath((List) cp, "cordapps"); return (T) augmentClasspath(classpath, "dependencies"); } return super.attribute(attr); diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 00fa22724f..279d34f6b2 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -74,7 +74,6 @@ fun verifierDriver( debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), systemProperties: Map = emptyMap(), useTestClock: Boolean = false, - networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false), startNodesInProcess: Boolean = false, extraCordappPackagesToScan: List = emptyList(), dsl: VerifierExposedDSLInterface.() -> A @@ -86,7 +85,6 @@ fun verifierDriver( systemProperties = systemProperties, driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, - networkMapStartStrategy = networkMapStartStrategy, isDebug = isDebug, startNodesInProcess = startNodesInProcess, extraCordappPackagesToScan = extraCordappPackagesToScan diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index d12dbc7a41..dc228fd6f6 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -1,22 +1,26 @@ package net.corda.verifier +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.config.VerifierType import net.corda.testing.ALICE +import net.corda.testing.ALICE_NAME import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.chooseIdentity -import net.corda.testing.driver.NetworkMapStartStrategy +import net.corda.testing.DUMMY_NOTARY_SERVICE_NAME import org.junit.Test import java.util.* import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertTrue +import kotlin.test.assertNotNull class VerifierTests { private fun generateTransactions(number: Int): List { @@ -48,6 +52,21 @@ class VerifierTests { } } + @Test + fun `single verification fails`() { + verifierDriver(extraCordappPackagesToScan = listOf("net.corda.finance.contracts")) { + val aliceFuture = startVerificationRequestor(ALICE.name) + // Generate transactions as per usual, but then remove attachments making transaction invalid. + val transactions = generateTransactions(1).map { it.copy(attachments = emptyList()) } + val alice = aliceFuture.get() + startVerifier(alice) + alice.waitUntilNumberOfVerifiers(1) + val verificationRejection = transactions.map { alice.verifyTransaction(it) }.transpose().get().single() + assertTrue { verificationRejection is TransactionVerificationException.MissingAttachmentRejection} + assertTrue { verificationRejection!!.message!!.contains("Contract constraints failed, could not find attachment") } + } + } + @Test fun `multiple verifiers work with requestor`() { verifierDriver { @@ -110,20 +129,21 @@ class VerifierTests { @Test fun `single verifier works with a node`() { - verifierDriver( - networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), - extraCordappPackagesToScan = listOf("net.corda.finance.contracts") - ) { + verifierDriver(extraCordappPackagesToScan = listOf("net.corda.finance.contracts")) { val aliceFuture = startNode(providedName = ALICE.name) val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, verifierType = VerifierType.OutOfProcess) - val alice = aliceFuture.get() - val notary = notaryFuture.get() - val notaryIdentity = notary.nodeInfo.legalIdentities[1] - startVerifier(notary) - alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notaryIdentity).returnValue.get() - notary.waitUntilNumberOfVerifiers(1) + val aliceNode = aliceFuture.get() + val notaryNode = notaryFuture.get() + val alice = aliceNode.rpc.wellKnownPartyFromX500Name(ALICE_NAME)!! + val notary = notaryNode.rpc.notaryPartyFromX500Name(DUMMY_NOTARY_SERVICE_NAME)!! + startVerifier(notaryNode) + notaryNode.pollUntilKnowsAbout(aliceNode).getOrThrow() + aliceNode.pollUntilKnowsAbout(notaryNode).getOrThrow() + aliceNode.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notary).returnValue.get() + notaryNode.waitUntilNumberOfVerifiers(1) for (i in 1..10) { - alice.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice.nodeInfo.chooseIdentity()).returnValue.get() + val cashFlowResult = aliceNode.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice).returnValue.get() + assertNotNull(cashFlowResult) } } } diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index aaf0c3ac73..22ff4e9f60 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -17,17 +17,16 @@ import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue import net.corda.nodeapi.internal.addShutdownHook -import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme -import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT -import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 -import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.* +import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import org.apache.activemq.artemis.api.core.client.ActiveMQClient import java.nio.file.Path import java.nio.file.Paths data class VerifierConfiguration( override val baseDirectory: Path, - val config: Config + val config: Config // NB: This property is being used via reflection. ) : NodeSSLConfiguration { val nodeHostAndPort: NetworkHostAndPort by config override val keyStorePassword: String by config @@ -66,7 +65,7 @@ class Verifier { val consumer = session.createConsumer(VERIFICATION_REQUESTS_QUEUE_NAME) val replyProducer = session.createProducer() consumer.setMessageHandler { - val request = VerifierApi.VerificationRequest.fromClientMessage(it) + val (request, context) = VerifierApi.VerificationRequest.fromClientMessage(it) log.debug { "Received verification request with id ${request.verificationId}" } val error = try { request.transaction.verify() @@ -77,7 +76,7 @@ class Verifier { } val reply = session.createMessage(false) val response = VerifierApi.VerificationResponse(request.verificationId, error) - response.writeToClientMessage(reply) + response.writeToClientMessage(reply, context) replyProducer.send(request.responseAddress, reply) it.acknowledge() } @@ -88,13 +87,18 @@ class Verifier { private fun initialiseSerialization() { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { - registerScheme(KryoVerifierSerializationScheme()) + registerScheme(KryoVerifierSerializationScheme) + registerScheme(AMQPVerifierSerializationScheme) } + /** + * Even though default context is set to Kryo P2P, the encoding will be adjusted depending on the incoming + * request received, see use of [context] in [main] method. + */ SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT } } - class KryoVerifierSerializationScheme : AbstractKryoSerializationScheme() { + private object KryoVerifierSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P } @@ -102,4 +106,13 @@ class Verifier { override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException() override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() } + + private object AMQPVerifierSerializationScheme : AbstractAMQPSerializationScheme() { + override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { + return (byteSequence == AmqpHeaderV1_0 && (target == SerializationContext.UseCase.P2P)) + } + + override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory = throw UnsupportedOperationException() + override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory = throw UnsupportedOperationException() + } } \ No newline at end of file