diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 198a957a38..4471dd86d8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -10,6 +10,8 @@ + + diff --git a/build.gradle b/build.gradle index 2408dffeb7..2da8daa2e8 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 = "0.16-SNAPSHOT" + ext.corda_release_version = "3.0-ENT-RC01" // Increment this on any release that changes public APIs anywhere in the Corda platform ext.corda_platform_version = constants.getProperty("platformVersion") ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") @@ -20,7 +20,21 @@ buildscript { ext.capsule_version = '1.0.1' ext.asm_version = '0.5.3' - ext.artemis_version = '2.4.0' + + /* + * TODO Upgrade to version 2.4 for large message streaming support + * + * Due to a memory leak in the connection handling code in Artemis, we are + * temporarily downgrading to version 2.2.0. + * + * The memory leak essentially triggers an out-of-memory exception within + * less than 10 seconds and can take down a node if a non-TLS connection is + * attempted against the P2P port. + * + * The issue has been reported to upstream: + * https://issues.apache.org/jira/browse/ARTEMIS-1559 + */ + ext.artemis_version = '2.2.0' ext.jackson_version = '2.9.2' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 546d2dc088..65800b7a8f 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -12,6 +12,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.finance.USD import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import org.junit.Before import org.junit.Rule import org.junit.Test @@ -24,7 +25,7 @@ class JacksonSupportTest { private companion object { val SEED = BigInteger.valueOf(20170922L)!! val mapper = JacksonSupport.createNonRpcMapper() - val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey + val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party } 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 218258c305..7bfc292895 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 @@ -31,8 +31,8 @@ import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.contracts.GetBalances.getCashBalance; import static net.corda.node.services.Permissions.invokeRpc; import static net.corda.node.services.Permissions.startFlow; -import static net.corda.testing.TestConstants.getALICE_NAME; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME; +import static net.corda.testing.TestConstants.ALICE_NAME; +import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME; public class CordaRPCJavaClientTest extends NodeBasedTest { public CordaRPCJavaClientTest() { @@ -41,7 +41,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @ClassRule public static IntegrationTestSchemas databaseSchemas = new IntegrationTestSchemas(IntegrationTestKt.toDatabaseSchemaName(getALICE_NAME()), - IntegrationTestKt.toDatabaseSchemaName(getDUMMY_NOTARY_NAME())); + IntegrationTestKt.toDatabaseSchemaName(DUMMY_NOTARY_NAME)); private List perms = Arrays.asList( startFlow(CashPaymentFlow.class), @@ -65,7 +65,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws Exception { super.setUp(); - node = startNode(getALICE_NAME(), 1, singletonList(rpcUser)); + node = startNode(ALICE_NAME, 1, singletonList(rpcUser)); client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/Measure.kt similarity index 98% rename from testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt rename to client/rpc/src/test/kotlin/net/corda/client/rpc/Measure.kt index a44c28ade2..1e288f2741 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/Measure.kt @@ -1,4 +1,4 @@ -package net.corda.testing +package net.corda.client.rpc import net.corda.core.internal.uncheckedCast import kotlin.reflect.KCallable diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 22c160a1d0..5ea77a66fd 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -9,9 +9,9 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.node.services.messaging.RPCServerConfiguration +import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.node.internal.rpcDriver -import net.corda.testing.internal.testThreadFactory import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet import org.junit.After import org.junit.Test diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index 85d3351ade..b945487cb9 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -12,7 +12,6 @@ import net.corda.testing.node.internal.performance.startPublishingFixedRateInjec import net.corda.testing.node.internal.performance.startReporter import net.corda.testing.node.internal.performance.startTightLoopInjector import net.corda.testing.node.internal.rpcDriver -import net.corda.testing.measure import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 9d57aa401e..fae5fbc6d5 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -1,6 +1,9 @@ package net.corda.client.rpc +import net.corda.core.flows.FlowLogic +import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps +import net.corda.node.services.Permissions import net.corda.node.services.messaging.rpcContext import net.corda.nodeapi.internal.config.User import net.corda.testing.node.internal.RPCDriverDSL 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 bc5ad98a35..2c94e44717 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -15,7 +15,7 @@ class SwapIdentitiesFlowTests { @Before fun setup() { // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + mockNet = MockNetwork(emptyList(), networkSendManuallyPumped = false, threadPerNode = true) } @Test diff --git a/constants.properties b/constants.properties index 0434adb857..935fb23f75 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.1 +gradlePluginsVersion=3.0.2 kotlinVersion=1.1.60 platformVersion=1 guavaVersion=21.0 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 ccae80d47e..cd153a18c0 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -63,7 +63,7 @@ import javax.crypto.spec.SecretKeySpec */ object Crypto { /** - * RSA_SHA256 signature scheme using SHA256 as hash algorithm. + * RSA signature scheme using SHA256 for message hashing. * Note: Recommended key size >= 3072 bits. */ @JvmField @@ -80,7 +80,7 @@ object Crypto { "RSA_SHA256 signature scheme using SHA256 as hash algorithm." ) - /** ECDSA signature scheme using the secp256k1 Koblitz curve. */ + /** ECDSA signature scheme using the secp256k1 Koblitz curve and SHA256 for message hashing. */ @JvmField val ECDSA_SECP256K1_SHA256 = SignatureScheme( 2, @@ -95,7 +95,7 @@ object Crypto { "ECDSA signature scheme using the secp256k1 Koblitz curve." ) - /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */ + /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve and SHA256 for message hashing. */ @JvmField val ECDSA_SECP256R1_SHA256 = SignatureScheme( 3, @@ -110,7 +110,7 @@ object Crypto { "ECDSA signature scheme using the secp256r1 (NIST P-256) curve." ) - /** EdDSA signature scheme using the ed255519 twisted Edwards curve. */ + /** EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. */ @JvmField val EDDSA_ED25519_SHA512 = SignatureScheme( 4, @@ -127,13 +127,15 @@ object Crypto { "EdDSA signature scheme using the ed25519 twisted Edwards curve." ) - /** - * SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers - * at the cost of larger key sizes and loss of compatibility. - */ + /** DLSequence (ASN1Sequence) for SHA512 truncated to 256 bits, used in SPHINCS-256 signature scheme. */ @JvmField val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256)) + /** + * SPHINCS-256 hash-based signature scheme using SHA512 for message hashing. It provides 128bit security against + * post-quantum attackers at the cost of larger key nd signature sizes and loss of compatibility. + */ + // TODO: change val name to SPHINCS256_SHA512. This will break backwards compatibility. @JvmField val SPHINCS256_SHA256 = SignatureScheme( 5, @@ -149,7 +151,8 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) - /** Corda composite key type. */ + /** Corda [CompositeKey] signature type. */ + // TODO: change the val name to a more descriptive one as it's now confusing and looks like a Key type. @JvmField val COMPOSITE_KEY = SignatureScheme( 6, diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 0db44ba841..7bd11ec661 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -22,7 +22,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). */ @Throws(InvalidKeyException::class, SignatureException::class) - fun verify(content: ByteArray) = by.verify(content, this) + fun verify(content: ByteArray): Boolean = by.verify(content, this) /** * Utility to simplify the act of verifying a signature. @@ -32,7 +32,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). */ @Throws(InvalidKeyException::class, SignatureException::class) - fun verify(content: OpaqueBytes) = by.verify(content.bytes, this) + fun verify(content: OpaqueBytes): Boolean = by.verify(content.bytes, this) /** * Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an @@ -45,7 +45,8 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { * @return whether the signature is correct for this key. */ @Throws(InvalidKeyException::class, SignatureException::class) - fun isValid(content: ByteArray) = by.isValid(content, this) - fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes) + fun isValid(content: ByteArray): Boolean = by.isValid(content, this) + + fun withoutKey(): DigitalSignature = DigitalSignature(this.bytes) } } diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index c3e394b6b5..0b6a3ddeb7 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -2,6 +2,7 @@ package net.corda.core.identity import com.google.common.collect.ImmutableSet import net.corda.core.internal.LegalNameValidator +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.x500Name import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.ASN1Encodable @@ -78,8 +79,9 @@ data class CordaX500Name(val commonName: String?, const val MAX_LENGTH_ORGANISATION_UNIT = 64 const val MAX_LENGTH_COMMON_NAME = 64 private val supportedAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L, BCStyle.CN, BCStyle.ST, BCStyle.OU) - private val countryCodes: Set = ImmutableSet.copyOf(Locale.getISOCountries()) - + @VisibleForTesting + val unspecifiedCountry = "ZZ" + private val countryCodes: Set = ImmutableSet.copyOf(Locale.getISOCountries() + unspecifiedCountry) @JvmStatic fun build(principal: X500Principal): CordaX500Name { val x500Name = X500Name.getInstance(principal.encoded) diff --git a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt index 725e75da0f..973de9e942 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -12,4 +12,5 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable class MissingContractAttachments(val states: List>) - : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}") \ No newline at end of file + : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}. " + + "See https://docs.corda.net/api-contract-constraints.html#debugging") \ No newline at end of file diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index 35be3604ae..2661d2b118 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -13,21 +13,22 @@ import org.junit.Test; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import static java.util.Collections.emptyList; import static net.corda.testing.CoreTestUtils.singleIdentity; import static net.corda.testing.node.NodeTestUtils.startFlow; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.Assert.fail; public class FlowsInJavaTest { - private final MockNetwork mockNet = new MockNetwork(); + private final MockNetwork mockNet = new MockNetwork(emptyList()); private StartedNode aliceNode; private StartedNode bobNode; private Party bob; @Before public void setUp() throws Exception { - aliceNode = mockNet.createPartyNode(TestConstants.getALICE_NAME()); - bobNode = mockNet.createPartyNode(TestConstants.getBOB_NAME()); + aliceNode = mockNet.createPartyNode(TestConstants.ALICE_NAME); + bobNode = mockNet.createPartyNode(TestConstants.BOB_NAME); bob = singleIdentity(bobNode.getInfo()); } diff --git a/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt b/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt index 830398964c..78f1586d8a 100644 --- a/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt @@ -3,7 +3,7 @@ package net.corda.core.concurrent import com.nhaarman.mockito_kotlin.* import net.corda.core.internal.concurrent.openFuture import net.corda.core.utilities.getOrThrow -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import org.slf4j.Logger diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index 460fda3328..f7f1abf245 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -9,6 +9,7 @@ import net.corda.core.node.ServicesForResolution import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 +import net.corda.testing.internal.rigorousMock import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 12ea44c72b..f20a4b20c9 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -9,7 +9,7 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import net.corda.nodeapi.internal.crypto.* -import net.corda.testing.kryoSpecific +import net.corda.testing.internal.kryoSpecific import net.corda.testing.SerializationEnvironmentRule import org.junit.Rule import org.junit.Test 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 3968d8cae3..18ecbcfd24 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -14,6 +14,10 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* +import net.corda.testing.dsl.LedgerDSL +import net.corda.testing.dsl.TestLedgerDSLInterpreter +import net.corda.testing.dsl.TestTransactionDSLInterpreter +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger import org.junit.Before @@ -31,9 +35,9 @@ class PartialMerkleTreeTest { val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey val MINI_CORP get() = miniCorp.party - val MINI_CORP_PUBKEY get() = miniCorp.pubkey + val MINI_CORP_PUBKEY get() = miniCorp.publicKey } @Rule @@ -51,7 +55,7 @@ class PartialMerkleTreeTest { hashed = nodes.map { it.serialize().sha256() } expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash merkleTree = MerkleTree.getMerkleTree(hashed) - testLedger = MockServices(rigorousMock().also { + testLedger = MockServices(emptyList(), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) }, MEGA_CORP.name).ledger(DUMMY_NOTARY) { unverifiedTransaction { 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 c11589b8e9..527fd33a49 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -29,7 +29,7 @@ class AttachmentTests { @Before fun setUp() { - mockNet = MockNetwork() + mockNet = MockNetwork(emptyList()) } @After 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 944e7eaca5..6da0ce7f18 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -14,6 +14,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import net.corda.testing.node.startFlow diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt index d90bf3e2e6..237474c902 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -13,7 +13,7 @@ import org.junit.After import org.junit.Test class ReceiveMultipleFlowTests { - private val mockNet = MockNetwork() + private val mockNet = MockNetwork(emptyList()) private val nodes = (0..2).map { mockNet.createPartyNode() } @After fun stopNodes() { diff --git a/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt b/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt index 0b52c89491..d04fae86bb 100644 --- a/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt @@ -4,7 +4,7 @@ import com.nhaarman.mockito_kotlin.* import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.join import net.corda.core.utilities.getOrThrow -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions import org.junit.Test import org.slf4j.Logger 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 8d9303db22..940d959860 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -70,7 +70,7 @@ class AttachmentSerializationTest { @Before fun setUp() { - mockNet = MockNetwork() + mockNet = MockNetwork(emptyList()) server = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) client = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. 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 ca795cf4f0..8f70d9610d 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -9,6 +9,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds import net.corda.finance.POUNDS import net.corda.testing.* +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Rule @@ -25,9 +26,9 @@ class TransactionSerializationTests { val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party val DUMMY_NOTARY get() = dummyNotary.party - val DUMMY_NOTARY_KEY get() = dummyNotary.key + val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_KEY get() = megaCorp.key + val MEGA_CORP_KEY get() = megaCorp.keyPair } @Rule @@ -106,7 +107,7 @@ class TransactionSerializationTests { Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) val ptx2 = notaryServices.signInitialTransaction(tx2) - val dummyServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2) + val dummyServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2) val stx2 = dummyServices.addSignature(ptx2) stx.copy(sigs = stx2.sigs).verifyRequiredSignatures() @@ -124,8 +125,7 @@ class TransactionSerializationTests { @Test fun storeAndLoadWhenSigning() { val ptx = megaCorpServices.signInitialTransaction(tx) - ptx.verifySignaturesExcept(notaryServices.key.public) - + ptx.verifySignaturesExcept(DUMMY_NOTARY_KEY.public) val stored = ptx.serialize() val loaded = stored.deserialize() diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index b343a5ed2a..fe7e977962 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.Party import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Rule @@ -28,7 +29,7 @@ class LedgerTransactionQueryTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val keyPair = generateKeyPair() - private val services = MockServices(rigorousMock().also { + private val services = MockServices(emptyList(), rigorousMock().also { doReturn(null).whenever(it).partyFromKey(keyPair.public) }, CordaX500Name("MegaCorp", "London", "GB"), keyPair) private val identity: Party = services.myInfo.singleIdentity() diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt index 1f487f6808..3e923c5a7d 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -12,6 +12,7 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger import org.junit.Rule @@ -27,7 +28,7 @@ class TransactionEncumbranceTests { val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey } @Rule @@ -61,7 +62,7 @@ class TransactionEncumbranceTests { } } - private val ledgerServices = MockServices(rigorousMock().also { + private val ledgerServices = MockServices(emptyList(), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) }, MEGA_CORP.name) diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 8280d688e6..8191dce1ae 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import org.junit.Rule import org.junit.Test import java.math.BigInteger @@ -23,7 +24,7 @@ class TransactionTests { val BOB = TestIdentity(BOB_NAME, 80).party val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) val DUMMY_NOTARY get() = dummyNotary.party - val DUMMY_NOTARY_KEY get() = dummyNotary.key + val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair } @Rule diff --git a/core/src/test/kotlin/net/corda/core/utilities/NonEmptySetTest.kt b/core/src/test/kotlin/net/corda/core/utilities/NonEmptySetTest.kt index 8d7cb3e12f..0a26639ae4 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/NonEmptySetTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/NonEmptySetTest.kt @@ -7,9 +7,10 @@ import com.google.common.collect.testing.features.CollectionSize import junit.framework.TestSuite import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.testing.withTestSerialization +import net.corda.testing.SerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Suite @@ -36,6 +37,10 @@ class NonEmptySetTest { } class General { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + @Test fun `copyOf - empty source`() { assertThatThrownBy { NonEmptySet.copyOf(HashSet()) }.isInstanceOf(IllegalArgumentException::class.java) @@ -48,11 +53,9 @@ class NonEmptySetTest { @Test fun `serialize deserialize`() { - withTestSerialization { - val original = NonEmptySet.of(-17, 22, 17) - val copy = original.serialize().deserialize() - assertThat(copy).isEqualTo(original).isNotSameAs(original) - } + val original = NonEmptySet.of(-17, 22, 17) + val copy = original.serialize().deserialize() + assertThat(copy).isEqualTo(original).isNotSameAs(original) } } diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 6f7fad2ff4..4a200e1756 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -1,15 +1,18 @@ API: Contract Constraints ========================= -A basic understanding of contract key concepts, which can be found :doc:`here `, -is required reading for this page. +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-contracts`. +.. contents:: + +Contract constraints +-------------------- Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is -everything that matches the signature and contract constraints restricts this universe to a subset). +everything that matches the signature and contract constraints restrict this universe to a subset). A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be @@ -60,10 +63,10 @@ that they were loaded from. This makes it possible to find the attachment for an automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and contracts, attachments are associated with their respective contracts. -Implementations ---------------- +Implementations of AttachmentConstraint +--------------------------------------- -There are three implementations of ``AttachmentConstraints`` with more planned in the future. +There are three implementations of ``AttachmentConstraint`` with more planned in the future. ``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint. @@ -137,3 +140,18 @@ Full Nodes ********** When testing against full nodes simply place your CorDapp into the cordapps directory of the node. + +Debugging +--------- +If an attachment constraint cannot be resolved, a ``MissingContractAttachments`` exception is thrown. There are two +common sources of ``MissingContractAttachments`` exceptions: + +Not setting CorDapp packages in tests +************************************* +You are running a test and have not specified the CorDapp packages to scan. See the instructions above. + +Wrong fully-qualified contract name +*********************************** +You are specifying the fully-qualified name of the contract incorrectly. For example, you've defined ``MyContract`` in +the package ``com.mycompany.myapp.contracts``, but the fully-qualified contract name you pass to the +``TransactionBuilder`` is ``com.mycompany.myapp.MyContract`` (instead of ``com.mycompany.myapp.contracts.MyContract``). \ No newline at end of file diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index b0f295268e..1adb4631b9 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -55,8 +55,8 @@ Helper methods are also provided with default values for arguments: 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``) +- Use ``queryBy`` to obtain a current snapshot of data (for a given ``QueryCriteria``) +- Use ``trackBy`` to obtain 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) diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index a1373f9849..e71d285e93 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -6,6 +6,7 @@ CorDapps cordapp-overview writing-a-cordapp + upgrade-notes cordapp-build-systems building-against-master corda-api diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a5d6753bdf..1bdeb80ede 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,50 @@ from the previous milestone release. UNRELEASED ---------- + +* The network map service concept has been re-designed. More information can be found in :doc:`network-map`. + + * The previous design was never intended to be final but was rather a quick implementation in the earliest days of the + Corda project to unblock higher priority items. It suffers from numerous disadvantages including lack of scalability, + as one node is expected to hold open and manage connections to every node on the network; not reliable; hard to defend + against DoS attacks; etc. + + * There is no longer a special network map node for distributing the network map to the other nodes. Instead the network + map is now a collection of signed ``NodeInfo`` files distributed via HTTP. + + * The ``certificateSigningService`` config has been replaced by ``compatibilityZoneURL`` which is the base URL for the + doorman registration and for downloading the network map. There is also an end-point for the node to publish its node-info + object, which the node does each time it changes. ``networkMapService`` config has been removed. + + * To support local and test deployments, the node polls the ``additional-node-infos`` directory for these signed ``NodeInfo`` + objects which are stored in its local cache. On startup the node generates its own signed file with the filename format + "nodeInfo-*". This can be copied to every node's ``additional-node-infos`` directory that is part of the network. + + * Cordform (which is the ``deployNodes`` gradle task) does this copying automatically for the demos. The ``NetworkMap`` + parameter is no longer needed. + + * For test deployments we've introduced a bootstrapping tool (see :doc:`setting-up-a-corda-network`). + + * ``extraAdvertisedServiceIds``, ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` configs have been + removed. The configuration of notaries has been simplified into a single ``notary`` config object. See + :doc:`corda-configuration-file` for more details. + + * Introducing the concept of network parameters which are a set of constants which all nodes on a network must agree on + to correctly interop. + + * The set of valid notaries has been moved to the network parameters. Notaries are no longer identified by the CN in + their X500 name. + + * Single node notaries no longer have a second separate notary identity. Their main identity *is* their notary identity. + Use ``NetworkMapCache.notaryIdentities`` to get the list of available notaries. + + * The common name in the node's X500 legal name is no longer reserved and can be used as part of the node's name. + + * Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This + was needed to allow changes to the schema. + +* Support for external user credentials data source and password encryption [CORDA-827]. + * Integrate database migration tool: http://www.liquibase.org/ : * The migration files are split per ``MappedSchemas``. (added new property: migrationResource used to point to the resource file containing the db changes corresponding to the JPA entities) * config flag ``database.initialiseSchema`` was renamed to: ``database.runMigration`` (if true then the migration is run during startup just before hibernate is initialised.) @@ -45,24 +89,12 @@ UNRELEASED 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: - * 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. - -* Nodes read and poll the filesystem for serialized ``NodeInfo`` in the ``additional-node-info`` directory. - * ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. * 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. -* ``extraAdvertisedServiceIds`` config has been removed as part of the previous work to retire the concept of advertised - services. The remaining use of this config was for notaries, the configuring of which has been cleaned up and simplified. - ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` have also been removed and replaced by a single - ``notary`` config object. See :doc:`corda-configuration-file` for more details. - * Gradle task ``deployNodes`` can have an additional parameter ``configFile`` with the path to a properties file to be appended to node.conf. @@ -79,8 +111,8 @@ UNRELEASED 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. +* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window as a ``java.time.Duration`` object, + or ``null`` if the time-window isn't closed. * A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command`` signers. @@ -96,18 +128,16 @@ UNRELEASED * The ``ReceiveTransactionFlow`` can now be told to record the transaction at the same time as receiving it. Using this feature, better support for observer/regulator nodes has been added. See :doc:`tutorial-observer-nodes`. -* Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This is - needed to allow new ``node_info_hash`` column to be added for the network map redesign work. - * Added an overload of ``TransactionWithSignatures.verifySignaturesExcept`` which takes in a collection of ``PublicKey``s. -* Replaced node configuration parameter ``certificateSigningService`` with ``compatibilityZoneURL``, which is Corda - compatibility zone network management service's address. +* ``DriverDSLExposedInterface`` has been renamed to ``DriverDSL`` and the ``waitForAllNodesToFinish()`` method has instead + become a parameter on driver creation. -* ``waitForAllNodesToFinish()`` method in ``DriverDSLExposedInterface`` has instead become a parameter on driver creation. +* Values for the ``database.transactionIsolationLevel`` config now follow the ``java.sql.Connection`` int constants but + without the "TRANSACTION_" prefix, i.e. "NONE", "READ_UNCOMMITTED", etc. -* ``database.transactionIsolationLevel`` values now follow the ``java.sql.Connection`` int constants but without the - "TRANSACTION_" prefix, i.e. "NONE", "READ_UNCOMMITTED", etc. +* Peer-to-peer communications is now via AMQP 1.0 as default. + Although the legacy Artemis CORE bridging can still be used by setting the ``useAMQPBridges`` configuration property to false. * Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases. diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 14d29ba9b9..ae30828149 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -26,7 +26,7 @@ permissions that RPC can use for fine-grain access control. These users are added to the node's ``node.conf`` file. -The syntax for adding an RPC user is: +The simplest way of adding an RPC user is to include it in the ``rpcUsers`` list: .. container:: codeset @@ -59,9 +59,6 @@ Users need permissions to invoke any RPC call. By default, nothing is allowed. T ... ] -.. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without - logging in. This will be changed in a future release. - Permissions Syntax ^^^^^^^^^^^^^^^^^^ @@ -71,6 +68,134 @@ Fine grained permissions allow a user to invoke a specific RPC operation, or to - to invoke a RPC operation: ``InvokeRpc.`` e.g., ``InvokeRpc.nodeInfo``. .. note:: Permission ``InvokeRpc.startFlow`` allows a user to initiate all flows. +RPC security management +----------------------- + +Setting ``rpcUsers`` provides a simple way of granting RPC permissions to a fixed set of users, but has some +obvious shortcomings. To support use cases aiming for higher security and flexibility, Corda offers additional security +features such as: + + * Fetching users credentials and permissions from an external data source (e.g.: a remote RDBMS), with optional in-memory + caching. In particular, this allows credentials and permissions to be updated externally without requiring nodes to be + restarted. + * Password stored in hash-encrypted form. This is regarded as must-have when security is a concern. Corda currently supports + a flexible password hash format conforming to the Modular Crypt Format provided by the `Apache Shiro framework `_ + +These features are controlled by a set of options nested in the ``security`` field of ``node.conf``. +The following example shows how to configure retrieval of users credentials and permissions from a remote database with +passwords in hash-encrypted format and enable in-memory caching of users data: + +.. container:: codeset + + .. sourcecode:: groovy + + security = { + authService = { + dataSource = { + type = "DB", + passwordEncryption = "SHIRO_1_CRYPT", + connection = { + jdbcUrl = "" + username = "" + password = "" + driverClassName = "" + } + } + options = { + cache = { + expireAfterSecs = 120 + maxEntries = 10000 + } + } + } + } + +It is also possible to have a static list of users embedded in the ``security`` structure by specifying a ``dataSource`` +of ``INMEMORY`` type: + +.. container:: codeset + + .. sourcecode:: groovy + + security = { + authService = { + dataSource = { + type = "INMEMORY", + users = [ + { + username = "", + password = "", + permissions = ["", "", ...] + }, + ... + ] + } + } + } + +.. warning:: A valid configuration cannot specify both the ``rpcUsers`` and ``security`` fields. Doing so will trigger + an exception at node startup. + +Authentication/authorisation data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``dataSource`` structure defines the data provider supplying credentials and permissions for users. There exist two +supported types of such data source, identified by the ``dataSource.type`` field: + + :INMEMORY: A static list of user credentials and permissions specified by the ``users`` field. + + :DB: An external RDBMS accessed via the JDBC connection described by ``connection``. Note that, unlike the ``INMEMORY`` + case, in a user database permissions are assigned to *roles* rather than individual users. The current implementation + expects the database to store data according to the following schema: + + - Table ``users`` containing columns ``username`` and ``password``. The ``username`` column *must have unique values*. + - Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles*. + - Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of + permission strings. + + .. note:: There is no prescription on the SQL type of each column (although our tests were conducted on ``username`` and + ``role_name`` declared of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type). It is also possible to have extra columns + in each table alongside the expected ones. + +Password encryption +^^^^^^^^^^^^^^^^^^^ + +Storing passwords in plain text is discouraged in applications where security is critical. Passwords are assumed +to be in plain format by default, unless a different format is specified by the ``passwordEncryption`` field, like: + +.. container:: codeset + + .. sourcecode:: groovy + + passwordEncryption = SHIRO_1_CRYPT + +``SHIRO_1_CRYPT`` identifies the `Apache Shiro fully reversible +Modular Crypt Format `_, +it is currently the only non-plain password hash-encryption format supported. Hash-encrypted passwords in this +format can be produced by using the `Apache Shiro Hasher command line tool `_. + +Caching user accounts data +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A cache layer on top of the external data source of users credentials and permissions can significantly improve +performances in some cases, with the disadvantage of causing a (controllable) delay in picking up updates to the underlying data. +Caching is disabled by default, it can be enabled by defining the ``options.cache`` field in ``security.authService``, +for example: + +.. container:: codeset + + .. sourcecode:: groovy + + options = { + cache = { + expireAfterSecs = 120 + maxEntries = 10000 + } + } + +This will enable a non-persistent cache contained in the node's memory with maximum number of entries set to ``maxEntries`` +where entries are expired and refreshed after ``expireAfterSecs`` seconds. + Observables ----------- The RPC system handles observables in a special way. When a method returns an observable, whether directly or diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 7ac54bbe95..71e292e1d6 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -101,6 +101,9 @@ path to the node's base directory. :rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC. +:security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See + :doc:`clientrpc` for details. + :webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself. .. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one @@ -191,4 +194,7 @@ path to the node's base directory. :sshPort: Port to be used for SSH connection, default ``22``. :exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent. - Default Jolokia access url is http://127.0.0.1:7005/jolokia/ \ No newline at end of file + Default Jolokia access url is http://127.0.0.1:7005/jolokia/ + +:useAMQPBridges: Optionally can be set to ``false`` to use Artemis CORE Bridges for peer-to-peer communications. + Otherwise, defaults to ``true`` and the AMQP 1.0 protocol will be used for message transfer between nodes. \ No newline at end of file diff --git a/docs/source/corda-nodes-index.rst b/docs/source/corda-nodes-index.rst index 061902b1aa..c1dfa0b508 100644 --- a/docs/source/corda-nodes-index.rst +++ b/docs/source/corda-nodes-index.rst @@ -10,7 +10,6 @@ Corda nodes corda-configuration-file clientrpc shell - node-auth-config node-database node-administration out-of-process-verification \ No newline at end of file diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index db3ad5da93..50464033a5 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -36,6 +36,7 @@ dependencies { compile project(':core') compile project(':client:jfx') compile project(':node-driver') + compile project(':webserver') testCompile project(':verifier') testCompile project(':test-utils') diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/TemplateContract.java b/docs/source/example-code/src/main/java/com/template/TemplateContract.java similarity index 93% rename from docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/TemplateContract.java rename to docs/source/example-code/src/main/java/com/template/TemplateContract.java index a46712bb15..5c4278a408 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/TemplateContract.java +++ b/docs/source/example-code/src/main/java/com/template/TemplateContract.java @@ -1,4 +1,4 @@ -package net.corda.docs.java.tutorial.helloworld; +package com.template; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; 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 index cc0b20faed..fd35ab75ac 100644 --- 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 @@ -1,17 +1,21 @@ package net.corda.docs.java.tutorial.helloworld; -// DOCSTART 01 import co.paralleluniverse.fibers.Suspendable; +import com.template.TemplateContract; +import net.corda.core.flows.*; + +// DOCSTART 01 +// Add these imports: import net.corda.core.contracts.Command; import net.corda.core.contracts.CommandData; -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 static net.corda.docs.java.tutorial.helloworld.TemplateContract.TEMPLATE_CONTRACT_ID; +import static com.template.TemplateContract.TEMPLATE_CONTRACT_ID; +// Replace TemplateFlow's definition with: @InitiatingFlow @StartableByRPC public class IOUFlow extends FlowLogic { 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 index 977457fd29..f724508648 100644 --- 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 @@ -1,13 +1,15 @@ 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; +// DOCSTART 01 +// Add these imports: +import com.google.common.collect.ImmutableList; +import net.corda.core.identity.Party; + +// Replace TemplateState's definition with: public class IOUState implements ContractState { private final int value; private final Party lender; diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java index 6c83cabfc6..0339b73754 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -3,48 +3,33 @@ package net.corda.docs.java.tutorial.testdsl; import kotlin.Unit; import net.corda.core.contracts.PartyAndReference; import net.corda.core.identity.CordaX500Name; -import net.corda.core.identity.Party; import net.corda.finance.contracts.ICommercialPaperState; import net.corda.finance.contracts.JavaCommercialPaper; import net.corda.finance.contracts.asset.Cash; -import net.corda.node.services.api.IdentityServiceInternal; -import net.corda.testing.SerializationEnvironmentRule; import net.corda.testing.node.MockServices; import net.corda.testing.TestIdentity; -import org.junit.Rule; import org.junit.Test; import java.security.PublicKey; import java.time.temporal.ChronoUnit; +import static java.util.Collections.emptyList; import static net.corda.core.crypto.Crypto.generateKeyPair; import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.Currencies.issuedBy; import static net.corda.finance.contracts.JavaCommercialPaper.JCP_PROGRAM_ID; +import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static net.corda.testing.node.NodeTestUtils.ledger; import static net.corda.testing.node.NodeTestUtils.transaction; -import static net.corda.testing.CoreTestUtils.rigorousMock; import static net.corda.testing.TestConstants.*; -import static org.mockito.Mockito.doReturn; public class CommercialPaperTest { - private static final TestIdentity ALICE = new TestIdentity(getALICE_NAME(), 70L); + private static final TestIdentity ALICE = new TestIdentity(ALICE_NAME, 70L); private static final PublicKey BIG_CORP_PUBKEY = generateKeyPair().getPublic(); - private static final TestIdentity BOB = new TestIdentity(getBOB_NAME(), 80L); + private static final TestIdentity BOB = new TestIdentity(BOB_NAME, 80L); private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); - private static final Party DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L).getParty(); - @Rule - public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); private final byte[] defaultRef = {123}; - private final MockServices ledgerServices; - - { - IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class); - doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPubkey()); - doReturn(null).when(identityService).partyFromKey(BIG_CORP_PUBKEY); - doReturn(null).when(identityService).partyFromKey(ALICE.getPubkey()); - ledgerServices = new MockServices(identityService, MEGA_CORP.getName()); - } + private final MockServices ledgerServices = new MockServices(emptyList(), makeTestIdentityService(MEGA_CORP.getIdentity())); // DOCSTART 1 private ICommercialPaperState getPaper() { @@ -52,7 +37,7 @@ public class CommercialPaperTest { MEGA_CORP.ref(defaultRef), MEGA_CORP.getParty(), issuedBy(DOLLARS(1000), MEGA_CORP.ref(defaultRef)), - getTEST_TX_TIME().plus(7, ChronoUnit.DAYS) + TEST_TX_TIME.plus(7, ChronoUnit.DAYS) ); } // DOCEND 1 @@ -61,7 +46,7 @@ public class CommercialPaperTest { @Test public void simpleCP() { ICommercialPaperState inState = getPaper(); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.transaction(tx -> { tx.attachments(JCP_PROGRAM_ID); tx.input(JCP_PROGRAM_ID, inState); @@ -76,10 +61,10 @@ public class CommercialPaperTest { @Test public void simpleCPMove() { ICommercialPaperState inState = getPaper(); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); tx.attachments(JCP_PROGRAM_ID); return tx.verifies(); }); @@ -92,10 +77,10 @@ public class CommercialPaperTest { @Test public void simpleCPMoveFails() { ICommercialPaperState inState = getPaper(); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); tx.attachments(JCP_PROGRAM_ID); return tx.failsWith("the state is propagated"); }); @@ -108,10 +93,10 @@ public class CommercialPaperTest { @Test public void simpleCPMoveSuccess() { ICommercialPaperState inState = getPaper(); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); tx.attachments(JCP_PROGRAM_ID); tx.failsWith("the state is propagated"); tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE.getParty())); @@ -125,17 +110,17 @@ public class CommercialPaperTest { // DOCSTART 6 @Test public void simpleIssuanceWithTweak() { - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.transaction(tx -> { tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. tx.attachments(JCP_PROGRAM_ID); tx.tweak(tw -> { tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue()); - tw.timeWindow(getTEST_TX_TIME()); + tw.timeWindow(TEST_TX_TIME); return tw.failsWith("output states are issued by a command signer"); }); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue()); - tx.timeWindow(getTEST_TX_TIME()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); return Unit.INSTANCE; @@ -146,16 +131,16 @@ public class CommercialPaperTest { // DOCSTART 7 @Test public void simpleIssuanceWithTweakTopLevelTx() { - transaction(ledgerServices, DUMMY_NOTARY, tx -> { + transaction(ledgerServices, tx -> { tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. tx.attachments(JCP_PROGRAM_ID); tx.tweak(tw -> { tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue()); - tw.timeWindow(getTEST_TX_TIME()); + tw.timeWindow(TEST_TX_TIME); return tw.failsWith("output states are issued by a command signer"); }); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue()); - tx.timeWindow(getTEST_TX_TIME()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); } @@ -165,7 +150,7 @@ public class CommercialPaperTest { @Test public void chainCommercialPaper() { PartyAndReference issuer = MEGA_CORP.ref(defaultRef); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty())); @@ -176,9 +161,9 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { tx.output(JCP_PROGRAM_ID, "paper", getPaper()); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); - tx.timeWindow(getTEST_TX_TIME()); + tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); @@ -188,8 +173,8 @@ public class CommercialPaperTest { tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty())); - tx.command(ALICE.getPubkey(), new Cash.Commands.Move()); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(ALICE.getPublicKey(), new Cash.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); return Unit.INSTANCE; @@ -201,7 +186,7 @@ public class CommercialPaperTest { @Test public void chainCommercialPaperDoubleSpend() { PartyAndReference issuer = MEGA_CORP.ref(defaultRef); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty())); @@ -212,9 +197,9 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { tx.output(Cash.PROGRAM_ID, "paper", getPaper()); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); - tx.timeWindow(getTEST_TX_TIME()); + tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); @@ -224,8 +209,8 @@ public class CommercialPaperTest { tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty())); - tx.command(ALICE.getPubkey(), new Cash.Commands.Move()); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(ALICE.getPublicKey(), new Cash.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); @@ -234,7 +219,7 @@ public class CommercialPaperTest { JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); // We moved a paper to other pubkey. tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty())); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); l.fails(); @@ -247,7 +232,7 @@ public class CommercialPaperTest { @Test public void chainCommercialPaperTweak() { PartyAndReference issuer = MEGA_CORP.ref(defaultRef); - ledger(ledgerServices, DUMMY_NOTARY, l -> { + ledger(ledgerServices, l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty())); @@ -258,9 +243,9 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { tx.output(Cash.PROGRAM_ID, "paper", getPaper()); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); - tx.timeWindow(getTEST_TX_TIME()); + tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); @@ -270,8 +255,8 @@ public class CommercialPaperTest { tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty())); - tx.command(ALICE.getPubkey(), new Cash.Commands.Move()); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(ALICE.getPublicKey(), new Cash.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); @@ -281,7 +266,7 @@ public class CommercialPaperTest { JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); // We moved a paper to another pubkey. tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty())); - tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move()); + tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); lw.fails(); 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 index b74dfd3ef4..ed0031dc48 100644 --- 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 @@ -1,20 +1,25 @@ 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; +// DOCSTART 01 +// Add these imports: +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.identity.Party; + import java.security.PublicKey; import java.util.List; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; import static net.corda.core.contracts.ContractsDSL.requireThat; +// Replace TemplateContract's definition with: public class IOUContract implements Contract { + public static final String IOU_CONTRACT_ID = "com.template.IOUContract"; + // Our Create command. public static class Create implements CommandData { } 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 index 4e0533d323..5ad1a12ce2 100644 --- 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 @@ -45,15 +45,14 @@ public class IOUFlow extends FlowLogic { // We retrieve the notary identity from the network map. final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + // DOCSTART 02 // 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); + StateAndContract outputContractAndState = new StateAndContract(outputState, IOUContract.IOU_CONTRACT_ID); List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); 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 index ac1f312ab6..35b2a3f97b 100644 --- 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 @@ -1,16 +1,16 @@ package net.corda.docs.java.tutorial.twoparty; // DOCSTART 01 +// Add these imports: 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; +// Define IOUFlowResponder: @InitiatedBy(IOUFlow.class) public class IOUFlowResponder extends FlowLogic { private final FlowSession otherPartySession; 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 index 24832e76eb..676af37f4c 100644 --- 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 @@ -1,16 +1,19 @@ package net.corda.docs.tutorial.helloworld -// DOCSTART 01 import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.Command import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC + +// DOCSTART 01 +// Add these imports: +import net.corda.core.contracts.Command import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker +// Replace TemplateFlow's definition with: @InitiatingFlow @StartableByRPC class IOUFlow(val iouValue: Int, 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 index 447265a2ae..19f431bfb1 100644 --- 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 @@ -1,9 +1,12 @@ package net.corda.docs.tutorial.helloworld -// DOCSTART 01 import net.corda.core.contracts.ContractState + +// DOCSTART 01 +// Add these imports: import net.corda.core.identity.Party +// Replace TemplateState's definition with: class IOUState(val value: Int, val lender: Party, val borrower: Party) : ContractState { diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt index 911b7cefde..0147e4e623 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt @@ -67,7 +67,7 @@ class TutorialMockNetwork { @Before fun setUp() { - mockNet = MockNetwork() + mockNet = MockNetwork(emptyList()) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() nodeB.registerInitiatedFlow(FlowB::class.java) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index 6754a6082c..1fc5f4c832 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -14,6 +14,7 @@ import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger import net.corda.testing.node.transaction @@ -28,15 +29,15 @@ class CommercialPaperTest { val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val ALICE get() = alice.party - val ALICE_PUBKEY get() = alice.pubkey + val ALICE_PUBKEY get() = alice.publicKey val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey } @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val ledgerServices = MockServices(rigorousMock().also { + private val ledgerServices = MockServices(emptyList(), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) 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 index 96fb3482c1..05fb4af8bd 100644 --- 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 @@ -1,12 +1,16 @@ 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 +// DOCSTART 01 +// Add these imports: +import net.corda.core.contracts.* + +// Replace IOUContract's contract ID and definition with: +val IOU_CONTRACT_ID = "com.template.IOUContract" + class IOUContract : Contract { // Our Create command. class Create : CommandData @@ -20,7 +24,7 @@ class IOUContract : Contract { "There should be one output state of type IOUState." using (tx.outputs.size == 1) // IOU-specific constraints. - val out = tx.outputsOfType().single() + 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) 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 index 0d8ac221ad..27f4705501 100644 --- 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 @@ -6,9 +6,17 @@ 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.messaging.CordaRPCOps +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker -import kotlin.reflect.jvm.jvmName +import net.corda.webserver.services.WebServerPluginRegistry +import java.util.function.Function +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.Produces +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response // DOCEND 01 @InitiatingFlow @@ -25,14 +33,13 @@ class IOUFlow(val iouValue: Int, // We retrieve the notary identity from the network map. val notary = serviceHub.networkMapCache.notaryIdentities[0] - // We create a transaction builder + // DOCSTART 02 + // 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 outputContractAndState = StateAndContract(outputState, IOU_CONTRACT_ID) val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) // We add the items to the builder. 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 index b8007cc2ec..f2ff5ecdc9 100644 --- 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 @@ -1,16 +1,19 @@ 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 +// DOCSTART 01 +// Add these imports: +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.SignedTransaction + +// Define IOUFlowResponder: @InitiatedBy(IOUFlow::class) class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { @Suspendable diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 9d79832605..817de0545a 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -6,8 +6,8 @@ Writing the flow ================ -A flow encodes a sequence of steps that a node can run to achieve a specific ledger update. By installing new flows on -a node, we allow the node to handle new business processes. The flow we define will allow a node to issue an +A flow encodes a sequence of steps that a node can perform to achieve a specific ledger update. By installing new flows +on a node, we allow the node to handle new business processes. The flow we define will allow a node to issue an ``IOUState`` onto the ledger. Flow outline @@ -40,8 +40,8 @@ FlowLogic --------- All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``. -Let's define our ``IOUFlow`` in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the -template, and replace them with the following: +Let's define our ``IOUFlow`` in either ``TemplateFlow.java`` or ``App.kt``. Delete the two existing flows in the +template (``Initiator`` and ``Responder``), and replace them with the following: .. container:: codeset diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 17d506471d..592cfee44c 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -107,9 +107,15 @@ commands. We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing: -.. code:: bash +.. container:: codeset - start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" + .. code-block:: java + + start IOUFlow arg0: 99, arg1: "O=PartyB,L=New York,C=US" + + .. code-block:: kotlin + + start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" This single command will cause PartyA and PartyB to automatically agree an IOU. This is one of the great advantages of the flow framework - it allows you to reduce complex negotiation and update processes into a single function call. @@ -118,13 +124,7 @@ If the flow worked, it should have recorded a new IOU in the vaults of both Part We can check the contents of each node's vault by running: -.. container:: codeset - - .. code-block:: java - - run vaultQuery contractStateType: com.template.state.IOUState - - .. code-block:: kotlin +.. code-block:: base run vaultQuery contractStateType: com.template.IOUState @@ -174,6 +174,11 @@ parts: * The ``IOUState``, representing IOUs on the ledger * The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger +After completing this tutorial, your CorDapp should look like this: + +* Java: https://github.com/corda/corda-tut1-solution-java +* Kotlin: https://github.com/corda/corda-tut1-solution-kotlin + Next steps ---------- There are a number of improvements we could make to this CorDapp: @@ -183,4 +188,4 @@ 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 But for now, the biggest priority is to add an ``IOUContract`` imposing constraints on the evolution of each -``IOUState`` over time. This will be the focus of our next tutorial. +``IOUState`` over time. This will be the focus of our next tutorial. \ No newline at end of file diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 5d9148d333..fb078f0780 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -24,7 +24,6 @@ interface is defined as follows: val participants: List } -<<<<<<< HEAD The first thing you'll probably notice about this interface declaration is that its not written in Java or another common language. The core Corda platform, including the interface declaration above, is entirely written in Kotlin. @@ -70,7 +69,7 @@ later is often as simple as adding an additional property to your class definiti Defining IOUState ----------------- -Let's get started by opening ``TemplateState.java`` (for Java) or ``App.kt`` (for Kotlin) and updating +Let's get started by opening ``TemplateState.java`` (for Java) or ``StatesAndContracts.kt`` (for Kotlin) and updating ``TemplateState`` to define an ``IOUState``: .. container:: codeset diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 18ad47f515..cf34d8c892 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -41,34 +41,33 @@ https://docs.corda.net/tutorial-cordapp.html#opening-the-example-cordapp-in-inte Template structure ------------------ -The template has a number of files, but we can ignore most of them. To implement our IOU CorDapp in Java, we'll only -need to modify two files. For Kotlin, we'll simply be modifying the ``App.kt`` file: +The template has a number of files, but we can ignore most of them. We will only be modifying the following files: .. container:: codeset .. code-block:: java // 1. The state - src/main/java/com/template/TemplateState.java + cordapp-contracts-states/src/main/java/com/template/TemplateState.java // 2. The flow - src/main/java/com/template/TemplateFlow.java + cordapp/src/main/java/com/template/TemplateFlow.java .. code-block:: kotlin - src/main/kotlin/com/template/App.kt + // 1. The state + cordapp-contracts-states/src/main/kotlin/com/template/StatesAndContracts.kt + + // 2. The flow + cordapp/src/main/kotlin/com/template/App.kt Clean up -------- To prevent build errors later on, we should delete the following files before we begin: -* Java: - * ``src/main/java/com/template/TemplateClient.java`` - * ``src/test/java/com/template/FlowTests.java`` +* Java: ``cordapp/src/main/java/com/template/TemplateClient.java`` -* Kotlin: - * ``src/main/kotlin/com/template/TemplateClient.kt`` - * ``src/test/kotlin/com/template/FlowTests.kt`` +* Kotlin: ``cordapp/src/main/kotlin/com/template/TemplateClient.kt`` Progress so far --------------- diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 34356bad4f..efe8110843 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -2,7 +2,7 @@ Network Map =========== The network map stores a collection of ``NodeInfo`` objects, each representing another node with which the node can interact. -There two sources from which a Corda node can retrieve ``NodeInfo`` objects: +There are two sources from which a Corda node can retrieve ``NodeInfo`` objects: 1. the REST protocol with the network map service, which also provides a publishing API, @@ -13,7 +13,7 @@ Protocol Design --------------- The node info publishing protocol: -* Create a ``NodeInfo`` object, and sign it to create a ``SignedData`` object. TODO: We will need list of signatures in ``SignedData`` to support multiple node identities in the future. +* Create a ``NodeInfo`` object, and sign it to create a ``SignedNodeInfo`` object. * Serialise the signed data and POST the data to the network map server. @@ -25,7 +25,18 @@ Node side network map update protocol: * The Corda node will query the network map service periodically according to the ``Expires`` attribute in the HTTP header. -* The network map service returns a signed ``NetworkMap`` object, containing list of node info hashes and the network parameters hashes. +* The network map service returns a signed ``NetworkMap`` object which looks as follows: + +.. container:: codeset + + .. sourcecode:: kotlin + + data class NetworkMap { + val nodeInfoHashes: List, + val networkParametersHash: SecureHash + } + +The object contains list of node info hashes and hash of the network parameters data structure (without the signatures). * The node updates its local copy of ``NodeInfos`` if it is different from the newly downloaded ``NetworkMap``. @@ -34,13 +45,13 @@ Network Map service REST API: +----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | Request method | Path | Description | +================+===================================+========================================================================================================================================================+ -| POST | /api/network-map/publish | Publish new ``NodeInfo`` to the network map service, the legal identity in ``NodeInfo`` must match with the identity registered with the doorman. | +| POST | /network-map/publish | Publish new ``NodeInfo`` to the network map service, the legal identity in ``NodeInfo`` must match with the identity registered with the doorman. | +----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ -| GET | /api/network-map | Retrieve ``NetworkMap`` from the server, the ``NetworkMap`` object contains list of node info hashes and NetworkParameters hash. | +| GET | /network-map | Retrieve ``NetworkMap`` from the server, the ``NetworkMap`` object contains list of node info hashes and ``NetworkParameters`` hash. | +----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ -| GET | /api/network-map/node-info/{hash} | Retrieve ``NodeInfo`` object with the same hash. | +| GET | /network-map/node-info/{hash} | Retrieve ``NodeInfo`` object with the same hash. | +----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ -| GET | /api/network-map/parameters/{hash}| Retrieve ``NetworkParameters`` object with the same hash. | +| GET | /network-map/parameters/{hash} | Retrieve ``NetworkParameters`` object with the same hash. | +----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ TODO: Access control of the network map will be added in the future. @@ -50,8 +61,24 @@ The ``additional-node-infos`` directory --------------------------------------- Each Corda node reads, and continuously polls, the files contained in a directory named ``additional-node-infos`` inside the node base directory. -Nodes expect to find a serialized ``SignedData`` object, the same object which is sent to network map server. +Nodes expect to find a serialized ``SignedNodeInfo`` object, the same object which is sent to network map server. Whenever a node starts it writes on disk a file containing its own ``NodeInfo``, this file is called ``nodeInfo-XXX`` where ``XXX`` is a long string. Hence if an operator wants node A to see node B they can pick B's ``NodeInfo`` file from B base directory and drop it into A's ``additional-node-infos`` directory. + + +Network parameters +------------------ +Network parameters are constants that every node participating in the network needs to agree on and use for interop purposes. +The structure is distributed as a file containing serialized ``SignedData`` with a signature from +a sub-key of the compatibility zone root cert. Network map advertises the hash of currently used network parameters. +The ``NetworkParameters`` structure contains: + * ``minimumPlatformVersion`` - minimum version of Corda platform that is required for nodes in the network. + * ``notaries`` - list of well known and trusted notary identities with information on validation type. + * ``maxMessageSize`` - maximum P2P message size sent over the wire in bytes. + * ``maxTransactionSize`` - maximum permitted transaction size in bytes. + * ``modifiedTime`` - the time the network parameters were created by the CZ operator. + * ``epoch`` - version number of the network parameters. Starting from 1, this will always increment on each new set of parameters. + +The set of parameters is still under development and we may find the need to add additional fields. diff --git a/docs/source/node-auth-config.rst b/docs/source/node-auth-config.rst deleted file mode 100644 index 5f9c54c3e4..0000000000 --- a/docs/source/node-auth-config.rst +++ /dev/null @@ -1,136 +0,0 @@ -Access security settings -======================== - -Access to node functionalities via SSH or RPC is protected by an authentication and authorisation policy. - -The field ``security`` in ``node.conf`` exposes various sub-fields related to authentication/authorisation specifying: - - * The data source providing credentials and permissions for users (e.g.: a remote RDBMS) - * An optional password encryption method. - * An optional caching of users data from Node side. - -.. warning:: Specifying both ``rpcUsers`` and ``security`` fields in ``node.conf`` is considered an illegal setting and - rejected by the node at startup since ``rpcUsers`` is effectively deprecated in favour of ``security.authService``. - -**Example 1:** connect to remote RDBMS for credentials/permissions, with encrypted user passwords and -caching on node-side: - -.. container:: codeset - - .. sourcecode:: groovy - - security = { - authService = { - dataSource = { - type = "DB", - passwordEncryption = "SHIRO_1_CRYPT", - connection = { - jdbcUrl = "" - username = "" - password = "" - driverClassName = "" - } - } - options = { - cache = { - expiryTimeSecs = 120 - capacity = 10000 - } - } - } - } - -**Example 2:** list of user credentials and permissions hard-coded in ``node.conf`` - -.. container:: codeset - - .. sourcecode:: groovy - - security = { - authService = { - dataSource = { - type = "INMEMORY", - users =[ - { - username = "user1" - password = "password" - permissions = [ - "StartFlow.net.corda.flows.ExampleFlow1", - "StartFlow.net.corda.flows.ExampleFlow2", - ... - ] - }, - ... - ] - } - } - } - -Let us look in more details at the structure of ``security.authService``: - -Authentication/authorisation data ---------------------------------- - -The ``dataSource`` field defines the data provider supplying credentials and permissions for users. The ``type`` -subfield identify the type of data provider, currently supported one are: - - * **INMEMORY:** a list of user credentials and permissions hard-coded in configuration in the ``users`` field - (see example 2 above) - - * **DB:** An external RDBMS accessed via the JDBC connection described by ``connection``. The current implementation - expect the database to store data according to the following schema: - - - Table ``users`` containing columns ``username`` and ``password``. - The ``username`` column *must have unique values*. - - Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles* - - Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of - permission strings - - Note in particular how in the DB case permissions are assigned to _roles_ rather than individual users. - Also, there is no prescription on the SQL type of the columns (although in our tests we defined ``username`` and - ``role_name`` of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type) and it is allowed to put additional columns - besides the one expected by the implementation. - -Password encryption -------------------- - -Storing passwords in plain text is discouraged in production systems aiming for high security requirements. We support -reading passwords stored using the Apache Shiro fully reversible Modular Crypt Format, specified in the documentation -of ``org.apache.shiro.crypto.hash.format.Shiro1CryptFormat``. - -Password are assumed in plain format by default. To specify an encryption it is necessary to use the field: - -.. container:: codeset - - .. sourcecode:: groovy - - passwordEncryption = SHIRO_1_CRYPT - -Hash encrypted password based on the Shiro1CryptFormat can be produced with the `Apache Shiro Hasher tool `_ - -Cache ------ - -Adding a cache layer on top of an external provider of users credentials and permissions can significantly benefit -performances in some cases, with the disadvantage of introducing a latency in the propagation of changes to the data. - -Caching of users data is disabled by default, it can be enabled by defining the ``options.cache`` field, like seen in -the examples above: - -.. container:: codeset - - .. sourcecode:: groovy - - options = { - cache = { - expiryTimeSecs = 120 - capacity = 10000 - } - } - -This will enable an in-memory cache with maximum capacity (number of entries) and maximum life time of entries given by -respectively the values set by the ``capacity`` and ``expiryTimeSecs`` fields. - - - - diff --git a/docs/source/permissioning.rst b/docs/source/permissioning.rst index dcffd0bb49..5532157237 100644 --- a/docs/source/permissioning.rst +++ b/docs/source/permissioning.rst @@ -32,7 +32,13 @@ A Corda network has three types of certificate authorities (CAs): * The **node CAs** * Each node serves as its own CA in issuing the child certificates that it uses to sign its identity - keys, anonymous keys and TLS certificates + keys and TLS certificates + +We can visualise the permissioning structure as follows: + +.. image:: resources/certificate_structure.png + :scale: 55% + :align: center Keypair and certificate formats ------------------------------- @@ -45,6 +51,13 @@ public/private keypairs and certificates. The keypairs and certificates should o * The TLS certificates must follow the `TLS v1.2 standard `_ +* The root network CA, intermediate network CA and node CA keys, as well as the node TLS + keys, must follow one of the following schemes: + + * ECDSA using the NIST P-256 curve (secp256r1) + + * RSA with 3072-bit key size + Creating the root and intermediate network CAs ---------------------------------------------- diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst index eb81e833f0..1b59977668 100644 --- a/docs/source/release-process-index.rst +++ b/docs/source/release-process-index.rst @@ -6,6 +6,5 @@ Release process release-notes changelog - upgrade-notes codestyle testing \ No newline at end of file diff --git a/docs/source/resources/certificate_structure.png b/docs/source/resources/certificate_structure.png new file mode 100644 index 0000000000..6121b968da Binary files /dev/null and b/docs/source/resources/certificate_structure.png differ diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index 4b610ef104..4ae763dce2 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -6,11 +6,9 @@ Creating a Corda network A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in order to create and validate transactions. -There are four broader categories of functionality one such node may have. These pieces of functionality are provided +There are three broader categories of functionality one such node may have. These pieces of functionality are provided as services, and one node may run several of them. -* Network map: The node running the network map provides a way to resolve identities to physical node addresses and - associated public keys * Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a double-spend or not * Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of @@ -46,12 +44,38 @@ The most important fields regarding network configuration are: * ``rpcAddress``: The address to which Artemis will bind for RPC calls. * ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine. +Bootstrapping the network +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The nodes see each other using the network map. This is a collection of statically signed node-info files, one for each +node in the network. Most production deployments will use a highly available, secure distribution of the network map via HTTP. + +For test deployments where the nodes (at least initially) reside on the same filesystem, these node-info files can be +placed directly in the node's ``additional-node-infos`` directory from where the node will pick them up and store them +in its local network map cache. The node generates its own node-info file on startup. + +In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set +of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters +which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can +be done with the network bootstrapper. This a tool that scans all the node configurations from a common directory to +generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file +to every other node. + +The bootstrapper tool can be built with the command: + +``gradlew buildBootstrapperJar`` + +The resulting jar can be found in ``tools/bootstrapper/build/libs/``. + +To use it, run the following command, specifying the root directory which hosts all the node directories as the argument: + +``java -jar network-bootstrapper.jar `` + Starting the nodes ~~~~~~~~~~~~~~~~~~ -You may now start the nodes in any order. Note that the node is not fully started until it has successfully registered with the network map! - -You should see a banner, some log lines and eventually ``Node started up and registered``, indicating that the node is fully started. +You may now start the nodes in any order. You should see a banner, some log lines and eventually ``Node started up and registered``, +indicating that the node is fully started. .. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound. @@ -66,7 +90,6 @@ details/diagnosing problems check the logs. Logging is standard log4j2_ and may be configured accordingly. Logs are by default redirected to files in ``NODE_DIRECTORY/logs/``. - Connecting to the nodes ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 2011f73c55..17e4bc77f2 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -77,7 +77,7 @@ We can picture this transaction as follows: Defining IOUContract -------------------- Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or -``App.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: +``StatesAndContracts.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: .. container:: codeset diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index 84f373047c..4b6e1c3cda 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -17,7 +17,7 @@ We'll do this by modifying the flow we wrote in the previous tutorial. Verifying the transaction ------------------------- -In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following: +In ``IOUFlow.java``/``App.kt``, change the imports block to the following: .. container:: codeset @@ -31,7 +31,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following: :start-after: DOCSTART 01 :end-before: DOCEND 01 -And update ``IOUFlow.call`` by changing the code following the creation of the ``TransactionBuilder`` as follows: +And update ``IOUFlow.call`` by changing the code following the retrieval of the notary's identity from the network as +follows: .. container:: codeset @@ -138,6 +139,11 @@ Our CorDapp now imposes restrictions on the issuance of IOUs. Most importantly, from both the lender and the borrower before an IOU can be created on the ledger. This prevents either the lender or the borrower from unilaterally updating the ledger in a way that only benefits themselves. +After completing this tutorial, your CorDapp should look like this: + +* Java: https://github.com/corda/corda-tut2-solution-java +* Kotlin: https://github.com/corda/corda-tut2-solution-kotlin + You should now be ready to develop your own CorDapps. You can also find a list of sample CorDapps `here `_. As you write CorDapps, you'll also want to learn more about the :doc:`Corda API `. diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst index c443de09b0..dab76fb001 100644 --- a/docs/source/tutorials-index.rst +++ b/docs/source/tutorials-index.rst @@ -1,11 +1,23 @@ Tutorials ========= +This section is split into two parts. + +The Hello, World tutorials should be followed in sequence and show how to extend the Java or Kotlin CorDapp Template +into a full CorDapp. + .. toctree:: :maxdepth: 1 hello-world-introduction tut-two-party-introduction + +The remaining tutorials cover individual platform features in isolation. They don't depend on the code from the Hello, +World tutorials, and can be read in any order. + +.. toctree:: + :maxdepth: 1 + tutorial-contract tutorial-test-dsl contract-upgrade diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst index 7ddbd362f1..baa2328b4e 100644 --- a/docs/source/upgrade-notes.rst +++ b/docs/source/upgrade-notes.rst @@ -1,5 +1,5 @@ -Upgrade notes -============= +Upgrading a CorDapp to a new version +==================================== These notes provide instructions for upgrading your CorDapps from previous versions, starting with the upgrade from our first public Beta (:ref:`Milestone 12 `), to :ref:`V1.0 `. diff --git a/docs/source/versioning.rst b/docs/source/versioning.rst index f53e9fb852..b860f71507 100644 --- a/docs/source/versioning.rst +++ b/docs/source/versioning.rst @@ -32,8 +32,8 @@ Flow versioning --------------- In addition to the evolution of the platform, flows that run on top of the platform can also evolve. It may be that the -flow protocol between an initiating flow and it's intiated flow changes from one CorDapp release to the next in such as -way to be backwards incompatible with existing flows. For example, if a sequence of sends and receives needs to change +flow protocol between an initiating flow and its initiated flow changes from one CorDapp release to the next in such a +way to be backward incompatible with existing flows. For example, if a sequence of sends and receives needs to change or if the semantics of a particular receive changes. The ``InitiatingFlow`` annotation (see :doc:`flow-state-machine` for more information on the flow annotations) has a ``version`` 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 bb458188e6..ed12e6190c 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 @@ -9,6 +9,10 @@ import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* +import net.corda.testing.dsl.EnforceVerifyOrFail +import net.corda.testing.dsl.TransactionDSL +import net.corda.testing.dsl.TransactionDSLInterpreter +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.transaction import org.junit.Ignore diff --git a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java index ce7273f75b..5d3f27286a 100644 --- a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java @@ -12,51 +12,52 @@ import net.corda.testing.node.MockServices; import org.junit.Rule; import org.junit.Test; +import static java.util.Collections.emptyList; import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.Currencies.issuedBy; import static net.corda.testing.node.NodeTestUtils.transaction; -import static net.corda.testing.CoreTestUtils.rigorousMock; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME; +import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock; +import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME; import static org.mockito.Mockito.doReturn; /** * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL */ public class CashTestsJava { - private static final Party DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L).getParty(); + private static final Party DUMMY_NOTARY = new TestIdentity(DUMMY_NOTARY_NAME, 20L).getParty(); private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); private static final TestIdentity MINI_CORP = new TestIdentity(new CordaX500Name("MiniCorp", "London", "GB")); private final PartyAndReference defaultIssuer = MEGA_CORP.ref((byte) 1); - private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), new AnonymousParty(MEGA_CORP.getPubkey())); - private final Cash.State outState = new Cash.State(inState.getAmount(), new AnonymousParty(MINI_CORP.getPubkey())); + private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), new AnonymousParty(MEGA_CORP.getPublicKey())); + private final Cash.State outState = new Cash.State(inState.getAmount(), new AnonymousParty(MINI_CORP.getPublicKey())); @Rule public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); @Test public void trivial() { IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class); - doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPubkey()); - doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPubkey()); - transaction(new MockServices(identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> { + doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPublicKey()); + doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPublicKey()); + transaction(new MockServices(emptyList(), identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> { tx.attachment(Cash.PROGRAM_ID); tx.input(Cash.PROGRAM_ID, inState); tx.tweak(tw -> { - tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(MINI_CORP.getPubkey()))); - tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move()); + tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(MINI_CORP.getPublicKey()))); + tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move()); return tw.failsWith("the amounts balance"); }); tx.tweak(tw -> { tw.output(Cash.PROGRAM_ID, outState); - tw.command(MEGA_CORP.getPubkey(), DummyCommandData.INSTANCE); + tw.command(MEGA_CORP.getPublicKey(), DummyCommandData.INSTANCE); // Invalid command return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command"); }); tx.tweak(tw -> { tw.output(Cash.PROGRAM_ID, outState); - tw.command(MINI_CORP.getPubkey(), new Cash.Commands.Move()); + tw.command(MINI_CORP.getPublicKey(), new Cash.Commands.Move()); return tw.failsWith("the owning keys are a subset of the signing keys"); }); tx.tweak(tw -> { @@ -64,14 +65,14 @@ public class CashTestsJava { // issuedBy() can't be directly imported because it conflicts with other identically named functions // with different overloads (for some reason). tw.output(Cash.PROGRAM_ID, outState.issuedBy(MINI_CORP.getParty())); - tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move()); + tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move()); return tw.failsWith("at least one cash input"); }); // Simple reallocation works. return tx.tweak(tw -> { tw.output(Cash.PROGRAM_ID, outState); - tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move()); + tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move()); return tw.verifies(); }); }); 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 81f40d5c6f..6b28671413 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -18,7 +18,11 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.* import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* -import net.corda.testing.contracts.VaultFiller +import net.corda.testing.dsl.EnforceVerifyOrFail +import net.corda.testing.dsl.TransactionDSL +import net.corda.testing.dsl.TransactionDSLInterpreter +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.ledger @@ -46,7 +50,7 @@ interface ICommercialPaperTestTemplate { private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) private val MEGA_CORP get() = megaCorp.party private val MEGA_CORP_IDENTITY get() = megaCorp.identity -private val MEGA_CORP_PUBKEY get() = megaCorp.pubkey +private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey class JavaCommercialPaperTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State( @@ -105,13 +109,13 @@ class CommercialPaperTestsGeneric { private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) private val ALICE get() = alice.party - private val ALICE_KEY get() = alice.key - private val ALICE_PUBKEY get() = alice.pubkey + private val ALICE_KEY get() = alice.keyPair + private val ALICE_PUBKEY get() = alice.publicKey private val DUMMY_NOTARY get() = dummyNotary.party private val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity private val MINI_CORP get() = miniCorp.party private val MINI_CORP_IDENTITY get() = miniCorp.identity - private val MINI_CORP_PUBKEY get() = miniCorp.pubkey + private val MINI_CORP_PUBKEY get() = miniCorp.publicKey } @Parameterized.Parameter @@ -120,7 +124,7 @@ class CommercialPaperTestsGeneric { @JvmField val testSerialization = SerializationEnvironmentRule() val issuer = MEGA_CORP.ref(123) - private val ledgerServices = MockServices(rigorousMock().also { + private val ledgerServices = MockServices(emptyList(), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) @@ -260,16 +264,15 @@ class CommercialPaperTestsGeneric { private lateinit var aliceServices: MockServices private lateinit var aliceVaultService: VaultService private lateinit var alicesVault: Vault - private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, dummyNotary.key) - private val issuerServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.key) + private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair) + private val issuerServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.keyPair) private lateinit var moveTX: SignedTransaction @Test fun `issue move and then redeem`() { val aliceDatabaseAndServices = makeTestDatabaseAndMockServices( - listOf(ALICE_KEY), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), - listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), - MEGA_CORP.name) + listOf("net.corda.finance.contracts"), + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), + TestIdentity(MEGA_CORP.name, ALICE_KEY)) val databaseAlice = aliceDatabaseAndServices.first aliceServices = aliceDatabaseAndServices.second aliceVaultService = aliceServices.vaultService @@ -279,10 +282,9 @@ class CommercialPaperTestsGeneric { aliceVaultService = aliceServices.vaultService } val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices( - listOf(BIG_CORP_KEY), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), - listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), - MEGA_CORP.name) + listOf("net.corda.finance.contracts"), + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), + TestIdentity(MEGA_CORP.name, BIG_CORP_KEY)) val databaseBigCorp = bigCorpDatabaseAndServices.first bigCorpServices = bigCorpDatabaseAndServices.second bigCorpVaultService = bigCorpServices.vaultService @@ -308,8 +310,8 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { val builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) - CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) + Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(BIG_CORP_KEY.public)) + CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(ALICE_KEY.public)) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = bigCorpServices.addSignature(ptx) val stx = notaryServices.addSignature(ptx2) 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 66ed294997..feb9032fab 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,7 +3,6 @@ package net.corda.finance.contracts.asset import com.nhaarman.mockito_kotlin.* import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService @@ -21,7 +20,12 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.DummyState -import net.corda.testing.contracts.VaultFiller +import net.corda.testing.internal.LogHelper +import net.corda.testing.dsl.EnforceVerifyOrFail +import net.corda.testing.dsl.TransactionDSL +import net.corda.testing.dsl.TransactionDSLInterpreter +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.ledger @@ -37,27 +41,27 @@ import kotlin.test.* class CashTests { private companion object { val alice = TestIdentity(ALICE_NAME, 70) - val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).pubkey + val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).publicKey val charlie = TestIdentity(CHARLIE_NAME, 90) val DUMMY_CASH_ISSUER_IDENTITY = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10).identity val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) val ALICE get() = alice.party - val ALICE_PUBKEY get() = alice.pubkey + val ALICE_PUBKEY get() = alice.publicKey val CHARLIE get() = charlie.party val CHARLIE_IDENTITY get() = charlie.identity val DUMMY_NOTARY get() = dummyNotary.party val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity - val DUMMY_NOTARY_KEY get() = dummyNotary.key + val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair val MEGA_CORP get() = megaCorp.party val MEGA_CORP_IDENTITY get() = megaCorp.identity - val MEGA_CORP_KEY get() = megaCorp.key - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_KEY get() = megaCorp.keyPair + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey val MINI_CORP get() = miniCorp.party val MINI_CORP_IDENTITY get() = miniCorp.identity - val MINI_CORP_KEY get() = miniCorp.key - val MINI_CORP_PUBKEY get() = miniCorp.pubkey + val MINI_CORP_KEY get() = miniCorp.keyPair + val MINI_CORP_PUBKEY get() = miniCorp.publicKey } @Rule @@ -99,10 +103,9 @@ class CashTests { }, MINI_CORP.name, MINI_CORP_KEY) val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val databaseAndServices = makeTestDatabaseAndMockServices( - listOf(generateKeyPair()), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), - listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"), - CordaX500Name("Me", "London", "GB")) + listOf("net.corda.finance.contracts.asset"), + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), + TestIdentity(CordaX500Name("Me", "London", "GB"))) database = databaseAndServices.first ourServices = databaseAndServices.second @@ -138,7 +141,7 @@ class CashTests { } private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { - MockServices(rigorousMock().also { + MockServices(emptyList(), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) 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 2670b0674f..73f1363d36 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 @@ -22,6 +22,8 @@ import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState +import net.corda.testing.dsl.* +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger import net.corda.testing.node.transaction @@ -45,14 +47,14 @@ class ObligationTests { val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) val ALICE get() = alice.party - val ALICE_PUBKEY get() = alice.pubkey + val ALICE_PUBKEY get() = alice.publicKey val BOB get() = bob.party - val BOB_PUBKEY get() = bob.pubkey + val BOB_PUBKEY get() = bob.publicKey val DUMMY_NOTARY get() = dummyNotary.party val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey val MINI_CORP get() = miniCorp.party - val MINI_CORP_PUBKEY get() = miniCorp.pubkey + val MINI_CORP_PUBKEY get() = miniCorp.publicKey } @Rule @@ -77,7 +79,7 @@ class ObligationTests { ) private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), miniCorp) - private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, dummyNotary.key) + private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair) private val identityService = rigorousMock().also { doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY) @@ -86,7 +88,7 @@ class ObligationTests { doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY) } private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), identityService, MEGA_CORP.name) - private val ledgerServices get() = MockServices(identityService, MEGA_CORP.name) + private val ledgerServices get() = MockServices(emptyList(), identityService, MEGA_CORP.name) private fun cashObligationTestRoots( group: LedgerDSL ) = group.apply { 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 index cffc411f7f..dc131cc8b5 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt @@ -2,18 +2,16 @@ 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.lang.reflect.InvocationTargetException import java.net.URLClassLoader import java.nio.file.Path import java.nio.file.Paths -import java.util.concurrent.TimeUnit import java.util.jar.JarInputStream /** @@ -118,13 +116,13 @@ open class Cordform : DefaultTask() { } /** - * The parametersGenerator needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. + * The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. */ - private fun loadNetworkParamsGenClass(): Class<*> { + private fun loadNetworkBootstrapperClass(): Class<*> { 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, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.NetworkParametersGenerator") + return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper") } /** @@ -136,8 +134,7 @@ open class Cordform : DefaultTask() { initializeConfiguration() installRunScript() nodes.forEach(Node::build) - generateAndInstallNodeInfos() - generateAndInstallNetworkParameters() + bootstrapNetwork() } private fun initializeConfiguration() { @@ -164,14 +161,17 @@ open class Cordform : DefaultTask() { } } - private fun generateAndInstallNetworkParameters() { - project.logger.info("Generating and installing network parameters") - val networkParamsGenClass = loadNetworkParamsGenClass() - val nodeDirs = nodes.map(Node::fullPath) - val networkParamsGenObject = networkParamsGenClass.newInstance() - val runMethod = networkParamsGenClass.getMethod("run", List::class.java).apply { isAccessible = true } - // Call NetworkParametersGenerator.run - runMethod.invoke(networkParamsGenObject, nodeDirs) + private fun bootstrapNetwork() { + val networkBootstrapperClass = loadNetworkBootstrapperClass() + val networkBootstrapper = networkBootstrapperClass.newInstance() + val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true } + // Call NetworkBootstrapper.bootstrap + try { + val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize() + bootstrapMethod.invoke(networkBootstrapper, rootDir) + } catch (e: InvocationTargetException) { + throw e.cause!! + } } private fun CordformDefinition.getMatchingCordapps(): List { @@ -197,90 +197,4 @@ open class Cordform : DefaultTask() { return false } } - - private fun generateAndInstallNodeInfos() { - generateNodeInfos() - installNodeInfos() - } - - private fun generateNodeInfos() { - project.logger.info("Generating node infos") - val nodeProcesses = buildNodeProcesses() - try { - validateNodeProcessess(nodeProcesses) - } finally { - destroyNodeProcesses(nodeProcesses) - } - } - - private fun buildNodeProcesses(): Map { - val command = generateNodeInfoCommand() - return nodes.map { - it.makeLogDirectory() - buildProcess(it, command, "generate-info.log") }.toMap() - } - - private fun validateNodeProcessess(nodeProcesses: Map) { - nodeProcesses.forEach { (node, process) -> - validateNodeProcess(node, process) - } - } - - private fun destroyNodeProcesses(nodeProcesses: Map) { - nodeProcesses.forEach { (_, process) -> - process.destroyForcibly() - } - } - - private fun buildProcess(node: Node, command: List, logFile: String): Pair { - val process = ProcessBuilder(command) - .directory(node.fullPath().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(node.logFile(logFile).toFile()) - .addEnvironment("CAPSULE_CACHE_DIR", Node.capsuleCacheDir) - .start() - return Pair(node, process) - } - - private fun generateNodeInfoCommand(): List = listOf( - "java", - "-Dcapsule.log=verbose", - "-Dcapsule.dir=${Node.capsuleCacheDir}", - "-jar", - Node.nodeJarName, - "--just-generate-node-info" - ) - - private fun validateNodeProcess(node: Node, process: Process) { - val generateTimeoutSeconds = 60L - if (!process.waitFor(generateTimeoutSeconds, TimeUnit.SECONDS)) { - throw GradleException("Node took longer $generateTimeoutSeconds seconds than too to generate node info - see node log at ${node.fullPath()}/logs") - } - if (process.exitValue() != 0) { - throw GradleException("Node exited with ${process.exitValue()} when generating node infos - see node log at ${node.fullPath()}/logs") - } - project.logger.info("Generated node info for ${node.fullPath()}") - } - - 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(source.fullPath().toString()) - include("nodeInfo-*") - into(destination.fullPath().resolve(CordformNode.NODE_INFO_DIRECTORY).toString()) - } - } - } - } - } - } - - private fun Node.logFile(name: String): Path = this.logDirectory().resolve(name) - private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) } } 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 index 89f425dd60..ffaab5c5b1 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -20,13 +20,8 @@ class Node(private val project: Project) : CordformNode() { @JvmStatic val webJarName = "corda-webserver.jar" private val configFileProperty = "configFile" - val capsuleCacheDir: String = "./cache" } - fun fullPath(): Path = project.projectDir.toPath().resolve(nodeDir.toPath()) - fun logDirectory(): Path = fullPath().resolve("logs") - fun makeLogDirectory() = Files.createDirectories(logDirectory()) - /** * 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 diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index 94953584ab..c49bd847f7 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -122,7 +122,7 @@ private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: I end tell""") } OS.WINDOWS -> { - listOf("cmd", "/C", "start ${command.joinToString(" ")}") + listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") } OS.LINUX -> { // Start shell to keep window open unless java terminated normally or due to SIGTERM: @@ -136,12 +136,11 @@ end tell""") }) private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ") - override fun getJavaPath(): String { - val path = File(File(System.getProperty("java.home"), "bin"), "java").path - // Replace below is to fix an issue with spaces in paths on Windows. - // Quoting the entire path does not work, only the space or directory within the path. - return if (os == OS.WINDOWS) path.replace(" ", "\" \"") else path - } + override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path + + // Replace below is to fix an issue with spaces in paths on Windows. + // Quoting the entire path does not work, only the space or directory within the path. + private fun windowsSpaceEscape(s:String) = s.replace(" ", "\" \"") } private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index ab1720934a..9811800de0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -29,12 +29,14 @@ class ArtemisTcpTransport { // but we allow classical RSA certificates to work in case: // a) we need to use keytool certificates in some demos, // b) we use cloud providers or HSMs that do not support ECC. - private val CIPHER_SUITES = listOf( + val CIPHER_SUITES = listOf( "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" ) + val TLS_VERSIONS = listOf("TLSv1.2") + fun tcpTransport( direction: ConnectionDirection, hostAndPort: NetworkHostAndPort, @@ -68,7 +70,7 @@ class ArtemisTcpTransport { TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile, TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword, TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), - TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2", + TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","), TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true, VERIFY_PEER_LEGAL_NAME to (direction as? ConnectionDirection.Outbound)?.expectedCommonNames ) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersCopier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersCopier.kt deleted file mode 100644 index d464575a4f..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersCopier.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.corda.nodeapi.internal - -import net.corda.core.crypto.SignedData -import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.sign -import net.corda.core.internal.copyTo -import net.corda.core.internal.div -import net.corda.core.serialization.serialize -import net.corda.nodeapi.internal.NetworkParameters -import java.math.BigInteger -import java.nio.file.FileAlreadyExistsException -import java.nio.file.Path - -class NetworkParametersCopier(networkParameters: NetworkParameters) { - private companion object { - val DUMMY_MAP_KEY = entropyToKeyPair(BigInteger.valueOf(123)) - } - - private val serializedNetworkParameters = networkParameters.let { - val serialize = it.serialize() - val signature = DUMMY_MAP_KEY.sign(serialize) - SignedData(serialize, signature).serialize() - } - - fun install(dir: Path) { - try { - serializedNetworkParameters.open().copyTo(dir / "network-parameters") - } catch (e: FileAlreadyExistsException) { - // Leave the file untouched if it already exists - } - } -} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt deleted file mode 100644 index 185e8bda8f..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt +++ /dev/null @@ -1,111 +0,0 @@ -package net.corda.nodeapi.internal - -import com.typesafe.config.ConfigFactory -import net.corda.core.crypto.SignedData -import net.corda.core.identity.Party -import net.corda.core.internal.div -import net.corda.core.internal.list -import net.corda.core.internal.readAll -import net.corda.core.node.NodeInfo -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.SerializationEnvironmentImpl -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.utilities.ByteSequence -import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.days -import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT -import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT -import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl -import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme -import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme -import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 -import java.nio.file.Path -import java.time.Instant - -/** - * This class is loaded by Cordform using reflection to generate the network parameters. It is assumed that Cordform has - * already asked each node to generate its node info file. - */ -@Suppress("UNUSED") -class NetworkParametersGenerator { - companion object { - private val logger = contextLogger() - } - - fun run(nodesDirs: List) { - logger.info("NetworkParameters generation using node directories: $nodesDirs") - try { - initialiseSerialization() - val notaryInfos = gatherNotaryIdentities(nodesDirs) - val copier = NetworkParametersCopier(NetworkParameters( - minimumPlatformVersion = 1, - notaries = notaryInfos, - modifiedTime = Instant.now(), - eventHorizon = 10000.days, - maxMessageSize = 40000, - maxTransactionSize = 40000, - epoch = 1 - )) - nodesDirs.forEach(copier::install) - } finally { - _contextSerializationEnv.set(null) - } - } - - private fun gatherNotaryIdentities(nodesDirs: List): List { - return nodesDirs.mapNotNull { nodeDir -> - val nodeConfig = ConfigFactory.parseFile((nodeDir / "node.conf").toFile()) - if (nodeConfig.hasPath("notary")) { - val validating = nodeConfig.getConfig("notary").getBoolean("validating") - val nodeInfoFile = nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() } - processFile(nodeInfoFile)?.let { NotaryInfo(it.notaryIdentity(), validating) } - } else { - null - } - }.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity - } - - private fun NodeInfo.notaryIdentity(): Party { - return when (legalIdentities.size) { - // Single node notaries have just one identity like all other nodes. This identity is the notary identity - 1 -> legalIdentities[0] - // Nodes which are part of a distributed notary have a second identity which is the composite identity of the - // cluster and is shared by all the other members. This is the notary identity. - 2 -> legalIdentities[1] - else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") - } - } - - private fun processFile(file: Path): NodeInfo? { - return try { - logger.info("Reading NodeInfo from file: $file") - val signedData = file.readAll().deserialize>() - signedData.verified() - } catch (e: Exception) { - logger.warn("Exception parsing NodeInfo from file. $file", e) - null - } - } - - // We need to to set serialization env, because generation of parameters is run from Cordform. - // KryoServerSerializationScheme is not accessible from nodeapi. - private fun initialiseSerialization() { - val context = if (java.lang.Boolean.getBoolean("net.corda.testing.amqp.enable")) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT - _contextSerializationEnv.set(SerializationEnvironmentImpl( - SerializationFactoryImpl().apply { - registerScheme(KryoParametersSerializationScheme) - registerScheme(AMQPServerSerializationScheme()) - }, - context) - ) - } - - private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() { - override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { - return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P - } - override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException() - override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() - } -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt new file mode 100644 index 0000000000..1b0ed15348 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt @@ -0,0 +1,44 @@ +package net.corda.nodeapi.internal + +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.verify +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import java.security.SignatureException + +/** + * A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected + * to be in the same order as the identities. + */ +// TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key +// that the node owns. This check can only be done by the network map server as it can check with the doorman if a node +// is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite +// public keys. +@CordaSerializable +class SignedNodeInfo(val raw: SerializedBytes, val signatures: List) { + fun verified(): NodeInfo { + val nodeInfo = raw.deserialize() + val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey } + + if (identities.size < signatures.size) { + throw SignatureException("Extra signatures. Found ${signatures.size} expected ${identities.size}") + } + if (identities.size > signatures.size) { + throw SignatureException("Missing signatures. Found ${signatures.size} expected ${identities.size}") + } + + val rawBytes = raw.bytes // To avoid cloning the byte array multiple times + identities.zip(signatures).forEach { (identity, signature) -> + try { + identity.owningKey.verify(rawBytes, signature) + } catch (e: SignatureException) { + throw SignatureException("$identity: ${e.message}") + } + } + + return nodeInfo + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt new file mode 100644 index 0000000000..34ee05851a --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -0,0 +1,167 @@ +package net.corda.nodeapi.internal.network + +import com.typesafe.config.ConfigFactory +import net.corda.cordform.CordformNode +import net.corda.core.identity.Party +import net.corda.core.internal.* +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.core.utilities.ByteSequence +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT +import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme +import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme +import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.time.Instant +import java.util.concurrent.TimeUnit.SECONDS +import kotlin.streams.toList + +/** + * Class to bootstrap a local network of Corda nodes on the same filesystem. + */ +class NetworkBootstrapper { + companion object { + // TODO This will probably need to change once we start using a bundled JVM + private val nodeInfoGenCmd = listOf( + "java", + "-jar", + "corda.jar", + "--just-generate-node-info" + ) + + private const val LOGS_DIR_NAME = "logs" + + @JvmStatic + fun main(args: Array) { + val arg = args.singleOrNull() ?: throw IllegalArgumentException("Expecting single argument which is the nodes' parent directory") + NetworkBootstrapper().bootstrap(Paths.get(arg).toAbsolutePath().normalize()) + } + } + + fun bootstrap(directory: Path) { + directory.createDirectories() + println("Bootstrapping local network in $directory") + val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() } + require(nodeDirs.isNotEmpty()) { "No nodes found" } + println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}") + val processes = startNodeInfoGeneration(nodeDirs) + initialiseSerialization() + try { + println("Waiting for all nodes to generate their node-info files") + val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs) + println("Distributing all node info-files to all nodes") + distributeNodeInfos(nodeDirs, nodeInfoFiles) + println("Gathering notary identities") + val notaryInfos = gatherNotaryInfos(nodeInfoFiles) + println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}") + installNetworkParameters(notaryInfos, nodeDirs) + println("Bootstrapping complete!") + } finally { + _contextSerializationEnv.set(null) + processes.forEach { if (it.isAlive) it.destroyForcibly() } + } + } + + private fun startNodeInfoGeneration(nodeDirs: List): List { + return nodeDirs.map { nodeDir -> + val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories() + ProcessBuilder(nodeInfoGenCmd) + .directory(nodeDir.toFile()) + .redirectErrorStream(true) + .redirectOutput((logsDir / "node-info-gen.log").toFile()) + .apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" } + .start() + } + } + + private fun gatherNodeInfoFiles(processes: List, nodeDirs: List): List { + val timeOutInSeconds = 60L + return processes.zip(nodeDirs).map { (process, nodeDir) -> + check(process.waitFor(timeOutInSeconds, SECONDS)) { + "Node in ${nodeDir.fileName} took longer than ${timeOutInSeconds}s to generate its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}" + } + check(process.exitValue() == 0) { + "Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}" + } + nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() } + } + } + + private fun distributeNodeInfos(nodeDirs: List, nodeInfoFiles: List) { + for (nodeDir in nodeDirs) { + val additionalNodeInfosDir = (nodeDir / CordformNode.NODE_INFO_DIRECTORY).createDirectories() + for (nodeInfoFile in nodeInfoFiles) { + nodeInfoFile.copyToDirectory(additionalNodeInfosDir, StandardCopyOption.REPLACE_EXISTING) + } + } + } + + private fun gatherNotaryInfos(nodeInfoFiles: List): List { + return nodeInfoFiles.mapNotNull { nodeInfoFile -> + // The config contains the notary type + val nodeConfig = ConfigFactory.parseFile((nodeInfoFile.parent / "node.conf").toFile()) + if (nodeConfig.hasPath("notary")) { + val validating = nodeConfig.getConfig("notary").getBoolean("validating") + // And the node-info file contains the notary's identity + val nodeInfo = nodeInfoFile.readAll().deserialize().verified() + NotaryInfo(nodeInfo.notaryIdentity(), validating) + } else { + null + } + }.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity + } + + private fun installNetworkParameters(notaryInfos: List, nodeDirs: List) { + // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize + val copier = NetworkParametersCopier(NetworkParameters( + minimumPlatformVersion = 1, + notaries = notaryInfos, + modifiedTime = Instant.now(), + maxMessageSize = 10485760, + maxTransactionSize = 40000, + epoch = 1 + ), overwriteFile = true) + + nodeDirs.forEach(copier::install) + } + + private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)" + + private fun NodeInfo.notaryIdentity(): Party { + return when (legalIdentities.size) { + // Single node notaries have just one identity like all other nodes. This identity is the notary identity + 1 -> legalIdentities[0] + // Nodes which are part of a distributed notary have a second identity which is the composite identity of the + // cluster and is shared by all the other members. This is the notary identity. + 2 -> legalIdentities[1] + else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") + } + } + + // We need to to set serialization env, because generation of parameters is run from Cordform. + // KryoServerSerializationScheme is not accessible from nodeapi. + private fun initialiseSerialization() { + _contextSerializationEnv.set(SerializationEnvironmentImpl( + SerializationFactoryImpl().apply { + registerScheme(KryoParametersSerializationScheme) + registerScheme(AMQPServerSerializationScheme()) + }, + AMQP_P2P_CONTEXT) + ) + } + + private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() { + override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { + return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P + } + override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException() + override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt similarity index 65% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt index 871b1e514f..f784a4297b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt @@ -1,19 +1,21 @@ -package net.corda.nodeapi.internal +package net.corda.nodeapi.internal.network import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.verify import net.corda.core.identity.Party +import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize +import net.corda.nodeapi.internal.crypto.X509Utilities import java.security.SignatureException import java.security.cert.CertPathValidatorException import java.security.cert.X509Certificate -import java.time.Duration import java.time.Instant -// TODO: Need more discussion on rather we should move this class out of internal. +const val NETWORK_PARAMS_FILE_NAME = "network-parameters" + /** * Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes. */ @@ -21,21 +23,21 @@ import java.time.Instant data class NetworkMap(val nodeInfoHashes: List, val networkParameterHash: SecureHash) /** - * @property minimumPlatformVersion - * @property notaries - * @property eventHorizon + * @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network. + * @property notaries List of well known and trusted notary identities with information on validation type. * @property maxMessageSize Maximum P2P message sent over the wire in bytes. * @property maxTransactionSize Maximum permitted transaction size in bytes. * @property modifiedTime * @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set * of parameters. */ -// TODO Wire up the parameters +// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network. +// It needs separate design. +// TODO Currently maxTransactionSize is not wired. @CordaSerializable data class NetworkParameters( val minimumPlatformVersion: Int, val notaries: List, - val eventHorizon: Duration, val maxMessageSize: Int, val maxTransactionSize: Int, val modifiedTime: Instant, @@ -45,6 +47,8 @@ data class NetworkParameters( require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" } require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" } require(epoch > 0) { "epoch must be at least 1" } + require(maxMessageSize > 0) { "maxMessageSize must be at least 1" } + require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" } } } @@ -56,20 +60,23 @@ data class NotaryInfo(val identity: Party, val validating: Boolean) * contained within. */ @CordaSerializable -class SignedNetworkMap(val raw: SerializedBytes, val sig: DigitalSignatureWithCert) { +class SignedNetworkMap(val raw: SerializedBytes, val signature: DigitalSignatureWithCert) { /** * Return the deserialized NetworkMap if the signature and certificate can be verified. * * @throws CertPathValidatorException if the certificate path is invalid. * @throws SignatureException if the signature is invalid. */ - @Throws(SignatureException::class) - fun verified(): NetworkMap { - sig.by.publicKey.verify(raw.bytes, sig) + @Throws(SignatureException::class, CertPathValidatorException::class) + fun verified(trustedRoot: X509Certificate): NetworkMap { + signature.by.publicKey.verify(raw.bytes, signature) + // Assume network map cert is under the default trust root. + X509Utilities.validateCertificateChain(trustedRoot, signature.by, trustedRoot) return raw.deserialize() } } // TODO: This class should reside in the [DigitalSignature] class. +// TODO: Removing the val from signatureBytes causes serialisation issues /** A digital signature that identifies who the public key is owned by, and the certificate which provides prove of the identity */ -class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes) \ No newline at end of file +class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParametersCopier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParametersCopier.kt new file mode 100644 index 0000000000..fe9b88a24e --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParametersCopier.kt @@ -0,0 +1,35 @@ +package net.corda.nodeapi.internal.network + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignedData +import net.corda.core.crypto.sign +import net.corda.core.internal.copyTo +import net.corda.core.internal.div +import net.corda.core.serialization.serialize +import net.corda.nodeapi.internal.crypto.X509Utilities +import java.nio.file.FileAlreadyExistsException +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.security.KeyPair + +class NetworkParametersCopier( + networkParameters: NetworkParameters, + signingKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), + overwriteFile: Boolean = false +) { + private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray() + private val serializedNetworkParameters = networkParameters.let { + val serialize = it.serialize() + val signature = signingKeyPair.sign(serialize) + SignedData(serialize, signature).serialize() + } + + fun install(nodeDir: Path) { + try { + serializedNetworkParameters.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions) + } catch (e: FileAlreadyExistsException) { + // This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we + // ignore this exception as we're happy with the existing file. + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoFilesCopier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NodeInfoFilesCopier.kt similarity index 98% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoFilesCopier.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NodeInfoFilesCopier.kt index 00786c8da1..f11fa2265e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoFilesCopier.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NodeInfoFilesCopier.kt @@ -1,4 +1,4 @@ -package net.corda.nodeapi.internal +package net.corda.nodeapi.internal.network import net.corda.cordform.CordformNode import net.corda.core.internal.ThreadBox @@ -65,7 +65,6 @@ class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()) : AutoCloseabl } /** - * @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. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt b/node-api/src/test/kotlin/net/corda/nodeapi/Eventually.kt similarity index 96% rename from testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt rename to node-api/src/test/kotlin/net/corda/nodeapi/Eventually.kt index 693fffeca9..4c8136d1d3 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/Eventually.kt @@ -1,4 +1,4 @@ -package net.corda.testing +package net.corda.nodeapi import java.time.Duration 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 f3a24002cd..d0dd03ad6d 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 @@ -14,6 +14,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.* +import net.corda.testing.internal.rigorousMock import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.* import org.junit.Rule 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 1d624a32f3..bbf8e5ca21 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 @@ -19,6 +19,8 @@ import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName import net.corda.nodeapi.internal.serialization.withTokenContext import net.corda.testing.* +import net.corda.testing.internal.kryoSpecific +import net.corda.testing.internal.rigorousMock import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert.* diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/SignedNodeInfoTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/SignedNodeInfoTest.kt new file mode 100644 index 0000000000..0ad99ae579 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/SignedNodeInfoTest.kt @@ -0,0 +1,80 @@ +package net.corda.nodeapi.internal + +import net.corda.core.crypto.Crypto +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.TestNodeInfoBuilder +import net.corda.testing.internal.signWith +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Rule +import org.junit.Test +import java.security.SignatureException + +class SignedNodeInfoTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val nodeInfoBuilder = TestNodeInfoBuilder() + + @Test + fun `verifying single identity`() { + nodeInfoBuilder.addIdentity(ALICE_NAME) + val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned() + assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo) + } + + @Test + fun `verifying multiple identities`() { + nodeInfoBuilder.addIdentity(ALICE_NAME) + nodeInfoBuilder.addIdentity(BOB_NAME) + val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned() + assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo) + } + + @Test + fun `verifying missing signature`() { + val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) + nodeInfoBuilder.addIdentity(BOB_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining("Missing signatures") + } + + @Test + fun `verifying extra signature`() { + val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey, generateKeyPair().private)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining("Extra signatures") + } + + @Test + fun `verifying incorrect signature`() { + nodeInfoBuilder.addIdentity(ALICE_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(generateKeyPair().private)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining(ALICE_NAME.toString()) + } + + @Test + fun `verifying with signatures in wrong order`() { + val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) + val (_, bobKey) = nodeInfoBuilder.addIdentity(BOB_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(bobKey, aliceKey)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining(ALICE_NAME.toString()) + } + + private fun generateKeyPair() = Crypto.generateKeyPair() +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 30c8004659..8435ff5791 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -51,7 +51,7 @@ class X509UtilitiesTest { val bob = TestIdentity(BOB_NAME, 80) val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party val BOB get() = bob.party - val BOB_PUBKEY get() = bob.pubkey + val BOB_PUBKEY get() = bob.publicKey } @Rule diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NodeInfoFilesCopierTest.kt similarity index 94% rename from node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt rename to node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NodeInfoFilesCopierTest.kt index c14f2c854d..d4d95ba6fd 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NodeInfoFilesCopierTest.kt @@ -1,8 +1,7 @@ -package net.corda.nodeapi +package net.corda.nodeapi.internal.network import net.corda.cordform.CordformNode -import net.corda.nodeapi.internal.NodeInfoFilesCopier -import net.corda.testing.eventually +import net.corda.nodeapi.eventually import org.junit.Before import org.junit.Rule import org.junit.Test @@ -15,14 +14,7 @@ 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" @@ -34,6 +26,13 @@ class NodeInfoFilesCopierTest { private val BAD_NODE_INFO_NAME = "something" } + @Rule + @JvmField + val folder = TemporaryFolder() + + private val rootPath get() = folder.root.toPath() + private val scheduler = TestScheduler() + private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase()) private val node1RootPath by lazy { nodeDir(NODE_1_PATH) } @@ -41,7 +40,7 @@ class NodeInfoFilesCopierTest { 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 + private lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier @Before fun setUp() { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt index 80ee09701a..e24dc671dd 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt @@ -5,6 +5,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.* import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -23,7 +24,7 @@ class ContractAttachmentSerializerTest { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext private lateinit var contextWithToken: SerializationContext - private val mockServices = MockServices(rigorousMock(), CordaX500Name("MegaCorp", "London", "GB")) + private val mockServices = MockServices(emptyList(), rigorousMock(), CordaX500Name("MegaCorp", "London", "GB")) @Before fun setup() { factory = testSerialization.env.serializationFactory 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 654bf323e3..1743d9ee46 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 @@ -13,7 +13,7 @@ import net.corda.nodeapi.internal.AttachmentsClassLoaderTests import net.corda.nodeapi.internal.serialization.kryo.CordaKryo import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 import net.corda.testing.services.MockAttachmentStorage -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index 508a40f7c6..d48558a6fe 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -33,7 +33,7 @@ import kotlin.test.assertTrue class KryoTests { companion object { - private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey + private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey } @Rule 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 d6796023b3..2e7e4e24ce 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 @@ -8,8 +8,8 @@ import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput import net.corda.nodeapi.internal.serialization.amqp.Envelope import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 -import net.corda.testing.amqpSpecific -import net.corda.testing.kryoSpecific +import net.corda.testing.internal.amqpSpecific +import net.corda.testing.internal.kryoSpecific import net.corda.testing.SerializationEnvironmentRule import org.assertj.core.api.Assertions import org.junit.Assert.assertArrayEquals 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 3bb028f437..ef89c36f62 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 @@ -9,8 +9,8 @@ import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.DataSessionMessage import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.amqpSpecific -import net.corda.testing.kryoSpecific +import net.corda.testing.internal.amqpSpecific +import net.corda.testing.internal.kryoSpecific import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertArrayEquals import org.junit.Rule diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt index a51f5934e1..70f165d9a4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt @@ -8,7 +8,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.nodeapi.internal.serialization.kryo.CordaKryo import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import net.corda.testing.SerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThat import org.junit.Before 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 03c4fb08c1..4d66588057 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 @@ -6,8 +6,8 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.DataSessionMessage import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 +import net.corda.testing.internal.kryoSpecific import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.kryoSpecific import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Rule 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 68bc1a5786..9871bff1c6 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 @@ -29,6 +29,7 @@ import org.apache.qpid.proton.codec.EncoderImpl import org.assertj.core.api.Assertions.* import org.junit.Assert.* import org.junit.Ignore +import org.junit.Rule import org.junit.Test import java.io.ByteArrayInputStream import java.io.IOException @@ -49,11 +50,15 @@ class SerializationOutputTests { val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey val MINI_CORP get() = miniCorp.party - val MINI_CORP_PUBKEY get() = miniCorp.pubkey + val MINI_CORP_PUBKEY get() = miniCorp.publicKey } + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + data class Foo(val bar: String, val pub: Int) data class testFloat(val f: Float) @@ -473,9 +478,9 @@ class SerializationOutputTests { assertSerializedThrowableEquivalent(t, desThrowable) } - private fun serdesThrowableWithInternalInfo(t: Throwable, factory: SerializerFactory, factory2: SerializerFactory, expectedEqual: Boolean = true): Throwable = withTestSerialization { + private fun serdesThrowableWithInternalInfo(t: Throwable, factory: SerializerFactory, factory2: SerializerFactory, expectedEqual: Boolean = true): Throwable { val newContext = SerializationFactory.defaultFactory.defaultContext.withProperty(CommonPropertyNames.IncludeInternalInfo, true) - SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } } + return SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } } } @Test diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt new file mode 100644 index 0000000000..4c2f0ee881 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -0,0 +1,242 @@ +package net.corda.node.amqp + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.internal.div +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.toBase58String +import net.corda.node.internal.protonwrapper.netty.AMQPServer +import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.services.config.* +import net.corda.node.services.messaging.ArtemisMessagingClient +import net.corda.node.services.messaging.ArtemisMessagingServer +import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE +import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.testing.* +import net.corda.testing.internal.rigorousMock +import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID +import org.apache.activemq.artemis.api.core.RoutingType +import org.apache.activemq.artemis.api.core.SimpleString +import org.junit.Assert.assertArrayEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.Observable +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class AMQPBridgeTest { + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + + private val ALICE = TestIdentity(ALICE_NAME) + private val BOB = TestIdentity(BOB_NAME) + + private val artemisPort = freePort() + private val artemisPort2 = freePort() + private val amqpPort = freePort() + private val artemisAddress = NetworkHostAndPort("localhost", artemisPort) + private val artemisAddress2 = NetworkHostAndPort("localhost", artemisPort2) + private val amqpAddress = NetworkHostAndPort("localhost", amqpPort) + + private abstract class AbstractNodeConfiguration : NodeConfiguration + + @Test + fun `test acked and nacked messages`() { + // Create local queue + val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String() + val (artemisServer, artemisClient) = createArtemis(sourceQueueName) + + // Pre-populate local queue with 3 messages + val artemis = artemisClient.started!! + for (i in 0 until 3) { + val artemisMessage = artemis.session.createMessage(true).apply { + putIntProperty("CountProp", i) + writeBodyBufferBytes("Test$i".toByteArray()) + // Use the magic deduplication property built into Artemis as our message identity too + putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) + } + artemis.producer.send(sourceQueueName, artemisMessage) + } + + //Create target server + val amqpServer = createAMQPServer() + + val receive = amqpServer.onReceive.toBlocking().iterator + amqpServer.start() + + val received1 = receive.next() + val messageID1 = received1.applicationProperties["CountProp"] as Int + assertArrayEquals("Test$messageID1".toByteArray(), received1.payload) + assertEquals(0, messageID1) + received1.complete(true) // Accept first message + + val received2 = receive.next() + val messageID2 = received2.applicationProperties["CountProp"] as Int + assertArrayEquals("Test$messageID2".toByteArray(), received2.payload) + assertEquals(1, messageID2) + received2.complete(false) // Reject message + + while (true) { + val received3 = receive.next() + val messageID3 = received3.applicationProperties["CountProp"] as Int + assertArrayEquals("Test$messageID3".toByteArray(), received3.payload) + assertNotEquals(0, messageID3) + if (messageID3 != 1) { // keep rejecting any batched items following rejection + received3.complete(false) + } else { // beginnings of replay so accept again + received3.complete(true) + break + } + } + + while (true) { + val received4 = receive.next() + val messageID4 = received4.applicationProperties["CountProp"] as Int + assertArrayEquals("Test$messageID4".toByteArray(), received4.payload) + if (messageID4 != 1) { // we may get a duplicate of the rejected message, in which case skip + assertEquals(2, messageID4) // next message should be in order though + break + } + received4.complete(true) + } + + // Send a fresh item and check receive + val artemisMessage = artemis.session.createMessage(true).apply { + putIntProperty("CountProp", -1) + writeBodyBufferBytes("Test_end".toByteArray()) + // Use the magic deduplication property built into Artemis as our message identity too + putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) + } + artemis.producer.send(sourceQueueName, artemisMessage) + + val received5 = receive.next() + val messageID5 = received5.applicationProperties["CountProp"] as Int + assertArrayEquals("Test_end".toByteArray(), received5.payload) + assertEquals(-1, messageID5) // next message should be in order + received5.complete(true) + + amqpServer.stop() + artemisClient.stop() + artemisServer.stop() + } + + @Test + fun `Test legacy bridge still works`() { + // Create local queue + val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String() + val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName) + + + val (artemisServer, artemisClient) = createArtemis(null) + + val artemis = artemisLegacyClient.started!! + for (i in 0 until 3) { + val artemisMessage = artemis.session.createMessage(true).apply { + putIntProperty("CountProp", i) + writeBodyBufferBytes("Test$i".toByteArray()) + // Use the magic deduplication property built into Artemis as our message identity too + putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) + } + artemis.producer.send(sourceQueueName, artemisMessage) + } + + + val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE) + for (i in 0 until 3) { + val msg = subs.receive() + val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) } + assertArrayEquals("Test$i".toByteArray(), messageBody) + assertEquals(i, msg.getIntProperty("CountProp")) + } + + artemisClient.stop() + artemisServer.stop() + artemisLegacyClient.stop() + artemisLegacyServer.stop() + + } + + private fun createArtemis(sourceQueueName: String?): Pair { + val artemisConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory + doReturn(ALICE_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn("").whenever(it).exportJMXto + doReturn(emptyList()).whenever(it).certificateChainCheckPolicies + doReturn(true).whenever(it).useAMQPBridges + } + artemisConfig.configureWithDevSSLCertificate() + val networkMap = rigorousMock().also { + doReturn(Observable.never()).whenever(it).changed + doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any()) + } + val userService = rigorousMock() + val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE) + val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE) + artemisServer.start() + artemisClient.start() + val artemis = artemisClient.started!! + if (sourceQueueName != null) { + // Local queue for outgoing messages + artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true) + } + return Pair(artemisServer, artemisClient) + } + + private fun createLegacyArtemis(sourceQueueName: String): Pair { + val artemisConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "artemis2").whenever(it).baseDirectory + doReturn(BOB_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn("").whenever(it).exportJMXto + doReturn(emptyList()).whenever(it).certificateChainCheckPolicies + doReturn(false).whenever(it).useAMQPBridges + doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer + } + artemisConfig.configureWithDevSSLCertificate() + val networkMap = rigorousMock().also { + doReturn(Observable.never()).whenever(it).changed + doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any()) + } + val userService = rigorousMock() + val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE) + val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress2, MAX_MESSAGE_SIZE) + artemisServer.start() + artemisClient.start() + val artemis = artemisClient.started!! + // Local queue for outgoing messages + artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true) + return Pair(artemisServer, artemisClient) + } + + private fun createAMQPServer(): AMQPServer { + val serverConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(BOB_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + } + serverConfig.configureWithDevSSLCertificate() + + val serverTruststore = loadKeyStore(serverConfig.trustStoreFile, serverConfig.trustStorePassword) + val serverKeystore = loadKeyStore(serverConfig.sslKeystore, serverConfig.keyStorePassword) + val amqpServer = AMQPServer("0.0.0.0", + amqpPort, + ArtemisMessagingComponent.PEER_USER, + ArtemisMessagingComponent.PEER_USER, + serverKeystore, + serverConfig.keyStorePassword, + serverTruststore, + trace = true) + return amqpServer + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt new file mode 100644 index 0000000000..871c7de400 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -0,0 +1,309 @@ +package net.corda.node.amqp + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import io.netty.channel.EventLoopGroup +import io.netty.channel.nio.NioEventLoopGroup +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.toFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.internal.protonwrapper.messages.MessageStatus +import net.corda.node.internal.protonwrapper.netty.AMQPClient +import net.corda.node.internal.protonwrapper.netty.AMQPServer +import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.services.config.CertChainPolicyConfig +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.node.services.messaging.ArtemisMessagingClient +import net.corda.node.services.messaging.ArtemisMessagingServer +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.testing.* +import net.corda.testing.internal.rigorousMock +import org.apache.activemq.artemis.api.core.RoutingType +import org.junit.Assert.assertArrayEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.Observable.never +import kotlin.test.assertEquals + +class ProtonWrapperTests { + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + + private val serverPort = freePort() + private val serverPort2 = freePort() + private val artemisPort = freePort() + + private abstract class AbstractNodeConfiguration : NodeConfiguration + + @Test + fun `Simple AMPQ Client to Server`() { + val amqpServer = createServer(serverPort) + amqpServer.use { + amqpServer.start() + val receiveSubs = amqpServer.onReceive.subscribe { + assertEquals(BOB_NAME.toString(), it.sourceLegalName) + assertEquals("p2p.inbound", it.topic) + assertEquals("Test", String(it.payload)) + it.complete(true) + } + val amqpClient = createClient() + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(true, serverConnect.connected) + assertEquals(BOB_NAME, CordaX500Name.parse(serverConnect.remoteCert!!.subject.toString())) + val clientConnect = clientConnected.get() + assertEquals(true, clientConnect.connected) + assertEquals(ALICE_NAME, CordaX500Name.parse(clientConnect.remoteCert!!.subject.toString())) + val msg = amqpClient.createMessage("Test".toByteArray(), + "p2p.inbound", + ALICE_NAME.toString(), + emptyMap()) + amqpClient.write(msg) + assertEquals(MessageStatus.Acknowledged, msg.onComplete.get()) + receiveSubs.unsubscribe() + } + } + } + + @Test + fun `AMPQ Client refuses to connect to unexpected server`() { + val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB")) + amqpServer.use { + amqpServer.start() + val amqpClient = createClient() + amqpClient.use { + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val clientConnect = clientConnected.get() + assertEquals(false, clientConnect.connected) + } + } + } + + @Test + fun `Client Failover for multiple IP`() { + val amqpServer = createServer(serverPort) + val amqpServer2 = createServer(serverPort2) + val amqpClient = createClient() + try { + val serverConnected = amqpServer.onConnection.toFuture() + val serverConnected2 = amqpServer2.onConnection.toFuture() + val clientConnected = amqpClient.onConnection.toBlocking().iterator + amqpServer.start() + amqpClient.start() + val serverConn1 = serverConnected.get() + assertEquals(true, serverConn1.connected) + assertEquals(BOB_NAME, CordaX500Name.parse(serverConn1.remoteCert!!.subject.toString())) + val connState1 = clientConnected.next() + assertEquals(true, connState1.connected) + assertEquals(ALICE_NAME, CordaX500Name.parse(connState1.remoteCert!!.subject.toString())) + assertEquals(serverPort, connState1.remoteAddress.port) + + // Fail over + amqpServer2.start() + amqpServer.stop() + val connState2 = clientConnected.next() + assertEquals(false, connState2.connected) + assertEquals(serverPort, connState2.remoteAddress.port) + val serverConn2 = serverConnected2.get() + assertEquals(true, serverConn2.connected) + assertEquals(BOB_NAME, CordaX500Name.parse(serverConn2.remoteCert!!.subject.toString())) + val connState3 = clientConnected.next() + assertEquals(true, connState3.connected) + assertEquals(ALICE_NAME, CordaX500Name.parse(connState3.remoteCert!!.subject.toString())) + assertEquals(serverPort2, connState3.remoteAddress.port) + + // Fail back + amqpServer.start() + amqpServer2.stop() + val connState4 = clientConnected.next() + assertEquals(false, connState4.connected) + assertEquals(serverPort2, connState4.remoteAddress.port) + val serverConn3 = serverConnected.get() + assertEquals(true, serverConn3.connected) + assertEquals(BOB_NAME, CordaX500Name.parse(serverConn3.remoteCert!!.subject.toString())) + val connState5 = clientConnected.next() + assertEquals(true, connState5.connected) + assertEquals(ALICE_NAME, CordaX500Name.parse(connState5.remoteCert!!.subject.toString())) + assertEquals(serverPort, connState5.remoteAddress.port) + } finally { + amqpClient.close() + amqpServer.close() + amqpServer2.close() + } + } + + @Test + fun `Send a message from AMQP to Artemis inbox`() { + val (server, artemisClient) = createArtemisServerAndClient() + val amqpClient = createClient() + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + assertEquals(true, clientConnected.get().connected) + assertEquals(CHARLIE_NAME, CordaX500Name.parse(clientConnected.get().remoteCert!!.subject.toString())) + val artemis = artemisClient.started!! + val sendAddress = "p2p.inbound" + artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true) + val consumer = artemis.session.createConsumer("queue") + val testData = "Test".toByteArray() + val testProperty = mutableMapOf() + testProperty["TestProp"] = "1" + val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) + amqpClient.write(message) + assertEquals(MessageStatus.Acknowledged, message.onComplete.get()) + val received = consumer.receive() + assertEquals("1", received.getStringProperty("TestProp")) + assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) }) + amqpClient.stop() + artemisClient.stop() + server.stop() + } + + @Test + fun `shared AMQPClient threadpool tests`() { + val amqpServer = createServer(serverPort) + amqpServer.use { + val connectionEvents = amqpServer.onConnection.toBlocking().iterator + amqpServer.start() + val sharedThreads = NioEventLoopGroup() + val amqpClient1 = createSharedThreadsClient(sharedThreads, 0) + val amqpClient2 = createSharedThreadsClient(sharedThreads, 1) + amqpClient1.start() + val connection1 = connectionEvents.next() + assertEquals(true, connection1.connected) + val connection1ID = CordaX500Name.parse(connection1.remoteCert!!.subject.toString()) + assertEquals("client 0", connection1ID.organisationUnit) + val source1 = connection1.remoteAddress + amqpClient2.start() + val connection2 = connectionEvents.next() + assertEquals(true, connection2.connected) + val connection2ID = CordaX500Name.parse(connection2.remoteCert!!.subject.toString()) + assertEquals("client 1", connection2ID.organisationUnit) + val source2 = connection2.remoteAddress + // Stopping one shouldn't disconnect the other + amqpClient1.stop() + val connection3 = connectionEvents.next() + assertEquals(false, connection3.connected) + assertEquals(source1, connection3.remoteAddress) + assertEquals(false, amqpClient1.connected) + assertEquals(true, amqpClient2.connected) + // Now shutdown both + amqpClient2.stop() + val connection4 = connectionEvents.next() + assertEquals(false, connection4.connected) + assertEquals(source2, connection4.remoteAddress) + assertEquals(false, amqpClient1.connected) + assertEquals(false, amqpClient2.connected) + // Now restarting one should work + amqpClient1.start() + val connection5 = connectionEvents.next() + assertEquals(true, connection5.connected) + val connection5ID = CordaX500Name.parse(connection5.remoteCert!!.subject.toString()) + assertEquals("client 0", connection5ID.organisationUnit) + assertEquals(true, amqpClient1.connected) + assertEquals(false, amqpClient2.connected) + // Cleanup + amqpClient1.stop() + sharedThreads.shutdownGracefully() + sharedThreads.terminationFuture().sync() + } + } + + private fun createArtemisServerAndClient(): Pair { + val artemisConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory + doReturn(CHARLIE_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn("").whenever(it).exportJMXto + doReturn(emptyList()).whenever(it).certificateChainCheckPolicies + doReturn(true).whenever(it).useAMQPBridges + } + artemisConfig.configureWithDevSSLCertificate() + + val networkMap = rigorousMock().also { + doReturn(never()).whenever(it).changed + } + val userService = rigorousMock() + val server = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE) + val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), MAX_MESSAGE_SIZE) + server.start() + client.start() + return Pair(server, client) + } + + private fun createClient(): AMQPClient { + val clientConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory + doReturn(BOB_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + } + clientConfig.configureWithDevSSLCertificate() + + val clientTruststore = loadKeyStore(clientConfig.trustStoreFile, clientConfig.trustStorePassword) + val clientKeystore = loadKeyStore(clientConfig.sslKeystore, clientConfig.keyStorePassword) + val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort), + NetworkHostAndPort("localhost", serverPort2), + NetworkHostAndPort("localhost", artemisPort)), + setOf(ALICE_NAME, CHARLIE_NAME), + PEER_USER, + PEER_USER, + clientKeystore, + clientConfig.keyStorePassword, + clientTruststore, true) + return amqpClient + } + + private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int): AMQPClient { + val clientConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory + doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + } + clientConfig.configureWithDevSSLCertificate() + + val clientTruststore = loadKeyStore(clientConfig.trustStoreFile, clientConfig.trustStorePassword) + val clientKeystore = loadKeyStore(clientConfig.sslKeystore, clientConfig.keyStorePassword) + val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)), + setOf(ALICE_NAME), + PEER_USER, + PEER_USER, + clientKeystore, + clientConfig.keyStorePassword, + clientTruststore, true, sharedEventGroup) + return amqpClient + } + + private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME): AMQPServer { + val serverConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(name).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + } + serverConfig.configureWithDevSSLCertificate() + + val serverTruststore = loadKeyStore(serverConfig.trustStoreFile, serverConfig.trustStorePassword) + val serverKeystore = loadKeyStore(serverConfig.sslKeystore, serverConfig.keyStorePassword) + val amqpServer = AMQPServer("0.0.0.0", + port, + PEER_USER, + PEER_USER, + serverKeystore, + serverConfig.keyStorePassword, + serverTruststore) + return amqpServer + } + +} \ No newline at end of file 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 e035e2d854..3b5159e6cb 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 @@ -24,15 +24,21 @@ import net.corda.testing.* import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver +import net.corda.testing.internal.withoutTestSerialization import net.corda.testing.services.MockAttachmentStorage +import net.corda.testing.internal.rigorousMock import org.junit.Assert.assertEquals import org.junit.ClassRule +import org.junit.Rule import org.junit.Test import java.net.URLClassLoader import java.nio.file.Files import kotlin.test.assertFailsWith class AttachmentLoadingTests : IntegrationTest() { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments) private val cordapp get() = provider.cordapps.first() @@ -83,7 +89,7 @@ class AttachmentLoadingTests : IntegrationTest() { } @Test - fun `test a wire transaction has loaded the correct attachment`() = withTestSerialization { + fun `test a wire transaction has loaded the correct attachment`() { val appClassLoader = appContext.classLoader val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java) val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java) @@ -99,7 +105,7 @@ class AttachmentLoadingTests : IntegrationTest() { } @Test - fun `test that attachments retrieved over the network are not used for code`() { + fun `test that attachments retrieved over the network are not used for code`() = withoutTestSerialization { driver { installIsolatedCordappTo(bankAName) val (bankA, bankB) = createTwoNodes() @@ -107,15 +113,17 @@ class AttachmentLoadingTests : IntegrationTest() { bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } } + Unit } @Test - fun `tests that if the attachment is loaded on both sides already that a flow can run`() { + fun `tests that if the attachment is loaded on both sides already that a flow can run`() = withoutTestSerialization { driver { installIsolatedCordappTo(bankAName) installIsolatedCordappTo(bankBName) val (bankA, bankB) = createTwoNodes() bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } + Unit } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 624590fab8..4e6ba5d42f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -24,10 +24,12 @@ import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.NetworkParametersCopier -import net.corda.nodeapi.internal.NotaryInfo import net.corda.nodeapi.internal.ServiceIdentityGenerator -import net.corda.testing.* +import net.corda.nodeapi.internal.network.NotaryInfo +import net.corda.testing.chooseIdentity +import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.testing.IntegrationTest +import net.corda.testing.IntegrationTestSchemas import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -55,7 +57,7 @@ class BFTNotaryServiceTests : IntegrationTest() { @Before fun before() { - mockNet = MockNetwork() + mockNet = MockNetwork(emptyList()) node = mockNet.createNode() } @After diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 8ca2736f7a..c890e1f6cd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -1,26 +1,37 @@ package net.corda.node.services.network +import net.corda.core.crypto.SignedData +import net.corda.core.internal.list +import net.corda.core.internal.readAll import net.corda.core.node.NodeInfo +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME import net.corda.testing.* import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.node.internal.internalDriver -import net.corda.testing.node.network.NetworkMapServer +import net.corda.testing.node.internal.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before -import org.junit.ClassRule -import org.junit.Test +import org.junit.* import java.net.URL +import kotlin.test.assertEquals class NetworkMapTest : IntegrationTest() { companion object { - @ClassRule @JvmField + @ClassRule + @JvmField val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(), DUMMY_NOTARY_NAME.toDatabaseSchemaName()) } + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(true) private val cacheTimeout = 1.seconds private val portAllocation = PortAllocation.Incremental(10000) @@ -39,9 +50,22 @@ class NetworkMapTest : IntegrationTest() { networkMapServer.close() } + @Test + fun `node correctly downloads and saves network parameters file on startup`() { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val networkParameters = alice.configuration.baseDirectory + .list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() } + .readAll() + .deserialize>() + .verified() + assertEquals(NetworkMapServer.stubNetworkParameter, networkParameters) + } + } + @Test fun `nodes can see each other using the http network map`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { val alice = startNode(providedName = ALICE_NAME) val bob = startNode(providedName = BOB_NAME) val notaryNode = defaultNotaryNode.get() @@ -56,7 +80,7 @@ class NetworkMapTest : IntegrationTest() { @Test fun `nodes process network map add updates correctly when adding new node to network map`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { val alice = startNode(providedName = ALICE_NAME) val notaryNode = defaultNotaryNode.get() val aliceNode = alice.get() @@ -77,7 +101,7 @@ class NetworkMapTest : IntegrationTest() { @Test fun `nodes process network map remove updates correctly`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { val alice = startNode(providedName = ALICE_NAME) val bob = startNode(providedName = BOB_NAME) val notaryNode = defaultNotaryNode.get() 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 8977afda78..b813558eca 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 @@ -3,14 +3,15 @@ package net.corda.node.services.network import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs import net.corda.cordform.CordformNode -import net.corda.core.crypto.SignedData import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService -import net.corda.core.serialization.serialize -import net.corda.nodeapi.internal.NodeInfoFilesCopier -import net.corda.testing.* +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.network.NodeInfoFilesCopier +import net.corda.testing.ALICE_NAME +import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat @@ -27,20 +28,20 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class NodeInfoWatcherTest { - private companion object { - val alice = TestIdentity(ALICE_NAME, 70) - val nodeInfo = NodeInfo(listOf(), listOf(alice.identity), 0, 0) - } - @Rule @JvmField val testSerialization = SerializationEnvironmentRule() + @Rule @JvmField val tempFolder = TemporaryFolder() - private lateinit var nodeInfoPath: Path + private val scheduler = TestScheduler() private val testSubscriber = TestSubscriber() + + private lateinit var nodeInfo: NodeInfo + private lateinit var signedNodeInfo: SignedNodeInfo + private lateinit var nodeInfoPath: Path private lateinit var keyManagementService: KeyManagementService // Object under test @@ -48,8 +49,11 @@ class NodeInfoWatcherTest { @Before fun start() { + val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) + nodeInfo = nodeInfoAndSigned.first + signedNodeInfo = nodeInfoAndSigned.second val identityService = makeTestIdentityService() - keyManagementService = MockKeyManagementService(identityService, alice.key) + keyManagementService = MockKeyManagementService(identityService) nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler) nodeInfoPath = tempFolder.root.toPath() / CordformNode.NODE_INFO_DIRECTORY } @@ -58,7 +62,6 @@ class NodeInfoWatcherTest { fun `save a NodeInfo`() { assertEquals(0, tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size) - val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey)) NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), signedNodeInfo) val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) } @@ -74,7 +77,6 @@ class NodeInfoWatcherTest { fun `save a NodeInfo to JimFs`() { val jimFs = Jimfs.newFileSystem(Configuration.unix()) val jimFolder = jimFs.getPath("/nodeInfo") - val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey)) NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo) } @@ -82,11 +84,9 @@ class NodeInfoWatcherTest { fun `load an empty Directory`() { nodeInfoPath.createDirectories() - val subscription = nodeInfoWatcher.nodeInfoUpdates() - .subscribe(testSubscriber) + val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) try { advanceTime() - val readNodes = testSubscriber.onNextEvents.distinct() assertEquals(0, readNodes.size) } finally { @@ -96,15 +96,13 @@ class NodeInfoWatcherTest { @Test fun `load a non empty Directory`() { - createNodeInfoFileInPath(nodeInfo) + createNodeInfoFileInPath() - val subscription = nodeInfoWatcher.nodeInfoUpdates() - .subscribe(testSubscriber) + val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) advanceTime() try { val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(1, readNodes.size) assertEquals(nodeInfo, readNodes.first()) } finally { @@ -117,14 +115,13 @@ class NodeInfoWatcherTest { nodeInfoPath.createDirectories() // Start polling with an empty folder. - val subscription = nodeInfoWatcher.nodeInfoUpdates() - .subscribe(testSubscriber) + val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) try { // Ensure the watch service is started. advanceTime() // Check no nodeInfos are read. assertEquals(0, testSubscriber.valueCount) - createNodeInfoFileInPath(nodeInfo) + createNodeInfoFileInPath() advanceTime() @@ -143,8 +140,7 @@ class NodeInfoWatcherTest { } // Write a nodeInfo under the right path. - private fun createNodeInfoFileInPath(nodeInfo: NodeInfo) { - val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey)) + private fun createNodeInfoFileInPath() { NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 34779ac2c3..3465caebdc 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -14,14 +14,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA -import net.corda.testing.ALICE_NAME import net.corda.testing.IntegrationTest import net.corda.testing.IntegrationTestSchemas +import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.driver.PortAllocation import net.corda.testing.node.internal.internalDriver -import net.corda.testing.node.network.NetworkMapServer -import net.corda.testing.toDatabaseSchemaName +import net.corda.testing.node.internal.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -29,6 +28,7 @@ import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.After import org.junit.Before import org.junit.ClassRule +import org.junit.Rule import org.junit.Test import java.io.ByteArrayOutputStream import java.io.InputStream @@ -47,7 +47,9 @@ class NodeRegistrationTest : IntegrationTest() { @ClassRule @JvmField val databaseSchemas = IntegrationTestSchemas("Alice") } - + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(true) private val portAllocation = PortAllocation.Incremental(13000) private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate() private val registrationHandler = RegistrationHandler(rootCertAndKeyPair) @@ -57,7 +59,7 @@ class NodeRegistrationTest : IntegrationTest() { @Before fun startServer() { - server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), registrationHandler) + server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), rootCertAndKeyPair, registrationHandler) serverHostAndPort = server.start() } @@ -74,7 +76,8 @@ class NodeRegistrationTest : IntegrationTest() { internalDriver( portAllocation = portAllocation, notarySpecs = emptyList(), - compatibilityZone = compatibilityZone + compatibilityZone = compatibilityZone, + initialiseSerialization = false ) { startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() assertThat(registrationHandler.idsPolled).contains("Alice") diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 94eb96a7a8..f8da5302a4 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -8,7 +8,6 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.* -import net.corda.testing.messaging.SimpleMQClient import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt index d4cd7ca4e4..eab85f937a 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt @@ -1,7 +1,6 @@ package net.corda.services.messaging import net.corda.nodeapi.internal.config.User -import net.corda.testing.messaging.SimpleMQClient import org.junit.Test /** diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 561fdea008..cc146d78d2 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -26,7 +26,6 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREF import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.* -import net.corda.testing.messaging.SimpleMQClient import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.startFlow import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 5dfa2963b1..4841d20750 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -101,13 +101,10 @@ class P2PMessagingTest : IntegrationTest() { // Restart the node and expect a response val aliceRestarted = startAlice() - - val responseFuture = openFuture() - aliceRestarted.network.runOnNextMessage("test.response") { + val responseFuture = openFuture() aliceRestarted.network.runOnNextMessage("test.response") { responseFuture.set(it.data.deserialize()) } val response = responseFuture.getOrThrow() - assertThat(crashingNodes.requestsReceived.get()).isGreaterThan(numberOfRequestsReceived) assertThat(response).isEqualTo(responseMessage) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt similarity index 98% rename from testing/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt rename to node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt index 235692b111..4257e7e3c0 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt @@ -1,4 +1,4 @@ -package net.corda.testing.messaging +package net.corda.services.messaging import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.internal.nodeSerializationEnv 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 790ccfbf2d..91e9ae6afb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -10,6 +10,8 @@ import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.sign import net.corda.core.crypto.newSecureRandom @@ -33,10 +35,10 @@ 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.internal.security.RPCSecurityManager import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler -import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.api.* import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration @@ -60,7 +62,10 @@ import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor import net.corda.nodeapi.internal.NetworkParameters import net.corda.nodeapi.internal.persistence.SchemaMigration +import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME +import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration @@ -139,11 +144,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() protected val _nodeReadyFuture = openFuture() - protected val networkMapClient: NetworkMapClient? by lazy { - configuration.compatibilityZoneURL?.let { - NetworkMapClient(it, services.identityService.trustRoot) - } - } + protected var networkMapClient: NetworkMapClient? = null lateinit var securityManager: RPCSecurityManager get @@ -178,6 +179,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, validateKeystore() } + private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes) -> DigitalSignature): SignedNodeInfo { + // For now we assume the node has only one identity (excluding any composite ones) + val owningKey = nodeInfo.legalIdentities.single { it.owningKey !is CompositeKey }.owningKey + val serialised = nodeInfo.serialize() + val signature = sign(owningKey, serialised) + return SignedNodeInfo(serialised, listOf(signature)) + } + open fun generateNodeInfo() { check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") @@ -189,11 +198,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // a code smell. val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) - val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey } - val serialisedNodeInfo = info.serialize() - val signature = identityKeypair.sign(serialisedNodeInfo) - // TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature. - NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature)) + val signedNodeInfo = signNodeInfo(info) { publicKey, serialised -> + val privateKey = keyPairs.single { it.public == publicKey }.private + privateKey.sign(serialised.bytes) + } + NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) } } @@ -213,10 +222,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() - readNetworkParameters() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val identityService = makeIdentityService(identity.certificate) + networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) } + retrieveNetworkParameters() // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService) @@ -260,16 +270,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration, startShell(rpcOps) Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) } - val networkMapUpdater = NetworkMapUpdater(services.networkMapCache, NodeInfoWatcher(configuration.baseDirectory, getRxIoScheduler(), Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)), - networkMapClient) + networkMapClient, + networkParameters.serialize().hash) runOnStop += networkMapUpdater::close networkMapUpdater.updateNodeInfo(services.myInfo) { - val serialisedNodeInfo = it.serialize() - val signature = services.keyManagementService.sign(serialisedNodeInfo.bytes, it.legalIdentities.first().owningKey) - SignedData(serialisedNodeInfo, signature) + signNodeInfo(it) { publicKey, serialised -> + services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() + } } networkMapUpdater.subscribeToNetworkMap() @@ -658,11 +668,31 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return PersistentKeyManagementService(identityService, keyPairs) } - private fun readNetworkParameters() { - val file = configuration.baseDirectory / "network-parameters" - networkParameters = file.readAll().deserialize>().verified() - log.info(networkParameters.toString()) - check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { "Node is too old for the network" } + private fun retrieveNetworkParameters() { + val networkParamsFile = configuration.baseDirectory.list { paths -> + paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().orElse(null) + } + + networkParameters = if (networkParamsFile != null) { + networkParamsFile.readAll().deserialize>().verified() + } else { + log.info("No network-parameters file found. Expecting network parameters to be available from the network map.") + val networkMapClient = checkNotNull(networkMapClient) { + "Node hasn't been configured to connect to a network map from which to get the network parameters" + } + val (networkMap, _) = networkMapClient.getNetworkMap() + val signedParams = checkNotNull(networkMapClient.getNetworkParameter(networkMap.networkParameterHash)) { + "Failed loading network parameters from network map server" + } + val verifiedParams = signedParams.verified() + signedParams.serialize().open().copyTo(configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME) + verifiedParams + } + + log.info("Loaded network parameters: $networkParameters") + check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { + "Node's platform version is lower than network's required minimumPlatformVersion" + } } private fun makeCoreNotaryService(notaryConfig: NotaryConfig, database: CordaPersistence): NotaryService { 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 df43650e8e..db5b2eb12e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -144,9 +144,9 @@ open class Node(configuration: NodeConfiguration, val advertisedAddress = info.addresses.single() printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) - rpcMessagingClient = RPCMessagingClient(configuration, serverAddress) + rpcMessagingClient = RPCMessagingClient(configuration, serverAddress, networkParameters.maxMessageSize) verifierMessagingClient = when (configuration.verifierType) { - VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics) + VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize) VerifierType.InMemory -> null } return P2PMessagingClient( @@ -156,12 +156,13 @@ open class Node(configuration: NodeConfiguration, info.legalIdentities[0].owningKey, serverThread, database, - advertisedAddress) + advertisedAddress, + networkParameters.maxMessageSize) } private fun makeLocalMessageBroker(): NetworkHostAndPort { with(configuration) { - messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager) + messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager, networkParameters.maxMessageSize) return NetworkHostAndPort("localhost", p2pAddress.port) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/ConnectionStateMachine.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/ConnectionStateMachine.kt new file mode 100644 index 0000000000..8ae8acda99 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/ConnectionStateMachine.kt @@ -0,0 +1,483 @@ +package net.corda.node.internal.protonwrapper.engine + +import io.netty.buffer.ByteBuf +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import io.netty.channel.Channel +import io.netty.channel.ChannelHandlerContext +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.debug +import net.corda.node.internal.protonwrapper.messages.MessageStatus +import net.corda.node.internal.protonwrapper.messages.impl.ReceivedMessageImpl +import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl +import org.apache.qpid.proton.Proton +import org.apache.qpid.proton.amqp.Binary +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.amqp.messaging.* +import org.apache.qpid.proton.amqp.messaging.Properties +import org.apache.qpid.proton.amqp.messaging.Target +import org.apache.qpid.proton.amqp.transaction.Coordinator +import org.apache.qpid.proton.amqp.transport.ErrorCondition +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode +import org.apache.qpid.proton.amqp.transport.SenderSettleMode +import org.apache.qpid.proton.engine.* +import org.apache.qpid.proton.message.Message +import org.apache.qpid.proton.message.ProtonJMessage +import org.slf4j.LoggerFactory +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.util.* + +/** + * This ConnectionStateMachine class handles the events generated by the proton-j library to track + * various logical connection, transport and link objects and to drive packet processing. + * It is single threaded per physical SSL connection just like the proton-j library, + * but this threading lock is managed by the EventProcessor class that calls this. + * It ultimately posts application packets to/from from the netty transport pipeline. + */ +internal class ConnectionStateMachine(serverMode: Boolean, + collector: Collector, + private val localLegalName: String, + private val remoteLegalName: String, + userName: String?, + password: String?) : BaseHandler() { + companion object { + private const val IDLE_TIMEOUT = 10000 + } + + val connection: Connection + private val log = LoggerFactory.getLogger(localLegalName) + private val transport: Transport + private val id = UUID.randomUUID().toString() + private var session: Session? = null + private val messageQueues = mutableMapOf>() + private val unackedQueue = LinkedList() + private val receivers = mutableMapOf() + private val senders = mutableMapOf() + private var tagId: Int = 0 + + init { + connection = Engine.connection() + connection.container = "CORDA:$id" + transport = Engine.transport() + transport.idleTimeout = IDLE_TIMEOUT + transport.context = connection + transport.setEmitFlowEventOnSend(true) + connection.collect(collector) + val sasl = transport.sasl() + if (userName != null) { + //TODO This handshake is required for our queue permission logic in Artemis + sasl.setMechanisms("PLAIN") + if (serverMode) { + sasl.server() + sasl.done(Sasl.PN_SASL_OK) + } else { + sasl.plain(userName, password) + sasl.client() + } + } else { + sasl.setMechanisms("ANONYMOUS") + if (serverMode) { + sasl.server() + sasl.done(Sasl.PN_SASL_OK) + } else { + sasl.client() + } + } + transport.bind(connection) + if (!serverMode) { + connection.open() + } + } + + override fun onConnectionInit(event: Event) { + val connection = event.connection + log.debug { "Connection init $connection" } + } + + override fun onConnectionLocalOpen(event: Event) { + val connection = event.connection + log.info("Connection local open $connection") + val session = connection.session() + session.open() + this.session = session + for (target in messageQueues.keys) { + getSender(target) + } + } + + override fun onConnectionLocalClose(event: Event) { + val connection = event.connection + log.info("Connection local close $connection") + connection.close() + connection.free() + } + + override fun onConnectionUnbound(event: Event) { + if (event.connection == this.connection) { + val channel = connection.context as? Channel + if (channel != null) { + if (channel.isActive) { + channel.close() + } + } + } + } + + override fun onConnectionFinal(event: Event) { + val connection = event.connection + log.debug { "Connection final $connection" } + if (connection == this.connection) { + this.connection.context = null + for (queue in messageQueues.values) { + // clear any dead messages + while (true) { + val msg = queue.poll() + if (msg != null) { + msg.doComplete(MessageStatus.Rejected) + msg.release() + } else { + break + } + } + } + messageQueues.clear() + while (true) { + val msg = unackedQueue.poll() + if (msg != null) { + msg.doComplete(MessageStatus.Rejected) + msg.release() + } else { + break + } + } + // shouldn't happen, but close socket channel now if not already done + val channel = connection.context as? Channel + if (channel != null && channel.isActive) { + channel.close() + } + // shouldn't happen, but cleanup any stranded items + transport.context = null + session = null + receivers.clear() + senders.clear() + } + } + + override fun onTransportHeadClosed(event: Event) { + val transport = event.transport + log.debug { "Transport Head Closed $transport" } + transport.close_tail() + } + + override fun onTransportTailClosed(event: Event) { + val transport = event.transport + log.debug { "Transport Tail Closed $transport" } + transport.close_head() + } + + override fun onTransportClosed(event: Event) { + val transport = event.transport + log.debug { "Transport Closed $transport" } + if (transport == this.transport) { + transport.unbind() + transport.free() + transport.context = null + } + } + + override fun onTransportError(event: Event) { + val transport = event.transport + log.info("Transport Error $transport") + val condition = event.transport.condition + if (condition != null) { + log.info("Error: ${condition.description}") + } else { + log.info("Error (no description returned).") + } + } + + override fun onTransport(event: Event) { + val transport = event.transport + log.debug { "Transport $transport" } + onTransportInternal(transport) + } + + private fun onTransportInternal(transport: Transport) { + if (!transport.isClosed) { + val pending = transport.pending() // Note this drives frame generation, which the susbsequent writes push to the socket + if (pending > 0) { + val connection = transport.context as? Connection + val channel = connection?.context as? Channel + channel?.writeAndFlush(transport) + } + } + } + + override fun onSessionInit(event: Event) { + val session = event.session + log.debug { "Session init $session" } + } + + override fun onSessionLocalOpen(event: Event) { + val session = event.session + log.debug { "Session local open $session" } + } + + private fun getSender(target: String): Sender { + if (!senders.containsKey(target)) { + val sender = session!!.sender(UUID.randomUUID().toString()) + sender.source = Source().apply { + address = target + dynamic = false + durable = TerminusDurability.NONE + } + sender.target = Target().apply { + address = target + dynamic = false + durable = TerminusDurability.UNSETTLED_STATE + } + sender.senderSettleMode = SenderSettleMode.UNSETTLED + sender.receiverSettleMode = ReceiverSettleMode.FIRST + senders[target] = sender + sender.open() + } + return senders[target]!! + } + + override fun onSessionLocalClose(event: Event) { + val session = event.session + log.debug { "Session local close $session" } + session.close() + session.free() + } + + override fun onSessionFinal(event: Event) { + val session = event.session + log.debug { "Session final $session" } + if (session == this.session) { + this.session = null + } + } + + override fun onLinkLocalOpen(event: Event) { + val link = event.link + if (link is Sender) { + log.debug { "Sender Link local open ${link.name} ${link.source} ${link.target}" } + senders[link.target.address] = link + transmitMessages(link) + } + if (link is Receiver) { + log.debug { "Receiver Link local open ${link.name} ${link.source} ${link.target}" } + receivers[link.target.address] = link + } + } + + override fun onLinkRemoteOpen(event: Event) { + val link = event.link + if (link is Receiver) { + if (link.remoteTarget is Coordinator) { + log.debug { "Coordinator link received" } + } + } + } + + override fun onLinkFinal(event: Event) { + val link = event.link + if (link is Sender) { + log.debug { "Sender Link final ${link.name} ${link.source} ${link.target}" } + senders.remove(link.target.address) + } + if (link is Receiver) { + log.debug { "Receiver Link final ${link.name} ${link.source} ${link.target}" } + receivers.remove(link.target.address) + } + } + + override fun onLinkFlow(event: Event) { + val link = event.link + if (link is Sender) { + log.debug { "Sender Flow event: ${link.name} ${link.source} ${link.target}" } + if (senders.containsKey(link.target.address)) { + transmitMessages(link) + } + } else if (link is Receiver) { + log.debug { "Receiver Flow event: ${link.name} ${link.source} ${link.target}" } + } + } + + fun processTransport() { + onTransportInternal(transport) + } + + private fun transmitMessages(sender: Sender) { + val messageQueue = messageQueues.getOrPut(sender.target.address, { LinkedList() }) + while (sender.credit > 0) { + log.debug { "Sender credit: ${sender.credit}" } + val nextMessage = messageQueue.poll() + if (nextMessage != null) { + try { + val messageBuf = nextMessage.buf!! + val buf = ByteBuffer.allocate(4) + buf.putInt(tagId++) + val delivery = sender.delivery(buf.array()) + delivery.context = nextMessage + sender.send(messageBuf.array(), messageBuf.arrayOffset() + messageBuf.readerIndex(), messageBuf.readableBytes()) + nextMessage.status = MessageStatus.Sent + log.debug { "Put tag ${javax.xml.bind.DatatypeConverter.printHexBinary(delivery.tag)} on wire uuid: ${nextMessage.applicationProperties["_AMQ_DUPL_ID"]}" } + unackedQueue.offer(nextMessage) + sender.advance() + } finally { + nextMessage.release() + } + } else { + break + } + } + } + + override fun onDelivery(event: Event) { + val delivery = event.delivery + log.debug { "Delivery $delivery" } + val link = delivery.link + if (link is Receiver) { + if (delivery.isReadable && !delivery.isPartial) { + val pending = delivery.pending() + val amqpMessage = decodeAMQPMessage(pending, link) + val payload = (amqpMessage.body as Data).value.array + val connection = event.connection + val channel = connection?.context as? Channel + if (channel != null) { + val appProperties = HashMap(amqpMessage.applicationProperties.value) + appProperties["_AMQ_VALIDATED_USER"] = remoteLegalName + val localAddress = channel.localAddress() as InetSocketAddress + val remoteAddress = channel.remoteAddress() as InetSocketAddress + val receivedMessage = ReceivedMessageImpl( + payload, + link.source.address, + remoteLegalName, + NetworkHostAndPort(localAddress.hostString, localAddress.port), + localLegalName, + NetworkHostAndPort(remoteAddress.hostString, remoteAddress.port), + appProperties, + channel, + delivery) + log.debug { "Full message received uuid: ${appProperties["_AMQ_DUPL_ID"]}" } + channel.writeAndFlush(receivedMessage) + if (link.current() == delivery) { + link.advance() + } + } else { + delivery.disposition(Rejected()) + delivery.settle() + } + } + } else if (link is Sender) { + log.debug { "Sender delivery confirmed tag ${javax.xml.bind.DatatypeConverter.printHexBinary(delivery.tag)}" } + val ok = delivery.remotelySettled() && delivery.remoteState == Accepted.getInstance() + val sourceMessage = delivery.context as? SendableMessageImpl + unackedQueue.remove(sourceMessage) + sourceMessage?.doComplete(if (ok) MessageStatus.Acknowledged else MessageStatus.Rejected) + delivery.settle() + } + } + + private fun encodeAMQPMessage(message: ProtonJMessage): ByteBuf { + val buffer = PooledByteBufAllocator.DEFAULT.heapBuffer(1500) + try { + try { + message.encode(NettyWritable(buffer)) + val bytes = ByteArray(buffer.writerIndex()) + buffer.readBytes(bytes) + return Unpooled.wrappedBuffer(bytes) + } catch (ex: Exception) { + log.error("Unable to encode message as AMQP packet", ex) + throw ex + } + } finally { + buffer.release() + } + } + + private fun encodePayloadBytes(msg: SendableMessageImpl): ByteBuf { + val message = Proton.message() as ProtonJMessage + message.body = Data(Binary(msg.payload)) + message.properties = Properties() + val appProperties = HashMap(msg.applicationProperties) + //TODO We shouldn't have to do this, but Artemis Server doesn't set the header on AMQP packets. + // Fortunately, when we are bridge to bridge/bridge to float we can authenticate links there. + appProperties["_AMQ_VALIDATED_USER"] = localLegalName + message.applicationProperties = ApplicationProperties(appProperties) + return encodeAMQPMessage(message) + } + + private fun decodeAMQPMessage(pending: Int, link: Receiver): Message { + val msgBuf = PooledByteBufAllocator.DEFAULT.heapBuffer(pending) + try { + link.recv(NettyWritable(msgBuf)) + val amqpMessage = Proton.message() + amqpMessage.decode(msgBuf.array(), msgBuf.arrayOffset() + msgBuf.readerIndex(), msgBuf.readableBytes()) + return amqpMessage + } finally { + msgBuf.release() + } + } + + fun transportWriteMessage(msg: SendableMessageImpl) { + log.debug { "Queue application message write uuid: ${msg.applicationProperties["_AMQ_DUPL_ID"]} ${javax.xml.bind.DatatypeConverter.printHexBinary(msg.payload)}" } + msg.buf = encodePayloadBytes(msg) + val messageQueue = messageQueues.getOrPut(msg.topic, { LinkedList() }) + messageQueue.offer(msg) + if (session != null) { + val sender = getSender(msg.topic) + transmitMessages(sender) + } + } + + fun transportProcessInput(msg: ByteBuf) { + val source = msg.nioBuffer() + try { + do { + val buffer = transport.inputBuffer + val limit = Math.min(buffer.remaining(), source.remaining()) + val duplicate = source.duplicate() + duplicate.limit(source.position() + limit) + buffer.put(duplicate) + transport.processInput().checkIsOk() + source.position(source.position() + limit) + } while (source.hasRemaining()) + } catch (ex: Exception) { + val condition = ErrorCondition() + condition.condition = Symbol.getSymbol("proton:io") + condition.description = ex.message + transport.condition = condition + transport.close_tail() + transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code) + } + } + + fun transportProcessOutput(ctx: ChannelHandlerContext) { + try { + var done = false + while (!done) { + val toWrite = transport.outputBuffer + if (toWrite != null && toWrite.hasRemaining()) { + val outbound = ctx.alloc().buffer(toWrite.remaining()) + outbound.writeBytes(toWrite) + ctx.write(outbound) + transport.outputConsumed() + } else { + done = true + } + } + ctx.flush() + } catch (ex: Exception) { + val condition = ErrorCondition() + condition.condition = Symbol.getSymbol("proton:io") + condition.description = ex.message + transport.condition = condition + transport.close_head() + transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/EventProcessor.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/EventProcessor.kt new file mode 100644 index 0000000000..31c7de7aed --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/EventProcessor.kt @@ -0,0 +1,136 @@ +package net.corda.node.internal.protonwrapper.engine + +import io.netty.buffer.ByteBuf +import io.netty.channel.Channel +import io.netty.channel.ChannelHandlerContext +import net.corda.core.utilities.debug +import net.corda.node.internal.protonwrapper.messages.MessageStatus +import net.corda.node.internal.protonwrapper.messages.impl.ReceivedMessageImpl +import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl +import org.apache.qpid.proton.Proton +import org.apache.qpid.proton.amqp.messaging.Accepted +import org.apache.qpid.proton.amqp.messaging.Rejected +import org.apache.qpid.proton.amqp.transport.DeliveryState +import org.apache.qpid.proton.amqp.transport.ErrorCondition +import org.apache.qpid.proton.engine.* +import org.apache.qpid.proton.engine.impl.CollectorImpl +import org.apache.qpid.proton.reactor.FlowController +import org.apache.qpid.proton.reactor.Handshaker +import org.slf4j.LoggerFactory +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * The EventProcessor class converts calls on the netty scheduler/pipeline + * into proton-j engine event calls into the ConnectionStateMachine. + * It also registers a couple of standard event processors for the basic connection handshake + * and simple sliding window flow control, so that these events don't have to live inside ConnectionStateMachine. + * Everything here is single threaded, because the proton-j library has to be run that way. + */ +internal class EventProcessor(channel: Channel, + serverMode: Boolean, + localLegalName: String, + remoteLegalName: String, + userName: String?, + password: String?) : BaseHandler() { + companion object { + private const val FLOW_WINDOW_SIZE = 10 + } + + private val log = LoggerFactory.getLogger(localLegalName) + private val lock = ReentrantLock() + private var pendingExecute: Boolean = false + private val executor: ScheduledExecutorService = channel.eventLoop() + private val collector = Proton.collector() as CollectorImpl + private val handlers = mutableListOf() + private val stateMachine: ConnectionStateMachine = ConnectionStateMachine(serverMode, + collector, + localLegalName, + remoteLegalName, + userName, + password) + + val connection: Connection = stateMachine.connection + + init { + addHandler(Handshaker()) + addHandler(FlowController(FLOW_WINDOW_SIZE)) + addHandler(stateMachine) + connection.context = channel + tick(stateMachine.connection) + } + + fun addHandler(handler: Handler) = handlers.add(handler) + + private fun popEvent(): Event? { + var ev = collector.peek() + if (ev != null) { + ev = ev.copy() // prevent mutation by collector.pop() + collector.pop() + } + return ev + } + + private fun tick(connection: Connection) { + lock.withLock { + try { + if ((connection.localState != EndpointState.CLOSED) && !connection.transport.isClosed) { + val now = System.currentTimeMillis() + val tickDelay = Math.max(0L, connection.transport.tick(now) - now) + executor.schedule({ tick(connection) }, tickDelay, TimeUnit.MILLISECONDS) + } + } catch (ex: Exception) { + connection.transport.close() + connection.condition = ErrorCondition() + } + } + } + + fun processEvents() { + lock.withLock { + pendingExecute = false + log.debug { "Process Events" } + while (true) { + val ev = popEvent() ?: break + log.debug { "Process event: $ev" } + for (handler in handlers) { + handler.handle(ev) + } + } + stateMachine.processTransport() + log.debug { "Process Events Done" } + } + } + + fun processEventsAsync() { + lock.withLock { + if (!pendingExecute) { + pendingExecute = true + executor.execute { processEvents() } + } + } + } + + fun close() { + if (connection.localState != EndpointState.CLOSED) { + connection.close() + processEvents() + connection.free() + processEvents() + } + } + + fun transportProcessInput(msg: ByteBuf) = lock.withLock { stateMachine.transportProcessInput(msg) } + + fun transportProcessOutput(ctx: ChannelHandlerContext) = lock.withLock { stateMachine.transportProcessOutput(ctx) } + + fun transportWriteMessage(msg: SendableMessageImpl) = lock.withLock { stateMachine.transportWriteMessage(msg) } + + fun complete(completer: ReceivedMessageImpl.MessageCompleter) = lock.withLock { + val status: DeliveryState = if (completer.status == MessageStatus.Acknowledged) Accepted.getInstance() else Rejected() + completer.delivery.disposition(status) + completer.delivery.settle() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/NettyWritable.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/NettyWritable.kt new file mode 100644 index 0000000000..b8e8085273 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/engine/NettyWritable.kt @@ -0,0 +1,63 @@ +package net.corda.node.internal.protonwrapper.engine + +import io.netty.buffer.ByteBuf +import org.apache.qpid.proton.codec.WritableBuffer +import java.nio.ByteBuffer + +/** + * NettyWritable is a utility class allow proton-j encoders to write directly into a + * netty ByteBuf, without any need to materialize a ByteArray copy. + */ +internal class NettyWritable(val nettyBuffer: ByteBuf) : WritableBuffer { + override fun put(b: Byte) { + nettyBuffer.writeByte(b.toInt()) + } + + override fun putFloat(f: Float) { + nettyBuffer.writeFloat(f) + } + + override fun putDouble(d: Double) { + nettyBuffer.writeDouble(d) + } + + override fun put(src: ByteArray, offset: Int, length: Int) { + nettyBuffer.writeBytes(src, offset, length) + } + + override fun putShort(s: Short) { + nettyBuffer.writeShort(s.toInt()) + } + + override fun putInt(i: Int) { + nettyBuffer.writeInt(i) + } + + override fun putLong(l: Long) { + nettyBuffer.writeLong(l) + } + + override fun hasRemaining(): Boolean { + return nettyBuffer.writerIndex() < nettyBuffer.capacity() + } + + override fun remaining(): Int { + return nettyBuffer.capacity() - nettyBuffer.writerIndex() + } + + override fun position(): Int { + return nettyBuffer.writerIndex() + } + + override fun position(position: Int) { + nettyBuffer.writerIndex(position) + } + + override fun put(payload: ByteBuffer) { + nettyBuffer.writeBytes(payload) + } + + override fun limit(): Int { + return nettyBuffer.capacity() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/ApplicationMessage.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/ApplicationMessage.kt new file mode 100644 index 0000000000..f90c1b172a --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/ApplicationMessage.kt @@ -0,0 +1,14 @@ +package net.corda.node.internal.protonwrapper.messages + +import net.corda.core.utilities.NetworkHostAndPort + +/** + * Represents a common interface for both sendable and received application messages. + */ +interface ApplicationMessage { + val payload: ByteArray + val topic: String + val destinationLegalName: String + val destinationLink: NetworkHostAndPort + val applicationProperties: Map +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/MessageStatus.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/MessageStatus.kt new file mode 100644 index 0000000000..6b792dd59e --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/MessageStatus.kt @@ -0,0 +1,11 @@ +package net.corda.node.internal.protonwrapper.messages + +/** + * The processing state of a message. + */ +enum class MessageStatus { + Unsent, + Sent, + Acknowledged, + Rejected +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/ReceivedMessage.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/ReceivedMessage.kt new file mode 100644 index 0000000000..df4a1fddff --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/ReceivedMessage.kt @@ -0,0 +1,13 @@ +package net.corda.node.internal.protonwrapper.messages + +import net.corda.core.utilities.NetworkHostAndPort + +/** + * An extension of ApplicationMessage that includes origin information. + */ +interface ReceivedMessage : ApplicationMessage { + val sourceLegalName: String + val sourceLink: NetworkHostAndPort + + fun complete(accepted: Boolean) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/SendableMessage.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/SendableMessage.kt new file mode 100644 index 0000000000..bc4a405e26 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/SendableMessage.kt @@ -0,0 +1,10 @@ +package net.corda.node.internal.protonwrapper.messages + +import net.corda.core.concurrent.CordaFuture + +/** + * An extension of ApplicationMessage to allow completion signalling. + */ +interface SendableMessage : ApplicationMessage { + val onComplete: CordaFuture +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt new file mode 100644 index 0000000000..1d5f25d59f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt @@ -0,0 +1,30 @@ +package net.corda.node.internal.protonwrapper.messages.impl + +import io.netty.channel.Channel +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.internal.protonwrapper.messages.MessageStatus +import net.corda.node.internal.protonwrapper.messages.ReceivedMessage +import org.apache.qpid.proton.engine.Delivery + +/** + * An internal packet management class that allows tracking of asynchronous acknowledgements + * that in turn send Delivery messages back to the originator. + */ +internal class ReceivedMessageImpl(override val payload: ByteArray, + override val topic: String, + override val sourceLegalName: String, + override val sourceLink: NetworkHostAndPort, + override val destinationLegalName: String, + override val destinationLink: NetworkHostAndPort, + override val applicationProperties: Map, + private val channel: Channel, + private val delivery: Delivery) : ReceivedMessage { + data class MessageCompleter(val status: MessageStatus, val delivery: Delivery) + + override fun complete(accepted: Boolean) { + val status = if (accepted) MessageStatus.Acknowledged else MessageStatus.Rejected + channel.writeAndFlush(MessageCompleter(status, delivery)) + } + + override fun toString(): String = "Received ${String(payload)} $topic" +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/impl/SendableMessageImpl.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/impl/SendableMessageImpl.kt new file mode 100644 index 0000000000..52dd1ed08f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/messages/impl/SendableMessageImpl.kt @@ -0,0 +1,37 @@ +package net.corda.node.internal.protonwrapper.messages.impl + +import io.netty.buffer.ByteBuf +import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.internal.protonwrapper.messages.MessageStatus +import net.corda.node.internal.protonwrapper.messages.SendableMessage + +/** + * An internal packet management class that allows handling of the encoded buffers and + * allows registration of an acknowledgement handler when the remote receiver confirms durable storage. + */ +internal class SendableMessageImpl(override val payload: ByteArray, + override val topic: String, + override val destinationLegalName: String, + override val destinationLink: NetworkHostAndPort, + override val applicationProperties: Map) : SendableMessage { + var buf: ByteBuf? = null + @Volatile + var status: MessageStatus = MessageStatus.Unsent + + private val _onComplete = openFuture() + override val onComplete: CordaFuture get() = _onComplete + + fun release() { + buf?.release() + buf = null + } + + fun doComplete(status: MessageStatus) { + this.status = status + _onComplete.set(status) + } + + override fun toString(): String = "Sendable ${String(payload)} $topic $status" +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPChannelHandler.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPChannelHandler.kt new file mode 100644 index 0000000000..0f9e6cd607 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPChannelHandler.kt @@ -0,0 +1,156 @@ +package net.corda.node.internal.protonwrapper.netty + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelDuplexHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelPromise +import io.netty.channel.socket.SocketChannel +import io.netty.handler.ssl.SslHandler +import io.netty.handler.ssl.SslHandshakeCompletionEvent +import io.netty.util.ReferenceCountUtil +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.toX509CertHolder +import net.corda.core.utilities.debug +import net.corda.node.internal.protonwrapper.engine.EventProcessor +import net.corda.node.internal.protonwrapper.messages.ReceivedMessage +import net.corda.node.internal.protonwrapper.messages.impl.ReceivedMessageImpl +import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl +import org.apache.qpid.proton.engine.ProtonJTransport +import org.apache.qpid.proton.engine.Transport +import org.apache.qpid.proton.engine.impl.ProtocolTracer +import org.apache.qpid.proton.framing.TransportFrame +import org.bouncycastle.cert.X509CertificateHolder +import org.slf4j.LoggerFactory +import java.net.InetSocketAddress + +/** + * An instance of AMQPChannelHandler sits inside the netty pipeline and controls the socket level lifecycle. + * It also add some extra checks to the SSL handshake to support our non-standard certificate checks of legal identity. + * When a valid SSL connections is made then it initialises a proton-j engine instance to handle the protocol layer. + */ +internal class AMQPChannelHandler(private val serverMode: Boolean, + private val allowedRemoteLegalNames: Set?, + private val userName: String?, + private val password: String?, + private val trace: Boolean, + private val onOpen: (Pair) -> Unit, + private val onClose: (Pair) -> Unit, + private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() { + private val log = LoggerFactory.getLogger(allowedRemoteLegalNames?.firstOrNull()?.toString() ?: "AMQPChannelHandler") + private lateinit var remoteAddress: InetSocketAddress + private lateinit var localCert: X509CertificateHolder + private lateinit var remoteCert: X509CertificateHolder + private var eventProcessor: EventProcessor? = null + + override fun channelActive(ctx: ChannelHandlerContext) { + val ch = ctx.channel() + remoteAddress = ch.remoteAddress() as InetSocketAddress + val localAddress = ch.localAddress() as InetSocketAddress + log.info("New client connection ${ch.id()} from ${remoteAddress} to ${localAddress}") + } + + private fun createAMQPEngine(ctx: ChannelHandlerContext) { + val ch = ctx.channel() + eventProcessor = EventProcessor(ch, serverMode, localCert.subject.toString(), remoteCert.subject.toString(), userName, password) + val connection = eventProcessor!!.connection + val transport = connection.transport as ProtonJTransport + if (trace) { + transport.protocolTracer = object : ProtocolTracer { + override fun sentFrame(transportFrame: TransportFrame) { + log.info("${transportFrame.body}") + } + + override fun receivedFrame(transportFrame: TransportFrame) { + log.info("${transportFrame.body}") + } + } + } + ctx.fireChannelActive() + eventProcessor!!.processEventsAsync() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val ch = ctx.channel() + log.info("Closed client connection ${ch.id()} from ${remoteAddress} to ${ch.localAddress()}") + onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, null, false))) + eventProcessor?.close() + ctx.fireChannelInactive() + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + if (evt is SslHandshakeCompletionEvent) { + if (evt.isSuccess) { + val sslHandler = ctx.pipeline().get(SslHandler::class.java) + localCert = sslHandler.engine().session.localCertificates.first().toX509CertHolder() + remoteCert = sslHandler.engine().session.peerCertificates.first().toX509CertHolder() + try { + val remoteX500Name = CordaX500Name.parse(remoteCert.subject.toString()) + require(allowedRemoteLegalNames == null || remoteX500Name in allowedRemoteLegalNames) + log.info("handshake completed subject: ${remoteX500Name}") + } catch (ex: IllegalArgumentException) { + log.error("Invalid certificate subject", ex) + ctx.close() + return + } + createAMQPEngine(ctx) + onOpen(Pair(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, true))) + } else { + log.error("Handshake failure $evt") + ctx.close() + } + } + } + + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + try { + log.debug { "Received $msg" } + if (msg is ByteBuf) { + eventProcessor!!.transportProcessInput(msg) + } + } finally { + ReferenceCountUtil.release(msg) + } + eventProcessor!!.processEventsAsync() + } + + override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { + try { + try { + log.debug { "Sent $msg" } + when (msg) { + // Transfers application packet into the AMQP engine. + is SendableMessageImpl -> { + val inetAddress = InetSocketAddress(msg.destinationLink.host, msg.destinationLink.port) + require(inetAddress == remoteAddress) { + "Message for incorrect endpoint" + } + require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.parse(remoteCert.subject.toString())) { + "Message for incorrect legal identity" + } + log.debug { "channel write ${msg.applicationProperties["_AMQ_DUPL_ID"]}" } + eventProcessor!!.transportWriteMessage(msg) + } + // A received AMQP packet has been completed and this self-posted packet will be signalled out to the + // external application. + is ReceivedMessage -> { + onReceive(msg) + } + // A general self-posted event that triggers creation of AMQP frames when required. + is Transport -> { + eventProcessor!!.transportProcessOutput(ctx) + } + // A self-posted event that forwards status updates for delivered packets to the application. + is ReceivedMessageImpl.MessageCompleter -> { + eventProcessor!!.complete(msg) + } + } + } catch (ex: Exception) { + log.error("Error in AMQP write processing", ex) + throw ex + } + } finally { + ReferenceCountUtil.release(msg) + } + eventProcessor!!.processEventsAsync() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPClient.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPClient.kt new file mode 100644 index 0000000000..761248797f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPClient.kt @@ -0,0 +1,194 @@ +package net.corda.node.internal.protonwrapper.netty + +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.logging.LogLevel +import io.netty.handler.logging.LoggingHandler +import io.netty.util.internal.logging.InternalLoggerFactory +import io.netty.util.internal.logging.Slf4JLoggerFactory +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.node.internal.protonwrapper.messages.ReceivedMessage +import net.corda.node.internal.protonwrapper.messages.SendableMessage +import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl +import rx.Observable +import rx.subjects.PublishSubject +import java.security.KeyStore +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManagerFactory +import kotlin.concurrent.withLock + +/** + * The AMQPClient creates a connection initiator that will try to connect in a round-robin fashion + * to the first open SSL socket. It will keep retrying until it is stopped. + * To allow thread resource control it can accept a shared thread pool as constructor input, + * otherwise it creates a self-contained Netty thraed pool and socket objects. + * Once connected it can accept application packets to send via the AMQP protocol. + */ +class AMQPClient(val targets: List, + val allowedRemoteLegalNames: Set, + private val userName: String?, + private val password: String?, + private val keyStore: KeyStore, + private val keyStorePrivateKeyPassword: String, + private val trustStore: KeyStore, + private val trace: Boolean = false, + private val sharedThreadPool: EventLoopGroup? = null) : AutoCloseable { + companion object { + init { + InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE) + } + + val log = contextLogger() + const val RETRY_INTERVAL = 1000L + const val NUM_CLIENT_THREADS = 2 + } + + private val lock = ReentrantLock() + @Volatile + private var stopping: Boolean = false + private var workerGroup: EventLoopGroup? = null + @Volatile + private var clientChannel: Channel? = null + // Offset into the list of targets, so that we can implement round-robin reconnect logic. + private var targetIndex = 0 + private var currentTarget: NetworkHostAndPort = targets.first() + + private val connectListener = object : ChannelFutureListener { + override fun operationComplete(future: ChannelFuture) { + if (!future.isSuccess) { + log.info("Failed to connect to $currentTarget") + + if (!stopping) { + workerGroup?.schedule({ + log.info("Retry connect to $currentTarget") + targetIndex = (targetIndex + 1).rem(targets.size) + restart() + }, RETRY_INTERVAL, TimeUnit.MILLISECONDS) + } + } else { + log.info("Connected to $currentTarget") + // Connection established successfully + clientChannel = future.channel() + clientChannel?.closeFuture()?.addListener(closeListener) + } + } + } + + private val closeListener = object : ChannelFutureListener { + override fun operationComplete(future: ChannelFuture) { + log.info("Disconnected from $currentTarget") + future.channel()?.disconnect() + clientChannel = null + if (!stopping) { + workerGroup?.schedule({ + log.info("Retry connect") + targetIndex = (targetIndex + 1).rem(targets.size) + restart() + }, RETRY_INTERVAL, TimeUnit.MILLISECONDS) + } + } + } + + private class ClientChannelInitializer(val parent: AMQPClient) : ChannelInitializer() { + private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + init { + keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray()) + trustManagerFactory.init(parent.trustStore) + } + + override fun initChannel(ch: SocketChannel) { + val pipeline = ch.pipeline() + val handler = createClientSslHelper(parent.currentTarget, keyManagerFactory, trustManagerFactory) + pipeline.addLast("sslHandler", handler) + if (parent.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO)) + pipeline.addLast(AMQPChannelHandler(false, + parent.allowedRemoteLegalNames, + parent.userName, + parent.password, + parent.trace, + { parent._onConnection.onNext(it.second) }, + { parent._onConnection.onNext(it.second) }, + { rcv -> parent._onReceive.onNext(rcv) })) + } + } + + fun start() { + lock.withLock { + log.info("connect to: $currentTarget") + workerGroup = sharedThreadPool ?: NioEventLoopGroup(NUM_CLIENT_THREADS) + restart() + } + } + + private fun restart() { + val bootstrap = Bootstrap() + // TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux + bootstrap.group(workerGroup). + channel(NioSocketChannel::class.java). + handler(ClientChannelInitializer(this)) + currentTarget = targets[targetIndex] + val clientFuture = bootstrap.connect(currentTarget.host, currentTarget.port) + clientFuture.addListener(connectListener) + } + + fun stop() { + lock.withLock { + log.info("disconnect from: $currentTarget") + stopping = true + try { + if (sharedThreadPool == null) { + workerGroup?.shutdownGracefully() + workerGroup?.terminationFuture()?.sync() + } else { + clientChannel?.close()?.sync() + } + clientChannel = null + workerGroup = null + } finally { + stopping = false + } + log.info("stopped connection to $currentTarget") + } + } + + override fun close() = stop() + + val connected: Boolean + get() { + val channel = lock.withLock { clientChannel } + return channel?.isActive ?: false + } + + fun createMessage(payload: ByteArray, + topic: String, + destinationLegalName: String, + properties: Map): SendableMessage { + return SendableMessageImpl(payload, topic, destinationLegalName, currentTarget, properties) + } + + fun write(msg: SendableMessage) { + val channel = clientChannel + if (channel == null) { + throw IllegalStateException("Connection to $targets not active") + } else { + channel.writeAndFlush(msg) + } + } + + private val _onReceive = PublishSubject.create().toSerialized() + val onReceive: Observable + get() = _onReceive + + private val _onConnection = PublishSubject.create().toSerialized() + val onConnection: Observable + get() = _onConnection +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPServer.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPServer.kt new file mode 100644 index 0000000000..6398a6776b --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/AMQPServer.kt @@ -0,0 +1,187 @@ +package net.corda.node.internal.protonwrapper.netty + +import io.netty.bootstrap.ServerBootstrap +import io.netty.channel.Channel +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.handler.logging.LogLevel +import io.netty.handler.logging.LoggingHandler +import io.netty.util.internal.logging.InternalLoggerFactory +import io.netty.util.internal.logging.Slf4JLoggerFactory +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.node.internal.protonwrapper.messages.ReceivedMessage +import net.corda.node.internal.protonwrapper.messages.SendableMessage +import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl +import org.apache.qpid.proton.engine.Delivery +import rx.Observable +import rx.subjects.PublishSubject +import java.net.BindException +import java.net.InetSocketAddress +import java.security.KeyStore +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManagerFactory +import kotlin.concurrent.withLock + +/** + * This create a socket acceptor instance that can receive possibly multiple AMQP connections. + * As of now this is not used outside of testing, but in future it will be used for standalone bridging components. + */ +class AMQPServer(val hostName: String, + val port: Int, + private val userName: String?, + private val password: String?, + private val keyStore: KeyStore, + private val keyStorePrivateKeyPassword: String, + private val trustStore: KeyStore, + private val trace: Boolean = false) : AutoCloseable { + + companion object { + init { + InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE) + } + + private val log = contextLogger() + const val NUM_SERVER_THREADS = 4 + } + + private val lock = ReentrantLock() + @Volatile + private var stopping: Boolean = false + private var bossGroup: EventLoopGroup? = null + private var workerGroup: EventLoopGroup? = null + private var serverChannel: Channel? = null + private val clientChannels = ConcurrentHashMap() + + init { + } + + private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer() { + private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + init { + keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray()) + trustManagerFactory.init(parent.trustStore) + } + + override fun initChannel(ch: SocketChannel) { + val pipeline = ch.pipeline() + val handler = createServerSslHelper(keyManagerFactory, trustManagerFactory) + pipeline.addLast("sslHandler", handler) + if (parent.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO)) + pipeline.addLast(AMQPChannelHandler(true, + null, + parent.userName, + parent.password, + parent.trace, + { + parent.clientChannels.put(it.first.remoteAddress(), it.first) + parent._onConnection.onNext(it.second) + }, + { + parent.clientChannels.remove(it.first.remoteAddress()) + parent._onConnection.onNext(it.second) + }, + { rcv -> parent._onReceive.onNext(rcv) })) + } + } + + fun start() { + lock.withLock { + stop() + + bossGroup = NioEventLoopGroup(1) + workerGroup = NioEventLoopGroup(NUM_SERVER_THREADS) + + val server = ServerBootstrap() + // TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux + server.group(bossGroup, workerGroup). + channel(NioServerSocketChannel::class.java). + option(ChannelOption.SO_BACKLOG, 100). + handler(LoggingHandler(LogLevel.INFO)). + childHandler(ServerChannelInitializer(this)) + + log.info("Try to bind $port") + val channelFuture = server.bind(hostName, port).sync() // block/throw here as better to know we failed to claim port than carry on + if (!channelFuture.isDone || !channelFuture.isSuccess) { + throw BindException("Failed to bind port $port") + } + log.info("Listening on port $port") + serverChannel = channelFuture.channel() + } + } + + fun stop() { + lock.withLock { + try { + stopping = true + serverChannel?.apply { close() } + serverChannel = null + + workerGroup?.shutdownGracefully() + workerGroup?.terminationFuture()?.sync() + + bossGroup?.shutdownGracefully() + bossGroup?.terminationFuture()?.sync() + + workerGroup = null + bossGroup = null + } finally { + stopping = false + } + } + } + + override fun close() = stop() + + val listening: Boolean + get() { + val channel = lock.withLock { serverChannel } + return channel?.isActive ?: false + } + + fun createMessage(payload: ByteArray, + topic: String, + destinationLegalName: String, + destinationLink: NetworkHostAndPort, + properties: Map): SendableMessage { + val dest = InetSocketAddress(destinationLink.host, destinationLink.port) + require(dest in clientChannels.keys) { + "Destination not available" + } + return SendableMessageImpl(payload, topic, destinationLegalName, destinationLink, properties) + } + + fun write(msg: SendableMessage) { + val dest = InetSocketAddress(msg.destinationLink.host, msg.destinationLink.port) + val channel = clientChannels[dest] + if (channel == null) { + throw IllegalStateException("Connection to ${msg.destinationLink} not active") + } else { + channel.writeAndFlush(msg) + } + } + + fun complete(delivery: Delivery, target: InetSocketAddress) { + val channel = clientChannels[target] + channel?.apply { + writeAndFlush(delivery) + } + } + + private val _onReceive = PublishSubject.create().toSerialized() + val onReceive: Observable + get() = _onReceive + + private val _onConnection = PublishSubject.create().toSerialized() + val onConnection: Observable + get() = _onConnection + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/ConnectionChange.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/ConnectionChange.kt new file mode 100644 index 0000000000..a576a25d2b --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/ConnectionChange.kt @@ -0,0 +1,6 @@ +package net.corda.node.internal.protonwrapper.netty + +import org.bouncycastle.cert.X509CertificateHolder +import java.net.InetSocketAddress + +data class ConnectionChange(val remoteAddress: InetSocketAddress, val remoteCert: X509CertificateHolder?, val connected: Boolean) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/SSLHelper.kt b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/SSLHelper.kt new file mode 100644 index 0000000000..7a778d26cf --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/protonwrapper/netty/SSLHelper.kt @@ -0,0 +1,39 @@ +package net.corda.node.internal.protonwrapper.netty + +import io.netty.handler.ssl.SslHandler +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.ArtemisTcpTransport +import java.security.SecureRandom +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory + +internal fun createClientSslHelper(target: NetworkHostAndPort, + keyManagerFactory: KeyManagerFactory, + trustManagerFactory: TrustManagerFactory): SslHandler { + val sslContext = SSLContext.getInstance("TLS") + val keyManagers = keyManagerFactory.keyManagers + val trustManagers = trustManagerFactory.trustManagers + sslContext.init(keyManagers, trustManagers, SecureRandom()) + val sslEngine = sslContext.createSSLEngine(target.host, target.port) + sslEngine.useClientMode = true + sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + sslEngine.enableSessionCreation = true + return SslHandler(sslEngine) +} + +internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, + trustManagerFactory: TrustManagerFactory): SslHandler { + val sslContext = SSLContext.getInstance("TLS") + val keyManagers = keyManagerFactory.keyManagers + val trustManagers = trustManagerFactory.trustManagers + sslContext.init(keyManagers, trustManagers, SecureRandom()) + val sslEngine = sslContext.createSSLEngine() + sslEngine.useClientMode = false + sslEngine.needClientAuth = true + sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + sslEngine.enableSessionCreation = true + return SslHandler(sslEngine) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt index dafa069833..4307df44e0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt @@ -34,7 +34,7 @@ fun RPCSecurityManager.tryAuthenticate(principal: String, password: Password): A password.use { return try { authenticate(principal, password) - } catch (e: AuthenticationException) { + } catch (e: FailedLoginException) { null } } diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt index add690620e..e667d673b7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -95,8 +95,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { // Setup optional cache layer if configured it.cacheManager = config.options?.cache?.let { GuavaCacheManager( - timeToLiveSeconds = it.expiryTimeInSecs, - maxSize = it.capacity) + timeToLiveSeconds = it.expireAfterSecs, + maxSize = it.maxEntries) } } } @@ -149,22 +149,29 @@ private object RPCPermissionResolver : PermissionResolver { private val ACTION_START_FLOW = "startflow" private val ACTION_INVOKE_RPC = "invokerpc" private val ACTION_ALL = "all" - - private val FLOW_RPC_CALLS = setOf("startFlowDynamic", "startTrackedFlowDynamic") + private val FLOW_RPC_CALLS = setOf( + "startFlowDynamic", + "startTrackedFlowDynamic", + "startFlow", + "startTrackedFlow") override fun resolvePermission(representation: String): Permission { - - val action = representation.substringBefore(SEPARATOR).toLowerCase() + val action = representation.substringBefore(SEPARATOR).toLowerCase() when (action) { ACTION_INVOKE_RPC -> { - val rpcCall = representation.substringAfter(SEPARATOR) - require(representation.count { it == SEPARATOR } == 1) { + val rpcCall = representation.substringAfter(SEPARATOR, "") + require(representation.count { it == SEPARATOR } == 1 && !rpcCall.isEmpty()) { "Malformed permission string" } - return RPCPermission(setOf(rpcCall)) + val permitted = when(rpcCall) { + "startFlow" -> setOf("startFlowDynamic", rpcCall) + "startTrackedFlow" -> setOf("startTrackedFlowDynamic", rpcCall) + else -> setOf(rpcCall) + } + return RPCPermission(permitted) } ACTION_START_FLOW -> { - val targetFlow = representation.substringAfter(SEPARATOR) + val targetFlow = representation.substringAfter(SEPARATOR, "") require(targetFlow.isNotEmpty()) { "Missing target flow after StartFlow" } 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 c08b81efce..705f4e4fc0 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 @@ -6,10 +6,10 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.services.messaging.CertificateChainCheckPolicy -import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs +import net.corda.nodeapi.internal.persistence.DatabaseConfig import java.net.URL import java.nio.file.Path import java.util.* @@ -44,6 +44,7 @@ interface NodeConfiguration : NodeSSLConfiguration { val sshd: SSHDConfiguration? val database: DatabaseConfig val relay: RelayConfiguration? + val useAMQPBridges: Boolean get() = true } data class DevModeOptions(val disableCheckpointChecker: Boolean = false) @@ -120,7 +121,8 @@ data class NodeConfigurationImpl( // TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(), override val sshd: SSHDConfiguration? = null, - override val database: DatabaseConfig = DatabaseConfig(exportHibernateJMXStatistics = devMode) + override val database: DatabaseConfig = DatabaseConfig(exportHibernateJMXStatistics = devMode), + override val useAMQPBridges: Boolean = true ) : NodeConfiguration { override val exportJMXto: String get() = "http" @@ -200,7 +202,7 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ data class Options(val cache: Options.Cache?) { // Cache parameters - data class Cache(val expiryTimeInSecs: Long, val capacity: Long) + data class Cache(val expireAfterSecs: Long, val maxEntries: Long) } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index be682a7c05..719eea4b75 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -24,7 +24,7 @@ import javax.annotation.concurrent.ThreadSafe * @param identities initial set of identities for the service, typically only used for unit tests. */ @ThreadSafe -class InMemoryIdentityService(identities: Iterable, +class InMemoryIdentityService(identities: Array, trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal { companion object { private val log = contextLogger() diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt new file mode 100644 index 0000000000..8b285bf999 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt @@ -0,0 +1,203 @@ +package net.corda.node.services.messaging + +import io.netty.channel.EventLoopGroup +import io.netty.channel.nio.NioEventLoopGroup +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.debug +import net.corda.node.internal.protonwrapper.messages.MessageStatus +import net.corda.node.internal.protonwrapper.netty.AMQPClient +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName +import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.crypto.loadKeyStore +import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE +import org.apache.activemq.artemis.api.core.client.ClientConsumer +import org.apache.activemq.artemis.api.core.client.ClientMessage +import org.apache.activemq.artemis.api.core.client.ClientSession +import org.slf4j.LoggerFactory +import rx.Subscription +import java.security.KeyStore +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * The AMQPBridgeManager holds the list of independent AMQPBridge objects that actively ferry messages to remote Artemis + * inboxes. + * The AMQPBridgeManager also provides a single shared connection to Artemis, although each bridge then creates an + * independent Session for message consumption. + * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. + */ +internal class AMQPBridgeManager(val config: NodeConfiguration, val p2pAddress: NetworkHostAndPort, val maxMessageSize: Int) : BridgeManager { + + private val lock = ReentrantLock() + private val bridgeNameToBridgeMap = mutableMapOf() + private var sharedEventLoopGroup: EventLoopGroup? = null + private val keyStore = loadKeyStore(config.sslKeystore, config.keyStorePassword) + private val keyStorePrivateKeyPassword: String = config.keyStorePassword + private val trustStore = loadKeyStore(config.trustStoreFile, config.trustStorePassword) + private var artemis: ArtemisMessagingClient? = null + + companion object { + private const val NUM_BRIDGE_THREADS = 0 // Default sized pool + } + + /** + * Each AMQPBridge is an independent consumer of messages from the Artemis local queue per designated endpoint. + * It attempts to deliver these messages via an AMQPClient instance to the remote Artemis inbox. + * To prevent race conditions the Artemis session/consumer is only created when the AMQPClient has a stable AMQP connection. + * The acknowledgement and removal of messages from the local queue only occurs if there successful end-to-end delivery. + * If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery, + * however Artemis and the remote Corda instanced will deduplicate these messages. + */ + private class AMQPBridge(private val queueName: String, + private val target: NetworkHostAndPort, + private val legalNames: Set, + keyStore: KeyStore, + keyStorePrivateKeyPassword: String, + trustStore: KeyStore, + sharedEventGroup: EventLoopGroup, + private val artemis: ArtemisMessagingClient) { + companion object { + fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" + } + + private val log = LoggerFactory.getLogger("$bridgeName:${legalNames.first()}") + + val amqpClient = AMQPClient(listOf(target), legalNames, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, sharedThreadPool = sharedEventGroup) + val bridgeName: String get() = getBridgeName(queueName, target) + private val lock = ReentrantLock() // lock to serialise session level access + private var session: ClientSession? = null + private var consumer: ClientConsumer? = null + private var connectedSubscription: Subscription? = null + + fun start() { + log.info("Create new AMQP bridge") + connectedSubscription = amqpClient.onConnection.subscribe({ x -> onSocketConnected(x.connected) }) + amqpClient.start() + } + + fun stop() { + log.info("Stopping AMQP bridge") + lock.withLock { + synchronized(artemis) { + consumer?.close() + consumer = null + session?.stop() + session = null + } + } + amqpClient.stop() + connectedSubscription?.unsubscribe() + connectedSubscription = null + } + + private fun onSocketConnected(connected: Boolean) { + lock.withLock { + synchronized(artemis) { + if (connected) { + log.info("Bridge Connected") + val sessionFactory = artemis.started!!.sessionFactory + val session = sessionFactory.createSession(NODE_USER, NODE_USER, false, false, false, false, DEFAULT_ACK_BATCH_SIZE) + this.session = session + val consumer = session.createConsumer(queueName) + this.consumer = consumer + consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler) + session.start() + } else { + log.info("Bridge Disconnected") + consumer?.close() + consumer = null + session?.stop() + session = null + } + } + } + } + + private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { + lock.withLock { + val data = ByteArray(artemisMessage.bodySize).apply { artemisMessage.bodyBuffer.readBytes(this) } + val properties = HashMap() + for (key in artemisMessage.propertyNames) { + var value = artemisMessage.getObjectProperty(key) + if (value is SimpleString) { + value = value.toString() + } + properties[key.toString()] = value + } + log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" } + val sendableMessage = amqpClient.createMessage(data, P2P_QUEUE, + legalNames.first().toString(), + properties) + sendableMessage.onComplete.then { + log.debug { "Bridge ACK ${sendableMessage.onComplete.get()}" } + lock.withLock { + if (sendableMessage.onComplete.get() == MessageStatus.Acknowledged) { + artemisMessage.acknowledge() + session?.commit() + } else { + log.info("Rollback rejected message uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}") + session?.rollback(false) + } + } + } + amqpClient.write(sendableMessage) + } + } + } + + private fun gatherAddresses(node: NodeInfo): Sequence { + val address = node.addresses.first() + return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, address) }.asSequence() + } + + override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) { + if (bridgeExists(getBridgeName(queueName, target))) { + return + } + val newBridge = AMQPBridge(queueName, target, legalNames, keyStore, keyStorePrivateKeyPassword, trustStore, sharedEventLoopGroup!!, artemis!!) + lock.withLock { + bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge + } + newBridge.start() + } + + override fun destroyBridges(node: NodeInfo) { + lock.withLock { + gatherAddresses(node).forEach { + val bridge = bridgeNameToBridgeMap.remove(getBridgeName(it.queueName, it.hostAndPort)) + bridge?.stop() + } + } + } + + override fun bridgeExists(bridgeName: String): Boolean = lock.withLock { bridgeNameToBridgeMap.containsKey(bridgeName) } + + override fun start() { + sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS) + val artemis = ArtemisMessagingClient(config, p2pAddress, maxMessageSize) + this.artemis = artemis + artemis.start() + } + + override fun stop() = close() + + override fun close() { + lock.withLock { + for (bridge in bridgeNameToBridgeMap.values) { + bridge.stop() + } + sharedEventLoopGroup?.shutdownGracefully() + sharedEventLoopGroup?.terminationFuture()?.sync() + sharedEventLoopGroup = null + bridgeNameToBridgeMap.clear() + artemis?.stop() + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingClient.kt index fd36dc9ba7..84dd18298d 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingClient.kt @@ -3,14 +3,17 @@ package net.corda.node.services.messaging import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.config.SSLConfiguration -import org.apache.activemq.artemis.api.core.client.* +import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE +import org.apache.activemq.artemis.api.core.client.ClientProducer +import org.apache.activemq.artemis.api.core.client.ClientSession +import org.apache.activemq.artemis.api.core.client.ClientSessionFactory -class ArtemisMessagingClient(private val config: SSLConfiguration, private val serverAddress: NetworkHostAndPort) { +class ArtemisMessagingClient(private val config: SSLConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) { companion object { private val log = loggerFor() } @@ -30,7 +33,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s // would be the default and the two lines below can be deleted. connectionTTL = -1 clientFailureCheckPeriod = -1 - minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE + minLargeMessageSize = maxMessageSize isUseGlobalPools = nodeSerializationEnv != null } val sessionFactory = locator.createSessionFactory() @@ -46,7 +49,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s } fun stop() = synchronized(this) { - started!!.run { + started?.run { producer.close() // Ensure any trailing messages are committed to the journal session.commit() diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 5634ca74ed..285d4ce94c 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -1,6 +1,5 @@ package net.corda.node.services.messaging -import io.netty.handler.ssl.SslHandler import net.corda.core.crypto.AddressFormatException import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.CordaX500Name @@ -24,11 +23,10 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.RPC_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA -import net.corda.nodeapi.internal.crypto.loadKeyStore -import net.corda.nodeapi.* +import net.corda.nodeapi.ArtemisTcpTransport +import net.corda.nodeapi.ConnectionDirection +import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER @@ -37,15 +35,17 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.requireOnDefaultFileSystem import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl -import org.apache.activemq.artemis.core.config.BridgeConfiguration import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration -import org.apache.activemq.artemis.core.remoting.impl.netty.* +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.SecuritySettingPlugin @@ -53,22 +53,17 @@ import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.settings.HierarchicalRepository import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings -import org.apache.activemq.artemis.spi.core.remoting.* import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal -import org.apache.activemq.artemis.utils.ConfigurationHelper import rx.Subscription import java.io.IOException import java.math.BigInteger import java.security.KeyStore import java.security.KeyStoreException import java.security.Principal -import java.time.Duration import java.util.* -import java.util.concurrent.Executor -import java.util.concurrent.ScheduledExecutorService import javax.annotation.concurrent.ThreadSafe import javax.security.auth.Subject import javax.security.auth.callback.CallbackHandler @@ -80,7 +75,6 @@ import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.RE import javax.security.auth.login.FailedLoginException import javax.security.auth.login.LoginException import javax.security.auth.spi.LoginModule -import javax.security.auth.x500.X500Principal import javax.security.cert.CertificateException // TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman. @@ -101,14 +95,11 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, private val p2pPort: Int, val rpcPort: Int?, val networkMapCache: NetworkMapCache, - val securityManager: RPCSecurityManager) : SingletonSerializeAsToken() { + val securityManager: RPCSecurityManager, + val maxMessageSize: Int) : SingletonSerializeAsToken() { companion object { private val log = contextLogger() - /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ - @JvmStatic - val MAX_FILE_SIZE = 10485760 } - private class InnerState { var running = false } @@ -117,6 +108,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, private lateinit var activeMQServer: ActiveMQServer val serverControl: ActiveMQServerControl get() = activeMQServer.activeMQServerControl private var networkChangeHandle: Subscription? = null + private lateinit var bridgeManager: BridgeManager init { config.baseDirectory.requireOnDefaultFileSystem() @@ -136,6 +128,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, } fun stop() = mutex.locked { + bridgeManager.close() networkChangeHandle?.unsubscribe() networkChangeHandle = null activeMQServer.stop() @@ -156,7 +149,14 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, registerPostQueueCreationCallback { deployBridgesFromNewQueue(it.toString()) } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } } + // Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges. + bridgeManager = if (config.useAMQPBridges) { + AMQPBridgeManager(config, NetworkHostAndPort("localhost", p2pPort), maxMessageSize) + } else { + CoreBridgeManager(config, activeMQServer) + } activeMQServer.start() + bridgeManager.start() Node.printBasicNodeInfo("Listening on port", p2pPort.toString()) if (rpcPort != null) { Node.printBasicNodeInfo("RPC service listening on port", rpcPort.toString()) @@ -181,9 +181,9 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess isPersistIDCache = true isPopulateValidatedUser = true - journalBufferSize_NIO = MAX_FILE_SIZE // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store. - journalBufferSize_AIO = MAX_FILE_SIZE // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store. - journalFileSize = MAX_FILE_SIZE // The size of each journal file in bytes. Artemis default is 10MiB. + journalBufferSize_NIO = maxMessageSize // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store. + journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store. + journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB. managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS) // Artemis allows multiple servers to be grouped together into a cluster for load balancing purposes. The cluster // user is used for connecting the nodes together. It has super-user privileges and so it's imperative that its @@ -211,15 +211,17 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, ) addressesSettings = mapOf( "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply { - maxSizeBytes = 10L * MAX_FILE_SIZE + maxSizeBytes = 10L * maxMessageSize addressFullMessagePolicy = AddressFullMessagePolicy.FAIL } ) - // JMX enablement - if (config.exportJMXto.isNotEmpty()) {isJMXManagementEnabled = true - isJMXUseBrokerName = true} + // JMX enablement + if (config.exportJMXto.isNotEmpty()) { + isJMXManagementEnabled = true + isJMXUseBrokerName = true + } - }.configureAddressSecurity() + }.configureAddressSecurity() private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration { @@ -302,7 +304,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, fun deployBridgeToPeer(nodeInfo: NodeInfo) { log.debug("Deploying bridge for $queueName to $nodeInfo") val address = nodeInfo.addresses.first() - deployBridge(queueName, address, nodeInfo.legalIdentitiesAndCerts.map { it.name }.toSet()) + bridgeManager.deployBridge(queueName, address, nodeInfo.legalIdentitiesAndCerts.map { it.name }.toSet()) } if (queueName.startsWith(PEERS_PREFIX)) { @@ -337,147 +339,39 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, fun deployBridges(node: NodeInfo) { gatherAddresses(node) - .filter { queueExists(it.queueName) && !bridgeExists(it.bridgeName) } + .filter { queueExists(it.queueName) && !bridgeManager.bridgeExists(it.bridgeName) } .forEach { deployBridge(it, node.legalIdentitiesAndCerts.map { it.name }.toSet()) } } - fun destroyBridges(node: NodeInfo) { - gatherAddresses(node).forEach { - activeMQServer.destroyBridge(it.bridgeName) - } - } - when (change) { is MapChange.Added -> { deployBridges(change.node) } is MapChange.Removed -> { - destroyBridges(change.node) + bridgeManager.destroyBridges(change.node) } is MapChange.Modified -> { // TODO Figure out what has actually changed and only destroy those bridges that need to be. - destroyBridges(change.previousNode) + bridgeManager.destroyBridges(change.previousNode) deployBridges(change.node) } } } private fun deployBridge(address: ArtemisPeerAddress, legalNames: Set) { - deployBridge(address.queueName, address.hostAndPort, legalNames) + bridgeManager.deployBridge(address.queueName, address.hostAndPort, legalNames) } private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) = ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL) - /** - * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving - * messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it, - * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's - * P2P address. - */ - private fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) { - val connectionDirection = ConnectionDirection.Outbound( - connectorFactoryClassName = VerifyingNettyConnectorFactory::class.java.name, - expectedCommonNames = legalNames - ) - val tcpTransport = createTcpTransport(connectionDirection, target.host, target.port) - tcpTransport.params[ArtemisMessagingServer::class.java.name] = this - // We intentionally overwrite any previous connector config in case the peer legal name changed - activeMQServer.configuration.addConnectorConfiguration(target.toString(), tcpTransport) - - activeMQServer.deployBridge(BridgeConfiguration().apply { - name = getBridgeName(queueName, target) - this.queueName = queueName - forwardingAddress = P2P_QUEUE - staticConnectors = listOf(target.toString()) - confirmationWindowSize = 100000 // a guess - isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic - // We keep trying until the network map deems the node unreachable and tells us it's been removed at which - // point we destroy the bridge - retryInterval = config.activeMQServer.bridge.retryIntervalMs - retryIntervalMultiplier = config.activeMQServer.bridge.retryIntervalMultiplier - maxRetryInterval = Duration.ofMinutes(config.activeMQServer.bridge.maxRetryIntervalMin).toMillis() - // As a peer of the target node we must connect to it using the peer user. Actual authentication is done using - // our TLS certificate. - user = PEER_USER - password = PEER_USER - }) - } - private fun queueExists(queueName: String): Boolean = activeMQServer.queueQuery(SimpleString(queueName)).isExists - private fun bridgeExists(bridgeName: String): Boolean = activeMQServer.clusterManager.bridges.containsKey(bridgeName) - private val ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort) private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" } -class VerifyingNettyConnectorFactory : NettyConnectorFactory() { - override fun createConnector(configuration: MutableMap, - handler: BufferHandler?, - listener: ClientConnectionLifeCycleListener?, - closeExecutor: Executor?, - threadPool: Executor?, - scheduledThreadPool: ScheduledExecutorService?, - protocolManager: ClientProtocolManager?): Connector { - return VerifyingNettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, - protocolManager) - } -} - -private class VerifyingNettyConnector(configuration: MutableMap, - handler: BufferHandler?, - listener: ClientConnectionLifeCycleListener?, - closeExecutor: Executor?, - threadPool: Executor?, - scheduledThreadPool: ScheduledExecutorService?, - protocolManager: ClientProtocolManager?) : - NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) { - companion object { - private val log = contextLogger() - } - - private val sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration) - - override fun createConnection(): Connection? { - val connection = super.createConnection() as? NettyConnection - if (sslEnabled && connection != null) { - val expectedLegalNames: Set = uncheckedCast(configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet()) - try { - val session = connection.channel - .pipeline() - .get(SslHandler::class.java) - .engine() - .session - // Checks the peer name is the one we are expecting. - // TODO Some problems here: after introduction of multiple legal identities on the node and removal of the main one, - // we run into the issue, who are we connecting to. There are some solutions to that: advertise `network identity`; - // have mapping port -> identity (but, design doc says about removing SingleMessageRecipient and having just NetworkHostAndPort, - // it was convenient to store that this way); SNI. - val peerLegalName = CordaX500Name.parse(session.peerPrincipal.name) - val expectedLegalName = expectedLegalNames.singleOrNull { it == peerLegalName } - require(expectedLegalName != null) { - "Peer has wrong CN - expected $expectedLegalNames but got $peerLegalName. This is either a fatal " + - "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" - } - // Make sure certificate has the same name. - val peerCertificateName = CordaX500Name.build(X500Principal(session.peerCertificateChain[0].subjectDN.name)) - require(peerCertificateName == expectedLegalName) { - "Peer has wrong subject name in the certificate - expected $expectedLegalNames but got $peerCertificateName. This is either a fatal " + - "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" - } - X509Utilities.validateCertificateChain(session.localCertificates.last() as java.security.cert.X509Certificate, *session.peerCertificates) - } catch (e: IllegalArgumentException) { - connection.close() - log.error(e.message) - return null - } - } - return connection - } -} - sealed class CertificateChainCheckPolicy { @FunctionalInterface diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/BridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/BridgeManager.kt new file mode 100644 index 0000000000..5997921848 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/messaging/BridgeManager.kt @@ -0,0 +1,20 @@ +package net.corda.node.services.messaging + +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort + +/** + * Provides an internal interface that the [ArtemisMessagingServer] delegates to for Bridge activities. + */ +internal interface BridgeManager : AutoCloseable { + fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) + + fun destroyBridges(node: NodeInfo) + + fun bridgeExists(bridgeName: String): Boolean + + fun start() + + fun stop() +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt new file mode 100644 index 0000000000..4161d89836 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt @@ -0,0 +1,166 @@ +package net.corda.node.services.messaging + +import io.netty.handler.ssl.SslHandler +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.uncheckedCast +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.ArtemisTcpTransport +import net.corda.nodeapi.ConnectionDirection +import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.apache.activemq.artemis.core.config.BridgeConfiguration +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants +import org.apache.activemq.artemis.core.server.ActiveMQServer +import org.apache.activemq.artemis.spi.core.remoting.* +import org.apache.activemq.artemis.utils.ConfigurationHelper +import java.time.Duration +import java.util.concurrent.Executor +import java.util.concurrent.ScheduledExecutorService +import javax.security.auth.x500.X500Principal + +/** + * This class simply moves the legacy CORE bridge code from [ArtemisMessagingServer] + * into a class implementing [BridgeManager]. + * It has no lifecycle events, because the bridges are internal to the ActiveMQServer instance and thus + * stop when it is stopped. + */ +internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServer: ActiveMQServer) : BridgeManager { + companion object { + private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" + + private val ArtemisMessagingComponent.ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort) + } + + private fun gatherAddresses(node: NodeInfo): Sequence { + val address = node.addresses.first() + return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, address) }.asSequence() + } + + + /** + * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving + * messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it, + * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's + * P2P address. + */ + override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) { + val connectionDirection = ConnectionDirection.Outbound( + connectorFactoryClassName = VerifyingNettyConnectorFactory::class.java.name, + expectedCommonNames = legalNames + ) + val tcpTransport = ArtemisTcpTransport.tcpTransport(connectionDirection, target, config, enableSSL = true) + tcpTransport.params[ArtemisMessagingServer::class.java.name] = this + // We intentionally overwrite any previous connector config in case the peer legal name changed + activeMQServer.configuration.addConnectorConfiguration(target.toString(), tcpTransport) + + activeMQServer.deployBridge(BridgeConfiguration().apply { + name = getBridgeName(queueName, target) + this.queueName = queueName + forwardingAddress = P2P_QUEUE + staticConnectors = listOf(target.toString()) + confirmationWindowSize = 100000 // a guess + isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic + // We keep trying until the network map deems the node unreachable and tells us it's been removed at which + // point we destroy the bridge + retryInterval = config.activeMQServer.bridge.retryIntervalMs + retryIntervalMultiplier = config.activeMQServer.bridge.retryIntervalMultiplier + maxRetryInterval = Duration.ofMinutes(config.activeMQServer.bridge.maxRetryIntervalMin).toMillis() + // As a peer of the target node we must connect to it using the peer user. Actual authentication is done using + // our TLS certificate. + user = PEER_USER + password = PEER_USER + }) + } + + override fun bridgeExists(bridgeName: String): Boolean = activeMQServer.clusterManager.bridges.containsKey(bridgeName) + + override fun start() { + // Nothing to do + } + + override fun stop() { + // Nothing to do + } + + override fun close() = stop() + + override fun destroyBridges(node: NodeInfo) { + gatherAddresses(node).forEach { + activeMQServer.destroyBridge(it.bridgeName) + } + } +} + +class VerifyingNettyConnectorFactory : NettyConnectorFactory() { + override fun createConnector(configuration: MutableMap, + handler: BufferHandler?, + listener: ClientConnectionLifeCycleListener?, + closeExecutor: Executor?, + threadPool: Executor?, + scheduledThreadPool: ScheduledExecutorService?, + protocolManager: ClientProtocolManager?): Connector { + return VerifyingNettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, + protocolManager) + } + + private class VerifyingNettyConnector(configuration: MutableMap, + handler: BufferHandler?, + listener: ClientConnectionLifeCycleListener?, + closeExecutor: Executor?, + threadPool: Executor?, + scheduledThreadPool: ScheduledExecutorService?, + protocolManager: ClientProtocolManager?) : + NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) { + companion object { + private val log = contextLogger() + } + + private val sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration) + + override fun createConnection(): Connection? { + val connection = super.createConnection() as? NettyConnection + if (sslEnabled && connection != null) { + val expectedLegalNames: Set = uncheckedCast(configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet()) + try { + val session = connection.channel + .pipeline() + .get(SslHandler::class.java) + .engine() + .session + // Checks the peer name is the one we are expecting. + // TODO Some problems here: after introduction of multiple legal identities on the node and removal of the main one, + // we run into the issue, who are we connecting to. There are some solutions to that: advertise `network identity`; + // have mapping port -> identity (but, design doc says about removing SingleMessageRecipient and having just NetworkHostAndPort, + // it was convenient to store that this way); SNI. + val peerLegalName = CordaX500Name.parse(session.peerPrincipal.name) + val expectedLegalName = expectedLegalNames.singleOrNull { it == peerLegalName } + require(expectedLegalName != null) { + "Peer has wrong CN - expected $expectedLegalNames but got $peerLegalName. This is either a fatal " + + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" + } + // Make sure certificate has the same name. + val peerCertificateName = CordaX500Name.build(X500Principal(session.peerCertificateChain[0].subjectDN.name)) + require(peerCertificateName == expectedLegalName) { + "Peer has wrong subject name in the certificate - expected $expectedLegalNames but got $peerCertificateName. This is either a fatal " + + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" + } + X509Utilities.validateCertificateChain(session.localCertificates.last() as java.security.cert.X509Certificate, *session.peerCertificates) + } catch (e: IllegalArgumentException) { + connection.close() + log.error(e.message) + return null + } + } + return connection + } + } +} + diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 753e9ff330..0e305c6f4c 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -67,7 +67,8 @@ class P2PMessagingClient(config: NodeConfiguration, private val myIdentity: PublicKey, private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, private val database: CordaPersistence, - advertisedAddress: NetworkHostAndPort = serverAddress + advertisedAddress: NetworkHostAndPort = serverAddress, + private val maxMessageSize: Int ) : SingletonSerializeAsToken(), MessagingService { companion object { private val log = contextLogger() @@ -143,7 +144,7 @@ class P2PMessagingClient(config: NodeConfiguration, override val myAddress: SingleMessageRecipient = NodeAddress(myIdentity, advertisedAddress) private val messageRedeliveryDelaySeconds = config.messageRedeliveryDelaySeconds.toLong() - private val artemis = ArtemisMessagingClient(config, serverAddress) + private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize) private val state = ThreadBox(InnerState()) private val handlers = ConcurrentHashMap() diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt index 3d1ee3e3d6..e9fe065ac6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt @@ -12,8 +12,8 @@ import net.corda.nodeapi.internal.crypto.getX509Certificate import net.corda.nodeapi.internal.crypto.loadKeyStore import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl -class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort) : SingletonSerializeAsToken() { - private val artemis = ArtemisMessagingClient(config, serverAddress) +class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) : SingletonSerializeAsToken() { + private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize) private var rpcServer: RPCServer? = null fun start(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt index 8216b6846b..9a8c0cae66 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt @@ -17,13 +17,13 @@ import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.* import java.util.concurrent.* -class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry) : SingletonSerializeAsToken() { +class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() { companion object { private val log = loggerFor() private val verifierResponseAddress = "$VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX.${random63BitValue()}" } - private val artemis = ArtemisMessagingClient(config, serverAddress) + private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize) /** An executor for sending messages */ private val messagingExecutor = AffinityExecutor.ServiceAffinityExecutor("Messaging", 1) private var verificationResponseConsumer: ClientConsumer? = null diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 66fe9470bd..db7bea9480 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -12,10 +12,10 @@ import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NamedThreadFactory -import net.corda.nodeapi.internal.NetworkMap -import net.corda.nodeapi.internal.NetworkParameters -import net.corda.nodeapi.internal.SignedNetworkMap -import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.network.NetworkMap +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.network.SignedNetworkMap +import net.corda.nodeapi.internal.SignedNodeInfo import okhttp3.CacheControl import okhttp3.Headers import rx.Subscription @@ -31,13 +31,13 @@ import java.util.concurrent.TimeUnit class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) { private val networkMapUrl = URL("$compatibilityZoneURL/network-map") - fun publish(signedNodeInfo: SignedData) { + fun publish(signedNodeInfo: SignedNodeInfo) { val publishURL = URL("$networkMapUrl/publish") val conn = publishURL.openHttpConnection() conn.doOutput = true conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/octet-stream") - conn.outputStream.use { it.write(signedNodeInfo.serialize().bytes) } + conn.outputStream.use { signedNodeInfo.serialize().open().copyTo(it) } // This will throw IOException if the response code is not HTTP 200. // This gives a much better exception then reading the error stream. @@ -47,9 +47,7 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C fun getNetworkMap(): NetworkMapResponse { val conn = networkMapUrl.openHttpConnection() val signedNetworkMap = conn.inputStream.use { it.readBytes() }.deserialize() - val networkMap = signedNetworkMap.verified() - // Assume network map cert is issued by the root. - X509Utilities.validateCertificateChain(trustedRoot, signedNetworkMap.sig.by, trustedRoot) + val networkMap = signedNetworkMap.verified(trustedRoot) val timeout = CacheControl.parse(Headers.of(conn.headerFields.filterKeys { it != null }.mapValues { it.value.first() })).maxAgeSeconds().seconds return NetworkMapResponse(networkMap, timeout) } @@ -59,16 +57,12 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) { null } else { - val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize>() - val nodeInfo = signedNodeInfo.verified() - // Verify node info is signed by node identity - // TODO : Validate multiple signatures when NodeInfo supports multiple identities. - require(nodeInfo.legalIdentities.any { it.owningKey == signedNodeInfo.sig.by }) { "NodeInfo must be signed by the node owning key." } - nodeInfo + val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize() + signedNodeInfo.verified() } } - fun getNetworkParameter(networkParameterHash: SecureHash): NetworkParameters? { + fun getNetworkParameter(networkParameterHash: SecureHash): SignedData? { val conn = URL("$networkMapUrl/network-parameter/$networkParameterHash").openHttpConnection() return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) { null @@ -87,7 +81,8 @@ data class NetworkMapResponse(val networkMap: NetworkMap, val cacheMaxAge: Durat class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val fileWatcher: NodeInfoWatcher, - private val networkMapClient: NetworkMapClient?) : Closeable { + private val networkMapClient: NetworkMapClient?, + private val currentParametersHash: SecureHash) : Closeable { companion object { private val logger = contextLogger() private val retryInterval = 1.minutes @@ -101,7 +96,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS) } - fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedData) { + fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedNodeInfo) { val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) // Compare node info without timestamp. if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return @@ -127,15 +122,21 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, override fun run() { val nextScheduleDelay = try { val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap() + // TODO NetworkParameters updates are not implemented yet. Every mismatch should result in node shutdown. + if (currentParametersHash != networkMap.networkParameterHash) { + logger.error("Node is using parameters with hash: $currentParametersHash but network map is advertising: ${networkMap.networkParameterHash}.\n" + + "Please update node to use correct network parameters file.\"") + System.exit(1) + } val currentNodeHashes = networkMapCache.allNodeHashes val hashesFromNetworkMap = networkMap.nodeInfoHashes (hashesFromNetworkMap - currentNodeHashes).mapNotNull { // Download new node info from network map try { networkMapClient.getNodeInfo(it) - } catch (t: Throwable) { + } catch (e: Exception) { // Failure to retrieve one node info shouldn't stop the whole update, log and return null instead. - logger.warn("Error encountered when downloading node info '$it', skipping...", t) + logger.warn("Error encountered when downloading node info '$it', skipping...", e) null } }.forEach { @@ -146,7 +147,6 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, (currentNodeHashes - hashesFromNetworkMap - fileWatcher.processedNodeInfoHashes) .mapNotNull(networkMapCache::getNodeByHash) .forEach(networkMapCache::removeNode) - // TODO: Check NetworkParameter. cacheTimeout } catch (t: Throwable) { logger.warn("Error encountered while updating network map, will retry in ${retryInterval.seconds} seconds", t) @@ -159,7 +159,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, executor.submit(task) // The check may be expensive, so always run it in the background even the first time. } - private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedData, networkMapClient: NetworkMapClient) { + private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) { val task = object : Runnable { override fun run() { try { 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 771604ba6b..3031349d84 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 @@ -2,14 +2,14 @@ package net.corda.node.services.network import net.corda.cordform.CordformNode import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignedData import net.corda.core.internal.* import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds -import net.corda.nodeapi.internal.NodeInfoFilesCopier +import net.corda.nodeapi.internal.network.NodeInfoFilesCopier +import net.corda.nodeapi.internal.SignedNodeInfo import rx.Observable import rx.Scheduler import java.io.IOException @@ -41,14 +41,14 @@ class NodeInfoWatcher(private val nodePath: Path, private val logger = contextLogger() /** * Saves the given [NodeInfo] to a path. - * The node is 'encoded' as a SignedData, signed with the owning key of its first identity. + * The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity. * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename * is used so that one can freely copy these files without fearing to overwrite another one. * * @param path the path where to write the file, if non-existent it will be created. * @param signedNodeInfo the signed NodeInfo. */ - fun saveToFile(path: Path, signedNodeInfo: SignedData) { + fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) { try { path.createDirectories() signedNodeInfo.serialize() @@ -85,7 +85,7 @@ class NodeInfoWatcher(private val nodePath: Path, .flatMapIterable { loadFromDirectory() } } - fun saveToFile(signedNodeInfo: SignedData) = Companion.saveToFile(nodePath, signedNodeInfo) + fun saveToFile(signedNodeInfo: SignedNodeInfo) = Companion.saveToFile(nodePath, signedNodeInfo) /** * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. @@ -118,7 +118,7 @@ class NodeInfoWatcher(private val nodePath: Path, private fun processFile(file: Path): NodeInfo? { return try { logger.info("Reading NodeInfo from file: $file") - val signedData = file.readAll().deserialize>() + val signedData = file.readAll().deserialize() signedData.verified() } catch (e: Exception) { logger.warn("Exception parsing NodeInfo from file. $file", e) 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 be52529785..13881de63c 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 @@ -25,7 +25,7 @@ import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction -import net.corda.nodeapi.internal.NotaryInfo +import net.corda.nodeapi.internal.network.NotaryInfo import org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject @@ -208,9 +208,6 @@ open class PersistentNetworkMapCache( getAllInfos(session).map { it.toNodeInfo() } } - // Changes related to NetworkMap redesign - // TODO It will be properly merged into network map cache after services removal. - private fun getAllInfos(session: Session): List { val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java) criteria.select(criteria.from(NodeInfoSchemaV1.PersistentNodeInfo::class.java)) @@ -293,7 +290,6 @@ open class PersistentNetworkMapCache( else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") } - /** Object Relational Mapping support. */ private fun generateMappedObject(nodeInfo: NodeInfo): NodeInfoSchemaV1.PersistentNodeInfo { return NodeInfoSchemaV1.PersistentNodeInfo( diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 6205b0d073..9caf275e2f 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -33,3 +33,5 @@ enterpriseConfiguration = { waitInterval = 40000 } } + +useAMQPBridges = true \ No newline at end of file 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 bee1dba5ee..7a5b41fcc1 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 @@ -24,10 +24,9 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence; import net.corda.nodeapi.internal.persistence.DatabaseTransaction; import net.corda.testing.SerializationEnvironmentRule; import net.corda.testing.TestIdentity; -import net.corda.testing.contracts.DummyLinearContract; -import net.corda.testing.contracts.VaultFiller; +import net.corda.testing.internal.vault.DummyLinearContract; +import net.corda.testing.internal.vault.VaultFiller; import net.corda.testing.node.MockServices; -import net.corda.testing.schemas.DummyLinearStateSchemaV1; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -46,20 +45,20 @@ import java.util.stream.StreamSupport; import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM; import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; import static net.corda.core.utilities.ByteArrays.toHexString; -import static net.corda.testing.CoreTestUtils.rigorousMock; -import static net.corda.testing.TestConstants.getBOC_NAME; -import static net.corda.testing.TestConstants.getCHARLIE_NAME; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME; +import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock; +import static net.corda.testing.TestConstants.BOC_NAME; +import static net.corda.testing.TestConstants.CHARLIE_NAME; +import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME; import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices; import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests { - private static final TestIdentity BOC = new TestIdentity(getBOC_NAME()); - private static final Party CHARLIE = new TestIdentity(getCHARLIE_NAME(), 90L).getParty(); + private static final TestIdentity BOC = new TestIdentity(BOC_NAME); + private static final Party CHARLIE = new TestIdentity(CHARLIE_NAME, 90L).getParty(); private static final TestIdentity DUMMY_CASH_ISSUER_INFO = new TestIdentity(new CordaX500Name("Snake Oil Issuer", "London", "GB"), 10L); private static final PartyAndReference DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_INFO.ref((byte) 1); - private static final TestIdentity DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L); + private static final TestIdentity DUMMY_NOTARY = new TestIdentity(DUMMY_NOTARY_NAME, 20L); private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); @Rule public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); @@ -71,17 +70,17 @@ public class VaultQueryJavaTests { @Before public void setUp() throws CertificateException, InvalidAlgorithmParameterException { List cordappPackages = Arrays.asList( - "net.corda.testing.contracts", + "net.corda.testing.internal.vault", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName(), DummyLinearStateSchemaV1.class.getPackage().getName()); - IdentityServiceInternal identitySvc = makeTestIdentityService(Arrays.asList(MEGA_CORP.getIdentity(), DUMMY_CASH_ISSUER_INFO.getIdentity(), DUMMY_NOTARY.getIdentity())); + IdentityServiceInternal identitySvc = makeTestIdentityService(MEGA_CORP.getIdentity(), DUMMY_CASH_ISSUER_INFO.getIdentity(), DUMMY_NOTARY.getIdentity()); Pair databaseAndServices = makeTestDatabaseAndMockServices( - Arrays.asList(MEGA_CORP.getKey(), DUMMY_NOTARY.getKey()), - identitySvc, cordappPackages, - MEGA_CORP.getName()); - issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), DUMMY_CASH_ISSUER_INFO, BOC.getKey()); + identitySvc, + MEGA_CORP, + DUMMY_NOTARY.getKeyPair()); + issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), DUMMY_CASH_ISSUER_INFO, BOC.getKeyPair()); database = databaseAndServices.getFirst(); MockServices services = databaseAndServices.getSecond(); vaultFiller = new VaultFiller(services, DUMMY_NOTARY); @@ -471,16 +470,16 @@ public class VaultQueryJavaTests { assertThat(results.getOtherResults()).hasSize(12); assertThat(results.getOtherResults().get(0)).isEqualTo(400L); - assertThat(results.getOtherResults().get(1)).isEqualTo(CryptoUtils.toStringShort(BOC.getPubkey())); + assertThat(results.getOtherResults().get(1)).isEqualTo(CryptoUtils.toStringShort(BOC.getPublicKey())); assertThat(results.getOtherResults().get(2)).isEqualTo("GBP"); assertThat(results.getOtherResults().get(3)).isEqualTo(300L); - assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPubkey())); + assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPublicKey())); assertThat(results.getOtherResults().get(5)).isEqualTo("GBP"); assertThat(results.getOtherResults().get(6)).isEqualTo(200L); - assertThat(results.getOtherResults().get(7)).isEqualTo(CryptoUtils.toStringShort(BOC.getPubkey())); + assertThat(results.getOtherResults().get(7)).isEqualTo(CryptoUtils.toStringShort(BOC.getPublicKey())); assertThat(results.getOtherResults().get(8)).isEqualTo("USD"); assertThat(results.getOtherResults().get(9)).isEqualTo(100L); - assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPubkey())); + assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPublicKey())); assertThat(results.getOtherResults().get(11)).isEqualTo("USD"); } catch (NoSuchFieldException e) { diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index d2caa2f214..eb3e5ba532 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -17,7 +17,7 @@ import net.corda.node.internal.configureDatabase import net.corda.testing.TestIdentity import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.junit.After import org.junit.Before import org.junit.Test @@ -52,7 +52,7 @@ class InteractiveShellTest { override fun call() = a } - private val ids = makeTestIdentityService(listOf(megaCorp.identity)) + private val ids = makeTestIdentityService(megaCorp.identity) private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory()) private fun check(input: String, expected: String) { diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt new file mode 100644 index 0000000000..0b9e42640c --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -0,0 +1,78 @@ +package net.corda.node.internal + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +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.node.services.config.NotaryConfig +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.nodeapi.internal.network.NotaryInfo +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.chooseIdentity +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.node.* +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.After +import org.junit.Test +import java.nio.file.Path +import kotlin.test.assertFails + +class NetworkParametersTest { + private val mockNet = MockNetwork( + emptyList(), + MockNetworkParameters(networkSendManuallyPumped = true), + notarySpecs = listOf(MockNetwork.NotarySpec(DUMMY_NOTARY_NAME))) + + @After + fun tearDown() { + mockNet.stopNodes() + } + + // Minimum Platform Version tests + @Test + fun `node shutdowns when on lower platform version than network`() { + val alice = mockNet.createUnstartedNode(MockNodeParameters(legalName = ALICE_NAME, forcedID = 100, version = MockServices.MOCK_VERSION_INFO.copy(platformVersion = 1))) + val aliceDirectory = mockNet.baseDirectory(100) + val netParams = testNetworkParameters( + notaries = listOf(NotaryInfo(mockNet.defaultNotaryIdentity, true)), + minimumPlatformVersion = 2) + dropParametersToDir(aliceDirectory, netParams) + assertThatThrownBy { alice.start() }.hasMessageContaining("platform version") + } + + @Test + fun `node works fine when on higher platform version`() { + val alice = mockNet.createUnstartedNode(MockNodeParameters(legalName = ALICE_NAME, forcedID = 100, version = MockServices.MOCK_VERSION_INFO.copy(platformVersion = 2))) + val aliceDirectory = mockNet.baseDirectory(100) + val netParams = testNetworkParameters( + notaries = listOf(NotaryInfo(mockNet.defaultNotaryIdentity, true)), + minimumPlatformVersion = 1) + dropParametersToDir(aliceDirectory, netParams) + alice.start() + } + + // Notaries tests + @Test + fun `choosing notary not specified in network parameters will fail`() { + val fakeNotary = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME, configOverrides = { + val notary = NotaryConfig(false) + doReturn(notary).whenever(it).notary})) + val fakeNotaryId = fakeNotary.info.chooseIdentity() + val alice = mockNet.createPartyNode(ALICE_NAME) + assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId) + assertFails { + alice.services.startFlow(CashIssueFlow(500.DOLLARS, OpaqueBytes.of(0x01), fakeNotaryId)).resultFuture.getOrThrow() + } + } + + // Helpers + private fun dropParametersToDir(dir: Path, params: NetworkParameters) { + NetworkParametersCopier(params).install(dir) + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index 1965e3dc12..7eedfc2fec 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -1,7 +1,9 @@ package net.corda.node.messaging +import net.corda.core.messaging.AllPossibleRecipients import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.TopicStringValidator +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before @@ -16,7 +18,7 @@ class InMemoryMessagingTests { @Before fun setUp() { - mockNet = MockNetwork() + mockNet = MockNetwork(emptyList()) } @After @@ -72,7 +74,7 @@ class InMemoryMessagingTests { var counter = 0 listOf(node1, node2, node3).forEach { it.network.addMessageHandler("test.topic") { _, _, _ -> counter++ } } - node1.network.send(node2.network.createMessage("test.topic", data = bits), mockNet.messagingNetwork.everyoneOnline) + node1.network.send(node2.network.createMessage("test.topic", data = bits), rigorousMock()) mockNet.runNetwork(rounds = 1) assertEquals(3, counter) } 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 cf5f84e301..6fa98122a5 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -41,7 +41,12 @@ import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.checkpoints import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* -import net.corda.testing.contracts.VaultFiller +import net.corda.testing.internal.LogHelper +import net.corda.testing.dsl.LedgerDSL +import net.corda.testing.dsl.TestLedgerDSLInterpreter +import net.corda.testing.dsl.TestTransactionDSLInterpreter +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.* import org.assertj.core.api.Assertions.assertThat import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt index 2866d41dc4..be0d40ae8e 100644 --- a/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt @@ -1,10 +1,19 @@ package net.corda.node.services import net.corda.core.context.AuthServiceId +import net.corda.core.flows.FlowLogic +import net.corda.core.messaging.CordaRPCOps +import net.corda.node.internal.security.Password import net.corda.node.internal.security.RPCSecurityManagerImpl +import net.corda.node.internal.security.tryAuthenticate import net.corda.nodeapi.internal.config.User import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test +import javax.security.auth.login.FailedLoginException +import kotlin.reflect.KFunction +import kotlin.test.assertFails +import kotlin.test.assertFailsWith +import kotlin.test.assertNull class RPCSecurityManagerTest { @@ -15,7 +24,147 @@ class RPCSecurityManagerTest { assertThatThrownBy { configWithRPCUsername("user#1") }.hasMessageContaining("#") } - private fun configWithRPCUsername(username: String) { - RPCSecurityManagerImpl.fromUserList(users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST")) + @Test + fun `Generic RPC call authorization`() { + checkUserPermissions( + permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")), + permissions = setOf( + Permissions.invokeRpc(CordaRPCOps::nodeInfo), + Permissions.invokeRpc(CordaRPCOps::notaryIdentities))) } + + @Test + fun `Flow invocation authorization`() { + checkUserPermissions( + permissions = setOf(Permissions.startFlow()), + permitted = setOf( + arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"), + arrayListOf("startFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"))) + } + + @Test + fun `Check startFlow RPC permission implies startFlowDynamic`() { + checkUserPermissions( + permissions = setOf(Permissions.invokeRpc("startFlow")), + permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic"))) + } + + @Test + fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() { + checkUserPermissions( + permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")), + permissions = setOf(Permissions.invokeRpc("startTrackedFlow"))) + } + + @Test + fun `Admin authorization`() { + checkUserPermissions( + permissions = setOf("all"), + permitted = allActions.map { arrayListOf(it) }.toSet()) + } + + @Test + fun `Malformed permission strings`() { + assertMalformedPermission("bar") + assertMalformedPermission("InvokeRpc.nodeInfo.XXX") + assertMalformedPermission("") + assertMalformedPermission(".") + assertMalformedPermission("..") + assertMalformedPermission("startFlow") + assertMalformedPermission("startFlow.") + } + + @Test + fun `Login with unknown user`() { + val userRealm = RPCSecurityManagerImpl.fromUserList( + users = listOf(User("user", "xxxx", emptySet())), + id = AuthServiceId("TEST")) + userRealm.authenticate("user", Password("xxxx")) + assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") { + userRealm.authenticate("foo", Password("xxxx")) + } + assertNull(userRealm.tryAuthenticate("foo", Password("wrong")), + "Login with wrong password should fail") + } + + @Test + fun `Login with wrong credentials`() { + val userRealm = RPCSecurityManagerImpl.fromUserList( + users = listOf(User("user", "password", emptySet())), + id = AuthServiceId("TEST")) + userRealm.authenticate("user", Password("password")) + assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") { + userRealm.authenticate("user", Password("wrong")) + } + assertNull(userRealm.tryAuthenticate("user", Password("wrong")), + "Login with wrong password should fail") + } + + @Test + fun `Build invalid subject`() { + val userRealm = RPCSecurityManagerImpl.fromUserList( + users = listOf(User("user", "password", emptySet())), + id = AuthServiceId("TEST")) + val subject = userRealm.buildSubject("foo") + for (action in allActions) { + assert(!subject.isPermitted(action)) { + "Invalid subject should not be allowed to call $action" + } + } + } + + private fun configWithRPCUsername(username: String) { + RPCSecurityManagerImpl.fromUserList( + users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST")) + } + + private fun checkUserPermissions(permissions: Set, permitted: Set>) { + val user = User(username = "user", password = "password", permissions = permissions) + val userRealms = RPCSecurityManagerImpl.fromUserList(users = listOf(user), id = AuthServiceId("TEST")) + val disabled = allActions.filter { !permitted.contains(listOf(it)) } + for (subject in listOf( + userRealms.authenticate("user", Password("password")), + userRealms.tryAuthenticate("user", Password("password"))!!, + userRealms.buildSubject("user"))) { + for (request in permitted) { + val call = request.first() + val args = request.drop(1).toTypedArray() + assert(subject.isPermitted(request.first(), *args)) { + "User ${subject.principal} should be permitted ${call} with target '${request.toList()}'" + } + if (args.isEmpty()) { + assert(subject.isPermitted(request.first(), "XXX")) { + "User ${subject.principal} should be permitted ${call} with any target" + } + } + } + + disabled.forEach { + assert(!subject.isPermitted(it)) { + "Permissions $permissions should not allow to call $it" + } + } + + disabled.filter { !permitted.contains(listOf(it, "foo")) }.forEach { + assert(!subject.isPermitted(it, "foo")) { + "Permissions $permissions should not allow to call $it with argument 'foo'" + } + } + } + } + + private fun assertMalformedPermission(permission: String) { + assertFails { + RPCSecurityManagerImpl.fromUserList( + users = listOf(User("x", "x", setOf(permission))), + id = AuthServiceId("TEST")) + } + } + + companion object { + private val allActions = CordaRPCOps::class.members.filterIsInstance>().map { it.name }.toSet() + + setOf("startFlow", "startTrackedFlow") + } + + private abstract class DummyFlow : FlowLogic() } \ 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 fddea92a22..3c3e356f12 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.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.doneFuture import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializeAsToken @@ -23,7 +24,6 @@ import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.api.ServiceHubInternal -import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.StateMachineManager @@ -31,10 +31,13 @@ import net.corda.node.services.statemachine.StateMachineManagerImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor import net.corda.node.internal.configureDatabase +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.services.MockAttachmentStorage @@ -43,7 +46,6 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import java.nio.file.Paths import java.time.Clock import java.time.Instant import java.util.concurrent.CountDownLatch @@ -53,7 +55,7 @@ import kotlin.test.assertTrue class NodeSchedulerServiceTest : SingletonSerializeAsToken() { private companion object { - val ALICE_KEY = TestIdentity(ALICE_NAME, 70).key + val ALICE_KEY = TestIdentity(ALICE_NAME, 70).keyPair val DUMMY_IDENTITY_1 = getTestPartyAndCertificate(Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public)) val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val myInfo = NodeInfo(listOf(NetworkHostAndPort("mockHost", 30000)), listOf(DUMMY_IDENTITY_1), 1, serial = 1L) @@ -98,14 +100,19 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database = configureDatabase(dataSourceProps, DatabaseConfig(), rigorousMock()) val identityService = makeTestIdentityService() kms = MockKeyManagementService(identityService, ALICE_KEY) - val configuration = testNodeConfiguration(Paths.get("."), CordaX500Name("Alice", "London", "GB")) + val configuration = rigorousMock().also { + doReturn(true).whenever(it).devMode + doReturn(null).whenever(it).devModeOptions + } val validatedTransactions = MockTransactionStorage() database.transaction { services = rigorousMock().also { doReturn(configuration).whenever(it).configuration doReturn(MonitoringService(MetricRegistry())).whenever(it).monitoringService doReturn(validatedTransactions).whenever(it).validatedTransactions - doReturn(NetworkMapCacheImpl(MockNetworkMapCache(database), identityService)).whenever(it).networkMapCache + doReturn(rigorousMock().also { + doReturn(doneFuture(null)).whenever(it).nodeReady + }).whenever(it).networkMapCache doReturn(myInfo).whenever(it).myInfo doReturn(kms).whenever(it).keyManagementService doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts")), MockAttachmentStorage())).whenever(it).cordappProvider diff --git a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt index 46a5dc9db0..09c1041c54 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt @@ -13,6 +13,7 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.* +import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -27,13 +28,17 @@ class InMemoryIdentityServiceTests { val bob = TestIdentity(BOB_NAME, 80) val ALICE get() = alice.party val ALICE_IDENTITY get() = alice.identity - val ALICE_PUBKEY get() = alice.pubkey + val ALICE_PUBKEY get() = alice.publicKey val BOB get() = bob.party val BOB_IDENTITY get() = bob.identity - val BOB_PUBKEY get() = bob.pubkey - fun createService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities.toSet(), DEV_TRUST_ROOT) + val BOB_PUBKEY get() = bob.publicKey + fun createService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities, DEV_TRUST_ROOT) } + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + @Test fun `get all identities`() { val service = createService() @@ -94,18 +99,16 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert unknown anonymous key is unrecognised`() { - withTestSerialization { - val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) - val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val service = createService() - // TODO: Generate certificate with an EdDSA key rather than ECDSA - val identity = Party(rootCert.cert) - val txIdentity = AnonymousParty(txKey.public) + val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) + val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val service = createService() + // TODO: Generate certificate with an EdDSA key rather than ECDSA + val identity = Party(rootCert.cert) + val txIdentity = AnonymousParty(txKey.public) - assertFailsWith { - service.assertOwnership(identity, txIdentity) - } + assertFailsWith { + service.assertOwnership(identity, txIdentity) } } @@ -137,30 +140,28 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert ownership`() { - withTestSerialization { - val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) - val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) + val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) + val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) - // Now we have identities, construct the service and let it know about both - val service = createService(alice, bob) - service.verifyAndRegisterIdentity(anonymousAlice) - service.verifyAndRegisterIdentity(anonymousBob) + // Now we have identities, construct the service and let it know about both + val service = createService(alice, bob) + service.verifyAndRegisterIdentity(anonymousAlice) + service.verifyAndRegisterIdentity(anonymousBob) - // Verify that paths are verified - service.assertOwnership(alice.party, anonymousAlice.party.anonymise()) - service.assertOwnership(bob.party, anonymousBob.party.anonymise()) - assertFailsWith { - service.assertOwnership(alice.party, anonymousBob.party.anonymise()) - } - assertFailsWith { - service.assertOwnership(bob.party, anonymousAlice.party.anonymise()) - } + // Verify that paths are verified + service.assertOwnership(alice.party, anonymousAlice.party.anonymise()) + service.assertOwnership(bob.party, anonymousBob.party.anonymise()) + assertFailsWith { + service.assertOwnership(alice.party, anonymousBob.party.anonymise()) + } + assertFailsWith { + service.assertOwnership(bob.party, anonymousAlice.party.anonymise()) + } - assertFailsWith { - val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded) - val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal) - service.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise()) - } + assertFailsWith { + val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded) + val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal) + service.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise()) } } diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index 9b50b7f5b0..956bbbb4cf 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -21,6 +21,7 @@ import net.corda.testing.node.MockServices.Companion.makeTestDataSourcePropertie import net.corda.testing.node.makeTestIdentityService import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -35,12 +36,15 @@ class PersistentIdentityServiceTests { val bob = TestIdentity(BOB_NAME, 80) val ALICE get() = alice.party val ALICE_IDENTITY get() = alice.identity - val ALICE_PUBKEY get() = alice.pubkey + val ALICE_PUBKEY get() = alice.publicKey val BOB get() = bob.party val BOB_IDENTITY get() = bob.identity - val BOB_PUBKEY get() = bob.pubkey + val BOB_PUBKEY get() = bob.publicKey } + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() private lateinit var database: CordaPersistence private lateinit var identityService: IdentityService @@ -138,17 +142,15 @@ class PersistentIdentityServiceTests { */ @Test fun `assert unknown anonymous key is unrecognised`() { - withTestSerialization { - val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) - val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME) - val identity = Party(rootCert.cert) - val txIdentity = AnonymousParty(txKey.public) + val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) + val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME) + val identity = Party(rootCert.cert) + val txIdentity = AnonymousParty(txKey.public) - assertFailsWith { - database.transaction { - identityService.assertOwnership(identity, txIdentity) - } + assertFailsWith { + database.transaction { + identityService.assertOwnership(identity, txIdentity) } } } @@ -191,38 +193,36 @@ class PersistentIdentityServiceTests { */ @Test fun `assert ownership`() { - withTestSerialization { - val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) - val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) + val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) + val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) + database.transaction { + // Now we have identities, construct the service and let it know about both + identityService.verifyAndRegisterIdentity(anonymousAlice) + identityService.verifyAndRegisterIdentity(anonymousBob) + } + + // Verify that paths are verified + database.transaction { + identityService.assertOwnership(alice.party, anonymousAlice.party.anonymise()) + identityService.assertOwnership(bob.party, anonymousBob.party.anonymise()) + } + assertFailsWith { database.transaction { - // Now we have identities, construct the service and let it know about both - identityService.verifyAndRegisterIdentity(anonymousAlice) - identityService.verifyAndRegisterIdentity(anonymousBob) + identityService.assertOwnership(alice.party, anonymousBob.party.anonymise()) } - - // Verify that paths are verified + } + assertFailsWith { database.transaction { - identityService.assertOwnership(alice.party, anonymousAlice.party.anonymise()) - identityService.assertOwnership(bob.party, anonymousBob.party.anonymise()) - } - assertFailsWith { - database.transaction { - identityService.assertOwnership(alice.party, anonymousBob.party.anonymise()) - } - } - assertFailsWith { - database.transaction { - identityService.assertOwnership(bob.party, anonymousAlice.party.anonymise()) - } + identityService.assertOwnership(bob.party, anonymousAlice.party.anonymise()) } + } - assertFailsWith { - val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded) - database.transaction { - val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal) - identityService.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise()) - } + assertFailsWith { + val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded) + database.transaction { + val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal) + identityService.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise()) } } } 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 35eb3810a3..9a90b4a1e8 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,5 +1,7 @@ package net.corda.node.services.messaging +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.context.AuthServiceId import net.corda.core.crypto.generateKeyPair import net.corda.core.concurrent.CordaFuture @@ -7,21 +9,23 @@ import com.codahale.metrics.MetricRegistry import net.corda.core.crypto.generateKeyPair import net.corda.core.internal.concurrent.openFuture import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.internal.configureDatabase import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManagerImpl +import net.corda.node.services.config.CertChainPolicyConfig 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.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.testNodeConfiguration import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After @@ -65,9 +69,17 @@ class ArtemisMessagingTests { @Before fun setUp() { securityManager = RPCSecurityManagerImpl.fromUserList(users = emptyList(), id = AuthServiceId("TEST")) - config = testNodeConfiguration( - baseDirectory = temporaryFolder.root.toPath(), - myLegalName = ALICE_NAME) + abstract class AbstractNodeConfiguration : NodeConfiguration + config = rigorousMock().also { + doReturn(temporaryFolder.root.toPath()).whenever(it).baseDirectory + doReturn(ALICE_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn("").whenever(it).exportJMXto + doReturn(emptyList()).whenever(it).certificateChainCheckPolicies + doReturn(5).whenever(it).messageRedeliveryDelaySeconds + doReturn(true).whenever(it).useAMQPBridges + } LogHelper.setLevel(PersistentUniquenessProvider::class) database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), rigorousMock()) networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, emptyList()), rigorousMock()) @@ -159,7 +171,7 @@ class ArtemisMessagingTests { return Pair(messagingClient, receivedMessages) } - private fun createMessagingClient(server: NetworkHostAndPort = NetworkHostAndPort("localhost", serverPort), platformVersion: Int = 1): P2PMessagingClient { + private fun createMessagingClient(server: NetworkHostAndPort = NetworkHostAndPort("localhost", serverPort), platformVersion: Int = 1, maxMessageSize: Int = MAX_MESSAGE_SIZE): P2PMessagingClient { return database.transaction { P2PMessagingClient( config, @@ -167,16 +179,16 @@ class ArtemisMessagingTests { server, identity.public, ServiceAffinityExecutor("ArtemisMessagingTests", 1), - database - ).apply { + database, + maxMessageSize = maxMessageSize).apply { config.configureWithDevSSLCertificate() messagingClient = this } } } - private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort): ArtemisMessagingServer { - return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager).apply { + private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort, maxMessageSize: Int = MAX_MESSAGE_SIZE): ArtemisMessagingServer { + return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager, maxMessageSize).apply { config.configureWithDevSSLCertificate() messagingServer = this } 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 0597bfbd40..14f91e0178 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 @@ -13,7 +13,7 @@ import java.math.BigInteger import kotlin.test.assertEquals class NetworkMapCacheTest { - private val mockNet = MockNetwork() + private val mockNet = MockNetwork(emptyList()) @After fun teardown() { diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index 734a1ec860..608f583f89 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -5,14 +5,13 @@ import net.corda.core.crypto.sha256 import net.corda.core.internal.cert import net.corda.core.serialization.serialize import net.corda.core.utilities.seconds -import net.corda.node.services.network.TestNodeInfoFactory.createNodeInfo -import net.corda.testing.DEV_CA +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME import net.corda.testing.DEV_TRUST_ROOT -import net.corda.testing.ROOT_CA import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation -import net.corda.testing.node.network.NetworkMapServer -import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.internal.createNodeInfoAndSigned +import net.corda.testing.node.internal.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -26,13 +25,12 @@ class NetworkMapClientTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) + + private val cacheTimeout = 100000.seconds + private lateinit var server: NetworkMapServer private lateinit var networkMapClient: NetworkMapClient - companion object { - private val cacheTimeout = 100000.seconds - } - @Before fun setUp() { server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort()) @@ -47,9 +45,7 @@ class NetworkMapClientTest { @Test fun `registered node is added to the network map`() { - // Create node info. - val signedNodeInfo = createNodeInfo("Test1") - val nodeInfo = signedNodeInfo.verified() + val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(ALICE_NAME) networkMapClient.publish(signedNodeInfo) @@ -58,8 +54,8 @@ class NetworkMapClientTest { assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash) assertEquals(nodeInfo, networkMapClient.getNodeInfo(nodeInfoHash)) - val signedNodeInfo2 = createNodeInfo("Test2") - val nodeInfo2 = signedNodeInfo2.verified() + val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned(BOB_NAME) + networkMapClient.publish(signedNodeInfo2) val nodeInfoHash2 = nodeInfo2.serialize().sha256() @@ -72,7 +68,7 @@ class NetworkMapClientTest { @Test fun `download NetworkParameter correctly`() { // The test server returns same network parameter for any hash. - val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256()) + val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified() assertNotNull(networkParameter) assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index fc3542a3d6..b76c7d59d9 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -1,65 +1,70 @@ package net.corda.node.services.network -import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.verify -import net.corda.cordform.CordformNode -import net.corda.core.crypto.Crypto +import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignedData +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast -import net.corda.nodeapi.internal.NetworkMap import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize -import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.millis import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.network.NetworkMap +import net.corda.testing.ALICE_NAME import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.TestNodeInfoBuilder +import net.corda.testing.internal.createNodeInfoAndSigned +import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Rule import org.junit.Test import rx.schedulers.TestScheduler import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals class NetworkMapUpdaterTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) - private val jimFs = Jimfs.newFileSystem(Configuration.unix()) - private val baseDir = jimFs.getPath("/node") + + private val fs = Jimfs.newFileSystem(unix()) + private val baseDir = fs.getPath("/node") + private val networkMapCache = createMockNetworkMapCache() + private val nodeInfoMap = ConcurrentHashMap() + private val cacheExpiryMs = 100 + private val networkMapClient = createMockNetworkMapClient() + private val scheduler = TestScheduler() + private val networkParametersHash = SecureHash.randomSHA256() + private val fileWatcher = NodeInfoWatcher(baseDir, scheduler) + private val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, networkParametersHash) + private val nodeInfoBuilder = TestNodeInfoBuilder() + + @After + fun cleanUp() { + updater.close() + fs.close() + } @Test fun `publish node info`() { - val keyPair = Crypto.generateKeyPair() + nodeInfoBuilder.addIdentity(ALICE_NAME) - val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1").verified() - val signedNodeInfo = TestNodeInfoFactory.sign(keyPair, nodeInfo1) - - val sameNodeInfoDifferentTime = nodeInfo1.copy(serial = System.currentTimeMillis()) - val signedSameNodeInfoDifferentTime = TestNodeInfoFactory.sign(keyPair, sameNodeInfoDifferentTime) - - val differentNodeInfo = nodeInfo1.copy(addresses = listOf(NetworkHostAndPort("my.new.host.com", 1000))) - val signedDifferentNodeInfo = TestNodeInfoFactory.sign(keyPair, differentNodeInfo) - - val networkMapCache = getMockNetworkMapCache() - - val networkMapClient = mock() - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient) + val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned() + val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) // Publish node info for the first time. - updater.updateNodeInfo(nodeInfo1) { signedNodeInfo } + updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 } // Sleep as publish is asynchronous. // TODO: Remove sleep in unit test - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) verify(networkMapClient, times(1)).publish(any()) networkMapCache.addNode(nodeInfo1) @@ -67,167 +72,144 @@ class NetworkMapUpdaterTest { // Publish the same node info, but with different serial. updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime } // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) // Same node info should not publish twice verify(networkMapClient, times(0)).publish(signedSameNodeInfoDifferentTime) + val (differentNodeInfo, signedDifferentNodeInfo) = createNodeInfoAndSigned("Bob") + // Publish different node info. updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo } // TODO: Remove sleep in unit test. Thread.sleep(200) verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo) - - updater.close() } @Test fun `process add node updates from network map, with additional node infos from dir`() { - val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1") - val nodeInfo2 = TestNodeInfoFactory.createNodeInfo("Info 2") - val nodeInfo3 = TestNodeInfoFactory.createNodeInfo("Info 3") - val nodeInfo4 = TestNodeInfoFactory.createNodeInfo("Info 4") - val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file") - val networkMapCache = getMockNetworkMapCache() - - val nodeInfoMap = ConcurrentHashMap>() - val networkMapClient = mock { - on { publish(any()) }.then { - val signedNodeInfo: SignedData = uncheckedCast(it.arguments.first()) - nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) - } - on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) } - on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() } - } - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient) + val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1") + val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") + val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") + val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") + val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") // Test adding new node. - networkMapClient.publish(nodeInfo1) + networkMapClient.publish(signedNodeInfo1) // Not subscribed yet. verify(networkMapCache, times(0)).addNode(any()) updater.subscribeToNetworkMap() - networkMapClient.publish(nodeInfo2) + networkMapClient.publish(signedNodeInfo2) // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) verify(networkMapCache, times(2)).addNode(any()) - verify(networkMapCache, times(1)).addNode(nodeInfo1.verified()) - verify(networkMapCache, times(1)).addNode(nodeInfo2.verified()) + verify(networkMapCache, times(1)).addNode(nodeInfo1) + verify(networkMapCache, times(1)).addNode(nodeInfo2) - NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo) - networkMapClient.publish(nodeInfo3) - networkMapClient.publish(nodeInfo4) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + networkMapClient.publish(signedNodeInfo3) + networkMapClient.publish(signedNodeInfo4) scheduler.advanceTimeBy(10, TimeUnit.SECONDS) // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) // 4 node info from network map, and 1 from file. verify(networkMapCache, times(5)).addNode(any()) - verify(networkMapCache, times(1)).addNode(nodeInfo3.verified()) - verify(networkMapCache, times(1)).addNode(nodeInfo4.verified()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified()) - - updater.close() + verify(networkMapCache, times(1)).addNode(nodeInfo3) + verify(networkMapCache, times(1)).addNode(nodeInfo4) + verify(networkMapCache, times(1)).addNode(fileNodeInfo) } @Test fun `process remove node updates from network map, with additional node infos from dir`() { - val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1") - val nodeInfo2 = TestNodeInfoFactory.createNodeInfo("Info 2") - val nodeInfo3 = TestNodeInfoFactory.createNodeInfo("Info 3") - val nodeInfo4 = TestNodeInfoFactory.createNodeInfo("Info 4") - val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file") - val networkMapCache = getMockNetworkMapCache() - - val nodeInfoMap = ConcurrentHashMap>() - val networkMapClient = mock { - on { publish(any()) }.then { - val signedNodeInfo: SignedData = uncheckedCast(it.arguments.first()) - nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) - } - on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) } - on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() } - } - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient) + val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1") + val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") + val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") + val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") + val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") // Add all nodes. - NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo) - networkMapClient.publish(nodeInfo1) - networkMapClient.publish(nodeInfo2) - networkMapClient.publish(nodeInfo3) - networkMapClient.publish(nodeInfo4) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + networkMapClient.publish(signedNodeInfo1) + networkMapClient.publish(signedNodeInfo2) + networkMapClient.publish(signedNodeInfo3) + networkMapClient.publish(signedNodeInfo4) updater.subscribeToNetworkMap() scheduler.advanceTimeBy(10, TimeUnit.SECONDS) // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) // 4 node info from network map, and 1 from file. - assertEquals(4, nodeInfoMap.size) + assertThat(nodeInfoMap).hasSize(4) verify(networkMapCache, times(5)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified()) + verify(networkMapCache, times(1)).addNode(fileNodeInfo) // Test remove node. nodeInfoMap.clear() // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) verify(networkMapCache, times(4)).removeNode(any()) - verify(networkMapCache, times(1)).removeNode(nodeInfo1.verified()) - verify(networkMapCache, times(1)).removeNode(nodeInfo2.verified()) - verify(networkMapCache, times(1)).removeNode(nodeInfo3.verified()) - verify(networkMapCache, times(1)).removeNode(nodeInfo4.verified()) + verify(networkMapCache, times(1)).removeNode(nodeInfo1) + verify(networkMapCache, times(1)).removeNode(nodeInfo2) + verify(networkMapCache, times(1)).removeNode(nodeInfo3) + verify(networkMapCache, times(1)).removeNode(nodeInfo4) // Node info from file should not be deleted - assertEquals(1, networkMapCache.allNodeHashes.size) - assertEquals(fileNodeInfo.verified().serialize().hash, networkMapCache.allNodeHashes.first()) - - updater.close() + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) } @Test fun `receive node infos from directory, without a network map`() { - val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file") - - val networkMapCache = getMockNetworkMapCache() - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, null) + val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") // Not subscribed yet. verify(networkMapCache, times(0)).addNode(any()) updater.subscribeToNetworkMap() - NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) scheduler.advanceTimeBy(10, TimeUnit.SECONDS) verify(networkMapCache, times(1)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified()) + verify(networkMapCache, times(1)).addNode(fileNodeInfo) - assertEquals(1, networkMapCache.allNodeHashes.size) - assertEquals(fileNodeInfo.verified().serialize().hash, networkMapCache.allNodeHashes.first()) - - updater.close() + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) } - private fun getMockNetworkMapCache() = mock { - val data = ConcurrentHashMap() - on { addNode(any()) }.then { - val nodeInfo = it.arguments.first() as NodeInfo - data.put(nodeInfo.legalIdentities.first(), nodeInfo) + private fun createMockNetworkMapClient(): NetworkMapClient { + return mock { + on { publish(any()) }.then { + val signedNodeInfo: SignedNodeInfo = uncheckedCast(it.arguments[0]) + nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) + } + on { getNetworkMap() }.then { + NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), networkParametersHash), cacheExpiryMs.millis) + } + on { getNodeInfo(any()) }.then { + nodeInfoMap[it.arguments[0]]?.verified() + } } - on { removeNode(any()) }.then { data.remove((it.arguments.first() as NodeInfo).legalIdentities.first()) } - on { getNodeByLegalIdentity(any()) }.then { data[it.arguments.first()] } - on { allNodeHashes }.then { data.values.map { it.serialize().hash } } - on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments.first() } } + } + + private fun createMockNetworkMapCache(): NetworkMapCacheInternal { + return mock { + val data = ConcurrentHashMap() + on { addNode(any()) }.then { + val nodeInfo = it.arguments[0] as NodeInfo + data.put(nodeInfo.legalIdentities[0], nodeInfo) + } + on { removeNode(any()) }.then { data.remove((it.arguments[0] as NodeInfo).legalIdentities[0]) } + on { getNodeByLegalIdentity(any()) }.then { data[it.arguments[0]] } + on { allNodeHashes }.then { data.values.map { it.serialize().hash } } + on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments[0] } } + } + } + + private fun createNodeInfoAndSigned(org: String): Pair { + return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt b/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt deleted file mode 100644 index 975bd65bf4..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.node.services.network - -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.SignedData -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.NodeInfo -import net.corda.core.serialization.serialize -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509CertificateFactory -import net.corda.nodeapi.internal.crypto.X509Utilities -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder -import java.security.KeyPair -import java.security.cert.CertPath -import java.security.cert.Certificate -import java.security.cert.X509Certificate - -object TestNodeInfoFactory { - private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", organisation = "R3 LTD", locality = "London", country = "GB"), rootCAKey) - private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) - - fun createNodeInfo(organisation: String): SignedData { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.$organisation.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - return sign(keyPair, nodeInfo) - } - - fun sign(keyPair: KeyPair, t: T): SignedData { - // Create digital signature. - val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, t.serialize().bytes)) - return SignedData(t.serialize(), digitalSignature) - } - - private fun buildCertPath(vararg certificates: Certificate): CertPath { - return X509CertificateFactory().generateCertPath(*certificates) - } - - private fun X509CertificateHolder.toX509Certificate(): X509Certificate { - return X509CertificateFactory().generateCertificate(encoded.inputStream()) - } - -} \ No newline at end of file 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 d1c6820fd7..0a711088b8 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 @@ -3,7 +3,6 @@ package net.corda.node.services.persistence import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId -import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.serialize @@ -15,7 +14,10 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* +import net.corda.testing.internal.LogHelper +import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before 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 e495f12f72..3f559113da 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 @@ -13,6 +13,8 @@ import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -24,7 +26,7 @@ import kotlin.test.assertEquals class DBTransactionStorageTests { private companion object { - val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey + val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party } 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 f2e1e82d45..7844d0e52d 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 @@ -37,12 +37,12 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.* -import net.corda.testing.contracts.* +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.schemas.DummyDealStateSchemaV1 -import net.corda.testing.schemas.DummyLinearStateSchemaV1 -import net.corda.testing.schemas.DummyLinearStateSchemaV2 +import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 +import net.corda.testing.internal.vault.DummyLinearStateSchemaV2 import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.hibernate.SessionFactory @@ -62,7 +62,7 @@ class HibernateConfigurationTest { val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) val BOC get() = bankOfCorda.party - val BOC_KEY get() = bankOfCorda.key + val BOC_KEY get() = bankOfCorda.keyPair } @Rule @@ -93,7 +93,7 @@ class HibernateConfigurationTest { @Before fun setUp() { - val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") + val cordappPackages = listOf("net.corda.testing.internal.vault", "net.corda.finance.contracts.asset") bankServices = MockServices(cordappPackages, rigorousMock(), BOC.name, BOC_KEY) issuerServices = MockServices(cordappPackages, rigorousMock(), dummyCashIssuer) notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary) @@ -113,7 +113,7 @@ class HibernateConfigurationTest { // `consumeCash` expects we can self-notarise transactions services = object : MockServices(cordappPackages, rigorousMock().also { doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME }) - }, BOB_NAME, generateKeyPair(), dummyNotary.key) { + }, BOB_NAME, generateKeyPair(), dummyNotary.keyPair) { override val vaultService = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { 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 d21cd6d6db..5995f3c177 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 @@ -17,9 +17,9 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.LogHelper +import net.corda.testing.internal.LogHelper import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.junit.After import org.junit.Before import org.junit.Ignore 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 9ad3ec390c..dcda96ce30 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 @@ -16,11 +16,11 @@ import net.corda.node.services.api.SchemaService import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager -import net.corda.testing.LogHelper +import net.corda.testing.internal.LogHelper import net.corda.testing.TestIdentity import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.junit.After import org.junit.Before import org.junit.Test 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 aa0d9253cd..ac9a88afe0 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 @@ -12,7 +12,7 @@ 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 +import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType import org.junit.Test 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 a4737f44fa..1fe97568a8 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 @@ -32,6 +32,7 @@ import net.corda.node.services.persistence.checkpoints import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState +import net.corda.testing.internal.LogHelper import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork 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 57beddeb10..cd2e965712 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 @@ -13,11 +13,11 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.LogHelper +import net.corda.testing.internal.LogHelper import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.freeLocalHostAndPort import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.junit.After import org.junit.Before import org.junit.Rule 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 1c26932b19..f4d03c9c39 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 @@ -7,6 +7,8 @@ import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After import org.junit.Before 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 0a281f0713..bb7a01c77a 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 @@ -31,7 +31,9 @@ import net.corda.finance.utils.sumCash import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* -import net.corda.testing.contracts.VaultFiller +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat @@ -64,8 +66,8 @@ class NodeVaultServiceTest { val DUMMY_NOTARY get() = dummyNotary.party val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_KEY get() = megaCorp.key - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey + val MEGA_CORP_KEY get() = megaCorp.keyPair + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey val MEGA_CORP_IDENTITY get() = megaCorp.identity val MINI_CORP get() = miniCorp.party val MINI_CORP_IDENTITY get() = miniCorp.identity @@ -86,10 +88,9 @@ class NodeVaultServiceTest { fun setUp() { LogHelper.setLevel(NodeVaultService::class) val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( - listOf(MEGA_CORP_KEY), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), cordappPackages, - MEGA_CORP.name) + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), + megaCorp) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) @@ -136,7 +137,7 @@ class NodeVaultServiceTest { assertThat(w1).hasSize(3) val originalVault = vaultService - val services2 = object : MockServices(rigorousMock(), MEGA_CORP.name) { + val services2 = object : MockServices(emptyList(), rigorousMock(), MEGA_CORP.name) { override val vaultService: NodeVaultService get() = originalVault override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { @@ -586,7 +587,7 @@ class NodeVaultServiceTest { val identity = services.myInfo.singleIdentityAndCert() assertEquals(services.identityService.partyFromKey(identity.owningKey), identity.party) val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false) - val thirdPartyServices = MockServices(rigorousMock().also { + val thirdPartyServices = MockServices(emptyList(), rigorousMock().also { doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MEGA_CORP.name }) }, MEGA_CORP.name) val thirdPartyIdentity = thirdPartyServices.keyManagementService.freshKeyAndCert(thirdPartyServices.myInfo.singleIdentityAndCert(), false) 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 2f75c95959..1a977695eb 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 @@ -26,12 +26,15 @@ import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* -import net.corda.testing.contracts.* +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.DUMMY_LINEAR_CONTRACT_PROGRAM_ID +import net.corda.testing.internal.vault.DummyLinearContract +import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.makeTestIdentityService import net.corda.testing.schemas.DummyDealStateSchemaV1 -import net.corda.testing.schemas.DummyLinearStateSchemaV1 +import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.* @@ -65,17 +68,17 @@ open class VaultQueryTests { val BOB_IDENTITY get() = bob.identity val BOC get() = bankOfCorda.party val BOC_IDENTITY get() = bankOfCorda.identity - val BOC_KEY get() = bankOfCorda.key - val BOC_PUBKEY get() = bankOfCorda.pubkey + val BOC_KEY get() = bankOfCorda.keyPair + val BOC_PUBKEY get() = bankOfCorda.publicKey val CASH_NOTARY get() = cashNotary.party val CASH_NOTARY_IDENTITY get() = cashNotary.identity val CHARLIE get() = charlie.party val CHARLIE_IDENTITY get() = charlie.identity val DUMMY_NOTARY get() = dummyNotary.party - val DUMMY_NOTARY_KEY get() = dummyNotary.key + val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair val MEGA_CORP_IDENTITY get() = megaCorp.identity - val MEGA_CORP_PUBKEY get() = megaCorp.pubkey - val MEGA_CORP_KEY get() = megaCorp.key + val MEGA_CORP_PUBKEY get() = megaCorp.publicKey + val MEGA_CORP_KEY get() = megaCorp.keyPair val MEGA_CORP get() = megaCorp.party val MINI_CORP_IDENTITY get() = miniCorp.identity val MINI_CORP get() = miniCorp.party @@ -105,15 +108,15 @@ open class VaultQueryTests { open fun setUp() { // register additional identities val databaseAndServices = makeTestDatabaseAndMockServices( - listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity)), cordappPackages, - MEGA_CORP.name) + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), + megaCorp, + DUMMY_NOTARY_KEY) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) vaultFillerCashNotary = VaultFiller(services, dummyNotary, CASH_NOTARY) - notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary, dummyCashIssuer.key, BOC_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary, dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY) identitySvc = services.identityService // Register all of the identities we're going to use (notaryServices.myInfo.legalIdentitiesAndCerts + BOC_IDENTITY + CASH_NOTARY_IDENTITY + MINI_CORP_IDENTITY + MEGA_CORP_IDENTITY).forEach { identity -> 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 index 66f4973877..f55943c9f6 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -28,7 +28,7 @@ import net.corda.node.services.api.VaultServiceInternal import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.startFlow import org.junit.After 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 c2e074f9c5..70430f4f16 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 @@ -21,11 +21,12 @@ import net.corda.finance.contracts.getCashBalance import net.corda.finance.schemas.CashSchemaV1 import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* -import net.corda.testing.contracts.* +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.makeTestIdentityService -import net.corda.testing.schemas.DummyLinearStateSchemaV1 import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After @@ -41,7 +42,7 @@ import kotlin.test.fail class VaultWithCashTest { private companion object { private val cordappPackages = listOf( - "net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName) + "net.corda.testing.internal.vault", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName) val BOB = TestIdentity(BOB_NAME, 80).party val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1) @@ -51,13 +52,14 @@ class VaultWithCashTest { val DUMMY_NOTARY get() = dummyNotary.party val MEGA_CORP get() = megaCorp.party val MEGA_CORP_IDENTITY get() = megaCorp.identity - val MEGA_CORP_KEY get() = megaCorp.key + val MEGA_CORP_KEY get() = megaCorp.keyPair val MINI_CORP_IDENTITY get() = miniCorp.identity } @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) + private val servicesKey = generateKeyPair() lateinit var services: MockServices private lateinit var vaultFiller: VaultFiller lateinit var issuerServices: MockServices @@ -70,10 +72,10 @@ class VaultWithCashTest { fun setUp() { LogHelper.setLevel(VaultWithCashTest::class) val databaseAndServices = makeTestDatabaseAndMockServices( - listOf(generateKeyPair(), dummyNotary.key), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity)), cordappPackages, - MEGA_CORP.name) + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), + TestIdentity(MEGA_CORP.name, servicesKey), + dummyNotary.keyPair) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) @@ -100,8 +102,7 @@ class VaultWithCashTest { val state = w[0].state.data assertEquals(30.45.DOLLARS `issued by` DUMMY_CASH_ISSUER, state.amount) - assertEquals(services.key.public, state.owner.owningKey) - + assertEquals(servicesKey.public, state.owner.owningKey) assertEquals(34.70.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w[2].state.data).amount) assertEquals(34.85.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w[1].state.data).amount) } 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 f48a38ac0c..91210f052a 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -6,7 +6,7 @@ import net.corda.core.internal.tee import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index ec8d1c961d..50922e9fde 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -14,8 +14,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.getX509Certificate import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.ALICE_NAME -import net.corda.testing.rigorousMock -import net.corda.testing.node.testNodeConfiguration +import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before import org.junit.Rule @@ -45,7 +44,14 @@ class NetworkRegistrationHelperTest { @Before fun init() { - config = testNodeConfiguration(baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE_NAME) + abstract class AbstractNodeConfiguration : NodeConfiguration + config = rigorousMock().also { + doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(ALICE_NAME).whenever(it).myLegalName + doReturn("").whenever(it).emailAddress + } } @Test diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 11d6e8ac1b..1cbbadd253 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -19,6 +19,9 @@ import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* +import net.corda.testing.internal.withoutTestSerialization +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After @@ -38,7 +41,7 @@ class NodeInterestRatesTest { val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val MEGA_CORP_KEY = generateKeyPair() val ALICE get() = alice.party - val ALICE_PUBKEY get() = alice.pubkey + val ALICE_PUBKEY get() = alice.publicKey } @Rule diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 262681c0c6..b0d72e4917 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -25,6 +25,8 @@ import net.corda.finance.contracts.PaymentRule import net.corda.finance.contracts.Tenor import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* +import net.corda.testing.dsl.* +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger import net.corda.testing.node.transaction @@ -39,14 +41,14 @@ private val DUMMY_PARTY = Party(CordaX500Name("Dummy", "Madrid", "ES"), generate private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) -private val ORACLE_PUBKEY = TestIdentity(CordaX500Name("Oracle", "London", "GB")).pubkey +private val ORACLE_PUBKEY = TestIdentity(CordaX500Name("Oracle", "London", "GB")).publicKey private val DUMMY_NOTARY get() = dummyNotary.party -private val DUMMY_NOTARY_KEY get() = dummyNotary.key +private val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair private val MEGA_CORP get() = megaCorp.party -private val MEGA_CORP_KEY get() = megaCorp.key -private val MEGA_CORP_PUBKEY get() = megaCorp.pubkey +private val MEGA_CORP_KEY get() = megaCorp.keyPair +private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey private val MINI_CORP get() = miniCorp.party -private val MINI_CORP_KEY get() = miniCorp.key +private val MINI_CORP_KEY get() = miniCorp.keyPair fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { return when (irsSelect) { 1 -> { @@ -236,7 +238,7 @@ class IRSTests { private val miniCorpServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY) private val notaryServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) private val ledgerServices - get() = MockServices(rigorousMock().also { + get() = MockServices(emptyList(), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(ORACLE_PUBKEY) }, MEGA_CORP.name) diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 02fc5ca84d..df5bcb95a8 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -44,7 +44,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>()) override fun startMainSimulation(): CompletableFuture { - om = JacksonSupport.createInMemoryMapper(makeTestIdentityService((banks + regulators + ratesOracle).flatMap { it.started!!.info.legalIdentitiesAndCerts })) + om = JacksonSupport.createInMemoryMapper(makeTestIdentityService(*(banks + regulators + ratesOracle).flatMap { it.started!!.info.legalIdentitiesAndCerts }.toTypedArray())) registerFinanceJSONMappers(om) return startIRSDealBetween(0, 1).thenCompose { 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 6da9d13091..424071d5b3 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 @@ -1,7 +1,7 @@ package net.corda.netmap.simulation import net.corda.core.utilities.getOrThrow -import net.corda.testing.LogHelper +import net.corda.testing.internal.LogHelper import org.junit.Test class IRSSimulationTest { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index c4c5bde481..c3337e61e8 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -24,7 +24,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: data class DummyCommand(val dummy: Int = 0) : CommandData - data class State(override val participants: List, private val discriminator: Int) : ContractState + data class State(override val participants: List, val discriminator: Int) : ContractState @Suspendable override fun call(): SignedTransaction { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 9dde805ec6..7c2a65c031 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -18,7 +18,7 @@ import net.corda.finance.contracts.getCashBalance import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.testing.contracts.VaultFiller.Companion.calculateRandomlySizedAmounts +import net.corda.testing.internal.vault.VaultFiller.Companion.calculateRandomlySizedAmounts import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import java.util.* diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt index 7f73c6f306..bf36525538 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt @@ -9,6 +9,7 @@ import net.corda.core.transactions.WireTransaction import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.MockTransactionStorage import org.junit.Rule @@ -46,13 +47,13 @@ class TransactionGraphSearchTests { val notaryServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), dummyNotary) val originBuilder = TransactionBuilder(dummyNotary.party) .addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID) - .addCommand(command, megaCorp.pubkey) + .addCommand(command, megaCorp.publicKey) val originPtx = megaCorpServices.signInitialTransaction(originBuilder) val originTx = notaryServices.addSignature(originPtx) val inputBuilder = TransactionBuilder(dummyNotary.party) .addInputState(originTx.tx.outRef(0)) - .addCommand(dummyCommand(megaCorp.pubkey)) + .addCommand(dummyCommand(megaCorp.publicKey)) val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder) val inputTx = megaCorpServices.addSignature(inputPtx) diff --git a/settings.gradle b/settings.gradle index e16e3ca0aa..6cbb77c684 100644 --- a/settings.gradle +++ b/settings.gradle @@ -37,6 +37,7 @@ include 'tools:explorer:capsule' include 'tools:demobench' include 'tools:loadtest' include 'tools:graphs' +include 'tools:bootstrapper' include 'example-code' project(':example-code').projectDir = file("$settingsDir/docs/source/example-code") include 'samples:attachment-demo' diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt index 8c3636ffc1..4212902a73 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt @@ -288,11 +288,13 @@ class FlowStackSnapshotTest { @Test fun `flowStackSnapshot object is serializable`() { - val mockNet = MockNetwork(threadPerNode = true) + val mockNet = MockNetwork(emptyList(), threadPerNode = true) val node = mockNet.createPartyNode() node.registerInitiatedFlow(DummyFlow::class.java) node.services.startFlow(FlowStackSnapshotSerializationTestingFlow()).resultFuture.get() val thrown = try { + // Due to the [MockNetwork] implementation, the easiest way to trigger object serialization process is at + // the network stopping stage. mockNet.stopNodes() null } catch (exception: Exception) { 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 6135760dbb..cf52920202 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 @@ -45,8 +45,8 @@ import kotlin.concurrent.thread * a service is addressed. */ @ThreadSafe -class InMemoryMessagingNetwork( - val sendManuallyPumped: Boolean, +class InMemoryMessagingNetwork internal constructor( + private val sendManuallyPumped: Boolean, private val servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val messagesInFlight: ReusableLatch = ReusableLatch() ) : SingletonSerializeAsToken() { @@ -94,7 +94,7 @@ class InMemoryMessagingNetwork( /** * Creates a node at the given address: useful if you want to recreate a node to simulate a restart. * - * @param manuallyPumped if set to true, then you are expected to call the [InMemoryMessaging.pump] method on the [InMemoryMessaging] + * @param manuallyPumped if set to true, then you are expected to call [InMemoryMessaging.pumpReceive] * in order to cause the delivery of a single message, which will occur on the thread of the caller. If set to false * then this class will set up a background thread to deliver messages asynchronously, if the handler specifies no * executor. @@ -153,9 +153,6 @@ class InMemoryMessagingNetwork( } } - - val everyoneOnline: AllPossibleRecipients = object : AllPossibleRecipients {} - fun stop() { val nodes = synchronized(this) { counter = -1 @@ -226,7 +223,7 @@ class InMemoryMessagingNetwork( return transfer } - fun pumpSendInternal(transfer: MessageTransfer) { + private fun pumpSendInternal(transfer: MessageTransfer) { when (transfer.recipients) { is PeerHandle -> getQueueForPeerHandle(transfer.recipients).add(transfer) is ServiceHandle -> { @@ -271,7 +268,8 @@ class InMemoryMessagingNetwork( private val peerHandle: PeerHandle, private val executor: AffinityExecutor, private val database: CordaPersistence) : SingletonSerializeAsToken(), MessagingService { - inner class Handler(val topicSession: String, val callback: MessageHandler) : MessageHandlerRegistration + privateinner class Handler(val topicSession: String, + val callback: MessageHandler) : MessageHandlerRegistration @Volatile private var running = true 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 deleted file mode 100644 index 654b99bb12..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.testing.node - -import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.concurrent.doneFuture -import net.corda.core.node.NodeInfo -import net.corda.core.node.services.NetworkMapCache -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.PersistentNetworkMapCache -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.testing.getTestPartyAndCertificate -import rx.Observable -import rx.subjects.PublishSubject -import java.math.BigInteger - -/** - * Network map cache with no backing map service. - */ -class MockNetworkMapCache(database: CordaPersistence) : PersistentNetworkMapCache(database, emptyList()) { - 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) - val BANK_C_ADDR = NetworkHostAndPort("bankC", 8080) - val BANK_D_ADDR = NetworkHostAndPort("bankD", 8080) - } - - override val changed: Observable = PublishSubject.create() - override val nodeReady: CordaFuture get() = doneFuture(null) - - init { - val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), listOf(BANK_C), 1, serial = 1L) - val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), listOf(BANK_D), 1, serial = 1L) - addNode(mockNodeA) - addNode(mockNodeB) - } -} - 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 dfda6b82fe..482ea0fa47 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 @@ -22,14 +22,14 @@ import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.seconds +import net.corda.node.VersionInfo 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.SchemaService -import net.corda.node.services.config.BFTSMaRtConfiguration -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.NotaryConfig import net.corda.node.services.api.IdentityServiceInternal +import net.corda.node.services.api.SchemaService +import net.corda.node.services.config.* import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.transactions.BFTNonValidatingNotaryService @@ -37,12 +37,15 @@ import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.ServiceIdentityGenerator -import net.corda.nodeapi.internal.NotaryInfo -import net.corda.nodeapi.internal.NetworkParametersCopier -import net.corda.testing.common.internal.testNetworkParameters +import net.corda.nodeapi.internal.config.User +import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.nodeapi.internal.network.NotaryInfo +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -71,13 +74,13 @@ data class MockNetworkParameters( val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), val defaultFactory: (MockNodeArgs) -> MockNetwork.MockNode = MockNetwork::MockNode, val initialiseSerialization: Boolean = true, - val cordappPackages: List = emptyList()) { + val notarySpecs: List = listOf(MockNetwork.NotarySpec(DUMMY_NOTARY_NAME))) { 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: (MockNodeArgs) -> MockNetwork.MockNode) = copy(defaultFactory = defaultFactory) fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) - fun setCordappPackages(cordappPackages: List) = copy(cordappPackages = cordappPackages) + fun setNotarySpecs(notarySpecs: List) = copy(notarySpecs = notarySpecs) } /** @@ -90,7 +93,8 @@ data class MockNodeParameters( val forcedID: Int? = null, val legalName: CordaX500Name? = null, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - val configOverrides: (NodeConfiguration) -> Any? = {}) { + val configOverrides: (NodeConfiguration) -> Any? = {}, + val version: VersionInfo = MOCK_VERSION_INFO) { fun setForcedID(forcedID: Int?) = copy(forcedID = forcedID) fun setLegalName(legalName: CordaX500Name?) = copy(legalName = legalName) fun setEntropyRoot(entropyRoot: BigInteger) = copy(entropyRoot = entropyRoot) @@ -101,7 +105,8 @@ data class MockNodeArgs( val config: NodeConfiguration, val network: MockNetwork, val id: Int, - val entropyRoot: BigInteger + val entropyRoot: BigInteger, + val version: VersionInfo = MOCK_VERSION_INFO ) /** @@ -120,16 +125,17 @@ data class MockNodeArgs( * By default a single notary node is automatically started, which forms part of the network parameters for all the nodes. * This node is available by calling [defaultNotaryNode]. */ -class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParameters(), +class MockNetwork(private val cordappPackages: List, + defaultParameters: MockNetworkParameters = MockNetworkParameters(), private val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, private val threadPerNode: Boolean = defaultParameters.threadPerNode, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, private val defaultFactory: (MockNodeArgs) -> MockNode = defaultParameters.defaultFactory, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - private val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY_NAME)), - private val cordappPackages: List = defaultParameters.cordappPackages) { + private val notarySpecs: List = defaultParameters.notarySpecs) { /** Helper constructor for creating a [MockNetwork] with custom parameters from Java. */ - constructor(parameters: MockNetworkParameters) : this(defaultParameters = parameters) + @JvmOverloads + constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) init { // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. @@ -147,9 +153,13 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete private val networkId = random63BitValue() private val networkParameters: NetworkParametersCopier private val _nodes = mutableListOf() - private val serializationEnv = setGlobalSerialization(initialiseSerialization) + private val serializationEnv = try { + setGlobalSerialization(initialiseSerialization) + } catch (e: IllegalStateException) { + throw IllegalStateException("Using more than one MockNetwork simultaneously is not supported.", e) + } private val sharedUserCount = AtomicInteger(0) - /** A read only view of the current set of executing nodes. */ + /** A read only view of the current set of nodes. */ val nodes: List get() = _nodes /** @@ -220,12 +230,12 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete } private fun generateNotaryIdentities(): List { - return notarySpecs.mapIndexed { index, spec -> + return notarySpecs.mapIndexed { index, (name, validating) -> val identity = ServiceIdentityGenerator.generateToDisk( dirs = listOf(baseDirectory(nextNodeId + index)), - serviceName = spec.name, + serviceName = name, serviceId = "identity") - NotaryInfo(identity, spec.validating) + NotaryInfo(identity, validating) } } @@ -240,7 +250,7 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete open class MockNode(args: MockNodeArgs) : AbstractNode( args.config, TestClock(Clock.systemUTC()), - MOCK_VERSION_INFO, + args.version, CordappLoader.createDefaultWithTestPackages(args.config, args.network.cordappPackages), args.network.busyLatch ) { @@ -385,14 +395,14 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete private fun createNodeImpl(parameters: MockNodeParameters, nodeFactory: (MockNodeArgs) -> N, start: Boolean): N { val id = parameters.forcedID ?: nextNodeId++ - val config = testNodeConfiguration( - baseDirectory = baseDirectory(id).createDirectories(), - myLegalName = parameters.legalName ?: CordaX500Name(organisation = "Mock Company $id", locality = "London", country = "GB")).also { + val config = mockNodeConfiguration().also { + doReturn(baseDirectory(id).createDirectories()).whenever(it).baseDirectory + doReturn(parameters.legalName ?: CordaX500Name("Mock Company $id", "London", "GB")).whenever(it).myLegalName doReturn(makeTestDataSourceProperties("node_$id", "net_$networkId")).whenever(it).dataSourceProperties doReturn(makeTestDatabaseProperties("node_$id")).whenever(it).database parameters.configOverrides(it) } - val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot)) + val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version)) _nodes += node if (start) { node.start() @@ -474,3 +484,24 @@ open class MessagingServiceSpy(val messagingService: MessagingService) : Messagi fun StartedNode.setMessagingServiceSpy(messagingServiceSpy: MessagingServiceSpy) { internals.setMessagingServiceSpy(messagingServiceSpy) } + +private fun mockNodeConfiguration(): NodeConfiguration { + abstract class AbstractNodeConfiguration : NodeConfiguration + return rigorousMock().also { + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn("trustpass").whenever(it).trustStorePassword + doReturn(emptyList()).whenever(it).rpcUsers + doReturn(null).whenever(it).notary + doReturn(DatabaseConfig()).whenever(it).database + doReturn("").whenever(it).emailAddress + doReturn("").whenever(it).exportJMXto + doReturn(true).whenever(it).devMode + doReturn(null).whenever(it).compatibilityZoneURL + doReturn(emptyList()).whenever(it).certificateChainCheckPolicies + doReturn(VerifierType.InMemory).whenever(it).verifierType + doReturn(5).whenever(it).messageRedeliveryDelaySeconds + doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec + doReturn(null).whenever(it).devModeOptions + doReturn(true).whenever(it).useAMQPBridges + } +} 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 79cc9d361a..b4c6be5fb9 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 @@ -59,7 +59,7 @@ import java.sql.Connection import java.time.Clock import java.util.* -fun makeTestIdentityService(identities: Iterable = emptySet()) = InMemoryIdentityService(identities, DEV_TRUST_ROOT) +fun makeTestIdentityService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities, DEV_TRUST_ROOT) /** * A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for * building chains of transactions and verifying them. It isn't sufficient for testing flows however. @@ -68,8 +68,8 @@ open class MockServices private constructor( cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, override val identityService: IdentityServiceInternal, - private val initialIdentityName: CordaX500Name, - val keys: Array + private val initialIdentity: TestIdentity, + private val moreKeys: Array ) : ServiceHub, StateLoader by validatedTransactions { companion object { @JvmStatic @@ -144,22 +144,22 @@ open class MockServices private constructor( /** * Makes database and mock services appropriate for unit tests. - * @param keys a list of [KeyPair] instances to be used by [MockServices]. + * @param moreKeys a list of additional [KeyPair] instances to be used by [MockServices]. * @param identityService an instance of [IdentityServiceInternal], see [makeTestIdentityService]. - * @param initialIdentityName the name of the first (typically sole) identity the services will represent. + * @param initialIdentity the first (typically sole) identity the services will represent. * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic - fun makeTestDatabaseAndMockServices(keys: List, + fun makeTestDatabaseAndMockServices(cordappPackages: List, identityService: IdentityServiceInternal, - cordappPackages: List = emptyList(), - initialIdentityName: CordaX500Name): Pair { + initialIdentity: TestIdentity, + vararg moreKeys: KeyPair): Pair { val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties(initialIdentityName.organisation) val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(initialIdentityName.organisation), identityService, schemaService) val mockService = database.transaction { - object : MockServices(cordappLoader, identityService, initialIdentityName, keys.toTypedArray()) { + object : MockServices(cordappLoader, identityService, initialIdentity, moreKeys) { override val vaultService: VaultServiceInternal = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { @@ -175,13 +175,18 @@ open class MockServices private constructor( } } - private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name, keys: Array) : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentityName, keys) - constructor(cordappPackages: List, identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name, vararg keys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentityName, keys) - constructor(cordappPackages: List, identityService: IdentityServiceInternal, initialIdentity: TestIdentity, vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity.name, arrayOf(initialIdentity.key) + moreKeys) - constructor(identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name, vararg keys: KeyPair) : this(emptyList(), identityService, initialIdentityName, *keys) - constructor(identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name) : this(identityService, initialIdentityName, generateKeyPair()) + private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal, initialIdentity: TestIdentity, moreKeys: Array) : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys) + @JvmOverloads + constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentity: TestIdentity, vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys) - val key: KeyPair get() = keys.first() + @JvmOverloads + constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity(initialIdentityName, key), *moreKeys) + + @JvmOverloads + constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name) : this(cordappPackages, identityService, TestIdentity(initialIdentityName)) + + @JvmOverloads + constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity.fresh("MockServices"), *moreKeys) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { txs.forEach { @@ -190,16 +195,14 @@ open class MockServices private constructor( } final override val attachments = MockAttachmentStorage() - override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) } - + override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *arrayOf(initialIdentity.keyPair) + moreKeys) } override val vaultService: VaultService get() = throw UnsupportedOperationException() override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() { - val identity = getTestPartyAndCertificate(initialIdentityName, key.public) - return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) + return NodeInfo(emptyList(), listOf(initialIdentity.identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) val mockCordappProvider = MockCordappProvider(cordappLoader, attachments) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt index fcb923affd..b4dd36315a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt @@ -2,8 +2,6 @@ package net.corda.testing.node -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.whenever import net.corda.core.context.Actor import net.corda.core.context.AuthServiceId import net.corda.core.context.InvocationContext @@ -13,26 +11,34 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub +import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds import net.corda.node.services.api.StartedNodeServices -import net.corda.node.services.config.* -import net.corda.nodeapi.internal.config.User import net.corda.testing.* -import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.makeTestIdentityService -import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties -import java.nio.file.Path +import net.corda.testing.dsl.* /** * Creates and tests a ledger built by the passed in dsl. */ +@JvmOverloads fun ServiceHub.ledger( - notary: Party, - dsl: LedgerDSL.() -> Unit + notary: Party = TestIdentity.fresh("ledger notary").party, + script: LedgerDSL.() -> Unit ): LedgerDSL { - return LedgerDSL(TestLedgerDSLInterpreter(this), notary).apply(dsl) + val serializationExists = try { + effectiveSerializationEnv + true + } catch (e: IllegalStateException) { + false + } + return LedgerDSL(TestLedgerDSLInterpreter(this), notary).apply { + if (serializationExists) { + script() + } else { + SerializationEnvironmentRule.run("ledger") { script() } + } + } } /** @@ -40,37 +46,12 @@ fun ServiceHub.ledger( * * @see LedgerDSLInterpreter._transaction */ +@JvmOverloads fun ServiceHub.transaction( - notary: Party, - dsl: TransactionDSL.() -> EnforceVerifyOrFail + notary: Party = TestIdentity.fresh("transaction notary").party, + script: TransactionDSL.() -> EnforceVerifyOrFail ) = ledger(notary) { - dsl(TransactionDSL(TestTransactionDSLInterpreter(interpreter, TransactionBuilder(notary)), notary)) -} - -fun testNodeConfiguration( - baseDirectory: Path, - myLegalName: CordaX500Name): NodeConfiguration { - abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. - return rigorousMock().also { - doReturn(baseDirectory).whenever(it).baseDirectory - doReturn(myLegalName).whenever(it).myLegalName - doReturn("cordacadevpass").whenever(it).keyStorePassword - doReturn("trustpass").whenever(it).trustStorePassword - doReturn(emptyList()).whenever(it).rpcUsers - doReturn(null).whenever(it).notary - doReturn(makeTestDataSourceProperties(myLegalName.organisation)).whenever(it).dataSourceProperties - doReturn(makeTestDatabaseProperties(myLegalName.organisation)).whenever(it).database - doReturn("").whenever(it).emailAddress - doReturn("").whenever(it).exportJMXto - doReturn(true).whenever(it).devMode - doReturn(null).whenever(it).compatibilityZoneURL - doReturn(emptyList()).whenever(it).certificateChainCheckPolicies - doReturn(VerifierType.InMemory).whenever(it).verifierType - doReturn(5).whenever(it).messageRedeliveryDelaySeconds - doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec - doReturn(null).whenever(it).devModeOptions - doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration //Enterprise only - } + TransactionDSL(TestTransactionDSLInterpreter(interpreter, TransactionBuilder(notary)), notary).script() } fun testActor(owningLegalIdentity: CordaX500Name = CordaX500Name("Test Company Inc.", "London", "GB")) = Actor(Actor.Id("Only For Testing"), AuthServiceId("TEST"), owningLegalIdentity) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 9012446102..308ac4fe5a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -10,6 +10,7 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf +import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.* @@ -34,7 +35,8 @@ import net.corda.node.services.transactions.RaftNonValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper -import net.corda.nodeapi.internal.* +import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig @@ -42,8 +44,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import net.corda.nodeapi.internal.crypto.save +import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.nodeapi.internal.network.NodeInfoFilesCopier +import net.corda.nodeapi.internal.network.NotaryInfo +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.DUMMY_BANK_A_NAME import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.* import net.corda.testing.driver.* import net.corda.testing.node.ClusterSpec import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO @@ -244,6 +251,7 @@ class DriverDSLImpl( } internal fun startCordformNodes(cordforms: List): CordaFuture<*> { + check(compatibilityZone == null) { "Cordform nodes should be run without compatibilityZone configuration" } val clusterNodes = HashMultimap.create() val notaryInfos = ArrayList() @@ -353,7 +361,7 @@ class DriverDSLImpl( } val notaryInfos = generateNotaryIdentities() // The network parameters must be serialised before starting any of the nodes - networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + if (compatibilityZone == null) networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) val nodeHandles = startNotaries() _notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) } } @@ -514,7 +522,7 @@ class DriverDSLImpl( val configuration = config.parseAsNodeConfiguration() val baseDirectory = configuration.baseDirectory.createDirectories() nodeInfoFilesCopier?.addConfig(baseDirectory) - networkParameters!!.install(baseDirectory) + networkParameters?.install(baseDirectory) val onNodeExit: () -> Unit = { nodeInfoFilesCopier?.removeConfig(baseDirectory) countObservables.remove(configuration.myLegalName) @@ -889,7 +897,8 @@ fun internalDriver( } fun getTimestampAsDirectoryName(): String { - return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) + // Add a random number in case 2 tests are started in the same instant. + return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) + random63BitValue() } fun writeConfig(path: Path, filename: String, config: Config) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index ba69cbdc3f..4464724829 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -12,10 +12,10 @@ 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.nodeapi.internal.NetworkParametersCopier import net.corda.nodeapi.internal.config.User import net.corda.testing.IntegrationTest import net.corda.testing.SerializationEnvironmentRule +import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.getFreeLocalPorts import net.corda.testing.internal.testThreadFactory diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index f851d7fb8d..a5dff915ca 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -17,7 +17,6 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.security.RPCSecurityManagerImpl -import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.RPCServer import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.ArtemisTcpTransport @@ -25,6 +24,7 @@ import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT +import net.corda.testing.MAX_MESSAGE_SIZE import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.PortAllocation import net.corda.testing.node.NotarySpec @@ -46,7 +46,7 @@ import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings -import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection +import org.apache.activemq.artemis.spi.core.remoting.Connection import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager3 import java.lang.reflect.Method import java.nio.file.Path @@ -133,11 +133,11 @@ fun rpcDriver( private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityManager3 { override fun validateUser(user: String?, password: String?) = isValid(user, password) override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?) = isValid(user, password) - override fun validateUser(user: String?, password: String?, remotingConnection: RemotingConnection?): String? { + override fun validateUser(user: String?, password: String?, connection: Connection?): String? { return validate(user, password) } - override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?, address: String?, connection: RemotingConnection?): String? { + override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?, address: String?, connection: Connection?): String? { return validate(user, password) } @@ -226,8 +226,8 @@ data class RPCDriverDSL( fun startInVmRpcServer( rpcUser: User = rpcTestUser, nodeLegalName: CordaX500Name = fakeNodeLegalName, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + maxFileSize: Int = MAX_MESSAGE_SIZE, + maxBufferedBytesPerClient: Long = 10L * MAX_MESSAGE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, ops: I ): CordaFuture { @@ -294,8 +294,8 @@ data class RPCDriverDSL( serverName: String = "driver-rpc-server-${random63BitValue()}", rpcUser: User = rpcTestUser, nodeLegalName: CordaX500Name = fakeNodeLegalName, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + maxFileSize: Int = MAX_MESSAGE_SIZE, + maxBufferedBytesPerClient: Long = 10L * MAX_MESSAGE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, customPort: NetworkHostAndPort? = null, ops: I @@ -377,8 +377,8 @@ data class RPCDriverDSL( fun startRpcBroker( serverName: String = "driver-rpc-server-${random63BitValue()}", rpcUser: User = rpcTestUser, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + maxFileSize: Int = MAX_MESSAGE_SIZE, + maxBufferedBytesPerClient: Long = 10L * MAX_MESSAGE_SIZE, customPort: NetworkHostAndPort? = null ): CordaFuture { val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort() @@ -401,8 +401,8 @@ data class RPCDriverDSL( fun startInVmRpcBroker( rpcUser: User = rpcTestUser, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE + maxFileSize: Int = MAX_MESSAGE_SIZE, + maxBufferedBytesPerClient: Long = 10L * MAX_MESSAGE_SIZE ): CordaFuture { return driverDSL.executorService.fork { val artemisConfig = createInVmRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient) @@ -430,7 +430,7 @@ data class RPCDriverDSL( brokerHandle: RpcBrokerHandle ): RpcServerHandle { val locator = ActiveMQClient.createServerLocatorWithoutHA(brokerHandle.clientTransportConfiguration).apply { - minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE + minLargeMessageSize = MAX_MESSAGE_SIZE isUseGlobalPools = false } val rpcSecurityManager = RPCSecurityManagerImpl.fromUserList(users = listOf(rpcUser), id = AuthServiceId("TEST_SECURITY_MANAGER")) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt similarity index 83% rename from testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt index 4e6eb0a83e..cf7cb48fa0 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt @@ -1,20 +1,17 @@ -package net.corda.testing.node.network +package net.corda.testing.node.internal.network -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignedData -import net.corda.core.crypto.sha256 +import net.corda.core.crypto.* import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.hours -import net.corda.nodeapi.internal.DigitalSignatureWithCert -import net.corda.nodeapi.internal.NetworkMap -import net.corda.nodeapi.internal.NetworkParameters -import net.corda.nodeapi.internal.SignedNetworkMap +import net.corda.nodeapi.internal.network.DigitalSignatureWithCert +import net.corda.nodeapi.internal.network.NetworkMap +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.network.SignedNetworkMap +import net.corda.nodeapi.internal.* import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities @@ -39,9 +36,11 @@ import javax.ws.rs.core.Response.ok class NetworkMapServer(cacheTimeout: Duration, hostAndPort: NetworkHostAndPort, + root_ca: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing. vararg additionalServices: Any) : Closeable { companion object { - val stubNetworkParameter = NetworkParameters(1, emptyList(), 1.hours, 10, 10, Instant.now(), 10) + val stubNetworkParameter = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10) + private val serializedParameters = stubNetworkParameter.serialize() private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair { val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -56,9 +55,7 @@ class NetworkMapServer(cacheTimeout: Duration, } private val server: Server - // Default to ROOT_CA for testing. - // TODO: make this configurable? - private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(ROOT_CA)) + private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(root_ca)) init { server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { @@ -99,13 +96,18 @@ class NetworkMapServer(cacheTimeout: Duration, @Path("network-map") class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) { - private val nodeInfoMap = mutableMapOf>() + private val nodeInfoMap = mutableMapOf() + private val parametersHash = serializedParameters.hash + private val signedParameters = SignedData( + serializedParameters, + DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes)) + ) @POST @Path("publish") @Consumes(MediaType.APPLICATION_OCTET_STREAM) fun publishNodeInfo(input: InputStream): Response { - val registrationData = input.readBytes().deserialize>() + val registrationData = input.readBytes().deserialize() val nodeInfo = registrationData.verified() val nodeInfoHash = nodeInfo.serialize().sha256() nodeInfoMap.put(nodeInfoHash, registrationData) @@ -115,7 +117,7 @@ class NetworkMapServer(cacheTimeout: Duration, @GET @Produces(MediaType.APPLICATION_OCTET_STREAM) fun getNetworkMap(): Response { - val networkMap = NetworkMap(nodeInfoMap.keys.map { it }, SecureHash.randomSHA256()) + val networkMap = NetworkMap(nodeInfoMap.keys.toList(), parametersHash) val serializedNetworkMap = networkMap.serialize() val signature = Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedNetworkMap.bytes) val signedNetworkMap = SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(networkMapKeyAndCert.certificate.cert, signature)) @@ -143,7 +145,7 @@ class NetworkMapServer(cacheTimeout: Duration, @Path("network-parameter/{var}") @Produces(MediaType.APPLICATION_OCTET_STREAM) fun getNetworkParameter(@PathParam("var") networkParameterHash: String): Response { - return Response.ok(stubNetworkParameter.serialize().bytes).build() + return Response.ok(signedParameters.serialize().bytes).build() } @GET diff --git a/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java b/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java index 4e6b9215ca..b6b336ec7b 100644 --- a/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java +++ b/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java @@ -2,6 +2,8 @@ package net.corda.testing.node; import org.jetbrains.annotations.NotNull; +import static java.util.Collections.emptyList; + @SuppressWarnings("unused") public class MockNodeFactoryInJavaTest { private static class CustomNode extends MockNetwork.MockNode { @@ -16,10 +18,10 @@ public class MockNodeFactoryInJavaTest { @SuppressWarnings("unused") private static void factoryIsEasyToPassInUsingJava() { //noinspection Convert2MethodRef - new MockNetwork(new MockNetworkParameters().setDefaultFactory(args -> new CustomNode(args))); - new MockNetwork(new MockNetworkParameters().setDefaultFactory(CustomNode::new)); + new MockNetwork(emptyList(), new MockNetworkParameters().setDefaultFactory(args -> new CustomNode(args))); + new MockNetwork(emptyList(), new MockNetworkParameters().setDefaultFactory(CustomNode::new)); //noinspection Convert2MethodRef - new MockNetwork().createNode(new MockNodeParameters(), args -> new CustomNode(args)); - new MockNetwork().createNode(new MockNodeParameters(), CustomNode::new); + new MockNetwork(emptyList()).createNode(new MockNodeParameters(), args -> new CustomNode(args)); + new MockNetwork(emptyList()).createNode(new MockNodeParameters(), CustomNode::new); } } diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 2211c68b86..ffe478998d 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -8,7 +8,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger -import net.corda.nodeapi.internal.NetworkParametersCopier +import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.asContextEnv import java.nio.file.Path diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt index ea84cd88c8..3d448d9188 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -1,18 +1,23 @@ package net.corda.testing.common.internal -import net.corda.core.utilities.days -import net.corda.nodeapi.internal.NetworkParameters -import net.corda.nodeapi.internal.NotaryInfo +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.network.NotaryInfo import java.time.Instant -fun testNetworkParameters(notaries: List): NetworkParameters { +fun testNetworkParameters( + notaries: List, + minimumPlatformVersion: Int = 1, + modifiedTime: Instant = Instant.now(), + maxMessageSize: Int = 10485760, + maxTransactionSize: Int = 40000, + epoch: Int = 1 +): NetworkParameters { return NetworkParameters( - minimumPlatformVersion = 1, + minimumPlatformVersion = minimumPlatformVersion, notaries = notaries, - modifiedTime = Instant.now(), - eventHorizon = 10000.days, - maxMessageSize = 40000, - maxTransactionSize = 40000, - epoch = 1 + modifiedTime = modifiedTime, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + epoch = epoch ) } \ No newline at end of file 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 5b58e65086..aa2dc08533 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 @@ -3,11 +3,9 @@ package net.corda.testing +import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.StateRef -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.* import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -15,25 +13,20 @@ import net.corda.core.internal.cert import net.corda.core.internal.x500Name import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.loggerFor import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.cert.X509CertificateHolder -import org.mockito.Mockito.mock -import org.mockito.internal.stubbing.answers.ThrowsException -import java.lang.reflect.Modifier import java.math.BigInteger import java.nio.file.Files +import java.security.KeyPair import java.security.PublicKey -import java.util.* import java.util.concurrent.atomic.AtomicInteger /** @@ -81,7 +74,7 @@ fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + ( */ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } - return (freePort..freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } + return (0 until numberToAlloc).map { NetworkHostAndPort(hostName, freePort + it) } } fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration = object : SSLConfiguration { @@ -93,18 +86,29 @@ fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration = object : SSLC configureDevKeyAndTrustStores(legalName) } } + fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT val intermediate: CertificateAndKeyPair = DEV_CA val nodeCaName = party.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN) - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf()) - val issuerKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) - val issuerCertificate = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediate.certificate, intermediate.keyPair, nodeCaName, issuerKeyPair.public, - nameConstraints = nameConstraints) + val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, + intermediate.certificate, + intermediate.keyPair, + nodeCaName, + nodeCaKeyPair.public, + nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf())) - val certHolder = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCertificate, issuerKeyPair, party.name, party.owningKey) - val pathElements = listOf(certHolder, issuerCertificate, intermediate.certificate, trustRoot) + val identityCert = X509Utilities.createCertificate( + CertificateType.WELL_KNOWN_IDENTITY, + nodeCaCert, + nodeCaKeyPair, + party.name, + party.owningKey) + + val pathElements = listOf(identityCert, nodeCaCert, intermediate.certificate, trustRoot) val certPath = X509CertificateFactory().generateCertPath(pathElements.map(X509CertificateHolder::cert)) return PartyAndCertificate(certPath) } @@ -116,26 +120,29 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party return getTestPartyAndCertificate(Party(name, publicKey)) } -class TestIdentity @JvmOverloads constructor(val name: CordaX500Name, entropy: Long? = null) { - val key = if (entropy != null) entropyToKeyPair(BigInteger.valueOf(entropy)) else generateKeyPair() - val pubkey get() = key.public!! - val party = Party(name, pubkey) - val identity by lazy { getTestPartyAndCertificate(party) } // Often not needed. - fun ref(vararg bytes: Byte) = party.ref(*bytes) -} +class TestIdentity(val name: CordaX500Name, val keyPair: KeyPair) { + companion object { + /** + * Creates an identity that won't equal any other. This is mostly useful as a throwaway for test helpers. + * @param organisation the organisation part of the new identity's name. + */ + fun fresh(organisation: String): TestIdentity { + val keyPair = generateKeyPair() + val name = CordaX500Name(organisation, keyPair.public.toStringShort(), CordaX500Name.unspecifiedCountry) + return TestIdentity(name, keyPair) + } + } -@Suppress("unused") -inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if (!AMQP_ENABLED) { - function() -} else { - loggerFor().info("Ignoring Kryo specific test, reason: $reason") -} + /** Creates an identity with a deterministic [keyPair] i.e. same [entropy] same keyPair .*/ + constructor(name: CordaX500Name, entropy: Long) : this(name, entropyToKeyPair(BigInteger.valueOf(entropy))) -@Suppress("unused") -inline fun T.amqpSpecific(reason: String, function: () -> Unit) = if (AMQP_ENABLED) { - function() -} else { - loggerFor().info("Ignoring AMQP specific test, reason: $reason") + /** Creates an identity with the given name and a fresh keyPair. */ + constructor(name: CordaX500Name) : this(name, generateKeyPair()) + + val publicKey: PublicKey get() = keyPair.public + val party: Party = Party(name, publicKey) + val identity: PartyAndCertificate by lazy { getTestPartyAndCertificate(party) } // Often not needed. + fun ref(vararg bytes: Byte): PartyAndReference = party.ref(*bytes) } /** @@ -154,25 +161,3 @@ fun NodeInfo.singleIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe * Extract a single identity from the node info. Throws an error if the node has multiple identities. */ fun NodeInfo.singleIdentity(): Party = singleIdentityAndCert().party - -/** - * A method on a mock was called, but no behaviour was previously specified for that method. - * You can use [com.nhaarman.mockito_kotlin.doReturn] or similar to specify behaviour, see Mockito documentation for details. - */ -class UndefinedMockBehaviorException(message: String) : RuntimeException(message) - -inline fun rigorousMock() = rigorousMock(T::class.java) -/** - * Create a Mockito mock that has [UndefinedMockBehaviorException] as the default behaviour of all abstract methods, - * and [org.mockito.invocation.InvocationOnMock.callRealMethod] as the default for all concrete methods. - * @param T the type to mock. Note if you want concrete methods of a Kotlin interface to be invoked, - * it won't work unless you mock a (trivial) abstract implementation of that interface instead. - */ -fun rigorousMock(clazz: Class): T = mock(clazz) { - if (Modifier.isAbstract(it.method.modifiers)) { - // Use ThrowsException to hack the stack trace, and lazily so we can customise the message: - ThrowsException(UndefinedMockBehaviorException("Please specify what should happen when '${it.method}' is called, or don't call it. Args: ${Arrays.toString(it.arguments)}")).answer(it) - } else { - it.callRealMethod() - } -} 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 026c76e56d..8aa5a0cc61 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 @@ -9,6 +9,7 @@ import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.testThreadFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnector import org.junit.rules.TestRule @@ -33,19 +34,30 @@ class SerializationEnvironmentRule(private val inheritable: Boolean = false) : T }.whenever(it).execute(any()) } } + + /** Do not call, instead use [SerializationEnvironmentRule] as a [org.junit.Rule]. */ + fun run(taskLabel: String, task: (SerializationEnvironment) -> T): T { + return SerializationEnvironmentRule().apply { init(taskLabel) }.runTask(task) + } } lateinit var env: SerializationEnvironment override fun apply(base: Statement, description: Description): Statement { - env = createTestSerializationEnv(description.toString()) + init(description.toString()) return object : Statement() { - override fun evaluate() { - try { - env.asContextEnv(inheritable) { base.evaluate() } - } finally { - inVMExecutors.remove(env) - } - } + override fun evaluate() = runTask { base.evaluate() } + } + } + + private fun init(envLabel: String) { + env = createTestSerializationEnv(envLabel) + } + + private fun runTask(task: (SerializationEnvironment) -> T): T { + try { + return env.asContextEnv(inheritable, task) + } finally { + inVMExecutors.remove(env) } } } @@ -55,25 +67,6 @@ interface GlobalSerializationEnvironment : SerializationEnvironment { fun unset() } -/** @param inheritable whether new threads inherit the environment, use sparingly. */ -fun withTestSerialization(inheritable: Boolean = false, callable: (SerializationEnvironment) -> T): T { - return createTestSerializationEnv("").asContextEnv(inheritable, callable) -} - -/** - * For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method. - * Use sparingly, ideally a test class shouldn't mix serializers init mechanisms. - */ -fun withoutTestSerialization(callable: () -> T): T { - val (property, env) = listOf(_contextSerializationEnv, _inheritableContextSerializationEnv).map { Pair(it, it.get()) }.single { it.second != null } - property.set(null) - try { - return callable() - } finally { - property.set(env) - } -} - /** * Should only be used by Driver and MockNode. * @param armed true to install, false to do nothing and return a dummy env. @@ -95,22 +88,21 @@ fun setGlobalSerialization(armed: Boolean): GlobalSerializationEnvironment { } } -private fun createTestSerializationEnv(label: String) = object : SerializationEnvironmentImpl( - SerializationFactoryImpl().apply { - registerScheme(KryoClientSerializationScheme()) - registerScheme(KryoServerSerializationScheme()) - registerScheme(AMQPClientSerializationScheme(emptyList())) - registerScheme(AMQPServerSerializationScheme(emptyList())) - }, - AMQP_P2P_CONTEXT, - KRYO_RPC_SERVER_CONTEXT, - KRYO_RPC_CLIENT_CONTEXT, - AMQP_STORAGE_CONTEXT, - KRYO_CHECKPOINT_CONTEXT) { - override fun toString() = "testSerializationEnv($label)" +private fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { + val factory = SerializationFactoryImpl().apply { + registerScheme(KryoClientSerializationScheme()) + registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPClientSerializationScheme(emptyList())) + registerScheme(AMQPServerSerializationScheme(emptyList())) + } + return object : SerializationEnvironmentImpl( + factory, + AMQP_P2P_CONTEXT, + KRYO_RPC_SERVER_CONTEXT, + KRYO_RPC_CLIENT_CONTEXT, + AMQP_STORAGE_CONTEXT, + KRYO_CHECKPOINT_CONTEXT + ) { + override fun toString() = "testSerializationEnv($label)" + } } - -private const val AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable" - -// TODO: Remove usages of this function when we fully switched to AMQP -private fun isAmqpEnabled(): Boolean = java.lang.Boolean.getBoolean(AMQP_ENABLE_PROP_NAME) 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 fc4ee20cd4..27c52eb1bc 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 @@ -13,14 +13,23 @@ import java.security.PublicKey import java.time.Instant // A dummy time at which we will be pretending test transactions are created. -val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z") +@JvmField +val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") +@JvmField val DUMMY_NOTARY_NAME = CordaX500Name("Notary Service", "Zurich", "CH") +@JvmField val DUMMY_BANK_A_NAME = CordaX500Name("Bank A", "London", "GB") +@JvmField val DUMMY_BANK_B_NAME = CordaX500Name("Bank B", "New York", "US") +@JvmField val DUMMY_BANK_C_NAME = CordaX500Name("Bank C", "Tokyo", "JP") +@JvmField val BOC_NAME = CordaX500Name("BankOfCorda", "London", "GB") +@JvmField val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES") +@JvmField val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT") +@JvmField val CHARLIE_NAME = CordaX500Name("Charlie Ltd", "Athens", "GR") val DEV_CA: CertificateAndKeyPair by lazy { // TODO: Should be identity scheme @@ -42,3 +51,6 @@ val DEV_TRUST_ROOT: X509CertificateHolder by lazy { fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command(DummyCommandData, signers.toList()) object DummyCommandData : TypeOnlyCommandData() + +/** Maximum artemis message size. 10 MiB maximum allowed file size for attachments, including message headers. */ +const val MAX_MESSAGE_SIZE: Int = 10485760 diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt deleted file mode 100644 index 27a16a64f9..0000000000 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.testing - -import org.junit.Rule - -@Deprecated("Instead of extending this class, use SerializationEnvironmentRule in the same way.") -abstract class TestDependencyInjectionBase { - @Rule - @JvmField - val testSerialization = SerializationEnvironmentRule() -} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt deleted file mode 100644 index f9cef533b0..0000000000 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.corda.testing - -import net.corda.testing.TestTimestamp.Companion.timestamp -import java.text.SimpleDateFormat -import java.util.* - -/** - * [timestamp] holds a formatted (UTC) timestamp that's set the first time it is queried. This is used to - * provide a uniform timestamp for tests. - */ -class TestTimestamp { - companion object { - val timestamp: String = { - val tz = TimeZone.getTimeZone("UTC") - val df = SimpleDateFormat("yyyyMMddHHmmss") - df.timeZone = tz - df.format(Date()) - }() - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/LedgerDSLInterpreter.kt similarity index 99% rename from testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/dsl/LedgerDSLInterpreter.kt index 47d3f68b62..869643d2aa 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/LedgerDSLInterpreter.kt @@ -1,4 +1,4 @@ -package net.corda.testing +package net.corda.testing.dsl import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt similarity index 99% rename from testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 1fa551c6a1..bf48a07240 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -1,4 +1,4 @@ -package net.corda.testing +package net.corda.testing.dsl import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider @@ -12,9 +12,9 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.testing.contracts.DummyContract import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockCordappProvider +import net.corda.testing.dummyCommand import java.io.InputStream import java.security.PublicKey import java.util.* diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt similarity index 99% rename from testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt index 0412ddf2f7..d79260592f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt @@ -1,4 +1,4 @@ -package net.corda.testing +package net.corda.testing.dsl import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint import net.corda.core.contracts.Attachment diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt new file mode 100644 index 0000000000..659c889087 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -0,0 +1,19 @@ +package net.corda.testing.internal + +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.core.serialization.internal._inheritableContextSerializationEnv +import net.corda.testing.SerializationEnvironmentRule + +/** + * For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method. + * Use sparingly, ideally a test class shouldn't mix serializers init mechanisms. + */ +fun withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, see CORDA-858. + val (property, env) = listOf(_contextSerializationEnv, _inheritableContextSerializationEnv).map { Pair(it, it.get()) }.single { it.second != null } + property.set(null) + try { + return callable() + } finally { + property.set(env) + } +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt new file mode 100644 index 0000000000..fc65d3603c --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -0,0 +1,44 @@ +package net.corda.testing.internal + +import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED +import org.mockito.Mockito +import org.mockito.internal.stubbing.answers.ThrowsException +import java.lang.reflect.Modifier +import java.util.* + +@Suppress("unused") +inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if (!AMQP_ENABLED) { + function() +} else { + loggerFor().info("Ignoring Kryo specific test, reason: $reason") +} + +@Suppress("unused") +inline fun T.amqpSpecific(reason: String, function: () -> Unit) = if (AMQP_ENABLED) { + function() +} else { + loggerFor().info("Ignoring AMQP specific test, reason: $reason") +} + +/** + * A method on a mock was called, but no behaviour was previously specified for that method. + * You can use [com.nhaarman.mockito_kotlin.doReturn] or similar to specify behaviour, see Mockito documentation for details. + */ +class UndefinedMockBehaviorException(message: String) : RuntimeException(message) + +inline fun rigorousMock() = rigorousMock(T::class.java) +/** + * Create a Mockito mock that has [UndefinedMockBehaviorException] as the default behaviour of all abstract methods, + * and [org.mockito.invocation.InvocationOnMock.callRealMethod] as the default for all concrete methods. + * @param T the type to mock. Note if you want concrete methods of a Kotlin interface to be invoked, + * it won't work unless you mock a (trivial) abstract implementation of that interface instead. + */ +fun rigorousMock(clazz: Class): T = Mockito.mock(clazz) { + if (Modifier.isAbstract(it.method.modifiers)) { + // Use ThrowsException to hack the stack trace, and lazily so we can customise the message: + ThrowsException(UndefinedMockBehaviorException("Please specify what should happen when '${it.method}' is called, or don't call it. Args: ${Arrays.toString(it.arguments)}")).answer(it) + } else { + it.callRealMethod() + } +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/LogHelper.kt similarity index 98% rename from testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/LogHelper.kt index 1fab8508fc..2a8a575bdd 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/LogHelper.kt @@ -1,4 +1,4 @@ -package net.corda.testing +package net.corda.testing.internal import net.corda.core.internal.packageName import org.apache.logging.log4j.Level diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt new file mode 100644 index 0000000000..a367cff7e6 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -0,0 +1,55 @@ +package net.corda.testing.internal + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.sign +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.testing.getTestPartyAndCertificate +import java.security.PrivateKey + +class TestNodeInfoBuilder { + private val identitiesAndPrivateKeys = ArrayList>() + + fun addIdentity(name: CordaX500Name): Pair { + val identityKeyPair = Crypto.generateKeyPair() + val identity = getTestPartyAndCertificate(name, identityKeyPair.public) + return Pair(identity, identityKeyPair.private).also { + identitiesAndPrivateKeys += it + } + } + + fun build(serial: Long = 1): NodeInfo { + return NodeInfo( + listOf(NetworkHostAndPort("my.${identitiesAndPrivateKeys[0].first.party.name.organisation}.com", 1234)), + identitiesAndPrivateKeys.map { it.first }, + 1, + serial + ) + } + + fun buildWithSigned(serial: Long = 1): Pair { + val nodeInfo = build(serial) + val privateKeys = identitiesAndPrivateKeys.map { it.second } + return Pair(nodeInfo, nodeInfo.signWith(privateKeys)) + } + + fun reset() { + identitiesAndPrivateKeys.clear() + } +} + +fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1): Pair { + val nodeInfoBuilder = TestNodeInfoBuilder() + names.forEach { nodeInfoBuilder.addIdentity(it) } + return nodeInfoBuilder.buildWithSigned(serial) +} + +fun NodeInfo.signWith(keys: List): SignedNodeInfo { + val serialized = serialize() + val signatures = keys.map { it.sign(serialized.bytes) } + return SignedNodeInfo(serialized, signatures) +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyDealContract.kt similarity index 90% rename from testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyDealContract.kt index b19bfac076..453dac658c 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyDealContract.kt @@ -1,4 +1,4 @@ -package net.corda.testing.contracts +package net.corda.testing.internal.vault import net.corda.core.contracts.Contract import net.corda.core.contracts.UniqueIdentifier @@ -10,9 +10,8 @@ import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.finance.contracts.DealState -import net.corda.testing.schemas.DummyDealStateSchemaV1 -val DUMMY_DEAL_PROGRAM_ID = "net.corda.testing.contracts.DummyDealContract" +val DUMMY_DEAL_PROGRAM_ID = "net.corda.testing.internal.vault.DummyDealContract" class DummyDealContract : Contract { override fun verify(tx: LedgerTransaction) {} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyDealStateSchemaV1.kt similarity index 96% rename from testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyDealStateSchemaV1.kt index c1e73a5f71..46fcb35539 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyDealStateSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.testing.schemas +package net.corda.testing.internal.vault import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearContract.kt similarity index 94% rename from testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearContract.kt index 17737f4caa..0a2a962084 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearContract.kt @@ -1,4 +1,4 @@ -package net.corda.testing.contracts +package net.corda.testing.internal.vault import net.corda.core.contracts.Contract import net.corda.core.contracts.LinearState @@ -10,12 +10,10 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction -import net.corda.testing.schemas.DummyLinearStateSchemaV1 -import net.corda.testing.schemas.DummyLinearStateSchemaV2 import java.time.LocalDateTime import java.time.ZoneOffset.UTC -const val DUMMY_LINEAR_CONTRACT_PROGRAM_ID = "net.corda.testing.contracts.DummyLinearContract" +const val DUMMY_LINEAR_CONTRACT_PROGRAM_ID = "net.corda.testing.internal.vault.DummyLinearContract" class DummyLinearContract : Contract { override fun verify(tx: LedgerTransaction) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearStateSchemaV1.kt similarity index 98% rename from testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearStateSchemaV1.kt index 844ea0543b..666a397d20 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearStateSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.testing.schemas +package net.corda.testing.internal.vault import net.corda.core.contracts.ContractState import net.corda.core.identity.AbstractParty diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearStateSchemaV2.kt similarity index 96% rename from testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearStateSchemaV2.kt index 0b9f942678..99d37133be 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/DummyLinearStateSchemaV2.kt @@ -1,4 +1,4 @@ -package net.corda.testing.schemas +package net.corda.testing.internal.vault import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt similarity index 99% rename from testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index e303cba51f..5febc72bd8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -1,6 +1,4 @@ -@file:JvmName("VaultFiller") - -package net.corda.testing.contracts +package net.corda.testing.internal.vault import net.corda.core.contracts.* import net.corda.core.crypto.Crypto @@ -80,7 +78,7 @@ class VaultFiller @JvmOverloads constructor( addCommand(dummyCommand()) } val stx = issuerServices.signInitialTransaction(dummyIssue) - return@map services.addSignature(stx, defaultNotary.pubkey) + return@map services.addSignature(stx, defaultNotary.publicKey) } services.recordTransactions(transactions) // Get all the StateAndRefs of all the generated transactions. @@ -101,7 +99,7 @@ class VaultFiller @JvmOverloads constructor( linearTimestamp: Instant = now()): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) - val issuerKey = defaultNotary.key + val issuerKey = defaultNotary.keyPair val signatureMetadata = SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(issuerKey.public).schemeNumberID) val transactions: List = (1..numberToCreate).map { // Issue a Linear state diff --git a/testing/test-utils/src/test/kotlin/net/corda/testing/TestIdentityTests.kt b/testing/test-utils/src/test/kotlin/net/corda/testing/TestIdentityTests.kt new file mode 100644 index 0000000000..c5a4e673bd --- /dev/null +++ b/testing/test-utils/src/test/kotlin/net/corda/testing/TestIdentityTests.kt @@ -0,0 +1,27 @@ +package net.corda.testing + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class TestIdentityTests { + @Test + fun `entropy works`() { + val a = TestIdentity(ALICE_NAME, 123) + val b = TestIdentity(BOB_NAME, 123) + assertEquals(a.publicKey, b.publicKey) + assertEquals(a.keyPair.private, b.keyPair.private) + } + + @Test + fun `fresh works`() { + val x = TestIdentity.fresh("xx") + val y = TestIdentity.fresh("yy") + // The param is called organisation so we'd better use it as such: + assertEquals("xx", x.name.organisation) + assertEquals("yy", y.name.organisation) + // A fresh identity shouldn't be equal to anything by accident: + assertNotEquals(x.name, y.name) + assertNotEquals(x.publicKey, y.publicKey) + } +} diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle new file mode 100644 index 0000000000..dc578b2c76 --- /dev/null +++ b/tools/bootstrapper/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'us.kirchmeier.capsule' + +description 'Network bootstrapper' + +configurations { + runtimeArtifacts +} + +// TODO Fix SLF4J warnings that occur when running the bootstrapper +task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) { + applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper' + archiveName "network-bootstrapper.jar" + capsuleManifest { + applicationVersion = corda_release_version + systemProperties['visualvm.display.name'] = 'Network Bootstrapper' + minJavaVersion = '1.8.0' + jvmArgs = ['-XX:+UseG1GC'] + } + applicationSource = files( + project(':node-api').configurations.runtime, + project(':node-api').jar + ) +} + +artifacts { + runtimeArtifacts buildBootstrapperJar +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt index 1dc8e11f25..09233c0a02 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -5,7 +5,7 @@ import net.corda.client.rpc.internal.KryoClientSerializationScheme import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.demobench.views.DemoBenchView -import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT +import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import tornadofx.* @@ -57,13 +57,12 @@ class DemoBench : App(DemoBenchView::class) { } private fun initialiseSerialization() { - val context = KRYO_P2P_CONTEXT nodeSerializationEnv = SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) registerScheme(AMQPClientSerializationScheme()) }, - context) + AMQP_P2P_CONTEXT) } } 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 index 3b856ef5ac..a3d8586d26 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/DemoBenchNodeInfoFilesCopier.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DemoBenchNodeInfoFilesCopier.kt @@ -1,6 +1,6 @@ package net.corda.demobench.model -import net.corda.nodeapi.internal.NodeInfoFilesCopier +import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import rx.Scheduler import rx.schedulers.Schedulers import tornadofx.* 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 fd1be25923..37e019b367 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 @@ -8,12 +8,11 @@ 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.core.utilities.days import net.corda.demobench.plugin.CordappController import net.corda.demobench.pty.R3Pty -import net.corda.nodeapi.internal.NetworkParameters -import net.corda.nodeapi.internal.NetworkParametersCopier -import net.corda.nodeapi.internal.NotaryInfo +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.nodeapi.internal.ServiceIdentityGenerator import tornadofx.* import java.io.IOException @@ -143,8 +142,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { minimumPlatformVersion = 1, notaries = listOf(NotaryInfo(identity, config.nodeConfig.notary!!.validating)), modifiedTime = Instant.now(), - eventHorizon = 10000.days, - maxMessageSize = 40000, + maxMessageSize = 10485760, maxTransactionSize = 40000, epoch = 1 )) diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/pty/ZeroFilterTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/pty/ZeroFilterTest.kt index 41ccda83bf..0f50441e86 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/pty/ZeroFilterTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/pty/ZeroFilterTest.kt @@ -3,7 +3,7 @@ package net.corda.demobench.pty import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import net.corda.testing.rigorousMock +import net.corda.testing.internal.rigorousMock import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index d1d500b7a2..5e2031b098 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -27,7 +27,7 @@ data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTra val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> - val issuerServices = MockServices(makeTestIdentityService(listOf(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity)), megaCorp.name, dummyCashIssuer.key) + val issuerServices = MockServices(emptyList(), makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity), megaCorp.name, dummyCashIssuer.keyPair) val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> Generator.int().map { val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[0], DUMMY_CASH_ISSUER) // TODO notary choice