diff --git a/.ci/api-current.txt b/.ci/api-current.txt index c5219e1920..5fd0e2262b 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2756,6 +2756,10 @@ public static final class net.corda.core.serialization.SerializationContext$UseC public static net.corda.core.serialization.SerializationContext$UseCase valueOf(String) public static net.corda.core.serialization.SerializationContext$UseCase[] values() ## +public interface net.corda.core.serialization.SerializationCustomSerializer + public abstract Object fromProxy(Object) + public abstract Object toProxy(Object) +## public final class net.corda.core.serialization.SerializationDefaults extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getCHECKPOINT_CONTEXT() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getP2P_CONTEXT() diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 1c5f1d6d42..d4e352de90 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -26,7 +26,10 @@ import rx.Observable import rx.subjects.PublishSubject import rx.subjects.UnicastSubject import java.time.Duration -import java.util.concurrent.* +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger class RPCStabilityTests : IntegrationTest() { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt index eb9f22e937..5a6adc58b9 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt @@ -41,7 +41,7 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { return SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) - registerScheme(AMQPClientSerializationScheme()) + registerScheme(AMQPClientSerializationScheme(emptyList())) }, KRYO_P2P_CONTEXT, rpcClientContext = KRYO_RPC_CLIENT_CONTEXT) diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index d03f020d5b..30edeeb897 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -48,8 +48,7 @@ public class StandaloneCordaRPCJavaClientTest { port.getAndIncrement(), port.getAndIncrement(), true, - Collections.singletonList(rpcUser), - null + Collections.singletonList(rpcUser) ); @Before diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 0089d1cb24..12a3200105 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -4,8 +4,4 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10002" rpcAddress : "localhost:10003" webAddress : "localhost:10004" -networkMapService : { - address : "localhost:10000" - legalName : "O=Network Map Service,OU=corda,L=London,C=GB" -} useHTTPS : false diff --git a/constants.properties b/constants.properties index 71cec9ca52..0434adb857 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.0 +gradlePluginsVersion=3.0.1 kotlinVersion=1.1.60 platformVersion=1 guavaVersion=21.0 diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index f4fe71ead0..2ce4c24ce1 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -3,6 +3,7 @@ package net.corda.core.cordapp import net.corda.core.DoNotImplement import net.corda.core.flows.FlowLogic import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.net.URL @@ -22,6 +23,7 @@ import java.net.URL * @property schedulableFlows List of flows startable by the scheduler * @property services List of RPC services * @property serializationWhitelists List of Corda plugin registries + * @property serializationCustomSerializers List of serializers * @property customSchemas List of custom schemas * @property jarPath The path to the JAR for this CorDapp */ @@ -35,6 +37,7 @@ interface Cordapp { val schedulableFlows: List>> val services: List> val serializationWhitelists: List + val serializationCustomSerializers: List> val customSchemas: Set val jarPath: URL val cordappClasses: List diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 02410db37a..034f3a9d10 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -3,6 +3,7 @@ package net.corda.core.internal.cordapp import net.corda.core.cordapp.Cordapp import net.corda.core.flows.FlowLogic import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.io.File @@ -16,6 +17,7 @@ data class CordappImpl( override val schedulableFlows: List>>, override val services: List>, override val serializationWhitelists: List, + override val serializationCustomSerializers: List>, override val customSchemas: Set, override val jarPath: URL) : Cordapp { override val name: String = File(jarPath.toURI()).name.removeSuffix(".jar") diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 5b5226087a..6cfe29dbec 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -53,7 +53,6 @@ interface NetworkMapCacheBase { * * Note that the identities are sorted based on legal name, and the ordering might change once new notaries are introduced. */ - // TODO this list will be taken from NetworkParameters distributed by NetworkMap. val notaryIdentities: List // DOCEND 1 @@ -117,7 +116,7 @@ interface NetworkMapCacheBase { fun getNotary(name: CordaX500Name): Party? = notaryIdentities.firstOrNull { it.name == name } // DOCEND 2 - /** Checks whether a given party is an advertised notary identity. */ + /** Returns true if and only if the given [Party] is a notary, which is defined by the network parameters. */ fun isNotary(party: Party): Boolean = party in notaryIdentities /** diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt index c8ae7e2aec..dc37459899 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt @@ -18,11 +18,11 @@ annotation class CordaSerializationTransformRenames(vararg val value: CordaSeria // TODO When we have class renaming update the docs /** * This annotation is used to mark a class has having had a property element. It is used by the - * AMQP deserialiser to allow instances with different versions of the class on their Class Path - * to successfully deserialize the object + * AMQP deserializer to allow instances with different versions of the class on their Class Path + * to successfully deserialize the object. * - * NOTE: Renaming of the class itself is not be done with this annotation. For class renaming - * see ??? + * NOTE: Renaming of the class itself isn't done with this annotation or, at present, supported + * by Corda * * @property to [String] representation of the properties new name * @property from [String] representation of the properties old new diff --git a/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt b/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt index 71d2691e7e..ff76109f03 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt @@ -2,10 +2,10 @@ package net.corda.core.serialization /** * This annotation is a marker to indicate which secondary constructors should be considered, and in which - * order, for evolving objects during their deserialisation. + * order, for evolving objects during their deserialization. * * Versions will be considered in descending order, currently duplicate versions will result in - * non deterministic behaviour when deserialising objects + * non deterministic behaviour when deserializing objects */ @Target(AnnotationTarget.CONSTRUCTOR) @Retention(AnnotationRetention.RUNTIME) diff --git a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt index a5934be42d..0799e9a270 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt @@ -3,6 +3,6 @@ package net.corda.core.serialization import net.corda.core.CordaException import net.corda.core.crypto.SecureHash -/** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found. */ +/** Thrown during deserialization to indicate that an attachment needed to construct the [WireTransaction] is not found. */ @CordaSerializable class MissingAttachmentsException(val ids: List) : CordaException() \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt new file mode 100644 index 0000000000..2ad2aded38 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt @@ -0,0 +1,24 @@ +package net.corda.core.serialization + +/** + * Allows CorDapps to provide custom serializers for third party libraries where those libraries cannot + * be recompiled with the -parameters flag rendering their classes natively serializable by Corda. In this case + * a proxy serializer can be written that extends this type whose purpose is to move between those an + * unserializable types and an intermediate representation. + * + * NOTE: The proxy object should be specified as a seperate class. However, this can be defined within the + * scope of the custom serializer. + */ +interface SerializationCustomSerializer { + /** + * Should facilitate the conversion of the third party object into the serializable + * local class specified by [PROXY] + */ + fun toProxy(obj: OBJ) : PROXY + + /** + * Should facilitate the conversion of the proxy object into a new instance of the + * unserializable type + */ + fun fromProxy(proxy: PROXY) : OBJ +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt index 8ca6820711..9ec9fb75c0 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt @@ -6,7 +6,7 @@ import net.corda.core.serialization.SingletonSerializationToken.Companion.single /** * The interfaces and classes in this file allow large, singleton style classes to * mark themselves as needing converting to some form of token representation in the serialised form - * and converting back again when deserialising. + * and converting back again when deserializing. * * Typically these classes would be used for node services and subsystems that might become reachable from * Fibers and thus sucked into serialization when they are checkpointed. diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index c34fd26065..015e745987 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -50,7 +50,7 @@ data class SignedTransaction(val txBits: SerializedBytes, @Volatile @Transient private var cachedTransaction: CoreTransaction? = null - /** Lazily calculated access to the deserialised/hashed transaction data. */ + /** Lazily calculated access to the deserialized/hashed transaction data. */ private val transaction: CoreTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this } /** The id of the contained [WireTransaction]. */ 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 c0346e30fd..be02cc4599 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -1,5 +1,7 @@ package net.corda.core.crypto +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.identity.Party @@ -9,7 +11,9 @@ import net.corda.core.transactions.WireTransaction import net.corda.finance.DOLLARS 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.node.MockServices import org.junit.Before import org.junit.Rule import org.junit.Test @@ -35,7 +39,9 @@ class PartialMerkleTreeTest { hashed = nodes.map { it.serialize().sha256() } expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash merkleTree = MerkleTree.getMerkleTree(hashed) - testLedger = ledger { + testLedger = MockServices(rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }, MEGA_CORP.name).ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "MEGA_CORP cash", diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index b3b1f332e2..e135d1c93c 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -50,7 +50,7 @@ class X509NameConstraintsTest { val nameConstraints = NameConstraints(acceptableNames, arrayOf()) val pathValidator = CertPathValidator.getInstance("PKIX") - val certFactory = X509CertificateFactory().delegate + val certFactory = X509CertificateFactory() assertFailsWith(CertPathValidatorException::class) { val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints) 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 4cb955faa5..b4c824026d 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -1,5 +1,7 @@ package net.corda.core.transactions +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState import net.corda.core.contracts.requireThat @@ -7,10 +9,9 @@ import net.corda.core.identity.AbstractParty import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash -import net.corda.testing.MEGA_CORP -import net.corda.testing.MINI_CORP -import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.ledger +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.* +import net.corda.testing.node.MockServices import org.junit.Rule import org.junit.Test import java.time.Instant @@ -50,9 +51,13 @@ class TransactionEncumbranceTests { } } + private val ledgerServices = MockServices(rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }, MEGA_CORP.name) + @Test fun `state can be encumbered`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) input(Cash.PROGRAM_ID, state) @@ -66,7 +71,7 @@ class TransactionEncumbranceTests { @Test fun `state can transition if encumbrance rules are met`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state) @@ -87,7 +92,7 @@ class TransactionEncumbranceTests { @Test fun `state cannot transition if the encumbrance contract fails to verify`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state) @@ -108,7 +113,7 @@ class TransactionEncumbranceTests { @Test fun `state must be consumed along with its encumbrance`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state) @@ -127,7 +132,7 @@ class TransactionEncumbranceTests { @Test fun `state cannot be encumbered by itself`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, state) @@ -140,7 +145,7 @@ class TransactionEncumbranceTests { @Test fun `encumbrance state index must be valid`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) input(Cash.PROGRAM_ID, state) @@ -154,7 +159,7 @@ class TransactionEncumbranceTests { @Test fun `correct encumbrance state must be provided`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state) 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 c5a4976876..fadd968dd9 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -4,12 +4,12 @@ import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockAttachment import org.junit.Rule import org.junit.Test +import java.math.BigInteger import java.security.KeyPair import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -19,6 +19,7 @@ class TransactionTests { private companion object { val DUMMY_KEY_1 = generateKeyPair() val DUMMY_KEY_2 = generateKeyPair() + val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) } @Rule diff --git a/docs/source/CLI-vs-IDE.rst b/docs/source/CLI-vs-IDE.rst index 7da89e8cb1..a4888a7adb 100644 --- a/docs/source/CLI-vs-IDE.rst +++ b/docs/source/CLI-vs-IDE.rst @@ -9,11 +9,11 @@ a developer environment. IDE - IntelliJ -------------- -IntelliJ (R3's preferred IDE) integrates well with gradle (our chosen build, deployment and CLI tool). IntelliJ understands gradle -tasks and dependencies, automatically loading them in the background when a project is first opened or the gradle -project changes. Occasionally, however, you may need to refresh the gradle project manually - but this is hinted to you -by the IDE. It's a good idea to do this before carrying on with other work (and in fact you may find it is essential to pick -up new libraries, etc.). +IntelliJ (the preferred IDE for Corda) integrates well with gradle (Corda's default build, deployment and CLI tool). +IntelliJ understands gradle tasks and dependencies, automatically loading them in the background when a project is +first opened or the gradle project changes. Occasionally, however, you may need to refresh the gradle project manually +- but this is hinted to you by the IDE. It's a good idea to do this before carrying on with other work (and in fact you +may find it is essential to pick up new libraries, etc.). There are some great resources about how to get started using IntelliJ. As opposed to trying to repeat them here, we advise you to go to the `IntelliJ docs here `_. diff --git a/docs/source/codestyle.rst b/docs/source/codestyle.rst index 930e7f9868..71388c5e0d 100644 --- a/docs/source/codestyle.rst +++ b/docs/source/codestyle.rst @@ -1,7 +1,7 @@ Code style guide ================ -This document explains the coding style used in the R3 prototyping repository. You will be expected to follow these +This document explains the coding style used in the Corda repository. You will be expected to follow these recommendations when submitting patches for review. Please take the time to read them and internalise them, to save time during code review. diff --git a/docs/source/cordapp-custom-serializers.rst b/docs/source/cordapp-custom-serializers.rst new file mode 100644 index 0000000000..4541d18f3d --- /dev/null +++ b/docs/source/cordapp-custom-serializers.rst @@ -0,0 +1,73 @@ +Pluggable Serializers for CorDapps +================================== + +.. contents:: + +To be serializable by Corda Java classes must be compiled with the -parameters switch to enable matching of its properties +to constructor parameters. This is important because Corda's internal AMQP serialization scheme will only construct +objects using their constructors. However, when recompilation isn't possible, or classes are built in such a way that +they cannot be easily modified for simple serialization, CorDapps can provide custom proxy serializers that Corda +can use to move from types it cannot serialize to an interim representation that it can with the transformation to and +from this proxy object being handled by the supplied serializer. + +Serializer Location +------------------- +Custom serializer classes should follow the rules for including classes found in :doc:`cordapp-build-systems` + +Writing a Custom Serializer +--------------------------- +Serializers must + * Inherit from net.corda.core.serialization.SerializationCustomSerializer + * Provide a proxy class to transform the object to and from + * Implement the ``toProxy`` and ``fromProxy`` methods + +Serializers inheriting from SerializationCustomSerializer have to implement two methods and two types. + +Example +------- +Consider this example class + + +.. sourcecode:: java + + public final class Example { + private final Int a + private final Int b + + private Example(Int a, Int b) { + this.a = a; + this.b = b; + } + + public static Example of (int[] a) { return Example(a[0], a[1]); } + + public int getA() { return a; } + public int getB() { return b; } + } + +Without a custom serializer we cannot serialize this class as there is no public constructor that facilitates the +initialisation of all of its properties. + +To be serializable by Corda this would require a custom serializer as follows: + +.. sourcecode:: kotlin + + class ExampleSerializer : SerializationCustomSerializer { + data class Proxy(val a: Int, val b: Int) + + override fun toProxy(obj: Example) = Proxy(obj.a, obj.b) + + override fun fromProxy(proxy: Proxy) : Example { + val constructorArg = IntArray(2); + constructorArg[0] = proxy.a + constructorArg[1] = proxy.b + return Example.create(constructorArg) + } + } + +Whitelisting +------------ +By writing a custom serializer for a class it has the effect of adding that class to the whitelist, meaning such +classes don't need explicitly adding to the CorDapp's whitelist. + + diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index f2e1b11a3e..4f94c02c95 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -48,10 +48,6 @@ handling, and ensures the Corda service is run at boot. trustStorePassword : "trustpass" useHTTPS : false devMode : false - networkMapService { - address="networkmap.foo.bar.com:10002" - legalName="O=FooBar NetworkMap, L=Dublin, C=IE" - } rpcUsers=[ { user=corda @@ -223,10 +219,6 @@ at boot, and means the Corda service stays running with no users connected to th extraAdvertisedServiceIds: [ "" ] useHTTPS : false devMode : false - networkMapService { - address="networkmap.foo.bar.com:10002" - legalName="O=FooBar NetworkMap, L=Dublin, C=IE" - } rpcUsers=[ { user=corda 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 3a302d09e0..07f96740a0 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 @@ -6,7 +6,9 @@ import net.corda.core.utilities.OpaqueBytes; 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 org.junit.Rule; import org.junit.Test; @@ -19,11 +21,21 @@ import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.NodeTestUtils.ledger; import static net.corda.testing.NodeTestUtils.transaction; import static net.corda.testing.TestConstants.*; +import static org.mockito.Mockito.doReturn; public class CommercialPaperTest { @Rule public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{123}); + private final MockServices ledgerServices; + + { + IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class); + doReturn(getMEGA_CORP()).when(identityService).partyFromKey(getMEGA_CORP_PUBKEY()); + doReturn(null).when(identityService).partyFromKey(getBIG_CORP_PUBKEY()); + doReturn(null).when(identityService).partyFromKey(getALICE_PUBKEY()); + ledgerServices = new MockServices(identityService, getMEGA_CORP().getName()); + } // DOCSTART 1 private ICommercialPaperState getPaper() { @@ -40,7 +52,7 @@ public class CommercialPaperTest { @Test public void simpleCP() { ICommercialPaperState inState = getPaper(); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.transaction(tx -> { tx.attachments(JCP_PROGRAM_ID); tx.input(JCP_PROGRAM_ID, inState); @@ -55,7 +67,7 @@ public class CommercialPaperTest { @Test public void simpleCPMove() { ICommercialPaperState inState = getPaper(); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); @@ -71,7 +83,7 @@ public class CommercialPaperTest { @Test public void simpleCPMoveFails() { ICommercialPaperState inState = getPaper(); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); @@ -87,7 +99,7 @@ public class CommercialPaperTest { @Test public void simpleCPMoveSuccess() { ICommercialPaperState inState = getPaper(); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); @@ -104,7 +116,7 @@ public class CommercialPaperTest { // DOCSTART 6 @Test public void simpleIssuanceWithTweak() { - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), 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); @@ -125,7 +137,7 @@ public class CommercialPaperTest { // DOCSTART 7 @Test public void simpleIssuanceWithTweakTopLevelTx() { - transaction(tx -> { + transaction(ledgerServices, getDUMMY_NOTARY(), 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 -> { @@ -144,7 +156,7 @@ public class CommercialPaperTest { @Test public void chainCommercialPaper() { PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); @@ -180,7 +192,7 @@ public class CommercialPaperTest { @Test public void chainCommercialPaperDoubleSpend() { PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); @@ -226,7 +238,7 @@ public class CommercialPaperTest { @Test public void chainCommercialPaperTweak() { PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { + ledger(ledgerServices, getDUMMY_NOTARY(), l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); 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 cfde0084a1..eceadf55a7 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 @@ -1,5 +1,7 @@ package net.corda.docs.tutorial.testdsl +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.utilities.days import net.corda.finance.DOLLARS import net.corda.finance.`issued by` @@ -8,7 +10,9 @@ import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.ICommercialPaperState 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.node.MockServices import org.junit.Rule import org.junit.Test @@ -16,6 +20,11 @@ class CommercialPaperTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() + private val ledgerServices = MockServices(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) + }, MEGA_CORP.name) // DOCSTART 1 fun getPaper(): ICommercialPaperState = CommercialPaper.State( @@ -30,7 +39,7 @@ class CommercialPaperTest { @Test fun simpleCP() { val inState = getPaper() - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(CP_PROGRAM_ID) input(CP_PROGRAM_ID, inState) @@ -44,7 +53,7 @@ class CommercialPaperTest { @Test fun simpleCPMove() { val inState = getPaper() - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { input(CP_PROGRAM_ID, inState) command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) @@ -59,7 +68,7 @@ class CommercialPaperTest { @Test fun simpleCPMoveFails() { val inState = getPaper() - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { input(CP_PROGRAM_ID, inState) command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) @@ -74,7 +83,7 @@ class CommercialPaperTest { @Test fun simpleCPMoveSuccess() { val inState = getPaper() - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { input(CP_PROGRAM_ID, inState) command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) @@ -90,7 +99,7 @@ class CommercialPaperTest { // DOCSTART 6 @Test fun `simple issuance with tweak`() { - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp. attachments(CP_PROGRAM_ID) @@ -111,7 +120,7 @@ class CommercialPaperTest { // DOCSTART 7 @Test fun `simple issuance with tweak and top level transaction`() { - transaction { + ledgerServices.transaction(DUMMY_NOTARY) { output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp. attachments(CP_PROGRAM_ID) tweak { @@ -131,8 +140,7 @@ class CommercialPaperTest { @Test fun `chain commercial paper`() { val issuer = MEGA_CORP.ref(123) - - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) @@ -165,7 +173,7 @@ class CommercialPaperTest { @Test fun `chain commercial paper double spend`() { val issuer = MEGA_CORP.ref(123) - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) @@ -207,7 +215,7 @@ class CommercialPaperTest { @Test fun `chain commercial tweak`() { val issuer = MEGA_CORP.ref(123) - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index c9bad1eb5b..8ae9a42475 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -10,14 +10,9 @@ dataSourceProperties : { p2pAddress : "my-corda-node:10002" rpcAddress : "my-corda-node:10003" webAddress : "localhost:10004" -networkMapService : { - address : "my-network-map:10000" - legalName : "O=Network Map Service,OU=corda,L=London,C=GB" -} useHTTPS : false rpcUsers : [ { username=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] } ] devMode : true -// Certificate signing service will be hosted by R3 in the near future. -//certificateSigningService : "https://testnet.certificate.corda.net" +// certificateSigningService : "https://testnet.certificate.corda.net" diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 05d5211fa6..4432ddb2a2 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -53,8 +53,6 @@ Protocol The old name for a Corda "Flow" Quasar A library that provides performant lightweight threads that can be suspended and restored extremely quickly. -R3 - The consortium behind Corda SIMM Standard Initial Margin Model. A way of determining a counterparty's margin payment to another counterparty based on a collection of trades such that, in the event of default, the receiving counterparty has limited exposure. Serialization diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 36aeb6c6c5..2816383bc8 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -12,6 +12,20 @@ Unreleased That is the ability to alter an enum constant and, as long as certain rules are followed and the correct annotations applied, have older and newer instances of that enumeration be understood. +* **AMQP Enabled** + +AMQP Serialization is now enabled for both peer to peer communication and writing states to the vault. This change +brings a stable format Corda can support internally throughout it's lifetime that meets the needs of Corda and our +users. + +* **Custom Serializers** + +To allow interop with third party libraries that cannot be recompiled we add functionality that allows custom serializers +to be written for those classes. If needed, a proxy object can be created as an interim step that allows Corda's internal +serializers to operate on those types. + +A good example of this is the SIMM valuation demo which has a number of such serializers defined in the plugin/customserializers package + Release 2.0 ---------- Following quickly on the heels of the release of Corda 1.0, Corda version 2.0 consolidates diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index c5e43e01fd..4b610ef104 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -45,8 +45,6 @@ The most important fields regarding network configuration are: resolvable name of a machine in a VPN. * ``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. -* ``networkMapService``: Details of the node running the network map service. If it's this node that's running the service - then this field must not be specified. Starting the nodes ~~~~~~~~~~~~~~~~~~ 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 9d683682a6..e77ca43f4c 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 @@ -1,18 +1,31 @@ package net.corda.finance.contracts.universal +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.finance.contracts.BusinessCalendar import net.corda.finance.contracts.FixOf 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.node.MockServices +import net.corda.testing.node.makeTestIdentityService import org.junit.Ignore import org.junit.Rule import org.junit.Test +import java.math.BigInteger import java.time.Instant import java.time.LocalDate fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { - net.corda.testing.transaction(cordappPackages = listOf("net.corda.finance.contracts.universal"), dsl = script) + MockServices(listOf("net.corda.finance.contracts.universal"), rigorousMock().also { + listOf(acmeCorp, highStreetBank, momAndPop).forEach { party -> + doReturn(null).whenever(it).partyFromKey(party.owningKey) + } + }, MEGA_CORP.name).transaction(DUMMY_NOTARY, script) } class Cap { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 6234c7ab73..998edb5326 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -7,10 +7,8 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.contracts.Amount.Companion.sumOrThrow import net.corda.core.crypto.NullKeys.NULL_PARTY -import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.Emoji @@ -25,7 +23,6 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero -import java.math.BigInteger import java.security.PublicKey import java.util.* @@ -342,14 +339,7 @@ class Cash : OnLedgerAsset() { } // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. - -/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ -val DUMMY_CASH_ISSUER_NAME = CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB") -/** A randomly generated key. */ -val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } -/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ -val DUMMY_CASH_ISSUER by lazy { Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.public).ref(1) } /** An extension property that lets you write 100.DOLLARS.CASH */ -val Amount.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY) +val Amount.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(NULL_PARTY.ref(1), token)), NULL_PARTY) /** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */ val Amount>.STATE: Cash.State get() = Cash.State(this, NULL_PARTY) diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index c30ddc0875..23d905288f 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -6,10 +6,8 @@ import net.corda.finance.contracts.NetType import net.corda.finance.contracts.NettableState import net.corda.finance.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.Emoji import net.corda.core.internal.VisibleForTesting @@ -22,7 +20,6 @@ import net.corda.finance.utils.sumFungibleOrNull import net.corda.finance.utils.sumObligations import net.corda.finance.utils.sumObligationsOrNull import net.corda.finance.utils.sumObligationsOrZero -import java.math.BigInteger import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -791,8 +788,3 @@ fun Obligation.State.ownedBy(owner: AbstractParty) = copy(beneficia @Suppress("unused") fun Obligation.State.issuedBy(party: AnonymousParty) = copy(obligor = party) - -/** A randomly generated key. */ -val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } -/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ -val DUMMY_OBLIGATION_ISSUER by lazy { Party(CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB"), DUMMY_OBLIGATION_ISSUER_KEY.public) } 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 bfff1987d1..5ab3569b25 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 @@ -3,8 +3,10 @@ package net.corda.finance.contracts.asset; import net.corda.core.contracts.PartyAndReference; import net.corda.core.identity.AnonymousParty; import net.corda.core.utilities.OpaqueBytes; +import net.corda.node.services.api.IdentityServiceInternal; import net.corda.testing.DummyCommandData; import net.corda.testing.SerializationEnvironmentRule; +import net.corda.testing.node.MockServices; import org.junit.Rule; import org.junit.Test; @@ -12,6 +14,8 @@ import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.Currencies.issuedBy; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.NodeTestUtils.transaction; +import static net.corda.testing.TestConstants.getDUMMY_NOTARY; +import static org.mockito.Mockito.doReturn; /** * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL @@ -26,7 +30,10 @@ public class CashTestsJava { @Test public void trivial() { - transaction(tx -> { + IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class); + doReturn(getMEGA_CORP()).when(identityService).partyFromKey(getMEGA_CORP_PUBKEY()); + doReturn(getMINI_CORP()).when(identityService).partyFromKey(getMINI_CORP_PUBKEY()); + transaction(new MockServices(identityService, getMEGA_CORP().getName()), getDUMMY_NOTARY(), tx -> { tx.attachment(Cash.PROGRAM_ID); tx.input(Cash.PROGRAM_ID, inState); 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 fc02955c6a..ff6d194737 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -1,7 +1,11 @@ package net.corda.finance.contracts +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService @@ -12,6 +16,7 @@ import net.corda.core.utilities.seconds import net.corda.finance.DOLLARS 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.node.MockServices @@ -21,6 +26,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import java.math.BigInteger import java.time.Instant import java.util.* import kotlin.test.assertFailsWith @@ -84,6 +90,10 @@ class CommercialPaperTestsGeneric { @Parameterized.Parameters @JvmStatic fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest()) + + private val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) + private val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), DUMMY_CASH_ISSUER_KEY.public)) + private val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) } @Parameterized.Parameter @@ -92,11 +102,16 @@ class CommercialPaperTestsGeneric { @JvmField val testSerialization = SerializationEnvironmentRule() val issuer = MEGA_CORP.ref(123) + private val ledgerServices = MockServices(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) + }, MEGA_CORP.name) @Test fun `trade lifecycle test`() { val someProfits = 1200.DOLLARS `issued by` issuer - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachment(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) @@ -162,6 +177,10 @@ class CommercialPaperTestsGeneric { } } + private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + ledgerServices.transaction(DUMMY_NOTARY, script) + } + @Test fun `key mismatch at issue`() { transaction { 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 bb027e880c..261ee64b95 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,11 +3,9 @@ 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.entropyToKeyPair import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party +import net.corda.core.identity.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy @@ -32,10 +30,15 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import java.math.BigInteger import java.util.* import kotlin.test.* class CashTests { + companion object { + private val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), entropyToKeyPair(BigInteger.valueOf(10)).public)) + } + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() @@ -86,7 +89,7 @@ class CashTests { ourIdentity = ourServices.myInfo.singleIdentity() miniCorpAnonymised = miniCorpServices.myInfo.singleIdentityAndCert().party.anonymise() (miniCorpServices.myInfo.legalIdentitiesAndCerts + megaCorpServices.myInfo.legalIdentitiesAndCerts + notaryServices.myInfo.legalIdentitiesAndCerts).forEach { identity -> - ourServices.identityService.verifyAndRegisterIdentity(identity) + ourServices.identityService.verifyAndRegisterIdentity(identity) // TODO: Configure a mock identity service instead. } // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved. @@ -113,6 +116,15 @@ class CashTests { database.close() } + private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + MockServices(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) + doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY) + }, MEGA_CORP.name).transaction(DUMMY_NOTARY, script) + } + @Test fun trivial() { transaction { @@ -779,7 +791,7 @@ class CashTests { val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) }, MEGA_CORP.name, MEGA_CORP_KEY) - ledger(mockService) { + mockService.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachment(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "MEGA_CORP cash", 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 207955b0dd..803954541f 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 @@ -5,9 +5,12 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.sha256 import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes @@ -24,6 +27,7 @@ import net.corda.testing.contracts.DummyState import net.corda.testing.node.MockServices import org.junit.Rule import org.junit.Test +import java.math.BigInteger import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* @@ -33,6 +37,10 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ObligationTests { + companion object { + private val DUMMY_OBLIGATION_ISSUER = Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), entropyToKeyPair(BigInteger.valueOf(10)).public) + } + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() @@ -56,12 +64,15 @@ class ObligationTests { private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY) private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY) - private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { + private val identityService = rigorousMock().also { doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY) + doReturn(null).whenever(it).partyFromKey(CHARLIE.owningKey) doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - }, MEGA_CORP.name) - + 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 fun cashObligationTestRoots( group: LedgerDSL ) = group.apply { @@ -74,6 +85,10 @@ class ObligationTests { } } + private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + ledgerServices.transaction(DUMMY_NOTARY, script) + } + @Test fun trivial() { transaction { @@ -347,7 +362,7 @@ class ObligationTests { @Test fun `close-out netting`() { // Try netting out two obligations - ledger(mockService) { + mockService.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -363,7 +378,7 @@ class ObligationTests { // Try netting out two obligations, with the third uninvolved obligation left // as-is - ledger(mockService) { + mockService.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -379,7 +394,7 @@ class ObligationTests { } // Try having outputs mis-match the inputs - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -393,7 +408,7 @@ class ObligationTests { } // Have the wrong signature on the transaction - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -409,7 +424,7 @@ class ObligationTests { @Test fun `payment netting`() { // Try netting out two obligations - ledger(mockService) { + mockService.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -424,7 +439,7 @@ class ObligationTests { // Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both // signatures for payment netting - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -437,7 +452,7 @@ class ObligationTests { } // Multilateral netting, A -> B -> C which can net down to A -> C - ledger(mockService) { + mockService.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -452,7 +467,7 @@ class ObligationTests { } // Multilateral netting without the key of the receiving party - ledger(mockService) { + mockService.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -469,7 +484,7 @@ class ObligationTests { @Test fun `cash settlement`() { // Try settling an obligation - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Settlement") { attachments(Obligation.PROGRAM_ID) @@ -485,7 +500,7 @@ class ObligationTests { // Try partial settling of an obligation val halfAMillionDollars = 500000.DOLLARS `issued by` defaultIssuer - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction("Settlement") { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) @@ -501,7 +516,7 @@ class ObligationTests { // Make sure we can't settle an obligation that's defaulted val defaultedObligation: Obligation.State = (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED) - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction("Settlement") { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob @@ -514,7 +529,7 @@ class ObligationTests { } // Make sure settlement amount must match the amount leaving the ledger - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Settlement") { attachments(Obligation.PROGRAM_ID) @@ -538,7 +553,7 @@ class ObligationTests { val oneUnitFcojObligation = Obligation.State(Obligation.Lifecycle.NORMAL, ALICE, obligationDef, oneUnitFcoj.quantity, NULL_PARTY) // Try settling a simple commodity obligation - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Obligation.PROGRAM_ID) output(Obligation.PROGRAM_ID, "Alice's 1 FCOJ obligation to Bob", oneUnitFcojObligation between Pair(ALICE, BOB)) @@ -560,7 +575,7 @@ class ObligationTests { @Test fun `payment default`() { // Try defaulting an obligation without a time-window. - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Settlement") { attachments(Obligation.PROGRAM_ID) @@ -584,7 +599,7 @@ class ObligationTests { } // Try defaulting an obligation that is now in the past - ledger { + ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(Obligation.PROGRAM_ID) input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime) diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle index 3b9ab84805..be2fa0cf16 100644 --- a/gradle-plugins/cordform-common/build.gradle +++ b/gradle-plugins/cordform-common/build.gradle @@ -17,9 +17,6 @@ dependencies { // TypeSafe Config: for simple and human friendly config files. compile "com.typesafe:config:$typesafe_config_version" - - // Bouncy Castle: for X.500 distinguished name manipulation - compile "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" } publish { diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java index 06d4375659..7687f68a11 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java @@ -1,6 +1,5 @@ package net.corda.cordform; -import org.bouncycastle.asn1.x500.X500Name; import java.nio.file.Path; public interface CordformContext { diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 33b433a506..285260f6db 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -1,14 +1,16 @@ package net.corda.cordform; -import static java.util.Collections.emptyList; -import com.typesafe.config.*; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Collections; import java.util.List; import java.util.Map; +import static java.util.Collections.emptyList; + public class CordformNode implements NodeDefinition { /** * Path relative to the running node where the serialized NodeInfos are stored. 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 f43d39f6f4..cffc411f7f 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 @@ -117,6 +117,16 @@ open class Cordform : DefaultTask() { .newInstance() } + /** + * The parametersGenerator needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. + */ + private fun loadNetworkParamsGenClass(): 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") + } + /** * This task action will create and install the nodes based on the node configurations added. */ @@ -127,6 +137,7 @@ open class Cordform : DefaultTask() { installRunScript() nodes.forEach(Node::build) generateAndInstallNodeInfos() + generateAndInstallNetworkParameters() } private fun initializeConfiguration() { @@ -153,6 +164,16 @@ 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 CordformDefinition.getMatchingCordapps(): List { val cordappJars = project.configuration("cordapp").files return cordappPackages.map { `package` -> @@ -193,9 +214,10 @@ open class Cordform : DefaultTask() { } private fun buildNodeProcesses(): Map { - return nodes - .map { buildNodeProcess(it) } - .toMap() + val command = generateNodeInfoCommand() + return nodes.map { + it.makeLogDirectory() + buildProcess(it, command, "generate-info.log") }.toMap() } private fun validateNodeProcessess(nodeProcesses: Map) { @@ -210,14 +232,13 @@ open class Cordform : DefaultTask() { } } - private fun buildNodeProcess(node: Node): Pair { - node.makeLogDirectory() - val process = ProcessBuilder(generateNodeInfoCommand()) + 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().toFile()) + .redirectOutput(node.logFile(logFile).toFile()) .addEnvironment("CAPSULE_CACHE_DIR", Node.capsuleCacheDir) .start() return Pair(node, process) @@ -260,7 +281,6 @@ open class Cordform : DefaultTask() { } } - private fun Node.logFile(): Path = this.logDirectory().resolve("generate-info.log") - + 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 a009df7c4d..89f425dd60 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 @@ -4,8 +4,6 @@ import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory import net.corda.cordform.CordformNode -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle import org.gradle.api.Project import java.io.File import java.nio.charset.StandardCharsets @@ -62,21 +60,6 @@ class Node(private val project: Project) : CordformNode() { config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) } - /** - * Set the network map address for this node. - * - * @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the - * Cordform task instead. - * @param networkMapAddress Network map node address. - * @param networkMapLegalName Network map node legal name. - */ - fun networkMapAddress(networkMapAddress: String, networkMapLegalName: String) { - val networkMapService = mutableMapOf() - networkMapService.put("address", networkMapAddress) - networkMapService.put("legalName", networkMapLegalName) - config = config.withValue("networkMapService", ConfigValueFactory.fromMap(networkMapService)) - } - /** * Enables SSH access on given port * @@ -104,14 +87,10 @@ class Node(private val project: Project) : CordformNode() { project.logger.error("Node has a null name - cannot create node") throw IllegalStateException("Node has a null name - cannot create node") } - - val dirName = try { - val o = X500Name(name).getRDNs(BCStyle.O) - if (o.isNotEmpty()) o.first().first.value.toString() else name - } catch (_ : IllegalArgumentException) { - // Can't parse as an X500 name, use the full string - name - } + // Parsing O= part directly because importing BouncyCastle provider in Cordformation causes problems + // with loading our custom X509EdDSAEngine. + val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=") + val dirName = organizationName ?: name nodeDir = File(rootDir.toFile(), dirName.replace("\\s", "")) } @@ -129,7 +108,7 @@ class Node(private val project: Project) : CordformNode() { * Installs the corda fat JAR to the node directory. */ private fun installCordaJar() { - val cordaJar = verifyAndGetCordaJar() + val cordaJar = verifyAndGetRuntimeJar("corda") project.copy { it.apply { from(cordaJar) @@ -144,7 +123,7 @@ class Node(private val project: Project) : CordformNode() { * Installs the corda webserver JAR to the node directory */ private fun installWebserverJar() { - val webJar = verifyAndGetWebserverJar() + val webJar = verifyAndGetRuntimeJar("corda-webserver") project.copy { it.apply { from(webJar) @@ -250,34 +229,17 @@ class Node(private val project: Project) : CordformNode() { } /** - * Find the corda JAR amongst the dependencies. + * Find the given JAR amongst the dependencies + * @param jarName JAR name without the version part, for example for corda-2.0-SNAPSHOT.jar provide only "corda" as jarName * - * @return A file representing the Corda JAR. + * @return A file representing found JAR */ - private fun verifyAndGetCordaJar(): File { - val maybeCordaJAR = project.configuration("runtime").filter { - it.toString().contains("corda-$releaseVersion.jar") || it.toString().contains("corda-enterprise-$releaseVersion.jar") - } - if (maybeCordaJAR.isEmpty) { - throw RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-$releaseVersion.jar\"") - } else { - val cordaJar = maybeCordaJAR.singleFile - assert(cordaJar.isFile) - return cordaJar - } - } - - /** - * Find the corda JAR amongst the dependencies - * - * @return A file representing the Corda webserver JAR - */ - private fun verifyAndGetWebserverJar(): File { + private fun verifyAndGetRuntimeJar(jarName: String): File { val maybeJar = project.configuration("runtime").filter { - it.toString().contains("corda-webserver-$releaseVersion.jar") + "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-enterprise-$releaseVersion.jar" in it.toString() } if (maybeJar.isEmpty) { - throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"") + throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"") } else { val jar = maybeJar.singleFile require(jar.isFile) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt new file mode 100644 index 0000000000..871b1e514f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt @@ -0,0 +1,75 @@ +package net.corda.nodeapi.internal + +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.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +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. +/** + * Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes. + */ +@CordaSerializable +data class NetworkMap(val nodeInfoHashes: List, val networkParameterHash: SecureHash) + +/** + * @property minimumPlatformVersion + * @property notaries + * @property eventHorizon + * @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 +@CordaSerializable +data class NetworkParameters( + val minimumPlatformVersion: Int, + val notaries: List, + val eventHorizon: Duration, + val maxMessageSize: Int, + val maxTransactionSize: Int, + val modifiedTime: Instant, + val epoch: Int +) { + init { + 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" } + } +} + +@CordaSerializable +data class NotaryInfo(val identity: Party, val validating: Boolean) + +/** + * A serialized [NetworkMap] and its signature and certificate. Enforces signature validity in order to deserialize the data + * contained within. + */ +@CordaSerializable +class SignedNetworkMap(val raw: SerializedBytes, val sig: 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) + return raw.deserialize() + } +} + +// TODO: This class should reside in the [DigitalSignature] class. +/** 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 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 new file mode 100644 index 0000000000..d464575a4f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersCopier.kt @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000000..185e8bda8f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt @@ -0,0 +1,111 @@ +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/NodeInfoFilesCopier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoFilesCopier.kt similarity index 99% rename from node-api/src/main/kotlin/net/corda/nodeapi/NodeInfoFilesCopier.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoFilesCopier.kt index 7017e89a56..00786c8da1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/NodeInfoFilesCopier.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoFilesCopier.kt @@ -1,4 +1,4 @@ -package net.corda.nodeapi +package net.corda.nodeapi.internal import net.corda.cordform.CordformNode import net.corda.core.internal.ThreadBox diff --git a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt similarity index 81% rename from node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt index 4bb48e34c9..dd9c37bcba 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt @@ -1,4 +1,4 @@ -package net.corda.node.utilities +package net.corda.nodeapi.internal import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.generateKeyPair @@ -11,35 +11,40 @@ import net.corda.core.utilities.trace import net.corda.nodeapi.internal.crypto.* import org.slf4j.LoggerFactory import java.nio.file.Path +import java.security.cert.X509Certificate object ServiceIdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) + /** * Generates signing key pairs and a common distributed service identity for a set of nodes. * The key pairs and the group identity get serialized to disk in the corresponding node directories. * This method should be called *before* any of the nodes are started. * * @param dirs List of node directories to place the generated identity and key pairs in. - * @param serviceName The legal name of the distributed service, with service id as CN. + * @param serviceName The legal name of the distributed service. * @param threshold The threshold for the generated group [CompositeKey]. + * @param customRootCert the certificate to use a Corda root CA. If not specified the one in + * certificates/cordadevcakeys.jks is used. */ fun generateToDisk(dirs: List, serviceName: CordaX500Name, - threshold: Int = 1): Party { + serviceId: String, + threshold: Int = 1, + customRootCert: X509Certificate? = null): Party { log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) - val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val issuer = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") - val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) keyPairs.zip(dirs) { keyPair, dir -> val serviceKeyCert = X509Utilities.createCertificate(CertificateType.NODE_CA, issuer.certificate, issuer.keyPair, serviceName, keyPair.public) val compositeKeyCert = X509Utilities.createCertificate(CertificateType.NODE_CA, issuer.certificate, issuer.keyPair, serviceName, notaryKey) val certPath = (dir / "certificates").createDirectories() / "distributedService.jks" val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass") - val serviceId = serviceName.commonName keystore.setCertificateEntry("$serviceId-composite-key", compositeKeyCert.cert) keystore.setKeyEntry("$serviceId-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, issuer.certificate.cert, rootCert)) keystore.save(certPath, "cordacadevpass") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt index 2552e38cbb..7ba6f6268d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt @@ -18,7 +18,7 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) // Create new keys and store in keystore. val cert = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey) - val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(cert.cert) + clientCertPath) + val certPath = X509CertificateFactory().generateCertPath(cert.cert, *clientCertPath) require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } // TODO: X509Utilities.validateCertificateChain() return certPath diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index b739a4bc5c..273ed5fcba 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -4,8 +4,8 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureScheme import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.cert import net.corda.core.internal.read -import net.corda.core.internal.write import net.corda.core.internal.x500Name import net.corda.core.utilities.days import net.corda.core.utilities.millis @@ -27,10 +27,8 @@ import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.util.io.pem.PemReader -import java.io.FileWriter import java.io.InputStream import java.math.BigInteger -import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey @@ -153,7 +151,7 @@ object X509Utilities { require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) params.isRevocationEnabled = false - val certPath = X509CertificateFactory().delegate.generateCertPath(certificates.toList()) + val certPath = X509CertificateFactory().generateCertPath(*certificates) val pathValidator = CertPathValidator.getInstance("PKIX") pathValidator.validate(certPath, params) } @@ -164,7 +162,7 @@ object X509Utilities { * @param file Target file. */ @JvmStatic - fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, file: Path) { + fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, file: Path) { JcaPEMWriter(file.toFile().writer()).use { it.writeObject(x509Certificate) } @@ -176,14 +174,14 @@ object X509Utilities { * @return The X509Certificate that was encoded in the file. */ @JvmStatic - fun loadCertificateFromPEMFile(file: Path): X509CertificateHolder { - val cert = file.read { + fun loadCertificateFromPEMFile(file: Path): X509Certificate { + return file.read { val reader = PemReader(it.reader()) val pemObject = reader.readPemObject() - X509CertificateHolder(pemObject.content) + val certHolder = X509CertificateHolder(pemObject.content) + certHolder.isValidOn(Date()) + certHolder.cert } - cert.isValidOn(Date()) - return cert } /** @@ -310,9 +308,18 @@ object X509Utilities { */ class X509CertificateFactory { val delegate: CertificateFactory = CertificateFactory.getInstance("X.509") + fun generateCertificate(input: InputStream): X509Certificate { return delegate.generateCertificate(input) as X509Certificate } + + fun generateCertPath(certificates: List): CertPath { + return delegate.generateCertPath(certificates) + } + + fun generateCertPath(vararg certificates: Certificate): CertPath { + return delegate.generateCertPath(certificates.asList()) + } } enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index d4c98fc1e1..c41d3fe3ef 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -27,10 +27,8 @@ import java.util.* class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() { val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist) - /* - * These classes are assignment-compatible Java equivalents of Kotlin classes. - * The point is that we do not want to send Kotlin types "over the wire" via RPC. - */ + // These classes are assignment-compatible Java equivalents of Kotlin classes. + // The point is that we do not want to send Kotlin types "over the wire" via RPC. private val javaAliases: Map, Class<*>> = mapOf( listOf().javaClass to Collections.emptyList().javaClass, setOf().javaClass to Collections.emptySet().javaClass, @@ -176,7 +174,8 @@ class GlobalTransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableC } /** - * A whitelist that can be customised via the [net.corda.core.node.SerializationWhitelist], since it implements [MutableClassWhitelist]. + * A whitelist that can be customised via the [net.corda.core.serialization.SerializationWhitelist], + * since it implements [MutableClassWhitelist]. */ class TransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(Collections.synchronizedSet(mutableSetOf()), delegate) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt index 8a5cdbcaa3..2507678682 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt @@ -6,6 +6,7 @@ import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.SimpleString import rx.Notification import rx.exceptions.OnErrorNotImplementedException +import sun.security.x509.X509CertImpl import java.util.* /** @@ -49,8 +50,8 @@ object DefaultWhitelist : SerializationWhitelist { java.time.YearMonth::class.java, java.time.MonthDay::class.java, java.time.Period::class.java, - java.time.DayOfWeek::class.java, // No custom serialiser but it's an enum. - java.time.Month::class.java, // No custom serialiser but it's an enum. + java.time.DayOfWeek::class.java, // No custom serializer but it's an enum. + java.time.Month::class.java, // No custom serializer but it's an enum. java.util.Collections.emptyMap().javaClass, java.util.Collections.emptySet().javaClass, @@ -58,6 +59,9 @@ object DefaultWhitelist : SerializationWhitelist { java.util.LinkedHashMap::class.java, BitSet::class.java, OnErrorNotImplementedException::class.java, - StackTraceElement::class.java - ) + StackTraceElement::class.java, + + // Implementation of X509Certificate. + X509CertImpl::class.java + ) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt index 78ef9dc52a..3907a86fe8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt @@ -13,7 +13,7 @@ import org.apache.qpid.proton.amqp.UnsignedLong const val DESCRIPTOR_TOP_32BITS: Long = 0xc562L shl(32 + 16) /** - * AMQP desriptor ID's for our custom types. + * AMQP descriptor ID's for our custom types. * * NEVER DELETE OR CHANGE THE ID ASSOCIATED WITH A TYPE * diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializationScheme.kt index 4327d114ea..00a7825d06 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializationScheme.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.cordapp.Cordapp import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.DefaultWhitelist @@ -24,7 +25,8 @@ fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { } } -abstract class AbstractAMQPSerializationScheme : SerializationScheme { +abstract class AbstractAMQPSerializationScheme(val cordappLoader: List) : SerializationScheme { + companion object { private val serializationWhitelists: List by lazy { ServiceLoader.load(SerializationWhitelist::class.java, this::class.java.classLoader).toList() + DefaultWhitelist @@ -62,8 +64,15 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.ContractAttachmentSerializer(this)) } - for (whitelistProvider in serializationWhitelists) + for (whitelistProvider in serializationWhitelists) { factory.addToWhitelist(*whitelistProvider.whitelist.toTypedArray()) + } + + for (loader in cordappLoader) { + for (schema in loader.serializationCustomSerializers) { + factory.registerExternal(CorDappCustomSerializer(schema, factory)) + } + } } private val serializerFactoriesForContexts = ConcurrentHashMap, SerializerFactory>() @@ -97,11 +106,11 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { return SerializationOutput(serializerFactory).serialize(obj) } - protected fun canDeserializeVersion(byteSequence: ByteSequence): Boolean = AMQP_ENABLED && byteSequence == AmqpHeaderV1_0 + protected fun canDeserializeVersion(byteSequence: ByteSequence): Boolean = byteSequence == AmqpHeaderV1_0 } // TODO: This will eventually cover server RPC as well and move to node module, but for now this is not implemented -class AMQPServerSerializationScheme : AbstractAMQPSerializationScheme() { +class AMQPServerSerializationScheme(cordapps: List = emptyList()) : AbstractAMQPSerializationScheme(cordapps) { override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { throw UnsupportedOperationException() } @@ -118,7 +127,7 @@ class AMQPServerSerializationScheme : AbstractAMQPSerializationScheme() { } // TODO: This will eventually cover client RPC as well and move to client module, but for now this is not implemented -class AMQPClientSerializationScheme : AbstractAMQPSerializationScheme() { +class AMQPClientSerializationScheme(cordapps: List = emptyList()) : AbstractAMQPSerializationScheme(cordapps) { override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt index 732723a493..46046a88f2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt @@ -21,8 +21,8 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // id to generate it properly (it will always return [[[Ljava.lang.type -> type[][][] // for example). // - // We *need* to retain knowledge for AMQP deserialisation weather that lowest primitive - // was boxed or unboxed so just infer it recursively + // We *need* to retain knowledge for AMQP deserialization weather that lowest primitive + // was boxed or unboxed so just infer it recursively. private fun calcTypeName(type: Type): String = if (type.componentType().isArray()) { val typeName = calcTypeName(type.componentType()); "$typeName[]" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt new file mode 100644 index 0000000000..72373488b3 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt @@ -0,0 +1,86 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.internal.uncheckedCast +import net.corda.core.serialization.SerializationCustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.codec.Data +import java.io.NotSerializableException +import java.lang.reflect.Type +import kotlin.reflect.jvm.javaType +import kotlin.reflect.jvm.jvmErasure + +/** + * Index into the types list of the parent type of the serializer object, should be the + * type that this object proxies for + */ +const val CORDAPP_TYPE = 0 + +/** + * Index into the types list of the parent type of the serializer object, should be the + * type of the proxy object that we're using to represent the object we're proxying for + */ +const val PROXY_TYPE = 1 + +/** + * Wrapper class for user provided serializers + * + * Through the CorDapp JAR scanner we will have a list of custom serializer types that implement + * the toProxy and fromProxy methods. This class takes an instance of one of those objects and + * embeds it within a serialization context associated with a serializer factory by creating + * and instance of this class and registering that with a [SerializerFactory] + * + * Proxy serializers should transform an unserializable class into a representation that we can serialize + * + * @property serializer in instance of a user written serialization proxy, normally scanned and loaded + * automatically + * @property type the Java [Type] of the class which this serializes, inferred via reflection of the + * [serializer]'s super type + * @property proxyType the Java [Type] of the class into which instances of [type] are proxied for use by + * the underlying serialization engine + * + * @param factory a [SerializerFactory] belonging to the context this serializer is being instantiated + * for + */ +class CorDappCustomSerializer( + private val serializer: SerializationCustomSerializer<*, *>, + factory: SerializerFactory) : AMQPSerializer, SerializerFor { + override val revealSubclassesInSchema: Boolean get() = false + private val types = serializer::class.supertypes.filter { it.jvmErasure == SerializationCustomSerializer::class } + .flatMap { it.arguments } + .map { it.type!!.javaType } + + init { + if (types.size != 2) { + throw NotSerializableException("Unable to determine serializer parent types") + } + } + + override val type = types[CORDAPP_TYPE] + val proxyType = types[PROXY_TYPE] + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(type)}") + val descriptor: Descriptor = Descriptor(typeDescriptor) + private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyType, factory) } + + override fun writeClassInfo(output: SerializationOutput) {} + + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + val proxy = uncheckedCast, + SerializationCustomSerializer>(serializer).toProxy(obj) + + data.withDescribed(descriptor) { + data.withList { + for (property in proxySerializer.propertySerializers) { + property.writeProperty(proxy, this, output) + } + } + } + } + + override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput) = + uncheckedCast, SerializationCustomSerializer>( + serializer).fromProxy(uncheckedCast(proxySerializer.readObject(obj, schemas, input)))!! + + override fun isSerializerFor(clazz: Class<*>) = clazz == type +} + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 870bfbaccc..06cef5b6cd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -6,22 +6,27 @@ import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type +interface SerializerFor { + /** + * This method should return true if the custom serializer can serialize an instance of the class passed as the + * parameter. + */ + fun isSerializerFor(clazz: Class<*>): Boolean + + val revealSubclassesInSchema: Boolean +} + /** * Base class for serializers of core platform types that do not conform to the usual serialization rules and thus * cannot be automatically serialized. */ -abstract class CustomSerializer : AMQPSerializer { +abstract class CustomSerializer : AMQPSerializer, SerializerFor { /** * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects * that refer to other custom types etc. */ open val additionalSerializers: Iterable> = emptyList() - /** - * This method should return true if the custom serializer can serialize an instance of the class passed as the - * parameter. - */ - abstract fun isSerializerFor(clazz: Class<*>): Boolean protected abstract val descriptor: Descriptor /** @@ -33,7 +38,7 @@ abstract class CustomSerializer : AMQPSerializer { /** * Whether subclasses using this serializer via inheritance should have a mapping in the schema. */ - open val revealSubclassesInSchema: Boolean = false + override val revealSubclassesInSchema: Boolean get() = false override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { data.withDescribed(descriptor) { @@ -147,8 +152,8 @@ abstract class CustomSerializer : AMQPSerializer { * * @param clazz The type to be marshalled * @param withInheritance Whether subclasses of the class can also be marshalled. - * @param make A lambda for constructing an instance, that defaults to calling a constructor that expects a string. - * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. + * @param maker A lambda for constructing an instance, that defaults to calling a constructor that expects a string. + * @param unmaker A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ abstract class ToString(clazz: Class, withInheritance: Boolean = false, private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt index 411e405aad..797943cf93 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt @@ -10,8 +10,8 @@ import kotlin.reflect.full.findAnnotation import kotlin.reflect.jvm.javaType /** - * Serializer for deserialising objects whose definition has changed since they - * were serialised + * Serializer for deserializing objects whose definition has changed since they + * were serialised. */ class EvolutionSerializer( clazz: Type, @@ -38,16 +38,16 @@ class EvolutionSerializer( companion object { /** - * Unlike the generic deserialisation case where we need to locate the primary constructor + * Unlike the generic deserialization case where we need to locate the primary constructor * for the object (or our best guess) in the case of an object whose structure has changed - * since serialisation we need to attempt to locate a constructor that we can use. I.e. - * it's parameters match the serialised members and it will initialise any newly added - * elements + * since serialisation we need to attempt to locate a constructor that we can use. For example, + * its parameters match the serialised members and it will initialise any newly added + * elements. * * TODO: Type evolution * TODO: rename annotation */ - internal fun getEvolverConstructor(type: Type, oldArgs: Map): KFunction? { + private fun getEvolverConstructor(type: Type, oldArgs: Map): KFunction? { val clazz: Class<*> = type.asClass()!! if (!isConcrete(clazz)) return null @@ -70,13 +70,15 @@ class EvolutionSerializer( } /** - * Build a serialization object for deserialisation only of objects serialised - * as different versions of a class + * Build a serialization object for deserialization only of objects serialised + * as different versions of a class. * * @param old is an object holding the schema that represents the object * as it was serialised and the type descriptor of that type * @param new is the Serializer built for the Class as it exists now, not * how it was serialised and persisted. + * @param factory the [SerializerFactory] associated with the serialization + * context this serializer is being built for */ fun make(old: CompositeType, new: ObjectSerializer, factory: SerializerFactory): AMQPSerializer { @@ -117,7 +119,7 @@ class EvolutionSerializer( * to the object list of values we need to map that list, which is ordered per the * constructor of the original state of the object, we need to map the new parameter order * of the current constructor onto that list inserting nulls where new parameters are - * encountered + * encountered. * * TODO: Object references */ diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 6a1377d083..8b677630d2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -82,11 +82,13 @@ private fun propertiesForSerializationFromConstructor(kotlinConstructo for (param in kotlinConstructor.parameters) { val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") val matchingProperty = properties[name] ?: - throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'." + - " If using Java, check that you have the -parameters option specified in the Java compiler.") + throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'. " + + "If using Java, check that you have the -parameters option specified in the Java compiler. " + + "Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option") // Check that the method has a getter in java. - val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." + - " If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.") + val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz. " + + "If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler." + + "Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option") val returnType = resolveTypeVariables(getter.genericReturnType, type) if (constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param)) { rc += PropertySerializer.make(name, getter, returnType, factory) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 39abcc58f5..0dbc3f8be9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -36,7 +36,7 @@ data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() - private val customSerializers = CopyOnWriteArrayList>() + private val customSerializers = CopyOnWriteArrayList() val transformsCache = ConcurrentHashMap>>() open val classCarpenter = ClassCarpenter(cl, whitelist) @@ -196,9 +196,16 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { } } + fun registerExternal(customSerializer: CorDappCustomSerializer) { + if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) { + customSerializers += customSerializer + serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer + } + } + /** - * Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd - * if not use the [ClassCarpenter] to generate a class to use in it's place + * Iterate over an AMQP schema, for each type ascertain whether it's on ClassPath of [classloader] and, + * if not, use the [ClassCarpenter] to generate a class to use in it's place. */ private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) { val metaSchema = CarpenterMetaSchema.newInstance() @@ -267,11 +274,13 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { for (customSerializer in customSerializers) { if (customSerializer.isSerializerFor(clazz)) { val declaredSuperClass = declaredType.asClass()?.superclass - if (declaredSuperClass == null || !customSerializer.isSerializerFor(declaredSuperClass) || !customSerializer.revealSubclassesInSchema) { - return customSerializer + return if (declaredSuperClass == null + || !customSerializer.isSerializerFor(declaredSuperClass) + || !customSerializer.revealSubclassesInSchema) { + customSerializer as? AMQPSerializer } else { // Make a subclass serializer for the subclass and return that... - return CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer)) + CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer)) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformTypes.kt similarity index 100% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformTypes.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt index 0bbdf01033..6117695b77 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt @@ -6,6 +6,7 @@ import net.corda.nodeapi.internal.serialization.amqp.CompositeType import net.corda.nodeapi.internal.serialization.amqp.RestrictedType import net.corda.nodeapi.internal.serialization.amqp.Field as AMQPField import net.corda.nodeapi.internal.serialization.amqp.Schema as AMQPSchema +import net.corda.core.serialization.SerializationContext fun AMQPSchema.carpenterSchema(classloader: ClassLoader): CarpenterMetaSchema { val rtn = CarpenterMetaSchema.newInstance() @@ -34,7 +35,7 @@ fun AMQPField.typeAsString() = if (type == "*") requires[0] else type * b) add the class to the dependency tree in [carpenterSchemas] if it cannot be instantiated * at this time * - * @param classloader the class loader provided dby the [SerializationContext] + * @param classloader the class loader provided by the [SerializationContext] * @param carpenterSchemas structure that holds the dependency tree and list of classes that * need constructing * @param force by default a schema is not added to [carpenterSchemas] if it already exists @@ -121,7 +122,8 @@ val typeStrToType: Map, Class> = mapOf( fun AMQPField.getTypeAsClass(classloader: ClassLoader) = typeStrToType[Pair(type, mandatory)] ?: when (type) { "string" -> String::class.java - "*" -> classloader.loadClass(requires[0]) + "binary" -> ByteArray::class.java + "*" -> if (requires.isEmpty()) Any::class.java else classloader.loadClass(requires[0]) else -> classloader.loadClass(type) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt index 352ad498da..a5de6bf2b0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt @@ -9,7 +9,7 @@ enum class SchemaFlags { } /** - * A Schema is the representation of an object the Carpenter can contsruct + * A Schema is the representation of an object the Carpenter can construct * * Known Sub Classes * - [ClassSchema] @@ -62,7 +62,7 @@ fun EnumMap.simpleFieldAccess(): Boolean { } /** - * Represents a concrete object + * Represents a concrete object. */ class ClassSchema( name: String, @@ -77,7 +77,7 @@ class ClassSchema( /** * Represents an interface. Carpented interfaces can be used within [ClassSchema]s - * if that class should be implementing that interface + * if that class should be implementing that interface. */ class InterfaceSchema( name: String, @@ -91,7 +91,7 @@ class InterfaceSchema( } /** - * Represents an enumerated type + * Represents an enumerated type. */ class EnumSchema( name: String, @@ -111,8 +111,8 @@ class EnumSchema( } /** - * Factory object used by the serialiser when building [Schema]s based - * on an AMQP schema + * Factory object used by the serializer when building [Schema]s based + * on an AMQP schema. */ object CarpenterSchemaFactory { fun newInstance( diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt index 7d69264a43..5470dd2cf0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt @@ -44,6 +44,7 @@ import org.objenesis.strategy.StdInstantiatorStrategy import org.slf4j.Logger import sun.security.ec.ECPublicKeyImpl import sun.security.provider.certpath.X509CertPath +import sun.security.x509.X509CertImpl import java.io.BufferedInputStream import java.io.ByteArrayOutputStream import java.io.FileInputStream @@ -75,6 +76,7 @@ object DefaultKryoCustomizer { addDefaultSerializer(InputStream::class.java, InputStreamSerializer) addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) addDefaultSerializer(Logger::class.java, LoggerSerializer) + addDefaultSerializer(X509Certificate::class.java, X509CertificateSerializer) // WARNING: reordering the registrations here will cause a change in the serialized form, since classes // with custom serializers get written as registration ids. This will break backwards-compatibility. @@ -108,7 +110,6 @@ object DefaultKryoCustomizer { register(FileInputStream::class.java, InputStreamSerializer) register(CertPath::class.java, CertPathSerializer) register(X509CertPath::class.java, CertPathSerializer) - register(X509Certificate::class.java, X509CertificateSerializer) register(BCECPrivateKey::class.java, PrivateKeySerializer) register(BCECPublicKey::class.java, publicKeySerializer) register(BCRSAPrivateCrtKey::class.java, PrivateKeySerializer) diff --git a/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks b/node-api/src/main/resources/certificates/cordadevcakeys.jks similarity index 100% rename from node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks rename to node-api/src/main/resources/certificates/cordadevcakeys.jks diff --git a/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks b/node-api/src/main/resources/certificates/cordatruststore.jks similarity index 100% rename from node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks rename to node-api/src/main/resources/certificates/cordatruststore.jks diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt index 562717e641..c14f2c854d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/NodeInfoFilesCopierTest.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi import net.corda.cordform.CordformNode +import net.corda.nodeapi.internal.NodeInfoFilesCopier import net.corda.testing.eventually import org.junit.Before import org.junit.Rule 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 7ed2ff6527..ca4d2b2e7e 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 @@ -71,7 +71,7 @@ class X509UtilitiesTest { fun `load and save a PEM file certificate`() { val tmpCertificateFile = tempFile("cacert.pem") val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Test Cert", organisation = "R3 Ltd", locality = "London", country = "GB"), caKey) + val caCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Test Cert", organisation = "R3 Ltd", locality = "London", country = "GB"), caKey).cert X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile) val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) assertEquals(caCert, readCertificate) @@ -433,7 +433,7 @@ class X509UtilitiesTest { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey) val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name.x500Name, BOB_PUBKEY) - val expected = X509CertificateFactory().delegate.generateCertPath(listOf(certificate.cert, rootCACert.cert)) + val expected = X509CertificateFactory().generateCertPath(certificate.cert, rootCACert.cert) val serialized = expected.serialize(factory, context).bytes val actual: CertPath = serialized.deserialize(factory, context) assertEquals(expected, actual) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappSerializerTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappSerializerTests.kt new file mode 100644 index 0000000000..d809f7beda --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappSerializerTests.kt @@ -0,0 +1,148 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.junit.Test +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationCustomSerializer +import org.assertj.core.api.Assertions +import java.io.NotSerializableException +import kotlin.test.assertEquals + +class CorDappSerializerTests { + data class NeedsProxy (val a: String) + + class NeedsProxyProxySerializer : SerializationCustomSerializer { + data class Proxy(val proxy_a_: String) + + override fun fromProxy(proxy: Proxy) = NeedsProxy(proxy.proxy_a_) + override fun toProxy(obj: NeedsProxy) = Proxy(obj.a) + } + + // Standard proxy serializer used internally, here for comparison purposes + class InternalProxySerializer(factory: SerializerFactory) : + CustomSerializer.Proxy ( + NeedsProxy::class.java, + InternalProxySerializer.Proxy::class.java, + factory) { + data class Proxy(val proxy_a_: String) + + override fun toProxy(obj: NeedsProxy): Proxy { + return Proxy(obj.a) + } + + override fun fromProxy(proxy: Proxy): NeedsProxy { + return NeedsProxy(proxy.proxy_a_) + } + } + + @Test + fun `type uses proxy`() { + val internalProxyFactory = testDefaultFactory() + val proxyFactory = testDefaultFactory() + val defaultFactory = testDefaultFactory() + + val msg = "help" + + proxyFactory.registerExternal (CorDappCustomSerializer(NeedsProxyProxySerializer(), proxyFactory)) + internalProxyFactory.register (InternalProxySerializer(internalProxyFactory)) + + val needsProxy = NeedsProxy(msg) + + val bAndSProxy = SerializationOutput(proxyFactory).serializeAndReturnSchema (needsProxy) + val bAndSInternal = SerializationOutput(internalProxyFactory).serializeAndReturnSchema (needsProxy) + val bAndSDefault = SerializationOutput(defaultFactory).serializeAndReturnSchema (needsProxy) + + val objFromDefault = DeserializationInput(defaultFactory).deserializeAndReturnEnvelope(bAndSDefault.obj) + val objFromInternal = DeserializationInput(internalProxyFactory).deserializeAndReturnEnvelope(bAndSInternal.obj) + val objFromProxy = DeserializationInput(proxyFactory).deserializeAndReturnEnvelope(bAndSProxy.obj) + + assertEquals(msg, objFromDefault.obj.a) + assertEquals(msg, objFromInternal.obj.a) + assertEquals(msg, objFromProxy.obj.a) + } + + @Test + fun proxiedTypeIsNested() { + data class A (val a: Int, val b: NeedsProxy) + + val factory = testDefaultFactory() + factory.registerExternal (CorDappCustomSerializer(NeedsProxyProxySerializer(), factory)) + + val tv1 = 100 + val tv2 = "pants schmants" + val bAndS = SerializationOutput(factory).serializeAndReturnSchema (A(tv1, NeedsProxy(tv2))) + + val objFromDefault = DeserializationInput(factory).deserializeAndReturnEnvelope(bAndS.obj) + + assertEquals(tv1, objFromDefault.obj.a) + assertEquals(tv2, objFromDefault.obj.b.a) + } + + @Test + fun testWithWhitelistNotAllowed() { + data class A (val a: Int, val b: NeedsProxy) + + class WL : ClassWhitelist { + private val allowedClasses = emptySet() + + override fun hasListed(type: Class<*>): Boolean = type.name in allowedClasses + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + factory.registerExternal (CorDappCustomSerializer(NeedsProxyProxySerializer(), factory)) + + val tv1 = 100 + val tv2 = "pants schmants" + Assertions.assertThatThrownBy { + SerializationOutput(factory).serialize(A(tv1, NeedsProxy(tv2))) + }.isInstanceOf(NotSerializableException::class.java) + } + + @Test + fun testWithWhitelistAllowed() { + data class A (val a: Int, val b: NeedsProxy) + + class WL : ClassWhitelist { + private val allowedClasses = hashSetOf( + A::class.java.name, + NeedsProxy::class.java.name) + + override fun hasListed(type: Class<*>): Boolean = type.name in allowedClasses + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + factory.registerExternal (CorDappCustomSerializer(NeedsProxyProxySerializer(), factory)) + + val tv1 = 100 + val tv2 = "pants schmants" + val obj = DeserializationInput(factory).deserialize( + SerializationOutput(factory).serialize(A(tv1, NeedsProxy(tv2)))) + + assertEquals(tv1, obj.a) + assertEquals(tv2, obj.b.a) + } + + // The custom type not being whitelisted won't matter here because the act of adding a + // custom serializer bypasses the whitelist + @Test + fun testWithWhitelistAllowedOuterOnly() { + data class A (val a: Int, val b: NeedsProxy) + + class WL : ClassWhitelist { + // explicitly don't add NeedsProxy + private val allowedClasses = hashSetOf(A::class.java.name) + + override fun hasListed(type: Class<*>): Boolean = type.name in allowedClasses + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + factory.registerExternal (CorDappCustomSerializer(NeedsProxyProxySerializer(), factory)) + + val tv1 = 100 + val tv2 = "pants schmants" + val obj = DeserializationInput(factory).deserialize( + SerializationOutput(factory).serialize(A(tv1, NeedsProxy(tv2)))) + + assertEquals(tv1, obj.a) + assertEquals(tv2, obj.b.a) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt new file mode 100644 index 0000000000..9882c79b41 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt @@ -0,0 +1,114 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.SerializedBytes +import net.corda.nodeapi.internal.serialization.AllWhitelist +import org.junit.Test +import kotlin.test.assertEquals + +class GenericsTests { + + @Test + fun nestedSerializationOfGenerics() { + data class G(val a: T) + data class Wrapper(val a: Int, val b: G) + + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val altContextFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val ser = SerializationOutput(factory) + + val bytes = ser.serializeAndReturnSchema(G("hi")) + + assertEquals("hi", DeserializationInput(factory).deserialize(bytes.obj).a) + assertEquals("hi", DeserializationInput(altContextFactory).deserialize(bytes.obj).a) + + val bytes2 = ser.serializeAndReturnSchema(Wrapper(1, G("hi"))) + + DeserializationInput(factory).deserialize(bytes2.obj).apply { + assertEquals(1, a) + assertEquals("hi", b.a) + } + + DeserializationInput(altContextFactory).deserialize(bytes2.obj).apply { + assertEquals(1, a) + assertEquals("hi", b.a) + } + } + + @Test + fun nestedGenericsReferencesByteArrayViaSerializedBytes() { + data class G(val a : Int) + data class Wrapper(val a: Int, val b: SerializedBytes) + + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val ser = SerializationOutput(factory) + + val gBytes = ser.serialize(G(1)) + val bytes2 = ser.serializeAndReturnSchema(Wrapper(1, gBytes)) + + DeserializationInput(factory).deserialize(bytes2.obj).apply { + assertEquals(1, a) + assertEquals(1, DeserializationInput(factory).deserialize(b).a) + } + DeserializationInput(factory2).deserialize(bytes2.obj).apply { + assertEquals(1, a) + assertEquals(1, DeserializationInput(factory).deserialize(b).a) + } + } + + @Test + fun nestedSerializationInMultipleContextsDoesntColideGenericTypes() { + data class InnerA(val a_a: Int) + data class InnerB(val a_b: Int) + data class InnerC(val a_c: String) + data class Container(val b: T) + data class Wrapper(val c: Container) + + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val factories = listOf(factory, SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) + val ser = SerializationOutput(factory) + + ser.serialize(Wrapper(Container(InnerA(1)))).apply { + factories.forEach { + DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_a) } + } + } + + ser.serialize(Wrapper(Container(InnerB(1)))).apply { + factories.forEach { + DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_b) } + } + } + + ser.serialize(Wrapper(Container(InnerC("Ho ho ho")))).apply { + factories.forEach { + DeserializationInput(it).deserialize(this).apply { assertEquals("Ho ho ho", c.b.a_c) } + } + } + } + + @Test + fun nestedSerializationWhereGenericDoesntImpactFingerprint() { + data class Inner(val a : Int) + data class Container(val b: Inner) + data class Wrapper(val c: Container) + + val factorys = listOf( + SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()), + SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) + + val ser = SerializationOutput(factorys[0]) + + ser.serialize(Wrapper(Container(Inner(1)))).apply { + factorys.forEach { + assertEquals(1, DeserializationInput(it).deserialize(this).c.b.a) + } + } + + ser.serialize(Wrapper(Container(Inner(1)))).apply { + factorys.forEach { + assertEquals(1, DeserializationInput(it).deserialize(this).c.b.a) + } + } + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/OverridePKSerializerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/OverridePKSerializerTest.kt index 108f2328f4..2b6ee350f6 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/OverridePKSerializerTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/OverridePKSerializerTest.kt @@ -25,7 +25,7 @@ class OverridePKSerializerTest { get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. } - class AMQPTestSerializationScheme : AbstractAMQPSerializationScheme() { + class AMQPTestSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } 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 04c390202d..9a987f1b8a 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 @@ -593,7 +593,7 @@ class SerializationOutputTests { fun `test transaction state`() { val state = TransactionState(FooState(), FOO_PROGRAM_ID, MEGA_CORP) - val scheme = AMQPServerSerializationScheme() + val scheme = AMQPServerSerializationScheme(emptyList()) val func = scheme::class.superclasses.single { it.simpleName == "AbstractAMQPSerializationScheme" } .java.getDeclaredMethod("registerCustomSerializers", SerializerFactory::class.java) func.isAccessible = true 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 ad4816c57d..a8464b7375 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 @@ -13,6 +13,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div +import net.corda.core.node.services.NotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NetworkHostAndPort @@ -21,12 +22,15 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig -import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas +import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.NotaryInfo import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.IntegrationTest import net.corda.testing.chooseIdentity +import net.corda.nodeapi.internal.NetworkParametersCopier +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -68,19 +72,26 @@ class BFTNotaryServiceTests : IntegrationTest() { notary = ServiceIdentityGenerator.generateToDisk( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, - CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") - ) + CordaX500Name("BFT", "Zurich", "CH"), + NotaryService.constructId(validating = false, bft = true)) + + val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notary, false)))) val clusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } - replicaIds.forEach { replicaId -> - mockNet.createNode(MockNodeParameters(configOverrides = { + val nodes = replicaIds.map { replicaId -> + mockNet.createUnstartedNode(MockNodeParameters(configOverrides = { val notary = NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces)) doReturn(notary).whenever(it).notary })) - } + } + mockNet.createUnstartedNode() - node = mockNet.createNode() + // MockNetwork doesn't support BFT clusters, so we create all the nodes we need unstarted, and then install the + // network-parameters in their directories before they're started. + node = nodes.map { node -> + networkParameters.install(mockNet.baseDirectory(node.id)) + node.start() + }.last() } /** Failure mode is the redundant replica gets stuck in startup, so we can't dispose it cleanly at the end. */ diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 595ecb0532..155565cfca 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -13,7 +13,6 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.NodeHandle @@ -47,7 +46,7 @@ class DistributedServiceTests : IntegrationTest() { driver( extraCordappPackagesToScan = listOf("net.corda.finance.contracts"), - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name.copy(commonName = RaftValidatingNotaryService.id), rpcUsers = listOf(testUser), cluster = ClusterSpec.Raft(clusterSize = 3)))) + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name, rpcUsers = listOf(testUser), cluster = ClusterSpec.Raft(clusterSize = 3)))) { alice = startNode(providedName = ALICE.name, rpcUsers = listOf(testUser)).getOrThrow() raftNotaryIdentity = defaultNotaryIdentity 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 new file mode 100644 index 0000000000..8cd555f231 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -0,0 +1,100 @@ +package net.corda.node.services.network + +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.seconds +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.internal.CompatibilityZoneParams +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.internalDriver +import net.corda.testing.node.network.NetworkMapServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.net.URL + +class NetworkMapTest { + private val cacheTimeout = 1.seconds + private val portAllocation = PortAllocation.Incremental(10000) + + private lateinit var networkMapServer: NetworkMapServer + private lateinit var compatibilityZone: CompatibilityZoneParams + + @Before + fun start() { + networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort()) + val address = networkMapServer.start() + compatibilityZone = CompatibilityZoneParams(URL("http://$address")) + } + + @After + fun cleanUp() { + networkMapServer.close() + } + + @Test + fun `nodes can see each other using the http network map`() { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) { + val alice = startNode(providedName = ALICE.name) + val bob = startNode(providedName = BOB.name) + + val notaryNode = defaultNotaryNode.get() + val aliceNode = alice.get() + val bobNode = bob.get() + + notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + } + } + + @Test + fun `nodes process network map add updates correctly when adding new node to network map`() { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) { + val alice = startNode(providedName = ALICE.name) + val notaryNode = defaultNotaryNode.get() + val aliceNode = alice.get() + + notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo) + aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo) + + val bob = startNode(providedName = BOB.name) + val bobNode = bob.get() + + // Wait for network map client to poll for the next update. + Thread.sleep(cacheTimeout.toMillis() * 2) + + bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + } + } + + @Test + fun `nodes process network map remove updates correctly`() { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) { + val alice = startNode(providedName = ALICE.name) + val bob = startNode(providedName = BOB.name) + + val notaryNode = defaultNotaryNode.get() + val aliceNode = alice.get() + val bobNode = bob.get() + + notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) + + networkMapServer.removeNodeInfo(aliceNode.nodeInfo) + + // Wait for network map client to poll for the next update. + Thread.sleep(cacheTimeout.toMillis() * 2) + + notaryNode.onlySees(notaryNode.nodeInfo, bobNode.nodeInfo) + bobNode.onlySees(notaryNode.nodeInfo, bobNode.nodeInfo) + } + } + + private fun NodeHandle.onlySees(vararg nodes: NodeInfo) = assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes) +} 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 32716708de..72068d73fe 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 @@ -10,7 +10,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.serialize import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.nodeapi.NodeInfoFilesCopier +import net.corda.nodeapi.internal.NodeInfoFilesCopier import net.corda.testing.* import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.makeTestIdentityService 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 new file mode 100644 index 0000000000..6cd0058792 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -0,0 +1,158 @@ +package net.corda.node.utilities.registration + +import com.google.common.net.HostAndPort +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.cert +import net.corda.core.internal.toX509CertHolder +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.minutes +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.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.internal.CompatibilityZoneParams +import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.internalDriver +import net.corda.testing.node.network.NetworkMapServer +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.URL +import java.security.KeyPair +import java.security.cert.CertPath +import java.security.cert.Certificate +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +class NodeRegistrationTest { + private val portAllocation = PortAllocation.Incremental(13000) + private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate() + private val registrationHandler = RegistrationHandler(rootCertAndKeyPair) + + private lateinit var server: NetworkMapServer + private lateinit var serverHostAndPort: NetworkHostAndPort + + @Before + fun startServer() { + server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), registrationHandler) + serverHostAndPort = server.start() + } + + @After + fun stopServer() { + server.close() + } + + // TODO Ideally this test should be checking that two nodes that register are able to transact with each other. However + // starting a second node hangs so that needs to be fixed. + @Test + fun `node registration correct root cert`() { + val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = rootCertAndKeyPair.certificate.cert) + internalDriver( + portAllocation = portAllocation, + notarySpecs = emptyList(), + compatibilityZone = compatibilityZone + ) { + startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() + assertThat(registrationHandler.idsPolled).contains("Alice") + } + } + + @Test + fun `node registration wrong root cert`() { + val someCert = createSelfKeyAndSelfSignedCertificate().certificate.cert + val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someCert) + internalDriver( + portAllocation = portAllocation, + notarySpecs = emptyList(), + compatibilityZone = compatibilityZone, + // Changing the content of the truststore makes the node fail in a number of ways if started out process. + startNodesInProcess = true + ) { + assertThatThrownBy { + startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() + }.isInstanceOf(WrongRootCertException::class.java) + } + } + + private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair { + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate( + CordaX500Name( + commonName = "Integration Test Corda Node Root CA", + organisation = "R3 Ltd", + locality = "London", + country = "GB"), + rootCAKey) + return CertificateAndKeyPair(rootCACert, rootCAKey) + } +} + +@Path("certificate") +class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair) { + private val certPaths = HashMap() + val idsPolled = HashSet() + + @POST + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @Produces(MediaType.TEXT_PLAIN) + fun registration(input: InputStream): Response { + val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) } + val (certPath, name) = createSignedClientCertificate( + certificationRequest, + rootCertAndKeyPair.keyPair, + arrayOf(rootCertAndKeyPair.certificate.cert)) + certPaths[name.organisation] = certPath + return Response.ok(name.organisation).build() + } + + @GET + @Path("{id}") + fun reply(@PathParam("id") id: String): Response { + idsPolled += id + return buildResponse(certPaths[id]!!.certificates) + } + + private fun buildResponse(certificates: List): Response { + val baos = ByteArrayOutputStream() + ZipOutputStream(baos).use { zip -> + listOf(CORDA_CLIENT_CA, CORDA_INTERMEDIATE_CA, CORDA_ROOT_CA).zip(certificates).forEach { + zip.putNextEntry(ZipEntry("${it.first}.cer")) + zip.write(it.second.encoded) + zip.closeEntry() + } + } + return Response.ok(baos.toByteArray()) + .type("application/zip") + .header("Content-Disposition", "attachment; filename=\"certificates.zip\"").build() + } + + private fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest, + caKeyPair: KeyPair, + caCertPath: Array): Pair { + val request = JcaPKCS10CertificationRequest(certificationRequest) + val name = CordaX500Name.parse(request.subject.toString()) + val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.NODE_CA, + caCertPath.first().toX509CertHolder(), + caKeyPair, + name, + request.publicKey, + nameConstraints = null) + val certPath = X509CertificateFactory().generateCertPath(x509CertificateHolder.cert, *caCertPath) + return Pair(certPath, name) + } +} 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 671c352b44..fc2c15c746 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 @@ -89,11 +89,11 @@ class MQSecurityAsNodeTest : MQSecurityTest() { val legalName = MEGA_CORP.name certificatesDirectory.createDirectories() if (!trustStoreFile.exists()) { - javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) + javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").copyTo(trustStoreFile) } val caKeyStore = loadKeyStore( - javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), + javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder() 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 acfd0b17fb..4d34bf61ee 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -23,10 +23,7 @@ import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.* import net.corda.core.node.* import net.corda.core.node.services.* -import net.corda.core.serialization.SerializationWhitelist -import net.corda.core.serialization.SerializeAsToken -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.serialize +import net.corda.core.serialization.* import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug @@ -47,7 +44,6 @@ import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver -import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.messaging.MessagingService @@ -62,6 +58,7 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor +import net.corda.nodeapi.internal.NetworkParameters import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -96,6 +93,8 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair * Marked as SingletonSerializeAsToken to prevent the invisible reference to AbstractNode in the ServiceHub accidentally * sweeping up the Node into the Kryo checkpoint serialization via any flows holding a reference to ServiceHub. */ +// TODO Log warning if this node is a notary but not one of the ones specified in the network parameters, both for core and custom + // In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the // AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in. abstract class AbstractNode(val configuration: NodeConfiguration, @@ -125,6 +124,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // low-performance prototyping period. protected abstract val serverThread: AffinityExecutor + protected lateinit var networkParameters: NetworkParameters private val cordappServices = MutableClassToInstanceMap.create() private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() @@ -137,7 +137,11 @@ 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) } + protected val networkMapClient: NetworkMapClient? by lazy { + configuration.compatibilityZoneURL?.let { + NetworkMapClient(it, services.identityService.trustRoot) + } + } lateinit var securityManager: RPCSecurityManager get @@ -179,7 +183,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database -> - val persistentNetworkMapCache = PersistentNetworkMapCache(database) + // TODO The fact that we need to specify an empty list of notaries just to generate our node info looks like + // 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() @@ -193,12 +199,13 @@ 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) // 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), identityService) + val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService) val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair) identityService.loadIdentities(info.legalIdentitiesAndCerts) val transactionStorage = makeTransactionStorage(database) @@ -282,14 +289,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val keyPairs = mutableSetOf(identityKeyPair) myNotaryIdentity = configuration.notary?.let { - val (notaryIdentity, notaryIdentityKeyPair) = obtainIdentity(it) - keyPairs += notaryIdentityKeyPair - notaryIdentity + if (it.isClusterConfig) { + val (notaryIdentity, notaryIdentityKeyPair) = obtainIdentity(it) + keyPairs += notaryIdentityKeyPair + notaryIdentity + } else { + // In case of a single notary service myNotaryIdentity will be the node's single identity. + identity + } } var info = NodeInfo( myAddresses(), - listOf(identity, myNotaryIdentity).filterNotNull(), + setOf(identity, myNotaryIdentity).filterNotNull(), versionInfo.platformVersion, platformClock.instant().toEpochMilli() ) @@ -632,6 +644,13 @@ 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 makeCoreNotaryService(notaryConfig: NotaryConfig, database: CordaPersistence): NotaryService { val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service") return notaryConfig.run { @@ -687,22 +706,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) - val (id, singleName) = if (notaryConfig == null) { - // Node's main identity + val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { + // Node's main identity or if it's a single node notary Pair("identity", myLegalName) } else { val notaryId = notaryConfig.run { NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom) } - if (!notaryConfig.isClusterConfig) { - // Node's notary identity - Pair(notaryId, myLegalName.copy(commonName = notaryId)) - } else { - // The node is part of a distributed notary whose identity must already be generated beforehand - Pair(notaryId, null) - } + // The node is part of a distributed notary whose identity must already be generated beforehand. + Pair(notaryId, null) } - // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" @@ -740,7 +753,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject") } - val certPath = X509CertificateFactory().delegate.generateCertPath(certificates) + val certPath = X509CertificateFactory().generateCertPath(certificates) return Pair(PartyAndCertificate(certPath), keyPair) } 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 0396b0ef8b..df43650e8e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -194,11 +194,17 @@ open class Node(configuration: NodeConfiguration, return if (!AddressUtils.isPublic(host)) { val foundPublicIP = AddressUtils.tryDetectPublicIP() if (foundPublicIP == null) { - val retrievedHostName = networkMapClient?.myPublicHostname() - if (retrievedHostName != null) { - log.info("Retrieved public IP from Network Map Service: $this. This will be used instead of the provided \"$host\" as the advertised address.") + try { + val retrievedHostName = networkMapClient?.myPublicHostname() + if (retrievedHostName != null) { + log.info("Retrieved public IP from Network Map Service: $this. This will be used instead of the provided \"$host\" as the advertised address.") + } + retrievedHostName + } catch (ignore: Throwable) { + // Cannot reach the network map service, ignore the exception and use provided P2P address instead. + log.warn("Cannot connect to the network map service for public IP detection.") + null } - retrievedHostName } else { log.info("Detected public IP: ${foundPublicIP.hostAddress}. This will be used instead of the provided \"$host\" as the advertised address.") foundPublicIP.hostAddress @@ -309,11 +315,11 @@ open class Node(configuration: NodeConfiguration, nodeSerializationEnv = SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) - registerScheme(AMQPServerSerializationScheme()) + registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) }, - KRYO_P2P_CONTEXT.withClassLoader(classloader), + p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader), - storageContext = KRYO_STORAGE_CONTEXT.withClassLoader(classloader), + storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader), checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader)) } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 4863471957..50f0140338 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -10,6 +10,7 @@ import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger @@ -175,15 +176,16 @@ class CordappLoader private constructor(private val cordappJarPaths: List> { + return scanResult.getClassesImplementing(SerializationCustomSerializer::class) + } + private fun findCustomSchemas(scanResult: RestrictedScanResult): Set { return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet() } @@ -303,6 +310,14 @@ class CordappLoader private constructor(private val cordappJarPaths: List getClassesImplementing(type: KClass): List { + return scanResult.getNamesOfClassesImplementing(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + .map { it.kotlin.objectOrNewInstance() } + } + fun getClassesWithAnnotation(type: KClass, annotation: KClass): List> { return scanResult.getNamesOfClassesWithAnnotation(annotation.java) .filter { it.startsWith(qualifiedNamePrefix) } diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 6c195810c2..2b05bd6db8 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -63,10 +63,10 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { certificatesDirectory.createDirectories() if (!trustStoreFile.exists()) { - loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword) + loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword) } if (!sslKeystore.exists() || !nodeKeystore.exists()) { - val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) // Move distributed service composite key (generated by ServiceIdentityGenerator.generateToDisk) to keystore if exists. 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 49576ac15d..d411e2ff31 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 @@ -129,7 +129,6 @@ data class NodeConfigurationImpl( // This is a sanity feature do not remove. require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" } require(devModeOptions == null || devMode) { "Cannot use devModeOptions outside of dev mode" } - require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" } require(security == null || rpcUsers.isEmpty()) { "Cannot specify both 'rpcUsers' and 'security' in configuration" } 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 e3f3cc233f..be682a7c05 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 @@ -69,7 +69,7 @@ class InMemoryIdentityService(identities: Iterable, if (firstCertWithThisName != identity.certificate) { val certificates = identity.certPath.certificates val idx = certificates.lastIndexOf(firstCertWithThisName) - val firstPath = X509CertificateFactory().delegate.generateCertPath(certificates.slice(idx until certificates.size)) + val firstPath = X509CertificateFactory().generateCertPath(certificates.slice(idx until certificates.size)) verifyAndRegisterIdentity(PartyAndCertificate(firstPath)) } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 471a1f51f5..6230d98453 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -133,7 +133,7 @@ class PersistentIdentityService(override val trustRoot: X509Certificate, if (firstCertWithThisName != identity.certificate) { val certificates = identity.certPath.certificates val idx = certificates.lastIndexOf(firstCertWithThisName) - val firstPath = X509CertificateFactory().delegate.generateCertPath(certificates.slice(idx until certificates.size)) + val firstPath = X509CertificateFactory().generateCertPath(certificates.slice(idx until certificates.size)) verifyAndRegisterIdentity(PartyAndCertificate(firstPath)) } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 619bb8c9cd..c4bc163ad2 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -37,7 +37,7 @@ fun freshCertificate(identityService: IdentityServiceInternal, val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCert) val ourCertificate = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCert.subject, issuerSigner, issuer.name, subjectPublicKey, window) - val ourCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) + val ourCertPath = X509CertificateFactory().generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) val anonymisedIdentity = PartyAndCertificate(ourCertPath) identityService.justVerifyAndRegisterIdentity(anonymisedIdentity) return anonymisedIdentity 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 9563bb9d3d..66fe9470bd 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 @@ -1,6 +1,5 @@ package net.corda.node.services.network -import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.util.concurrent.MoreExecutors import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData @@ -13,6 +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 okhttp3.CacheControl import okhttp3.Headers import rx.Subscription @@ -20,11 +23,12 @@ import java.io.BufferedReader import java.io.Closeable import java.net.HttpURLConnection import java.net.URL +import java.security.cert.X509Certificate import java.time.Duration import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -class NetworkMapClient(compatibilityZoneURL: URL) { +class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) { private val networkMapUrl = URL("$compatibilityZoneURL/network-map") fun publish(signedNodeInfo: SignedData) { @@ -42,14 +46,30 @@ class NetworkMapClient(compatibilityZoneURL: URL) { fun getNetworkMap(): NetworkMapResponse { val conn = networkMapUrl.openHttpConnection() - val response = conn.inputStream.bufferedReader().use(BufferedReader::readLine) - val networkMap = ObjectMapper().readValue(response, List::class.java).map { SecureHash.parse(it.toString()) } + 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 timeout = CacheControl.parse(Headers.of(conn.headerFields.filterKeys { it != null }.mapValues { it.value.first() })).maxAgeSeconds().seconds return NetworkMapResponse(networkMap, timeout) } fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo? { - val conn = URL("$networkMapUrl/$nodeInfoHash").openHttpConnection() + val conn = URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection() + 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 + } + } + + fun getNetworkParameter(networkParameterHash: SecureHash): NetworkParameters? { + val conn = URL("$networkMapUrl/network-parameter/$networkParameterHash").openHttpConnection() return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) { null } else { @@ -63,7 +83,7 @@ class NetworkMapClient(compatibilityZoneURL: URL) { } } -data class NetworkMapResponse(val networkMap: List, val cacheMaxAge: Duration) +data class NetworkMapResponse(val networkMap: NetworkMap, val cacheMaxAge: Duration) class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val fileWatcher: NodeInfoWatcher, @@ -108,21 +128,28 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, val nextScheduleDelay = try { val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap() val currentNodeHashes = networkMapCache.allNodeHashes - (networkMap - currentNodeHashes).mapNotNull { + val hashesFromNetworkMap = networkMap.nodeInfoHashes + (hashesFromNetworkMap - currentNodeHashes).mapNotNull { // Download new node info from network map - networkMapClient.getNodeInfo(it) + try { + networkMapClient.getNodeInfo(it) + } catch (t: Throwable) { + // 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) + null + } }.forEach { // Add new node info to the network map cache, these could be new node info or modification of node info for existing nodes. networkMapCache.addNode(it) } // Remove node info from network map. - (currentNodeHashes - networkMap - fileWatcher.processedNodeInfoHashes) + (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", t) + logger.warn("Error encountered while updating network map, will retry in ${retryInterval.seconds} seconds", t) retryInterval } // Schedule the next update. @@ -138,7 +165,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, try { networkMapClient.publish(signedNodeInfo) } catch (t: Throwable) { - logger.warn("Error encountered while publishing node info, will retry in $retryInterval.", t) + logger.warn("Error encountered while publishing node info, will retry in ${retryInterval.seconds} seconds.", t) // TODO: Exponential backoff? executor.schedule(this, retryInterval.toMillis(), TimeUnit.MILLISECONDS) } 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 df583c4ffe..771604ba6b 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -9,7 +9,7 @@ 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.NodeInfoFilesCopier +import net.corda.nodeapi.internal.NodeInfoFilesCopier import rx.Observable import rx.Scheduler import java.io.IOException 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 c68f1ece30..be52529785 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 @@ -14,7 +14,6 @@ import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.node.services.NotaryService import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize @@ -26,6 +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 org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject @@ -34,6 +34,7 @@ import java.sql.Connection import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.collections.HashMap +import kotlin.collections.HashSet class NetworkMapCacheImpl( networkMapCacheBase: NetworkMapCacheBaseInternal, @@ -72,13 +73,15 @@ class NetworkMapCacheImpl( * Extremely simple in-memory cache of the network map. */ @ThreadSafe -open class PersistentNetworkMapCache(private val database: CordaPersistence) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { +open class PersistentNetworkMapCache( + private val database: CordaPersistence, + notaries: List +) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { companion object { private val logger = contextLogger() } - // TODO Small explanation, partyNodes and registeredNodes is left in memory as it was before, because it will be removed in - // next PR that gets rid of services. These maps are used only for queries by service. + // TODO Cleanup registered and party nodes protected val registeredNodes: MutableMap = Collections.synchronizedMap(HashMap()) protected val partyNodes: MutableList get() = registeredNodes.map { it.value }.toMutableList() private val _changed = PublishSubject.create() @@ -92,22 +95,9 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence) : S override val nodeReady: CordaFuture get() = _registrationFuture private var _loadDBSuccess: Boolean = false override val loadDBSuccess get() = _loadDBSuccess - // TODO From the NetworkMapService redesign doc: Remove the concept of network services. - // As a temporary hack, just assume for now that every network has a notary service named "Notary Service" that can be looked up in the map. - // This should eliminate the only required usage of services. - // It is ensured on node startup when constructing a notary that the name contains "notary". - override val notaryIdentities: List - get() { - return partyNodes - .flatMap { - // TODO: validate notary identity certificates before loading into network map cache. - // Notary certificates have to be signed by the doorman directly - it.legalIdentities - } - .filter { it.name.commonName?.startsWith(NotaryService.ID_PREFIX) ?: false } - .toSet() // Distinct, because of distributed service nodes - .sortedBy { it.name.toString() } - } + + override val notaryIdentities: List = notaries.map { it.identity } + private val validatingNotaries = notaries.mapNotNullTo(HashSet()) { if (it.validating) it.identity else null } init { database.transaction { loadFromDB(session) } @@ -122,7 +112,7 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence) : S select(get(NodeInfoSchemaV1.PersistentNodeInfo::hash.name)) } } - session.createQuery(query).resultList.map { SecureHash.sha256(it) } + session.createQuery(query).resultList.map { SecureHash.parse(it) } } } @@ -138,7 +128,7 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence) : S } } - override fun isValidatingNotary(party: Party): Boolean = isNotary(party) && "validating" in party.name.commonName!! + override fun isValidatingNotary(party: Party): Boolean = party in validatingNotaries override fun getPartyInfo(party: Party): PartyInfo? { val nodes = database.transaction { queryByIdentityKey(session, party.owningKey) } @@ -310,7 +300,6 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence) : S id = 0, hash = nodeInfo.serialize().hash.toString(), addresses = nodeInfo.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) }, - // TODO Another ugly hack with special first identity... legalIdentitiesAndCerts = nodeInfo.legalIdentitiesAndCerts.mapIndexed { idx, elem -> NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0) }, diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 3b7b78a374..16590715fd 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -239,4 +239,4 @@ private fun readMap(buffer: BufferInput>, serializer: Seriali put(serializer.readObject(buffer), serializer.readObject(buffer)) } } -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index d9a22e30bc..f24cf56f11 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -12,6 +12,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject import java.io.StringWriter +import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.cert.Certificate @@ -30,6 +31,23 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v private val keystorePassword = config.keyStorePassword // TODO: Use different password for private key. private val privateKeyPassword = config.keyStorePassword + private val trustStore: KeyStore + private val rootCert: Certificate + + init { + require(config.trustStoreFile.exists()) { + "${config.trustStoreFile} does not exist. This file must contain the root CA cert of your compatibility zone. " + + "Please contact your CZ operator." + } + trustStore = loadKeyStore(config.trustStoreFile, config.trustStorePassword) + val rootCert = trustStore.getCertificate(CORDA_ROOT_CA) + require(rootCert != null) { + "${config.trustStoreFile} does not contain a certificate with the key $CORDA_ROOT_CA." + + "This file must contain the root CA cert of your compatibility zone. " + + "Please contact your CZ operator." + } + this.rootCert = rootCert + } /** * Ensure the initial keystore for a node is set up. @@ -74,13 +92,14 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA, keyPair.private, privateKeyPassword.toCharArray(), certificates) caKeyStore.deleteEntry(SELF_SIGNED_PRIVATE_KEY) caKeyStore.save(config.nodeKeystore, keystorePassword) - // Save root certificates to trust store. - val trustStore = loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword) - // Assumes certificate chain always starts with client certificate and end with root certificate. - trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last()) - trustStore.save(config.trustStoreFile, config.trustStorePassword) println("Node private key and certificate stored in ${config.nodeKeystore}.") + // Check that the root of the signed certificate matches the expected certificate in the truststore. + if (rootCert != certificates.last()) { + // Assumes certificate chain always starts with client certificate and end with root certificate. + throw WrongRootCertException(rootCert, certificates.last(), config.trustStoreFile) + } + println("Generating SSL certificate for node messaging service.") val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA).toX509CertHolder() @@ -150,3 +169,17 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v } } } + +/** + * Exception thrown when the doorman root certificate doesn't match the expected (out-of-band) root certificate. + * This usually means that there has been a Man-in-the-middle attack when contacting the doorman. + */ +class WrongRootCertException(expected: Certificate, + actual: Certificate, + expectedFilePath: Path): + Exception(""" + The Root CA returned back from the registration process does not match the expected Root CA + expected: $expected + actual: $actual + the expected certificate is stored in: $expectedFilePath with alias $CORDA_ROOT_CA + """.trimMargin()) 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 48aa58a07f..f8a19ddb5f 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 @@ -6,6 +6,9 @@ import kotlin.Triple; import net.corda.core.contracts.*; import net.corda.core.crypto.CryptoUtils; import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.CordaX500Name; +import net.corda.core.identity.Party; +import net.corda.core.identity.PartyAndCertificate; import net.corda.core.messaging.DataFeed; import net.corda.core.node.services.Vault; import net.corda.core.node.services.VaultQueryException; @@ -33,17 +36,19 @@ import rx.Observable; import java.io.IOException; import java.lang.reflect.Field; +import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; import java.security.cert.CertificateException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static net.corda.core.crypto.CryptoUtils.entropyToKeyPair; 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.finance.contracts.asset.CashUtilities.*; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.TestConstants.*; import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices; @@ -51,6 +56,10 @@ import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests { + private static final CordaX500Name DUMMY_CASH_ISSUER_NAME = new CordaX500Name("Snake Oil Issuer", "London", "GB"); + private static final KeyPair DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)); + private static final PartyAndCertificate DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(new Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.getPublic())); + private static final PartyAndReference DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.getParty().ref((byte) 1); @Rule public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); private VaultFiller vaultFiller; @@ -61,13 +70,13 @@ public class VaultQueryJavaTests { @Before public void setUp() throws CertificateException, InvalidAlgorithmParameterException { List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName()); - IdentityServiceInternal identitySvc = makeTestIdentityService(Arrays.asList(getMEGA_CORP_IDENTITY(), getDUMMY_CASH_ISSUER_IDENTITY(), getDUMMY_NOTARY_IDENTITY())); + IdentityServiceInternal identitySvc = makeTestIdentityService(Arrays.asList(getMEGA_CORP_IDENTITY(), DUMMY_CASH_ISSUER_IDENTITY, getDUMMY_NOTARY_IDENTITY())); Pair databaseAndServices = makeTestDatabaseAndMockServices( Arrays.asList(getMEGA_CORP_KEY(), getDUMMY_NOTARY_KEY()), identitySvc, cordappPackages, getMEGA_CORP().getName()); - issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), getDUMMY_CASH_ISSUER_NAME(), getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); + issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY, getBOC_KEY()); database = databaseAndServices.getFirst(); MockServices services = databaseAndServices.getSecond(); vaultFiller = new VaultFiller(services, getDUMMY_NOTARY(), getDUMMY_NOTARY_KEY()); @@ -138,7 +147,7 @@ public class VaultQueryJavaTests { new Amount<>(100, Currency.getInstance("USD")), issuerServices, 3, - getDUMMY_CASH_ISSUER(), + DUMMY_CASH_ISSUER, null, new Random()); return tx; @@ -213,10 +222,10 @@ public class VaultQueryJavaTests { Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); Amount dollars10 = new Amount<>(10, Currency.getInstance("USD")); Amount dollars1 = new Amount<>(1, Currency.getInstance("USD")); - vaultFiller.fillWithSomeTestCash(pounds, issuerServices, 1, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars10, issuerServices, 1, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars1, issuerServices, 1, getDUMMY_CASH_ISSUER()); + vaultFiller.fillWithSomeTestCash(pounds, issuerServices, 1, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars10, issuerServices, 1, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars1, issuerServices, 1, DUMMY_CASH_ISSUER); return tx; }); database.transaction(tx -> { @@ -257,7 +266,7 @@ public class VaultQueryJavaTests { new Amount<>(100, Currency.getInstance("USD")), issuerServices, 3, - getDUMMY_CASH_ISSUER(), + DUMMY_CASH_ISSUER, null, new Random()); return tx; @@ -331,11 +340,11 @@ public class VaultQueryJavaTests { Amount dollars300 = new Amount<>(300, Currency.getInstance("USD")); Amount pounds = new Amount<>(400, Currency.getInstance("GBP")); Amount swissfrancs = new Amount<>(500, Currency.getInstance("CHF")); - vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars200, issuerServices, 2, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars300, issuerServices, 3, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(pounds, issuerServices, 4, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(swissfrancs, issuerServices, 5, getDUMMY_CASH_ISSUER()); + vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars200, issuerServices, 2, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars300, issuerServices, 3, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(pounds, issuerServices, 4, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(swissfrancs, issuerServices, 5, DUMMY_CASH_ISSUER); return tx; }); database.transaction(tx -> { @@ -377,11 +386,11 @@ public class VaultQueryJavaTests { Amount dollars300 = new Amount<>(300, Currency.getInstance("USD")); Amount pounds = new Amount<>(400, Currency.getInstance("GBP")); Amount swissfrancs = new Amount<>(500, Currency.getInstance("CHF")); - vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars200, issuerServices, 2, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(dollars300, issuerServices, 3, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(pounds, issuerServices, 4, getDUMMY_CASH_ISSUER()); - vaultFiller.fillWithSomeTestCash(swissfrancs, issuerServices, 5, getDUMMY_CASH_ISSUER()); + vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars200, issuerServices, 2, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(dollars300, issuerServices, 3, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(pounds, issuerServices, 4, DUMMY_CASH_ISSUER); + vaultFiller.fillWithSomeTestCash(swissfrancs, issuerServices, 5, DUMMY_CASH_ISSUER); return tx; }); database.transaction(tx -> { @@ -438,9 +447,9 @@ public class VaultQueryJavaTests { Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); Amount pounds300 = new Amount<>(300, Currency.getInstance("GBP")); Amount pounds400 = new Amount<>(400, Currency.getInstance("GBP")); - vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, getDUMMY_CASH_ISSUER()); + vaultFiller.fillWithSomeTestCash(dollars100, issuerServices, 1, DUMMY_CASH_ISSUER); vaultFiller.fillWithSomeTestCash(dollars200, issuerServices, 2, getBOC().ref(new OpaqueBytes("1".getBytes()))); - vaultFiller.fillWithSomeTestCash(pounds300, issuerServices, 3, getDUMMY_CASH_ISSUER()); + vaultFiller.fillWithSomeTestCash(pounds300, issuerServices, 3, DUMMY_CASH_ISSUER); vaultFiller.fillWithSomeTestCash(pounds400, issuerServices, 4, getBOC().ref(new OpaqueBytes("1".getBytes()))); return tx; }); @@ -460,13 +469,13 @@ public class VaultQueryJavaTests { assertThat(results.getOtherResults().get(1)).isEqualTo(CryptoUtils.toStringShort(getBOC_PUBKEY())); assertThat(results.getOtherResults().get(2)).isEqualTo("GBP"); assertThat(results.getOtherResults().get(3)).isEqualTo(300L); - assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_KEY.getPublic())); assertThat(results.getOtherResults().get(5)).isEqualTo("GBP"); assertThat(results.getOtherResults().get(6)).isEqualTo(200L); assertThat(results.getOtherResults().get(7)).isEqualTo(CryptoUtils.toStringShort(getBOC_PUBKEY())); assertThat(results.getOtherResults().get(8)).isEqualTo("USD"); assertThat(results.getOtherResults().get(9)).isEqualTo(100L); - assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_KEY.getPublic())); assertThat(results.getOtherResults().get(11)).isEqualTo("USD"); } catch (NoSuchFieldException e) { 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 c6fa6c2aac..c020b3380f 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -94,7 +94,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // allow interruption half way through. mockNet = MockNetwork(threadPerNode = true, cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) @@ -146,7 +146,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `trade cash for commercial paper fails using soft locking`() { mockNet = MockNetwork(threadPerNode = true, cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) @@ -204,7 +204,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `shutdown and restore`() { mockNet = MockNetwork(cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) var bobNode = mockNet.createPartyNode(BOB_NAME) @@ -325,9 +325,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val bob = bobNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() val issuer = bank.ref(1, 2, 3) - - ledger(aliceNode.services) { - + aliceNode.services.ledger(DUMMY_NOTARY) { // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() JarOutputStream(stream).use { @@ -431,8 +429,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val bank: Party = bankNode.info.singleIdentity() val bob = bobNode.info.singleIdentity() val issuer = bank.ref(1, 2, 3) - - ledger(aliceNode.services) { + aliceNode.services.ledger(DUMMY_NOTARY) { // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() JarOutputStream(stream).use { @@ -501,7 +498,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `dependency with error on buyer side`() { mockNet = MockNetwork(cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { runWithError(ledgerIdentityService, true, false, "at least one cash input") } } @@ -510,7 +507,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `dependency with error on seller side`() { mockNet = MockNetwork(cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { runWithError(ledgerIdentityService, false, true, "Issuances have a time-window") } } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index ee3e96d280..78539f9c39 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -28,10 +28,6 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class NotaryChangeTests { - companion object { - private val DUMMY_NOTARY_SERVICE_NAME: CordaX500Name = DUMMY_NOTARY.name.copy(commonName = "corda.notary.validating") - } - private lateinit var mockNet: MockNetwork private lateinit var oldNotaryNode: StartedNode private lateinit var clientNodeA: StartedNode @@ -42,7 +38,7 @@ class NotaryChangeTests { @Before fun setUp() { - val oldNotaryName = DUMMY_NOTARY.name.copy(organisation = "Old Dummy Notary") + val oldNotaryName = DUMMY_REGULATOR.name mockNet = MockNetwork( notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name), NotarySpec(oldNotaryName)), cordappPackages = listOf("net.corda.testing.contracts") @@ -51,8 +47,8 @@ class NotaryChangeTests { clientNodeB = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) clientA = clientNodeA.info.singleIdentity() oldNotaryNode = mockNet.notaryNodes[1] - newNotaryParty = clientNodeA.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! - oldNotaryParty = clientNodeA.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME.copy(organisation = "Old Dummy Notary"))!! + newNotaryParty = clientNodeA.services.networkMapCache.getNotary(DUMMY_NOTARY.name)!! + oldNotaryParty = clientNodeA.services.networkMapCache.getNotary(oldNotaryName)!! } @After 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 782c0fdf53..5ffac70560 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 @@ -161,7 +161,7 @@ class InMemoryIdentityServiceTests { val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public) val txKey = Crypto.generateKeyPair() val txCert = X509Utilities.createCertificate(CertificateType.CONFIDENTIAL_IDENTITY, issuer.certificate.toX509CertHolder(), issuerKeyPair, x500Name, txKey.public) - val txCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) + val txCertPath = X509CertificateFactory().generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) return Pair(issuer, PartyAndCertificate(txCertPath)) } 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 e3894fce27..0df6386b8b 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 @@ -256,7 +256,7 @@ class PersistentIdentityServiceTests { val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public) val txKey = Crypto.generateKeyPair() val txCert = X509Utilities.createCertificate(CertificateType.CONFIDENTIAL_IDENTITY, issuer.certificate.toX509CertHolder(), issuerKeyPair, x500Name, txKey.public) - val txCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) + val txCertPath = X509CertificateFactory().generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) return Pair(issuer, PartyAndCertificate(txCertPath)) } 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 07ed77e3b7..a56bd76955 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 @@ -69,7 +69,7 @@ class ArtemisMessagingTests { myLegalName = ALICE.name) LogHelper.setLevel(PersistentUniquenessProvider::class) database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), rigorousMock()) - networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database), rigorousMock()) + networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, emptyList()), rigorousMock()) } @After 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 151b2d98d5..734a1ec860 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 @@ -1,87 +1,48 @@ package net.corda.node.services.network -import com.fasterxml.jackson.databind.ObjectMapper -import net.corda.core.crypto.* -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.NodeInfo -import net.corda.core.serialization.deserialize +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.internal.cert import net.corda.core.serialization.serialize -import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.services.network.TestNodeInfoFactory.createNodeInfo -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.DEV_CA +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 org.assertj.core.api.Assertions.assertThat -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.server.ServerConnector -import org.eclipse.jetty.server.handler.HandlerCollection -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.servlet.ServletHolder -import org.glassfish.jersey.server.ResourceConfig -import org.glassfish.jersey.servlet.ServletContainer import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.net.InetSocketAddress import java.net.URL -import java.security.cert.CertPath -import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import javax.ws.rs.* -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import javax.ws.rs.core.Response.ok import kotlin.test.assertEquals +import kotlin.test.assertNotNull class NetworkMapClientTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) - private lateinit var server: Server - + private lateinit var server: NetworkMapServer private lateinit var networkMapClient: NetworkMapClient - 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) + + companion object { + private val cacheTimeout = 100000.seconds + } @Before fun setUp() { - server = Server(InetSocketAddress("localhost", 0)).apply { - handler = HandlerCollection().apply { - addHandler(ServletContextHandler().apply { - contextPath = "/" - val resourceConfig = ResourceConfig().apply { - // Add your API provider classes (annotated for JAX-RS) here - register(MockNetworkMapServer()) - } - val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start - addServlet(jerseyServlet, "/*") - }) - } - } - server.start() - - while (!server.isStarted) { - Thread.sleep(100) - } - - val hostAndPort = server.connectors.mapNotNull { it as? ServerConnector }.first() - networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.localPort}")) + server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort()) + val hostAndPort = server.start() + networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_TRUST_ROOT.cert) } @After fun tearDown() { - server.stop() + server.close() } @Test @@ -94,7 +55,7 @@ class NetworkMapClientTest { val nodeInfoHash = nodeInfo.serialize().sha256() - assertThat(networkMapClient.getNetworkMap().networkMap).containsExactly(nodeInfoHash) + assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash) assertEquals(nodeInfo, networkMapClient.getNodeInfo(nodeInfoHash)) val signedNodeInfo2 = createNodeInfo("Test2") @@ -102,53 +63,22 @@ class NetworkMapClientTest { networkMapClient.publish(signedNodeInfo2) val nodeInfoHash2 = nodeInfo2.serialize().sha256() - assertThat(networkMapClient.getNetworkMap().networkMap).containsExactly(nodeInfoHash, nodeInfoHash2) - assertEquals(100000.seconds, networkMapClient.getNetworkMap().cacheMaxAge) + assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash, nodeInfoHash2) + assertEquals(cacheTimeout, networkMapClient.getNetworkMap().cacheMaxAge) assertEquals(nodeInfo2, networkMapClient.getNodeInfo(nodeInfoHash2)) } + + @Test + fun `download NetworkParameter correctly`() { + // The test server returns same network parameter for any hash. + val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256()) + assertNotNull(networkParameter) + assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter) + } + @Test fun `get hostname string from http response correctly`() { - assertEquals("test.host.name", networkMapClient.myPublicHostname()) - } -} - -@Path("network-map") -// This is a stub implementation of the network map rest API. -internal class MockNetworkMapServer { - val nodeInfoMap = mutableMapOf() - @POST - @Path("publish") - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - fun publishNodeInfo(input: InputStream): Response { - val registrationData = input.readBytes().deserialize>() - val nodeInfo = registrationData.verified() - val nodeInfoHash = nodeInfo.serialize().sha256() - nodeInfoMap.put(nodeInfoHash, nodeInfo) - return ok().build() - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - fun getNetworkMap(): Response { - return Response.ok(ObjectMapper().writeValueAsString(nodeInfoMap.keys.map { it.toString() })).header("Cache-Control", "max-age=100000").build() - } - - @GET - @Path("{var}") - @Produces(MediaType.APPLICATION_OCTET_STREAM) - fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response { - val nodeInfo = nodeInfoMap[SecureHash.parse(nodeInfoHash)] - return if (nodeInfo != null) { - Response.ok(nodeInfo.serialize().bytes) - } else { - Response.status(Response.Status.NOT_FOUND) - }.build() - } - - @GET - @Path("my-hostname") - fun getHostName(): Response { - return Response.ok("test.host.name").build() + assertEquals("test.host.name", networkMapClient.myPublicHostname()) } } 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 f4c4e4ee41..fc3542a3d6 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 @@ -13,6 +13,7 @@ import net.corda.core.crypto.SignedData 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 @@ -95,7 +96,7 @@ class NetworkMapUpdaterTest { val signedNodeInfo: SignedData = uncheckedCast(it.arguments.first()) nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) } - on { getNetworkMap() }.then { NetworkMapResponse(nodeInfoMap.keys.toList(), 100.millis) } + on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) } on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() } } @@ -149,7 +150,7 @@ class NetworkMapUpdaterTest { val signedNodeInfo: SignedData = uncheckedCast(it.arguments.first()) nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) } - on { getNetworkMap() }.then { NetworkMapResponse(nodeInfoMap.keys.toList(), 100.millis) } + on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) } on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() } } 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 index 0b70adbc57..975bd65bf4 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt @@ -39,7 +39,7 @@ object TestNodeInfoFactory { } private fun buildCertPath(vararg certificates: Certificate): CertPath { - return X509CertificateFactory().delegate.generateCertPath(certificates.asList()) + return X509CertificateFactory().generateCertPath(*certificates) } private fun X509CertificateHolder.toX509Certificate(): X509Certificate { 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 98c06829b8..58666e4204 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 @@ -6,8 +6,10 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.StatesToRecord import net.corda.core.node.services.IdentityService @@ -22,8 +24,6 @@ import net.corda.finance.DOLLARS import net.corda.finance.POUNDS import net.corda.finance.SWISS_FRANCS import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_NAME import net.corda.finance.contracts.asset.DummyFungibleContract import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.SampleCashSchemaV2 @@ -48,6 +48,7 @@ import org.assertj.core.api.Assertions.assertThat import org.hibernate.SessionFactory import org.junit.* import java.math.BigDecimal +import java.math.BigInteger import java.time.Instant import java.util.* import javax.persistence.EntityManager @@ -55,6 +56,12 @@ import javax.persistence.Tuple import javax.persistence.criteria.CriteriaBuilder class HibernateConfigurationTest { + private companion object { + val DUMMY_CASH_ISSUER_NAME = CordaX500Name("Snake Oil Issuer", "London", "GB") + val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) + val DUMMY_CASH_ISSUER = Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.public) + } + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() @@ -91,7 +98,7 @@ class HibernateConfigurationTest { val dataSourceProps = makeTestDataSourceProperties() val identityService = rigorousMock().also { mock -> doReturn(null).whenever(mock).wellKnownPartyFromAnonymous(any()) - listOf(DUMMY_CASH_ISSUER_IDENTITY.party, DUMMY_NOTARY).forEach { + listOf(DUMMY_CASH_ISSUER, DUMMY_NOTARY).forEach { doReturn(it).whenever(mock).wellKnownPartyFromAnonymous(it) doReturn(it).whenever(mock).wellKnownPartyFromX500Name(it.name) } 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 8002741b5c..d40bc86126 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 @@ -4,16 +4,11 @@ import co.paralleluniverse.fibers.Suspendable import com.nhaarman.mockito_kotlin.argThat import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.contracts.Amount -import net.corda.core.contracts.Issued -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef +import net.corda.core.contracts.* import net.corda.core.crypto.NullKeys +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate +import net.corda.core.identity.* import net.corda.core.internal.packageName import net.corda.core.node.StatesToRecord import net.corda.core.node.services.StatesNotAvailableException @@ -31,9 +26,6 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_NAME import net.corda.finance.contracts.getCashBalance import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash @@ -51,6 +43,7 @@ import org.junit.Rule import org.junit.Test import rx.observers.TestSubscriber import java.math.BigDecimal +import java.math.BigInteger import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors @@ -59,8 +52,12 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeVaultServiceTest { - companion object { - private val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + private companion object { + val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + val DUMMY_CASH_ISSUER_NAME = CordaX500Name("Snake Oil Issuer", "London", "GB") + val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) + val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.public)) + val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) } @Rule 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 f6367e728d..14f8480444 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 @@ -21,9 +21,6 @@ import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.DealState import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.finance.contracts.asset.DUMMY_OBLIGATION_ISSUER import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CashSchemaV1.PersistentCashState import net.corda.finance.schemas.CommercialPaperSchemaV1 @@ -51,6 +48,12 @@ import java.time.temporal.ChronoUnit import java.util.* open class VaultQueryTests { + private companion object { + val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) + val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), DUMMY_CASH_ISSUER_KEY.public)) + val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) + val DUMMY_OBLIGATION_ISSUER = Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), entropyToKeyPair(BigInteger.valueOf(10)).public) + } @Rule @JvmField val testSerialization = SerializationEnvironmentRule() 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 451a82d9de..2bed2fd84e 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,11 +1,10 @@ package net.corda.node.services.vault -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.InsufficientBalanceException -import net.corda.core.contracts.LinearState -import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.contracts.* +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.transpose @@ -19,9 +18,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_NAME import net.corda.finance.contracts.getCashBalance import net.corda.finance.schemas.CashSchemaV1 import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -36,6 +32,7 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import java.math.BigInteger import java.util.concurrent.Executors import kotlin.test.assertEquals import kotlin.test.fail @@ -43,8 +40,12 @@ import kotlin.test.fail // TODO: Move this to the cash contract tests once mock services are further split up. class VaultWithCashTest { - companion object { - private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + private companion object { + val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + val DUMMY_CASH_ISSUER_NAME = CordaX500Name("Snake Oil Issuer", "London", "GB") + val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) + val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.public)) + val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) } @Rule 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 65745b7f3d..8c76a0ad82 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 @@ -1,54 +1,60 @@ package net.corda.node.utilities.registration -import com.nhaarman.mockito_kotlin.* +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.getX509Certificate -import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.createKeystoreForCordaNode +import net.corda.nodeapi.internal.crypto.* import net.corda.testing.ALICE import net.corda.testing.rigorousMock import net.corda.testing.testNodeConfiguration -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import java.security.cert.Certificate import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -val X500Name.commonName: String? get() = getRDNs(BCStyle.CN).firstOrNull()?.first?.value?.toString() - class NetworkRegistrationHelperTest { @Rule @JvmField val tempFolder = TemporaryFolder() + private val requestId = SecureHash.randomSHA256().toString() + private lateinit var config: NodeConfiguration + + private val identities = listOf("CORDA_CLIENT_CA", + "CORDA_INTERMEDIATE_CA", + "CORDA_ROOT_CA") + .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } + private val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } + .map { it.cert }.toTypedArray() + + private val certService = mockRegistrationResponse(*certs) + + @Before + fun init() { + config = testNodeConfiguration(baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE.name) + } + @Test - fun buildKeyStore() { - val id = SecureHash.randomSHA256().toString() - - val identities = listOf("CORDA_CLIENT_CA", - "CORDA_INTERMEDIATE_CA", - "CORDA_ROOT_CA") - .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } - val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } - .map { it.cert }.toTypedArray() - val certService = rigorousMock().also { - doReturn(id).whenever(it).submitRequest(any()) - doReturn(certs).whenever(it).retrieveCertificates(eq(id)) - } - - val config = testNodeConfiguration( - baseDirectory = tempFolder.root.toPath(), - myLegalName = ALICE.name) - + fun `successful registration`() { assertFalse(config.nodeKeystore.exists()) assertFalse(config.sslKeystore.exists()) - assertFalse(config.trustStoreFile.exists()) + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certs.last()) + it.save(config.trustStoreFile, config.trustStorePassword) + } NetworkRegistrationHelper(config, certService).buildKeystore() @@ -89,4 +95,32 @@ class NetworkRegistrationHelperTest { assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA)) } } + + @Test + fun `missing truststore`() { + assertThatThrownBy { + NetworkRegistrationHelper(config, certService).buildKeystore() + }.hasMessageContaining("This file must contain the root CA cert of your compatibility zone. Please contact your CZ operator.") + .isInstanceOf(IllegalArgumentException::class.java) + } + + @Test + fun `wrong root cert in truststore`() { + val someCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, someCert) + it.save(config.trustStoreFile, config.trustStorePassword) + } + assertThatThrownBy { + NetworkRegistrationHelper(config, certService).buildKeystore() + }.isInstanceOf(WrongRootCertException::class.java) + } + + private fun mockRegistrationResponse(vararg response: Certificate): NetworkRegistrationService { + return rigorousMock().also { + doReturn(requestId).whenever(it).submitRequest(any()) + doReturn(response).whenever(it).retrieveCertificates(eq(requestId)) + } + } } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 78ddbefdbd..5ebc7d6c60 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -39,7 +39,9 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow", + "InvokeRpc.wellKnownPartyFromX500Name", "InvokeRpc.attachmentExists", + "InvokeRpc.openAttachment", "InvokeRpc.uploadAttachment", "InvokeRpc.internalVerifiedTransactionsFeed"]]] diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt index 6ac1baa176..0e286a14a0 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -12,7 +12,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.config.NotaryConfig -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.config.User import net.corda.testing.BOC import net.corda.testing.internal.demorun.* @@ -102,7 +101,7 @@ object IssueCash { } private fun createParams(amount: Amount, notaryName: CordaX500Name): IssueRequestParams { - return IssueRequestParams(amount, BIGCORP_NAME, "1", BOC.name, notaryName.copy(commonName = ValidatingNotaryService.id)) + return IssueRequestParams(amount, BIGCORP_NAME, "1", BOC.name, notaryName) } private fun printHelp(parser: OptionParser) { diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index 66e7aa6953..26f7839d04 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -38,7 +38,7 @@ dependencies { // Specify your cordapp's dependencies below, including dependent cordapps compile group: 'commons-io', name: 'commons-io', version: '2.5' - testCompile project(':node-driver') + cordaCompile project(':node-driver') testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" } 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 9dd3180bd8..5e0f0971bb 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 @@ -222,6 +222,12 @@ class IRSTests { private val megaCorpServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) 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 { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + doReturn(null).whenever(it).partyFromKey(ORACLE_PUBKEY) + }, MEGA_CORP.name) + @Test fun ok() { trade().verifies() @@ -391,8 +397,7 @@ class IRSTests { val ld = LocalDate.of(2016, 3, 8) val bd = BigDecimal("0.0063518") - - return ledger { + return ledgerServices.ledger(DUMMY_NOTARY) { transaction("Agreement") { attachments(IRS_PROGRAM_ID) output(IRS_PROGRAM_ID, "irs post agreement", singleIRS()) @@ -419,6 +424,10 @@ class IRSTests { } } + private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + ledgerServices.transaction(DUMMY_NOTARY, script) + } + @Test fun `ensure failure occurs when there are inbound states for an agreement command`() { val irs = singleIRS() @@ -656,8 +665,7 @@ class IRSTests { val bd1 = BigDecimal("0.0063518") val irs = singleIRS() - - return ledger { + return ledgerServices.ledger(DUMMY_NOTARY) { transaction("Agreement") { attachments(IRS_PROGRAM_ID) output(IRS_PROGRAM_ID, "irs post agreement1", diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt index 692c252f7a..a55e1a3a5c 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt @@ -27,7 +27,7 @@ fun springDriver( useTestClock: Boolean = defaultParameters.useTestClock, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, - notarySpecs: List, + notarySpecs: List = defaultParameters.notarySpecs, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, dsl: SpringBootDriverDSL.() -> A ): A { @@ -91,7 +91,7 @@ data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalD log.debug("Retrying webserver info at ${handle.webAddress}") } - throw IllegalStateException("Webserver at ${handle.webAddress} has died or was not reachable at URL ${url}") + throw IllegalStateException("Webserver at ${handle.webAddress} has died or was not reachable at URL $url") } private fun startApplication(handle: NodeHandle, debugPort: Int?, clazz: Class<*>): Process { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 99e158b723..6735271686 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -4,12 +4,13 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name +import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.internal.demorun.* @@ -63,6 +64,10 @@ class BFTNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it.toString()) }, clusterName, threshold = minCorrectReplicas(clusterSize)) + ServiceIdentityGenerator.generateToDisk( + notaryNames.map { context.baseDirectory(it.toString()) }, + clusterName, + NotaryService.constructId(validating = false, bft = true), + minCorrectReplicas(clusterSize)) } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 7ba39a8dea..4ba0b53210 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -4,11 +4,13 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div +import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.RaftConfig import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.internal.demorun.* @@ -59,6 +61,9 @@ class RaftNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it.toString()) }, clusterName) + ServiceIdentityGenerator.generateToDisk( + notaryNames.map { context.baseDirectory(it.toString()) }, + clusterName, + NotaryService.constructId(validating = true, raft = true)) } } diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index fff2032368..33a110c2ad 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -31,7 +31,7 @@ class SimmValuationTest : IntegrationTest() { @Test fun `runs SIMM valuation demo`() { - driver(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.vega.contracts")) { + driver(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.vega.contracts", "net.corda.vega.plugin.customserializers")) { val nodeAFuture = startNode(providedName = nodeALegalName) val nodeBFuture = startNode(providedName = nodeBLegalName) val (nodeA, nodeB) = listOf(nodeAFuture, nodeBFuture).map { it.getOrThrow() } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt index 01de81585b..8b9b1d7414 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt @@ -19,7 +19,7 @@ val PORTFOLIO_SWAP_PROGRAM_ID = "net.corda.vega.contracts.PortfolioSwap" * given point in time. This state can be consumed to create a new state with a mutated valuation or portfolio. */ data class PortfolioState(val portfolio: List, - private val _parties: Pair, + val _parties: Pair, val valuationDate: LocalDate, val valuation: PortfolioValuation? = null, override val linearId: UniqueIdentifier = UniqueIdentifier()) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt index 626de57761..2c8bc6f473 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt @@ -10,6 +10,7 @@ import com.opengamma.strata.market.curve.CurveName import com.opengamma.strata.market.param.CurrencyParameterSensitivities import com.opengamma.strata.market.param.CurrencyParameterSensitivity import com.opengamma.strata.market.param.TenorDateParameterMetadata +import com.opengamma.strata.market.param.ParameterMetadata import net.corda.core.serialization.SerializationWhitelist import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple @@ -34,6 +35,7 @@ class SimmPluginRegistry : SerializationWhitelist { DoubleArray::class.java, CurveName::class.java, TenorDateParameterMetadata::class.java, - Tenor::class.java + Tenor::class.java, + ParameterMetadata::class.java ) } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencyParameterSensitivitiesSerializer.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencyParameterSensitivitiesSerializer.kt new file mode 100644 index 0000000000..e5f2e3b496 --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencyParameterSensitivitiesSerializer.kt @@ -0,0 +1,14 @@ +package net.corda.vega.plugin.customserializers + +import com.opengamma.strata.market.param.CurrencyParameterSensitivities +import com.opengamma.strata.market.param.CurrencyParameterSensitivity +import net.corda.core.serialization.SerializationCustomSerializer + +@Suppress("UNUSED") +class CurrencyParameterSensitivitiesSerializer : + SerializationCustomSerializer { + data class Proxy(val sensitivities: List) + + override fun fromProxy(proxy: Proxy) = CurrencyParameterSensitivities.of(proxy.sensitivities) + override fun toProxy(obj: CurrencyParameterSensitivities) = Proxy(obj.sensitivities) +} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencyParameterSensitivitySerialiser.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencyParameterSensitivitySerialiser.kt new file mode 100644 index 0000000000..f0762ec9be --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencyParameterSensitivitySerialiser.kt @@ -0,0 +1,26 @@ +package net.corda.vega.plugin.customserializers + +import com.opengamma.strata.market.param.CurrencyParameterSensitivity +import com.opengamma.strata.market.param.ParameterMetadata +import com.opengamma.strata.data.MarketDataName +import com.opengamma.strata.collect.array.DoubleArray +import com.opengamma.strata.basics.currency.Currency +import net.corda.core.serialization.SerializationCustomSerializer + +@Suppress("UNUSED") +class CurrencyParameterSensitivitySerializer : + SerializationCustomSerializer { + data class Proxy(val currency: Currency, val marketDataName: MarketDataName<*>, + val parameterMetadata: List, + val sensitivity: DoubleArray) + + override fun fromProxy(proxy: CurrencyParameterSensitivitySerializer.Proxy) = + CurrencyParameterSensitivity.of( + proxy.marketDataName, + proxy.parameterMetadata, + proxy.currency, + proxy.sensitivity) + + override fun toProxy(obj: CurrencyParameterSensitivity) = Proxy((obj as CurrencyParameterSensitivity).currency, + obj.marketDataName, obj.parameterMetadata, obj.sensitivity) +} diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencySerializer.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencySerializer.kt new file mode 100644 index 0000000000..2d93977323 --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/CurrencySerializer.kt @@ -0,0 +1,12 @@ +package net.corda.vega.plugin.customserializers + +import com.opengamma.strata.basics.currency.Currency +import net.corda.core.serialization.SerializationCustomSerializer + +@Suppress("UNUSED") +class CurrencySerializer : SerializationCustomSerializer { + data class Proxy(val currency: String) + + override fun fromProxy(proxy: Proxy) = Currency.parse(proxy.currency) + override fun toProxy(obj: Currency) = Proxy(obj.toString()) +} diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/DoubleArraySerializer.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/DoubleArraySerializer.kt new file mode 100644 index 0000000000..0b105726c9 --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/DoubleArraySerializer.kt @@ -0,0 +1,12 @@ +package net.corda.vega.plugin.customserializers + +import net.corda.core.serialization.SerializationCustomSerializer +import com.opengamma.strata.collect.array.DoubleArray + +@Suppress("UNUSED") +class DoubleArraySerializer : SerializationCustomSerializer { + data class Proxy(val amount: kotlin.DoubleArray) + + override fun fromProxy(proxy: Proxy) = DoubleArray.copyOf(proxy.amount) + override fun toProxy(obj: DoubleArray) = Proxy(obj.toArray()) +} diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/MultiCurrencyAmountSerializer.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/MultiCurrencyAmountSerializer.kt new file mode 100644 index 0000000000..a651aff9a1 --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/MultiCurrencyAmountSerializer.kt @@ -0,0 +1,17 @@ +package net.corda.vega.plugin.customserializers + +import com.opengamma.strata.basics.currency.MultiCurrencyAmount +import com.opengamma.strata.basics.currency.Currency +import net.corda.core.serialization.* + +@Suppress("UNUSED") +class MultiCurrencyAmountSerializer : + SerializationCustomSerializer { + data class Proxy(val curencies : Map) + + override fun toProxy(obj: MultiCurrencyAmount) = Proxy(obj.toMap()) + override fun fromProxy(proxy: Proxy) = MultiCurrencyAmount.of(proxy.curencies) +} + + + diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/TenorDateParameterMetadataSerializer.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/TenorDateParameterMetadataSerializer.kt new file mode 100644 index 0000000000..dbcbc2b6fd --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/TenorDateParameterMetadataSerializer.kt @@ -0,0 +1,15 @@ +package net.corda.vega.plugin.customserializers + +import com.opengamma.strata.basics.date.Tenor +import com.opengamma.strata.market.param.TenorDateParameterMetadata +import net.corda.core.serialization.* +import java.time.LocalDate + +@Suppress("UNUSED") +class TenorDateParameterMetadataSerializer : + SerializationCustomSerializer { + data class Proxy(val tenor: Tenor, val date: LocalDate, val identifier: Tenor, val label: String) + + override fun toProxy(obj: TenorDateParameterMetadata) = Proxy(obj.tenor, obj.date, obj.identifier, obj.label) + override fun fromProxy(proxy: Proxy) = TenorDateParameterMetadata.of(proxy.date, proxy.tenor, proxy.label) +} diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/TenorSerializer.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/TenorSerializer.kt new file mode 100644 index 0000000000..12e2dee1a5 --- /dev/null +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/customserializers/TenorSerializer.kt @@ -0,0 +1,13 @@ +package net.corda.vega.plugin.customserializers + +import com.opengamma.strata.basics.date.Tenor +import net.corda.core.serialization.* +import java.time.Period + +@Suppress("UNUSED") +class TenorSerializer : SerializationCustomSerializer { + data class Proxy(val years: Int, val months: Int, val days: Int, val name: String) + + override fun toProxy(obj: Tenor) = Proxy(obj.period.years, obj.period.months, obj.period.days, obj.toString()) + override fun fromProxy(proxy: Proxy) = Tenor.of (Period.of(proxy.years, proxy.months, proxy.days)) +} diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index c82b5bb2c2..c50a027047 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -31,6 +31,17 @@ dependencies { // Integration test helpers integrationTestCompile "org.assertj:assertj-core:${assertj_version}" integrationTestCompile "junit:junit:$junit_version" + + // Jetty dependencies for NetworkMapClient test. + // Web stuff: for HTTP[S] servlets + compile "org.eclipse.jetty:jetty-servlet:${jetty_version}" + compile "org.eclipse.jetty:jetty-webapp:${jetty_version}" + compile "javax.servlet:javax.servlet-api:3.1.0" + + // Jersey for JAX-RS implementation for use in Jetty + compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" + compile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" + compile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" } task integrationTest(type: Test) { diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 62601c34b0..411787bcff 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -1,24 +1,41 @@ package net.corda.testing.driver import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.copyTo import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds import net.corda.node.internal.NodeStartup +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.getX509Certificate +import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import net.corda.testing.* import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.http.HttpApi +import net.corda.testing.internal.CompatibilityZoneParams import net.corda.testing.internal.addressMustBeBound import net.corda.testing.internal.addressMustNotBeBound +import net.corda.testing.internal.internalDriver import net.corda.testing.node.NotarySpec +import net.corda.testing.node.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat import org.json.simple.JSONObject +import org.junit.Rule import org.junit.ClassRule import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.net.URL import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService +import javax.ws.rs.GET +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.core.Response +import javax.ws.rs.core.Response.ok + class DriverTests : IntegrationTest() { companion object { @@ -95,7 +112,7 @@ class DriverTests : IntegrationTest() { assertThat(baseDirectory / "process-id").exists() } - val baseDirectory = driver(notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name))) { + val baseDirectory = internalDriver(notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name))) { baseDirectory(DUMMY_NOTARY.name) } assertThat(baseDirectory / "process-id").doesNotExist() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index f0bcf80658..b964655a52 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -10,6 +10,7 @@ import net.corda.core.context.InvocationContext import net.corda.core.context.Origin import net.corda.core.flows.FlowLogic 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.transactions.TransactionBuilder @@ -18,22 +19,19 @@ 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.node.MockServices 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 /** - * Creates and tests a ledger built by the passed in dsl. The provided services can be customised, otherwise a default - * of a freshly built [MockServices] is used. + * Creates and tests a ledger built by the passed in dsl. */ -@JvmOverloads -fun ledger( - services: ServiceHub = MockServices(makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), MEGA_CORP.name), +fun ServiceHub.ledger( + notary: Party, dsl: LedgerDSL.() -> Unit ): LedgerDSL { - return LedgerDSL(TestLedgerDSLInterpreter(services)).also { dsl(it) } + return LedgerDSL(TestLedgerDSLInterpreter(this), notary).apply(dsl) } /** @@ -41,13 +39,11 @@ fun ledger( * * @see LedgerDSLInterpreter._transaction */ -@JvmOverloads -fun transaction( - transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), - cordappPackages: List = emptyList(), +fun ServiceHub.transaction( + notary: Party, dsl: TransactionDSL.() -> EnforceVerifyOrFail -) = ledger(services = MockServices(cordappPackages, makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), MEGA_CORP.name)) { - dsl(TransactionDSL(TestTransactionDSLInterpreter(this.interpreter, transactionBuilder))) +) = ledger(notary) { + dsl(TransactionDSL(TestTransactionDSLInterpreter(interpreter, TransactionBuilder(notary)), notary)) } fun testNodeConfiguration( diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index af592a2976..1279969c67 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -15,7 +15,6 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.nodeapi.internal.config.User import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.internal.InProcessNode import net.corda.testing.internal.DriverDSLImpl import net.corda.testing.internal.genericDriver import net.corda.testing.internal.getTimestampAsDirectoryName @@ -180,7 +179,7 @@ fun driver( waitForAllNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish, notarySpecs: List = defaultParameters.notarySpecs, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - jmxPolicy: JmxPolicy = JmxPolicy(), + jmxPolicy: JmxPolicy = defaultParameters.jmxPolicy, dsl: DriverDSL.() -> A ): A { return genericDriver( @@ -195,7 +194,8 @@ fun driver( waitForNodesToFinish = waitForAllNodesToFinish, notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, - jmxPolicy = jmxPolicy + jmxPolicy = jmxPolicy, + compatibilityZone = null ), coerce = { it }, dsl = dsl, @@ -232,7 +232,6 @@ data class DriverParameters( val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY.name)), val extraCordappPackagesToScan: List = emptyList(), val jmxPolicy: JmxPolicy = JmxPolicy() - ) { fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug) fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory) @@ -243,7 +242,7 @@ data class DriverParameters( fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean) = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) - fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) fun setNotarySpecs(notarySpecs: List) = copy(notarySpecs = notarySpecs) + fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) fun setJmxPolicy(jmxPolicy: JmxPolicy) = copy(jmxPolicy = jmxPolicy) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt index 42a394b51d..d8d3d1b49a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt @@ -64,7 +64,8 @@ interface DriverDSL { verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map = defaultParameters.customOverrides, startInSameProcess: Boolean? = defaultParameters.startInSameProcess, - maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture + maximumHeapSize: String = defaultParameters.maximumHeapSize + ): CordaFuture /** * Helper function for starting a [Node] with custom parameters from Java. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt index a4046764d6..09ad8aa7ac 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt @@ -1,7 +1,9 @@ package net.corda.testing.internal +import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.ThreadFactoryBuilder import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient import net.corda.cordform.CordformContext @@ -9,7 +11,6 @@ import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.* import net.corda.core.internal.copyTo @@ -28,15 +29,26 @@ import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions import net.corda.node.services.config.* -import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.NodeInfoFilesCopier -import net.corda.nodeapi.internal.addShutdownHook +import net.corda.node.services.transactions.BFTNonValidatingNotaryService +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.config.User +import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig +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.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.* +import net.corda.testing.internal.DriverDSLImpl.ClusterType.NON_VALIDATING_RAFT +import net.corda.testing.internal.DriverDSLImpl.ClusterType.VALIDATING_RAFT import net.corda.testing.node.ClusterSpec import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.NotarySpec @@ -45,12 +57,14 @@ import okhttp3.OkHttpClient import okhttp3.Request import rx.Observable import rx.observables.ConnectableObservable +import rx.schedulers.Schedulers import java.net.ConnectException import java.net.URL import java.net.URLClassLoader import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardCopyOption +import java.security.cert.X509Certificate import java.time.Duration import java.time.Instant import java.time.ZoneOffset @@ -73,7 +87,8 @@ class DriverDSLImpl( val waitForNodesToFinish: Boolean, extraCordappPackagesToScan: List, val jmxPolicy: JmxPolicy, - val notarySpecs: List + val notarySpecs: List, + val compatibilityZone: CompatibilityZoneParams? ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! @@ -83,11 +98,12 @@ class DriverDSLImpl( // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. // Investigate whether we can avoid that. - private val nodeInfoFilesCopier = NodeInfoFilesCopier() + private var nodeInfoFilesCopier: NodeInfoFilesCopier? = null // Map from a nodes legal name to an observable emitting the number of nodes in its network map. private val countObservables = mutableMapOf>() private lateinit var _notaries: List override val notaryHandles: List get() = _notaries + private var networkParameters: NetworkParametersCopier? = null class State { val processes = ArrayList() @@ -160,28 +176,130 @@ class DriverDSLImpl( maximumHeapSize: String ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() - val rpcAddress = portAllocation.nextHostAndPort() - val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB") - val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } - val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(name), - allowMissingConfig = true, - configOverrides = configOf( - "myLegalName" to name.toString(), - "p2pAddress" to p2pAddress.toString(), - "rpcAddress" to rpcAddress.toString(), - "webAddress" to webAddress.toString(), - "useTestClock" to useTestClock, - "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, - "verifierType" to verifierType.name - ) + customOverrides - ) - return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) + + val registrationFuture = if (compatibilityZone?.rootCert != null) { + nodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url) + } else { + doneFuture(Unit) + } + + return registrationFuture.flatMap { + val rpcAddress = portAllocation.nextHostAndPort() + val webAddress = portAllocation.nextHostAndPort() + val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } + val configMap = configOf( + "myLegalName" to name.toString(), + "p2pAddress" to p2pAddress.toString(), + "rpcAddress" to rpcAddress.toString(), + "webAddress" to webAddress.toString(), + "useTestClock" to useTestClock, + "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, + "verifierType" to verifierType.name + ) + customOverrides + val config = ConfigHelper.loadConfig( + baseDirectory = baseDirectory(name), + allowMissingConfig = true, + configOverrides = if (compatibilityZone != null) { + configMap + mapOf("compatibilityZoneURL" to compatibilityZone.url.toString()) + } else { + configMap + } + ) + startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) + } } - internal fun startCordformNode(cordform: CordformNode): CordaFuture { + private fun nodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { + val baseDirectory = baseDirectory(providedName).createDirectories() + val config = ConfigHelper.loadConfig( + baseDirectory = baseDirectory, + allowMissingConfig = true, + configOverrides = configOf( + "p2pAddress" to "localhost:1222", // required argument, not really used + "compatibilityZoneURL" to compatibilityZoneURL.toString(), + "myLegalName" to providedName.toString()) + ) + val configuration = config.parseAsNodeConfiguration() + + configuration.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(configuration.trustStoreFile, configuration.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) + it.save(configuration.trustStoreFile, configuration.trustStorePassword) + } + + return if (startNodesInProcess) { + // This is a bit cheating, we're not starting a full node, we're just calling the code nodes call + // when registering. + NetworkRegistrationHelper(configuration, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() + doneFuture(Unit) + } else { + startOutOfProcessNodeRegistration(config, configuration) + } + } + + private enum class ClusterType(val validating: Boolean, val clusterName: CordaX500Name) { + VALIDATING_RAFT(true, CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH")), + NON_VALIDATING_RAFT(false, CordaX500Name(RaftNonValidatingNotaryService.id, "Raft", "Zurich", "CH")), + NON_VALIDATING_BFT(false, CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH")) + } + + internal fun startCordformNodes(cordforms: List): CordaFuture<*> { + val clusterNodes = HashMultimap.create() + val notaryInfos = ArrayList() + + // Go though the node definitions and pick out the notaries so that we can generate their identities to be used + // in the network parameters + for (cordform in cordforms) { + if (cordform.notary == null) continue + val name = CordaX500Name.parse(cordform.name) + val notaryConfig = ConfigFactory.parseMap(cordform.notary).parseAs() + // We need to first group the nodes that form part of a cluser. We assume for simplicity that nodes of the + // same cluster type and validating flag are part of the same cluster. + if (notaryConfig.raft != null) { + val key = if (notaryConfig.validating) VALIDATING_RAFT else NON_VALIDATING_RAFT + clusterNodes.put(key, name) + } else if (notaryConfig.bftSMaRt != null) { + clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name) + } else { + // We have all we need here to generate the identity for single node notaries + val identity = ServiceIdentityGenerator.generateToDisk( + dirs = listOf(baseDirectory(name)), + serviceName = name, + serviceId = "identity" + ) + notaryInfos += NotaryInfo(identity, notaryConfig.validating) + } + } + + clusterNodes.asMap().forEach { type, nodeNames -> + val identity = ServiceIdentityGenerator.generateToDisk( + dirs = nodeNames.map { baseDirectory(it) }, + serviceName = type.clusterName, + serviceId = NotaryService.constructId( + validating = type.validating, + raft = type in setOf(VALIDATING_RAFT, NON_VALIDATING_RAFT), + bft = type == ClusterType.NON_VALIDATING_BFT + ) + ) + notaryInfos += NotaryInfo(identity, type.validating) + } + + networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + + return cordforms.map { + val startedNode = startCordformNode(it) + if (it.webAddress != null) { + // Start a webserver if an address for it was specified + startedNode.flatMap { startWebserver(it) } + } else { + startedNode + } + }.transpose() + } + + private fun startCordformNode(cordform: CordformNode): CordaFuture { val name = CordaX500Name.parse(cordform.name) // TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap() @@ -224,33 +342,50 @@ class DriverDSLImpl( } override fun start() { + if (startNodesInProcess) { + Schedulers.reset() + } _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) - shutdownManager.registerShutdown { nodeInfoFilesCopier.close() } + if (compatibilityZone == null) { + // Without a compatibility zone URL we have to copy the node info files ourselves to make sure the nodes see each other + nodeInfoFilesCopier = NodeInfoFilesCopier().also { + shutdownManager.registerShutdown(it::close) + } + } val notaryInfos = generateNotaryIdentities() + // The network parameters must be serialised before starting any of the nodes + networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) val nodeHandles = startNotaries() _notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) } } - private fun generateNotaryIdentities(): List> { + private fun generateNotaryIdentities(): List { return notarySpecs.map { spec -> val identity = if (spec.cluster == null) { ServiceIdentityGenerator.generateToDisk( dirs = listOf(baseDirectory(spec.name)), - serviceName = spec.name.copy(commonName = NotaryService.constructId(validating = spec.validating)) + serviceName = spec.name, + serviceId = "identity", + customRootCert = compatibilityZone?.rootCert ) } else { ServiceIdentityGenerator.generateToDisk( dirs = generateNodeNames(spec).map { baseDirectory(it) }, - serviceName = spec.name + serviceName = spec.name, + serviceId = NotaryService.constructId( + validating = spec.validating, + raft = spec.cluster is ClusterSpec.Raft + ), + customRootCert = compatibilityZone?.rootCert ) } - Pair(identity, spec.validating) + NotaryInfo(identity, spec.validating) } } private fun generateNodeNames(spec: NotarySpec): List { - return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(commonName = null, organisation = "${spec.name.organisation}-$it") } + return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") } } private fun startNotaries(): List>> { @@ -321,6 +456,8 @@ class DriverDSLImpl( return driverDirectory / nodeDirectoryName } + override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) + /** * @param initial number of nodes currently in the network map of a running node. * @param networkMapCacheChangeObservable an observable returning the updates to the node network map. @@ -365,15 +502,27 @@ class DriverDSLImpl( return future } + private fun startOutOfProcessNodeRegistration(config: Config, configuration: NodeConfiguration): CordaFuture { + val debugPort = if (isDebug) debugPortAllocation.nextPort() else null + val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null + val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, + systemProperties, cordappPackages, "200m", initialRegistration = true) + + return poll(executorService, "node registration (${configuration.myLegalName})") { + if (process.isAlive) null else Unit + } + } + private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val configuration = config.parseAsNodeConfiguration() val baseDirectory = configuration.baseDirectory.createDirectories() - nodeInfoFilesCopier.addConfig(baseDirectory) + nodeInfoFilesCopier?.addConfig(baseDirectory) + networkParameters!!.install(baseDirectory) val onNodeExit: () -> Unit = { - nodeInfoFilesCopier.removeConfig(baseDirectory) + nodeInfoFilesCopier?.removeConfig(baseDirectory) countObservables.remove(configuration.myLegalName) } if (startInProcess ?: startNodesInProcess) { @@ -396,7 +545,7 @@ class DriverDSLImpl( } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null - val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize) + val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, initialRegistration = false) if (waitForNodesToFinish) { state.locked { processes += process @@ -406,7 +555,7 @@ class DriverDSLImpl( } val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process) return p2pReadyFuture.flatMap { - val processDeathFuture = poll(executorService, "process death") { + val processDeathFuture = poll(executorService, "process death while waiting for RPC (${configuration.myLegalName})") { if (process.isAlive) null else process } establishRpc(configuration, processDeathFuture).flatMap { rpc -> @@ -479,8 +628,8 @@ class DriverDSLImpl( node.internals.run() } node to nodeThread - }.flatMap { - nodeAndThread -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread } + }.flatMap { nodeAndThread -> + addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread } } } @@ -493,19 +642,26 @@ class DriverDSLImpl( monitorPort: Int?, overriddenSystemProperties: Map, cordappPackages: List, - maximumHeapSize: String + maximumHeapSize: String, + initialRegistration: Boolean ): Process { log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled") + ", jolokia monitoring port is " + (monitorPort ?: "not enabled")) // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) - val systemProperties = overriddenSystemProperties + mapOf( + val systemProperties = mutableMapOf( "name" to nodeConf.myLegalName, "visualvm.display.name" to "corda-${nodeConf.myLegalName}", - Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator), "java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process "log4j2.debug" to if(debugPort != null) "true" else "false" ) + + if (cordappPackages.isNotEmpty()) { + systemProperties += Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator) + } + + systemProperties += overriddenSystemProperties + // See experimental/quasar-hook/README.md for how to generate. val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" + "com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" + @@ -519,13 +675,18 @@ class DriverDSLImpl( val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" } val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" + val arguments = mutableListOf( + "--base-directory=${nodeConf.baseDirectory}", + "--logging-level=$loggingLevel", + "--no-local-shell").also { + if (initialRegistration) { + it += "--initial-registration" + } + }.toList() + return ProcessUtilities.startCordaProcess( className = "net.corda.node.Corda", // cannot directly get class for this, so just use string - arguments = listOf( - "--base-directory=${nodeConf.baseDirectory}", - "--logging-level=$loggingLevel", - "--no-local-shell" - ), + arguments = arguments, jdwpPort = debugPort, extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent), errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", @@ -677,7 +838,8 @@ fun genericDriver( waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, - notarySpecs = notarySpecs + notarySpecs = notarySpecs, + compatibilityZone = null ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -694,6 +856,50 @@ fun genericDriver( } } +/** + * @property url The base CZ URL for registration and network map updates + * @property rootCert If specified then the node will register itself using [url] and expect the registration response + * to be rooted at this cert. + */ +data class CompatibilityZoneParams(val url: URL, val rootCert: X509Certificate? = null) + +fun internalDriver( + isDebug: Boolean = DriverParameters().isDebug, + driverDirectory: Path = DriverParameters().driverDirectory, + portAllocation: PortAllocation = DriverParameters().portAllocation, + debugPortAllocation: PortAllocation = DriverParameters().debugPortAllocation, + systemProperties: Map = DriverParameters().systemProperties, + useTestClock: Boolean = DriverParameters().useTestClock, + initialiseSerialization: Boolean = DriverParameters().initialiseSerialization, + startNodesInProcess: Boolean = DriverParameters().startNodesInProcess, + waitForAllNodesToFinish: Boolean = DriverParameters().waitForAllNodesToFinish, + notarySpecs: List = DriverParameters().notarySpecs, + extraCordappPackagesToScan: List = DriverParameters().extraCordappPackagesToScan, + jmxPolicy: JmxPolicy = DriverParameters().jmxPolicy, + compatibilityZone: CompatibilityZoneParams? = null, + dsl: DriverDSLImpl.() -> A +): A { + return genericDriver( + driverDsl = DriverDSLImpl( + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + driverDirectory = driverDirectory.toAbsolutePath(), + useTestClock = useTestClock, + isDebug = isDebug, + startNodesInProcess = startNodesInProcess, + waitForNodesToFinish = waitForAllNodesToFinish, + notarySpecs = notarySpecs, + extraCordappPackagesToScan = extraCordappPackagesToScan, + jmxPolicy = jmxPolicy, + compatibilityZone = compatibilityZone + ), + coerce = { it }, + dsl = dsl, + initialiseSerialization = initialiseSerialization + ) +} + fun getTimestampAsDirectoryName(): String { return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt index 21a0bfe288..4d62b97c39 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt @@ -12,13 +12,16 @@ 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.testing.common.internal.testNetworkParameters import net.corda.testing.getFreeLocalPorts import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import org.apache.logging.log4j.Level import org.junit.After +import org.junit.Before import org.junit.Rule import org.junit.rules.TemporaryFolder import rx.internal.schedulers.CachedThreadScheduler @@ -39,6 +42,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi @JvmField val tempFolder = TemporaryFolder() + private lateinit var defaultNetworkParameters: NetworkParametersCopier private val nodes = mutableListOf>() private val nodeInfos = mutableListOf() @@ -46,6 +50,11 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi System.setProperty("consoleLogLevel", Level.DEBUG.name().toLowerCase()) } + @Before + fun init() { + defaultNetworkParameters = NetworkParametersCopier(testNetworkParameters(emptyList())) + } + /** * Stops the network map node and all the nodes started by [startNode]. This is called automatically after each test * but can also be called manually within a test. @@ -89,6 +98,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi ) val parsedConfig = config.parseAsNodeConfiguration() + defaultNetworkParameters.install(baseDirectory) val node = InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), cordappPackages).start() nodes += node ensureAllNetworkMapCachesHaveAllNodeInfos() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt index 62902a24a2..9684db5450 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt @@ -25,7 +25,8 @@ 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.driver.* +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.driver.PortAllocation import net.corda.testing.node.NotarySpec import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration @@ -90,6 +91,7 @@ val fakeNodeLegalName = CordaX500Name(organisation = "Not:a:real:name", locality private val globalPortAllocation = PortAllocation.Incremental(10000) private val globalDebugPortAllocation = PortAllocation.Incremental(5005) private val globalMonitorPortAllocation = PortAllocation.Incremental(7005) + fun rpcDriver( isDebug: Boolean = false, driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), @@ -106,25 +108,27 @@ fun rpcDriver( dsl: RPCDriverDSL.() -> A ) : A { return genericDriver( - driverDsl = RPCDriverDSL( - DriverDSLImpl( - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - driverDirectory = driverDirectory.toAbsolutePath(), - useTestClock = useTestClock, - isDebug = isDebug, - startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForNodesToFinish, - extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs, - jmxPolicy = jmxPolicy - ), externalTrace - ), - coerce = { it }, - dsl = dsl, - initialiseSerialization = false -)} + driverDsl = RPCDriverDSL( + DriverDSLImpl( + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + driverDirectory = driverDirectory.toAbsolutePath(), + useTestClock = useTestClock, + isDebug = isDebug, + startNodesInProcess = startNodesInProcess, + waitForNodesToFinish = waitForNodesToFinish, + extraCordappPackagesToScan = extraCordappPackagesToScan, + notarySpecs = notarySpecs, + jmxPolicy = jmxPolicy, + compatibilityZone = null + ), externalTrace + ), + coerce = { it }, + dsl = dsl, + initialiseSerialization = false + ) +} private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityManager3 { override fun validateUser(user: String?, password: String?) = isValid(user, password) @@ -132,6 +136,7 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan override fun validateUser(user: String?, password: String?, remotingConnection: RemotingConnection?): String? { return validate(user, password) } + override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?, address: String?, connection: RemotingConnection?): String? { return validate(user, password) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt index 60dfe3262d..a13badccd4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt @@ -4,14 +4,11 @@ package net.corda.testing.internal.demorun import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode -import net.corda.core.internal.concurrent.flatMap -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.testing.driver.JmxPolicy -import net.corda.testing.internal.DriverDSLImpl import net.corda.testing.driver.PortAllocation -import net.corda.testing.driver.driver +import net.corda.testing.internal.internalDriver fun CordformDefinition.clean() { System.err.println("Deleting: $nodesDirectory") @@ -35,12 +32,13 @@ fun CordformDefinition.deployNodesThen(block: () -> Unit) { } private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { + clean() val nodes = nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } val maxPort = nodes .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } .max()!! - driver( + internalDriver( isDebug = true, jmxPolicy = JmxPolicy(true), driverDirectory = nodesDirectory, @@ -51,17 +49,8 @@ private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: portAllocation = PortAllocation.Incremental(maxPort + 1), waitForAllNodesToFinish = waitForAllNodesToFinish ) { - this as DriverDSLImpl // access internal API setup(this) - nodes.map { - val startedNode = startCordformNode(it) - if (it.webAddress != null) { - // Start a webserver if an address for it was specified - startedNode.flatMap { startWebserver(it) } - } else { - startedNode - } - }.transpose().getOrThrow() // Only proceed once everything is up and running + startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running println("All nodes and webservers are ready...") block() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 058e504962..654b99bb12 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -7,6 +7,7 @@ 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 @@ -17,7 +18,7 @@ import java.math.BigInteger /** * Network map cache with no backing map service. */ -class MockNetworkMapCache(database: CordaPersistence) : PersistentNetworkMapCache(database) { +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) 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 a7f247d98b..2e907b8a9f 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 @@ -38,7 +38,11 @@ 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.testing.DUMMY_NOTARY +import net.corda.nodeapi.internal.NetworkParametersCopier +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -48,6 +52,8 @@ import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.sshd.common.util.security.SecurityUtils import rx.internal.schedulers.CachedThreadScheduler +import org.slf4j.Logger +import java.io.Closeable import java.math.BigInteger import java.nio.file.Path import java.security.KeyPair @@ -142,6 +148,7 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete val messagingNetwork = InMemoryMessagingNetwork(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) // A unique identifier for this network to segregate databases with the same nodeID but different networks. private val networkId = random63BitValue() + private val networkParameters: NetworkParametersCopier private val _nodes = mutableListOf() private val serializationEnv = setGlobalSerialization(initialiseSerialization) private val sharedUserCount = AtomicInteger(0) @@ -158,29 +165,32 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete * Returns the single notary node on the network. Throws if there are none or more than one. * @see notaryNodes */ - val defaultNotaryNode: StartedNode get() { - return when (notaryNodes.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryNodes[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") + val defaultNotaryNode: StartedNode + get() { + return when (notaryNodes.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryNodes[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") + } } - } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentity: Party get() { - return defaultNotaryNode.info.legalIdentities[1] // TODO Resolve once network parameters is merged back in - } + val defaultNotaryIdentity: Party + get() { + return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentityAndCert: PartyAndCertificate get() { - return defaultNotaryNode.info.legalIdentitiesAndCerts[1] // TODO Resolve once network parameters is merged back in - } + val defaultNotaryIdentityAndCert: PartyAndCertificate + get() { + return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Because this executor is shared, we need to be careful about nodes shutting it down. @@ -206,9 +216,22 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete init { filesystem.getPath("/nodes").createDirectory() + val notaryInfos = generateNotaryIdentities() + // The network parameters must be serialised before starting any of the nodes + networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) notaryNodes = createNotaries() } + private fun generateNotaryIdentities(): List { + return notarySpecs.mapIndexed { index, spec -> + val identity = ServiceIdentityGenerator.generateToDisk( + dirs = listOf(baseDirectory(nextNodeId + index)), + serviceName = spec.name, + serviceId = "identity") + NotaryInfo(identity, spec.validating) + } + } + private fun createNotaries(): List> { return notarySpecs.map { spec -> createNode(MockNodeParameters(legalName = spec.name, configOverrides = { @@ -244,6 +267,7 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete override val started: StartedNode? get() = uncheckedCast(super.started) override fun start(): StartedNode { + mockNet.networkParameters.install(configuration.baseDirectory) val started: StartedNode = uncheckedCast(super.start()) advertiseNodeToNetwork(started) return started diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt index 4618bb4717..b8988e50c7 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt @@ -2,7 +2,6 @@ package net.corda.testing.node import net.corda.core.identity.CordaX500Name import net.corda.node.services.config.VerifierType -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.nodeapi.internal.config.User data class NotarySpec( @@ -11,15 +10,7 @@ data class NotarySpec( val rpcUsers: List = emptyList(), val verifierType: VerifierType = VerifierType.InMemory, val cluster: ClusterSpec? = null -) { - init { - // TODO This will be removed once network parameters define the notaries - when (cluster) { - is ClusterSpec.Raft -> require(name.commonName == RaftValidatingNotaryService.id) - null -> require(name.commonName == null) - } - } -} +) sealed class ClusterSpec { abstract val clusterSize: Int 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/network/NetworkMapServer.kt new file mode 100644 index 0000000000..4e6eb0a83e --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt @@ -0,0 +1,155 @@ +package net.corda.testing.node.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.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.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.ROOT_CA +import org.bouncycastle.asn1.x500.X500Name +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.handler.HandlerCollection +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import java.io.Closeable +import java.io.InputStream +import java.net.InetSocketAddress +import java.time.Duration +import java.time.Instant +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.Response.ok + +class NetworkMapServer(cacheTimeout: Duration, + hostAndPort: NetworkHostAndPort, + vararg additionalServices: Any) : Closeable { + companion object { + val stubNetworkParameter = NetworkParameters(1, emptyList(), 1.hours, 10, 10, Instant.now(), 10) + + private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair { + val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val networkMapCert = X509Utilities.createCertificate( + CertificateType.INTERMEDIATE_CA, + rootCAKeyAndCert.certificate, + rootCAKeyAndCert.keyPair, + X500Name("CN=Corda Network Map,L=London"), + networkMapKey.public).cert + return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey) + } + } + + private val server: Server + // Default to ROOT_CA for testing. + // TODO: make this configurable? + private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(ROOT_CA)) + + init { + server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { + handler = HandlerCollection().apply { + addHandler(ServletContextHandler().apply { + contextPath = "/" + val resourceConfig = ResourceConfig().apply { + // Add your API provider classes (annotated for JAX-RS) here + register(service) + additionalServices.forEach { register(it) } + } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 } // Initialise at server start + addServlet(jerseyServlet, "/*") + }) + } + } + } + + fun start(): NetworkHostAndPort { + server.start() + // Wait until server is up to obtain the host and port. + while (!server.isStarted) { + Thread.sleep(500) + } + return server.connectors + .mapNotNull { it as? ServerConnector } + .first() + .let { NetworkHostAndPort(it.host, it.localPort) } + } + + fun removeNodeInfo(nodeInfo: NodeInfo) { + service.removeNodeInfo(nodeInfo) + } + + override fun close() { + server.stop() + } + + @Path("network-map") + class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) { + private val nodeInfoMap = mutableMapOf>() + + @POST + @Path("publish") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + fun publishNodeInfo(input: InputStream): Response { + val registrationData = input.readBytes().deserialize>() + val nodeInfo = registrationData.verified() + val nodeInfoHash = nodeInfo.serialize().sha256() + nodeInfoMap.put(nodeInfoHash, registrationData) + return ok().build() + } + + @GET + @Produces(MediaType.APPLICATION_OCTET_STREAM) + fun getNetworkMap(): Response { + val networkMap = NetworkMap(nodeInfoMap.keys.map { it }, SecureHash.randomSHA256()) + val serializedNetworkMap = networkMap.serialize() + val signature = Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedNetworkMap.bytes) + val signedNetworkMap = SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(networkMapKeyAndCert.certificate.cert, signature)) + return Response.ok(signedNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${cacheTimeout.seconds}").build() + } + + // Remove nodeInfo for testing. + fun removeNodeInfo(nodeInfo: NodeInfo) { + nodeInfoMap.remove(nodeInfo.serialize().hash) + } + + @GET + @Path("node-info/{var}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response { + val signedNodeInfo = nodeInfoMap[SecureHash.parse(nodeInfoHash)] + return if (signedNodeInfo != null) { + Response.ok(signedNodeInfo.serialize().bytes) + } else { + Response.status(Response.Status.NOT_FOUND) + }.build() + } + + @GET + @Path("network-parameter/{var}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + fun getNetworkParameter(@PathParam("var") networkParameterHash: String): Response { + return Response.ok(stubNetworkParameter.serialize().bytes).build() + } + + @GET + @Path("my-hostname") + fun getHostName(): Response { + return Response.ok("test.host.name").build() + } + } +} diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 40ace413d3..4f002c6f9d 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -14,8 +14,7 @@ class NodeConfig( val rpcPort: Int, val webPort: Int, val isNotary: Boolean, - val users: List, - var networkMap: NodeConfig? = null + val users: List ) { companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) @@ -32,10 +31,6 @@ class NodeConfig( val config = empty() .withValue("myLegalName", valueFor(legalName.toString())) .withValue("p2pAddress", addressValueFor(p2pPort)) - .withFallback(optional("networkMapService", networkMap, { c, n -> - c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) - })) .withValue("webAddress", addressValueFor(webPort)) .withValue("rpcAddress", addressValueFor(rpcPort)) .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) @@ -50,8 +45,6 @@ class NodeConfig( fun toText(): String = toFileConfig().root().render(renderOptions) private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) + private fun addressValueFor(port: Int) = valueFor("localhost:$port") - private inline fun optional(path: String, obj: T?, body: (Config, T) -> Config): Config { - return if (obj == null) empty() else body(empty(), obj).atPath(path) - } } 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 17ccd5a689..2211c68b86 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 @@ -2,11 +2,15 @@ package net.corda.smoketesting import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection +import net.corda.client.rpc.internal.KryoClientSerializationScheme import net.corda.core.internal.copyTo 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.testing.common.internal.testNetworkParameters +import net.corda.testing.common.internal.asContextEnv import java.nio.file.Path import java.nio.file.Paths import java.time.Instant @@ -52,6 +56,14 @@ class NodeProcess( private companion object { val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault()) + val defaultNetworkParameters = run { + KryoClientSerializationScheme.createSerializationEnv().asContextEnv { + // There are no notaries in the network parameters for smoke test nodes. If this is required then we would + // need to introduce the concept of a "network" which predefines the notaries, like the driver and MockNetwork + NetworkParametersCopier(testNetworkParameters(emptyList())) + } + } + init { try { Class.forName("net.corda.node.Corda") @@ -71,6 +83,7 @@ class NodeProcess( log.info("Node directory: {}", nodeDir) config.toText().byteInputStream().copyTo(nodeDir / "node.conf") + defaultNetworkParameters.install(nodeDir) val process = startNode(nodeDir) val client = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) diff --git a/testing/test-common/build.gradle b/testing/test-common/build.gradle index dd2e2285b5..df37173fc1 100644 --- a/testing/test-common/build.gradle +++ b/testing/test-common/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':core') + compile project(':node-api') } jar { 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 new file mode 100644 index 0000000000..ea84cd88c8 --- /dev/null +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -0,0 +1,18 @@ +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 java.time.Instant + +fun testNetworkParameters(notaries: List): NetworkParameters { + return NetworkParameters( + minimumPlatformVersion = 1, + notaries = notaries, + modifiedTime = Instant.now(), + eventHorizon = 10000.days, + maxMessageSize = 40000, + maxTransactionSize = 40000, + epoch = 1 + ) +} \ 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 da67d7a289..a6dc7f1c09 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 @@ -16,7 +16,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -69,8 +68,6 @@ val ORACLE_PUBKEY: PublicKey get() = ORACLE_KEY.public val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public val BOB_PUBKEY: PublicKey get() = BOB_KEY.public -val CHARLIE_PUBKEY: PublicKey get() = CHARLIE_KEY.public - val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CordaX500Name(organisation = "MegaCorp", locality = "London", country = "GB"), MEGA_CORP_PUBKEY) val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CordaX500Name(organisation = "MiniCorp", locality = "London", country = "GB"), MINI_CORP_PUBKEY) @@ -89,9 +86,6 @@ val BIG_CORP: Party get() = BIG_CORP_IDENTITY.party val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference val ALL_TEST_KEYS: List get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY) - -val DUMMY_CASH_ISSUER_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_CASH_ISSUER.party as Party) - val MOCK_HOST_AND_PORT = NetworkHostAndPort("mockHost", 30000) fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) @@ -146,7 +140,7 @@ fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { val certHolder = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCertificate, issuerKeyPair, party.name, party.owningKey) val pathElements = listOf(certHolder, issuerCertificate, intermediate.certificate, trustRoot) - val certPath = X509CertificateFactory().delegate.generateCertPath(pathElements.map(X509CertificateHolder::cert)) + val certPath = X509CertificateFactory().generateCertPath(pathElements.map(X509CertificateHolder::cert)) return PartyAndCertificate(certPath) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt index 9f4f829433..47d3f68b62 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt @@ -4,6 +4,8 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import java.io.InputStream @@ -14,7 +16,7 @@ import java.io.InputStream */ interface OutputStateLookup { /** - * Retrieves an output previously defined by [TransactionDSLInterpreter._output] with a label passed in. + * Retrieves an output previously defined by [TransactionDSLInterpreter.output] with a label passed in. * @param clazz The class object holding the type of the output state expected. * @param label The label of the to-be-retrieved output state. * @return The output [StateAndRef]. @@ -90,7 +92,7 @@ interface LedgerDSLInterpreter : Verifies, Ou * @return The final [WireTransaction] of the built transaction. */ fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> EnforceVerifyOrFail): WireTransaction + dsl: T.() -> EnforceVerifyOrFail): WireTransaction /** * Creates and adds a transaction to the ledger that will not be verified by [verifies]. @@ -100,13 +102,13 @@ interface LedgerDSLInterpreter : Verifies, Ou * @return The final [WireTransaction] of the built transaction. */ fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> Unit): WireTransaction + dsl: T.() -> Unit): WireTransaction /** * Creates a local scoped copy of the ledger. * @param dsl The ledger DSL to be interpreted using the copy. */ - fun tweak(dsl: LedgerDSL>.() -> Unit) + fun _tweak(dsl: LedgerDSLInterpreter.() -> Unit) /** * Adds an attachment to the ledger. @@ -123,24 +125,27 @@ interface LedgerDSLInterpreter : Verifies, Ou * functionality then first add your primitive to [LedgerDSLInterpreter] and then add the convenience defaults/extension * methods here. */ -class LedgerDSL>(val interpreter: L) : +class LedgerDSL>(val interpreter: L, private val notary: Party) : LedgerDSLInterpreter by interpreter { /** * Creates and adds a transaction to the ledger. */ @JvmOverloads - fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), + fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = notary), dsl: TransactionDSL.() -> EnforceVerifyOrFail) = - _transaction(label, transactionBuilder, dsl) + _transaction(label, transactionBuilder) { TransactionDSL(this, notary).dsl() } /** * Creates and adds a transaction to the ledger that will not be verified by [verifies]. */ @JvmOverloads - fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), + fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = notary), dsl: TransactionDSL.() -> Unit) = - _unverifiedTransaction(label, transactionBuilder, dsl) + _unverifiedTransaction(label, transactionBuilder) { TransactionDSL(this, notary).dsl() } + + /** Creates a local scoped copy of the ledger. */ + fun tweak(dsl: LedgerDSL.() -> Unit) = _tweak { LedgerDSL(uncheckedCast(this), notary).dsl() } /** * Retrieves an output previously defined by [TransactionDSLInterpreter._output] with a label passed in. 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 17f9a83295..026c76e56d 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 @@ -62,7 +62,7 @@ fun withTestSerialization(inheritable: Boolean = false, callable: (Serializa /** * 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 serialization init mechanisms. + * 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 } @@ -99,13 +99,13 @@ private fun createTestSerializationEnv(label: String) = object : SerializationEn SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) registerScheme(KryoServerSerializationScheme()) - registerScheme(AMQPClientSerializationScheme()) - registerScheme(AMQPServerSerializationScheme()) + registerScheme(AMQPClientSerializationScheme(emptyList())) + registerScheme(AMQPServerSerializationScheme(emptyList())) }, - if (isAmqpEnabled()) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT, + AMQP_P2P_CONTEXT, KRYO_RPC_SERVER_CONTEXT, KRYO_RPC_CLIENT_CONTEXT, - if (isAmqpEnabled()) AMQP_STORAGE_CONTEXT else KRYO_STORAGE_CONTEXT, + AMQP_STORAGE_CONTEXT, KRYO_CHECKPOINT_CONTEXT) { override fun toString() = "testSerializationEnv($label)" } 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 aa8a88aea6..5e671729d8 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 @@ -66,12 +66,18 @@ val DUMMY_REGULATOR: Party get() = Party(CordaX500Name(organisation = "Regulator val DEV_CA: CertificateAndKeyPair by lazy { // TODO: Should be identity scheme - val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") } + +val ROOT_CA: CertificateAndKeyPair by lazy { + // TODO: Should be identity scheme + val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") + caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, "cordacadevkeypass") +} val DEV_TRUST_ROOT: X509CertificateHolder by lazy { // TODO: Should be identity scheme - val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") caKeyStore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last().toX509CertHolder() } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 3167eb7a38..cf32668002 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -133,9 +133,7 @@ data class TestTransactionDSLInterpreter private constructor( transactionBuilder.setTimeWindow(data) } - override fun tweak( - dsl: TransactionDSL.() -> EnforceVerifyOrFail - ) = dsl(TransactionDSL(copy())) + override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl() override fun _attachment(contractClassName: ContractClassName) { (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage) @@ -205,11 +203,9 @@ data class TestLedgerDSLInterpreter private constructor( private fun interpretTransactionDsl( transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> R + dsl: TestTransactionDSLInterpreter.() -> R ): TestTransactionDSLInterpreter { - val transactionInterpreter = TestTransactionDSLInterpreter(this, transactionBuilder) - dsl(TransactionDSL(transactionInterpreter)) - return transactionInterpreter + return TestTransactionDSLInterpreter(this, transactionBuilder).apply { dsl() } } fun transactionName(transactionHash: SecureHash): String? { @@ -227,7 +223,7 @@ data class TestLedgerDSLInterpreter private constructor( private fun recordTransactionWithTransactionMap( transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> R, + dsl: TestTransactionDSLInterpreter.() -> R, transactionMap: HashMap = HashMap(), /** If set to true, will add dummy components to [transactionBuilder] to make it valid. */ fillTransaction: Boolean = false @@ -267,19 +263,17 @@ data class TestLedgerDSLInterpreter private constructor( override fun _transaction( transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> EnforceVerifyOrFail + dsl: TestTransactionDSLInterpreter.() -> EnforceVerifyOrFail ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, transactionWithLocations) override fun _unverifiedTransaction( transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> Unit + dsl: TestTransactionDSLInterpreter.() -> Unit ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true) - override fun tweak( - dsl: LedgerDSL>.() -> Unit) = - dsl(LedgerDSL(copy())) + override fun _tweak(dsl: LedgerDSLInterpreter.() -> Unit) = + copy().dsl() override fun attachment(attachment: InputStream): SecureHash { return services.attachments.importAttachment(attachment) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index a0b3bce12b..8919966f12 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -73,7 +73,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * Creates a local scoped copy of the transaction. * @param dsl The transaction DSL to be interpreted using the copy. */ - fun tweak(dsl: TransactionDSL.() -> EnforceVerifyOrFail): EnforceVerifyOrFail + fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail): EnforceVerifyOrFail /** * Attaches an attachment containing the named contract to the transaction @@ -82,7 +82,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { fun _attachment(contractClassName: ContractClassName) } -class TransactionDSL(interpreter: T) : TransactionDSLInterpreter by interpreter { +class TransactionDSL(interpreter: T, private val notary: Party) : TransactionDSLInterpreter by interpreter { /** * Looks up the output label and adds the found state as an input. * @param stateLabel The label of the output state specified when calling [TransactionDSLInterpreter.output] and friends. @@ -100,7 +100,7 @@ class TransactionDSL(interpreter: T) : Transa * @param state The state to be added. */ fun input(contractClassName: ContractClassName, state: ContractState) { - val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary = DUMMY_NOTARY)) { + val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary)) { output(contractClassName, null, DUMMY_NOTARY, null, AlwaysAcceptAttachmentConstraint, state) } input(transaction.outRef(0).ref) @@ -116,13 +116,13 @@ class TransactionDSL(interpreter: T) : Transa * Adds a labelled output to the transaction. */ fun output(contractClassName: ContractClassName, label: String, encumbrance: Int, contractState: ContractState) = - output(contractClassName, label, DUMMY_NOTARY, encumbrance, AutomaticHashConstraint, contractState) + output(contractClassName, label, notary, encumbrance, AutomaticHashConstraint, contractState) /** * Adds a labelled output to the transaction. */ fun output(contractClassName: ContractClassName, label: String, contractState: ContractState) = - output(contractClassName, label, DUMMY_NOTARY, null, AutomaticHashConstraint, contractState) + output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState) /** * Adds an output to the transaction. @@ -134,13 +134,13 @@ class TransactionDSL(interpreter: T) : Transa * Adds an output to the transaction. */ fun output(contractClassName: ContractClassName, encumbrance: Int, contractState: ContractState) = - output(contractClassName, null, DUMMY_NOTARY, encumbrance, AutomaticHashConstraint, contractState) + output(contractClassName, null, notary, encumbrance, AutomaticHashConstraint, contractState) /** * Adds an output to the transaction. */ fun output(contractClassName: ContractClassName, contractState: ContractState) = - output(contractClassName, null, DUMMY_NOTARY, null, AutomaticHashConstraint, contractState) + output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState) /** * Adds a command to the transaction. @@ -156,6 +156,10 @@ class TransactionDSL(interpreter: T) : Transa fun timeWindow(time: Instant, tolerance: Duration = 30.seconds) = timeWindow(TimeWindow.withTolerance(time, tolerance)) + /** Creates a local scoped copy of the transaction. */ + fun tweak(dsl: TransactionDSL.() -> EnforceVerifyOrFail) = + _tweak { TransactionDSL(this, notary).dsl() } + /** * @see TransactionDSLInterpreter._attachment */ diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index 5c8736335e..e50f4dea84 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -14,7 +14,17 @@ class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: Attac val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { - val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) + val cordapp = CordappImpl( + contractClassNames = listOf(contractClassName), + initiatedFlows = emptyList(), + rpcFlows = emptyList(), + serviceFlows = emptyList(), + schedulableFlows = emptyList(), + services = emptyList(), + serializationWhitelists = emptyList(), + serializationCustomSerializers = emptyList(), + customSchemas = emptySet(), + jarPath = Paths.get(".").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), attachments))) } 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 2dd37fd648..1dc8e11f25 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -1,7 +1,13 @@ package net.corda.demobench import javafx.scene.image.Image +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.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import tornadofx.* import java.io.InputStreamReader import java.nio.charset.StandardCharsets.UTF_8 @@ -47,6 +53,17 @@ class DemoBench : App(DemoBenchView::class) { init { addStageIcon(Image("cordalogo.png")) + initialiseSerialization() + } + + private fun initialiseSerialization() { + val context = KRYO_P2P_CONTEXT + nodeSerializationEnv = SerializationEnvironmentImpl( + SerializationFactoryImpl().apply { + registerScheme(KryoClientSerializationScheme()) + registerScheme(AMQPClientSerializationScheme()) + }, + 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 a0818c3532..3b856ef5ac 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.NodeInfoFilesCopier +import net.corda.nodeapi.internal.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 59237e97a8..fd1be25923 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 @@ -2,19 +2,26 @@ package net.corda.demobench.model import javafx.beans.binding.IntegerExpression import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.internal.copyToDirectory 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.ServiceIdentityGenerator import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory import java.nio.file.Files import java.nio.file.Path import java.text.SimpleDateFormat +import java.time.Instant import java.util.* import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level @@ -35,6 +42,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { private val command = jvm.commandFor(cordaPath).toTypedArray() private val nodes = LinkedHashMap() + private var notaryIdentity: Party? = null + private var networkParametersCopier: NetworkParametersCopier? = null private val port = AtomicInteger(firstPort) val activeNodes: List @@ -58,6 +67,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { fun IntegerExpression.toLocalAddress() = NetworkHostAndPort("localhost", value) val location = nodeData.nearestCity.value + val notary = nodeData.extraServices.filterIsInstance().noneOrSingle() val nodeConfig = NodeConfig( myLegalName = CordaX500Name( organisation = nodeData.legalName.value.trim(), @@ -67,7 +77,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { p2pAddress = nodeData.p2pPort.toLocalAddress(), rpcAddress = nodeData.rpcPort.toLocalAddress(), webAddress = nodeData.webPort.toLocalAddress(), - notary = nodeData.extraServices.filterIsInstance().noneOrSingle(), + notary = notary, h2port = nodeData.h2Port.value, issuableCurrencies = nodeData.extraServices.filterIsInstance().map { it.currency.toString() } ) @@ -102,6 +112,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { fun runCorda(pty: R3Pty, config: NodeConfigWrapper): Boolean { try { + // Notary can be removed and then added again, that's why we need to perform this check. + require((config.nodeConfig.notary != null).xor(notaryIdentity != null)) { "There must be exactly one notary in the network" } config.nodeDir.createDirectories() // Install any built-in plugins into the working directory. @@ -115,6 +127,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { val cordaEnv = System.getenv().toMutableMap().apply { jvm.setCapsuleCacheDir(this) } + (networkParametersCopier ?: makeNetworkParametersCopier(config)).install(config.nodeDir) pty.run(command, cordaEnv, config.nodeDir.toString()) log.info("Launched node: ${config.nodeConfig.myLegalName}") return true @@ -124,6 +137,30 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { } } + private fun makeNetworkParametersCopier(config: NodeConfigWrapper): NetworkParametersCopier { + val identity = getNotaryIdentity(config) + val parametersCopier = NetworkParametersCopier(NetworkParameters( + minimumPlatformVersion = 1, + notaries = listOf(NotaryInfo(identity, config.nodeConfig.notary!!.validating)), + modifiedTime = Instant.now(), + eventHorizon = 10000.days, + maxMessageSize = 40000, + maxTransactionSize = 40000, + epoch = 1 + )) + notaryIdentity = identity + networkParametersCopier = parametersCopier + return parametersCopier + } + + // Generate notary identity and save it into node's directory. This identity will be used in network parameters. + private fun getNotaryIdentity(config: NodeConfigWrapper): Party { + return ServiceIdentityGenerator.generateToDisk( + dirs = listOf(config.nodeDir), + serviceName = config.nodeConfig.myLegalName, + serviceId = "identity") + } + fun reset() { baseDir = baseDirFor(System.currentTimeMillis()) log.info("Changed base directory: $baseDir") @@ -131,6 +168,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Wipe out any knowledge of previous nodes. nodes.clear() nodeInfoFilesCopier.reset() + notaryIdentity = null + networkParametersCopier = null } /** diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index bf3ef7944d..fa9f56f1d1 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -68,12 +68,12 @@ class ExplorerSimulation(private val options: OptionSet) { driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance", IOUFlow::class.java.`package`.name), isDebug = true, waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true)) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. - val alice = startNode(providedName = ALICE.name, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to "true")) + val alice = startNode(providedName = ALICE.name, rpcUsers = listOf(user)) val bob = startNode(providedName = BOB.name, rpcUsers = listOf(user)) val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager), - customOverrides = mapOf("issuableCurrencies" to listOf("GBP"), "" to "true")) + customOverrides = mapOf("issuableCurrencies" to listOf("GBP"))) val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager), customOverrides = mapOf("issuableCurrencies" to listOf("USD"))) 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 5c8900ebb3..592f0375a4 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 @@ -1,13 +1,14 @@ package net.corda.loadtest.tests import net.corda.client.mock.Generator +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowException +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.internal.concurrent.thenMatch import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection import net.corda.testing.* @@ -15,8 +16,12 @@ import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService import org.slf4j.LoggerFactory +import java.math.BigInteger private val log = LoggerFactory.getLogger("NotaryTest") +private val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10)) +private val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), DUMMY_CASH_ISSUER_KEY.public)) +private val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTransaction, val node: NodeConnection) @@ -26,7 +31,7 @@ val dummyNotarisationTest = LoadTest( val issuerServices = MockServices(makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), MEGA_CORP.name, DUMMY_CASH_ISSUER_KEY) val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> Generator.int().map { - val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[1], DUMMY_CASH_ISSUER) // TODO notary choice + val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[0], DUMMY_CASH_ISSUER) // TODO notary choice val issueTx = issuerServices.signInitialTransaction(issueBuilder) val asset = issueTx.tx.outRef(0) val moveBuilder = DummyContract.move(asset, DUMMY_CASH_ISSUER.party) diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index b5096b46fa..4fa35ff2b9 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -19,9 +19,9 @@ import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.VerifierApi +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation @@ -75,7 +75,8 @@ fun verifierDriver( waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, - jmxPolicy = jmxPolicy + jmxPolicy = jmxPolicy, + compatibilityZone = null ) ), coerce = { it }, @@ -183,7 +184,6 @@ data class VerifierDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDri val securityManager = object : ActiveMQSecurityManager { // We don't need auth, SSL is good enough override fun validateUser(user: String?, password: String?) = true - override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?) = true } diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index 5db494cad1..81571dd722 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -91,11 +91,7 @@ class Verifier { registerScheme(KryoVerifierSerializationScheme) registerScheme(AMQPVerifierSerializationScheme) }, - /** - * Even though default context is set to Kryo P2P, the encoding will be adjusted depending on the incoming - * request received, see use of [context] in [main] method. - */ - KRYO_P2P_CONTEXT) + AMQP_P2P_CONTEXT) } } @@ -108,7 +104,7 @@ class Verifier { override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() } - private object AMQPVerifierSerializationScheme : AbstractAMQPSerializationScheme() { + private object AMQPVerifierSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return (byteSequence == AmqpHeaderV1_0 && (target == SerializationContext.UseCase.P2P)) } @@ -116,4 +112,4 @@ class Verifier { override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory = throw UnsupportedOperationException() override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory = throw UnsupportedOperationException() } -} \ No newline at end of file +}