mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge remote-tracking branch 'open/master' into shams-os-merge-020118
# Conflicts: # client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java # node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt # node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt # node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt # node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt # node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt # node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt # testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
This commit is contained in:
commit
52173219c3
@ -110,7 +110,11 @@ object Crypto {
|
|||||||
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
||||||
)
|
)
|
||||||
|
|
||||||
/** EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. */
|
/**
|
||||||
|
* EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing.
|
||||||
|
* The actual algorithm is PureEdDSA Ed25519 as defined in https://tools.ietf.org/html/rfc8032
|
||||||
|
* Not to be confused with the EdDSA variants, Ed25519ctx and Ed25519ph.
|
||||||
|
*/
|
||||||
@JvmField
|
@JvmField
|
||||||
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||||
4,
|
4,
|
||||||
|
@ -11,6 +11,8 @@ import net.corda.core.serialization.CordaSerializable
|
|||||||
@DoNotImplement
|
@DoNotImplement
|
||||||
interface FlowLogicRefFactory {
|
interface FlowLogicRefFactory {
|
||||||
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||||
|
fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||||
|
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*>
|
||||||
}
|
}
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
|
@ -27,8 +27,8 @@ import kotlin.test.*
|
|||||||
*/
|
*/
|
||||||
class CryptoUtilsTest {
|
class CryptoUtilsTest {
|
||||||
|
|
||||||
val testString = "Hello World"
|
private val testString = "Hello World"
|
||||||
val testBytes = testString.toByteArray()
|
private val testBytes = testString.toByteArray()
|
||||||
|
|
||||||
// key generation test
|
// key generation test
|
||||||
@Test
|
@Test
|
||||||
|
190
core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt
Normal file
190
core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.utilities.hexToByteArray
|
||||||
|
import net.corda.core.utilities.toHex
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||||
|
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||||
|
import org.junit.Test
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.Signature
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing PureEdDSA Ed25519 using test vectors from https://tools.ietf.org/html/rfc8032#section-7.1
|
||||||
|
*/
|
||||||
|
class EdDSATests {
|
||||||
|
@Test
|
||||||
|
fun `PureEdDSA Ed25519 test vectors`() {
|
||||||
|
val edParams = Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
|
||||||
|
|
||||||
|
// MESSAGE (length 0 bytes).
|
||||||
|
val testVector1 = SignatureTestVector(
|
||||||
|
"9d61b19deffd5a60ba844af492ec2cc4" +
|
||||||
|
"4449c5697b326919703bac031cae7f60",
|
||||||
|
"d75a980182b10ab7d54bfed3c964073a" +
|
||||||
|
"0ee172f3daa62325af021a68f707511a",
|
||||||
|
"",
|
||||||
|
"e5564300c360ac729086e2cc806e828a" +
|
||||||
|
"84877f1eb8e5d974d873e06522490155" +
|
||||||
|
"5fb8821590a33bacc61e39701cf9b46b" +
|
||||||
|
"d25bf5f0595bbe24655141438e7a100b"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MESSAGE (length 1 byte).
|
||||||
|
val testVector2 = SignatureTestVector(
|
||||||
|
"4ccd089b28ff96da9db6c346ec114e0f" +
|
||||||
|
"5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||||
|
"3d4017c3e843895a92b70aa74d1b7ebc" +
|
||||||
|
"9c982ccf2ec4968cc0cd55f12af4660c",
|
||||||
|
"72",
|
||||||
|
"92a009a9f0d4cab8720e820b5f642540" +
|
||||||
|
"a2b27b5416503f8fb3762223ebdb69da" +
|
||||||
|
"085ac1e43e15996e458f3613d0f11d8c" +
|
||||||
|
"387b2eaeb4302aeeb00d291612bb0c00"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MESSAGE (length 2 bytes).
|
||||||
|
val testVector3 = SignatureTestVector(
|
||||||
|
"c5aa8df43f9f837bedb7442f31dcb7b1" +
|
||||||
|
"66d38535076f094b85ce3a2e0b4458f7",
|
||||||
|
"fc51cd8e6218a1a38da47ed00230f058" +
|
||||||
|
"0816ed13ba3303ac5deb911548908025",
|
||||||
|
"af82",
|
||||||
|
"6291d657deec24024827e69c3abe01a3" +
|
||||||
|
"0ce548a284743a445e3680d7db5ac3ac" +
|
||||||
|
"18ff9b538d16f290ae67f760984dc659" +
|
||||||
|
"4a7c15e9716ed28dc027beceea1ec40a"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MESSAGE (length 1023 bytes).
|
||||||
|
val testVector1024 = SignatureTestVector(
|
||||||
|
"f5e5767cf153319517630f226876b86c" +
|
||||||
|
"8160cc583bc013744c6bf255f5cc0ee5",
|
||||||
|
"278117fc144c72340f67d0f2316e8386" +
|
||||||
|
"ceffbf2b2428c9c51fef7c597f1d426e",
|
||||||
|
"08b8b2b733424243760fe426a4b54908" +
|
||||||
|
"632110a66c2f6591eabd3345e3e4eb98" +
|
||||||
|
"fa6e264bf09efe12ee50f8f54e9f77b1" +
|
||||||
|
"e355f6c50544e23fb1433ddf73be84d8" +
|
||||||
|
"79de7c0046dc4996d9e773f4bc9efe57" +
|
||||||
|
"38829adb26c81b37c93a1b270b20329d" +
|
||||||
|
"658675fc6ea534e0810a4432826bf58c" +
|
||||||
|
"941efb65d57a338bbd2e26640f89ffbc" +
|
||||||
|
"1a858efcb8550ee3a5e1998bd177e93a" +
|
||||||
|
"7363c344fe6b199ee5d02e82d522c4fe" +
|
||||||
|
"ba15452f80288a821a579116ec6dad2b" +
|
||||||
|
"3b310da903401aa62100ab5d1a36553e" +
|
||||||
|
"06203b33890cc9b832f79ef80560ccb9" +
|
||||||
|
"a39ce767967ed628c6ad573cb116dbef" +
|
||||||
|
"efd75499da96bd68a8a97b928a8bbc10" +
|
||||||
|
"3b6621fcde2beca1231d206be6cd9ec7" +
|
||||||
|
"aff6f6c94fcd7204ed3455c68c83f4a4" +
|
||||||
|
"1da4af2b74ef5c53f1d8ac70bdcb7ed1" +
|
||||||
|
"85ce81bd84359d44254d95629e9855a9" +
|
||||||
|
"4a7c1958d1f8ada5d0532ed8a5aa3fb2" +
|
||||||
|
"d17ba70eb6248e594e1a2297acbbb39d" +
|
||||||
|
"502f1a8c6eb6f1ce22b3de1a1f40cc24" +
|
||||||
|
"554119a831a9aad6079cad88425de6bd" +
|
||||||
|
"e1a9187ebb6092cf67bf2b13fd65f270" +
|
||||||
|
"88d78b7e883c8759d2c4f5c65adb7553" +
|
||||||
|
"878ad575f9fad878e80a0c9ba63bcbcc" +
|
||||||
|
"2732e69485bbc9c90bfbd62481d9089b" +
|
||||||
|
"eccf80cfe2df16a2cf65bd92dd597b07" +
|
||||||
|
"07e0917af48bbb75fed413d238f5555a" +
|
||||||
|
"7a569d80c3414a8d0859dc65a46128ba" +
|
||||||
|
"b27af87a71314f318c782b23ebfe808b" +
|
||||||
|
"82b0ce26401d2e22f04d83d1255dc51a" +
|
||||||
|
"ddd3b75a2b1ae0784504df543af8969b" +
|
||||||
|
"e3ea7082ff7fc9888c144da2af58429e" +
|
||||||
|
"c96031dbcad3dad9af0dcbaaaf268cb8" +
|
||||||
|
"fcffead94f3c7ca495e056a9b47acdb7" +
|
||||||
|
"51fb73e666c6c655ade8297297d07ad1" +
|
||||||
|
"ba5e43f1bca32301651339e22904cc8c" +
|
||||||
|
"42f58c30c04aafdb038dda0847dd988d" +
|
||||||
|
"cda6f3bfd15c4b4c4525004aa06eeff8" +
|
||||||
|
"ca61783aacec57fb3d1f92b0fe2fd1a8" +
|
||||||
|
"5f6724517b65e614ad6808d6f6ee34df" +
|
||||||
|
"f7310fdc82aebfd904b01e1dc54b2927" +
|
||||||
|
"094b2db68d6f903b68401adebf5a7e08" +
|
||||||
|
"d78ff4ef5d63653a65040cf9bfd4aca7" +
|
||||||
|
"984a74d37145986780fc0b16ac451649" +
|
||||||
|
"de6188a7dbdf191f64b5fc5e2ab47b57" +
|
||||||
|
"f7f7276cd419c17a3ca8e1b939ae49e4" +
|
||||||
|
"88acba6b965610b5480109c8b17b80e1" +
|
||||||
|
"b7b750dfc7598d5d5011fd2dcc5600a3" +
|
||||||
|
"2ef5b52a1ecc820e308aa342721aac09" +
|
||||||
|
"43bf6686b64b2579376504ccc493d97e" +
|
||||||
|
"6aed3fb0f9cd71a43dd497f01f17c0e2" +
|
||||||
|
"cb3797aa2a2f256656168e6c496afc5f" +
|
||||||
|
"b93246f6b1116398a346f1a641f3b041" +
|
||||||
|
"e989f7914f90cc2c7fff357876e506b5" +
|
||||||
|
"0d334ba77c225bc307ba537152f3f161" +
|
||||||
|
"0e4eafe595f6d9d90d11faa933a15ef1" +
|
||||||
|
"369546868a7f3a45a96768d40fd9d034" +
|
||||||
|
"12c091c6315cf4fde7cb68606937380d" +
|
||||||
|
"b2eaaa707b4c4185c32eddcdd306705e" +
|
||||||
|
"4dc1ffc872eeee475a64dfac86aba41c" +
|
||||||
|
"0618983f8741c5ef68d3a101e8a3b8ca" +
|
||||||
|
"c60c905c15fc910840b94c00a0b9d0",
|
||||||
|
"0aab4c900501b3e24d7cdf4663326a3a" +
|
||||||
|
"87df5e4843b2cbdb67cbf6e460fec350" +
|
||||||
|
"aa5371b1508f9f4528ecea23c436d94b" +
|
||||||
|
"5e8fcd4f681e30a6ac00a9704a188a03"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MESSAGE (length 64 bytes). TEST SHA(abc).
|
||||||
|
val testVectorSHAabc = SignatureTestVector(
|
||||||
|
"833fe62409237b9d62ec77587520911e" +
|
||||||
|
"9a759cec1d19755b7da901b96dca3d42",
|
||||||
|
"ec172b93ad5e563bf4932c70e1245034" +
|
||||||
|
"c35467ef2efd4d64ebf819683467e2bf",
|
||||||
|
"ddaf35a193617abacc417349ae204131" +
|
||||||
|
"12e6fa4e89a97ea20a9eeee64b55d39a" +
|
||||||
|
"2192992a274fc1a836ba3c23a3feebbd" +
|
||||||
|
"454d4423643ce80e2a9ac94fa54ca49f",
|
||||||
|
"dc2a4459e7369633a52b1bf277839a00" +
|
||||||
|
"201009a3efbf3ecb69bea2186c26b589" +
|
||||||
|
"09351fc9ac90b3ecfdfbc7c66431e030" +
|
||||||
|
"3dca179c138ac17ad9bef1177331a704"
|
||||||
|
)
|
||||||
|
|
||||||
|
val testVectors = listOf(testVector1, testVector2, testVector3, testVector1024, testVectorSHAabc)
|
||||||
|
testVectors.forEach {
|
||||||
|
val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(it.privateKeyHex.hexToByteArray(), edParams))
|
||||||
|
assertEquals(it.signatureOutputHex, doSign(privateKey, it.messageToSignHex.hexToByteArray()).toHex().toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test vector for the variant Ed25519ctx, expected to fail.
|
||||||
|
val testVectorEd25519ctx = SignatureTestVector(
|
||||||
|
"0305334e381af78f141cb666f6199f57" +
|
||||||
|
"bc3495335a256a95bd2a55bf546663f6",
|
||||||
|
"dfc9425e4f968f7f0c29f0259cf5f9ae" +
|
||||||
|
"d6851c2bb4ad8bfb860cfee0ab248292",
|
||||||
|
"f726936d19c800494e3fdaff20b276a8",
|
||||||
|
"fc60d5872fc46b3aa69f8b5b4351d580" +
|
||||||
|
"8f92bcc044606db097abab6dbcb1aee3" +
|
||||||
|
"216c48e8b3b66431b5b186d1d28f8ee1" +
|
||||||
|
"5a5ca2df6668346291c2043d4eb3e90d"
|
||||||
|
)
|
||||||
|
|
||||||
|
val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(testVectorEd25519ctx.privateKeyHex.hexToByteArray(), edParams))
|
||||||
|
assertNotEquals(testVectorEd25519ctx.signatureOutputHex, doSign(privateKey, testVectorEd25519ctx.messageToSignHex.hexToByteArray()).toHex().toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A test vector object for digital signature schemes. */
|
||||||
|
private data class SignatureTestVector(val privateKeyHex: String,
|
||||||
|
val publicKeyHex: String,
|
||||||
|
val messageToSignHex: String,
|
||||||
|
val signatureOutputHex: String)
|
||||||
|
|
||||||
|
// Required to implement a custom doSign function, because Corda's Crypto.doSign does not allow empty messages (testVector1).
|
||||||
|
private fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||||
|
val signature = Signature.getInstance(Crypto.EDDSA_ED25519_SHA512.signatureName, EdDSASecurityProvider())
|
||||||
|
signature.initSign(privateKey)
|
||||||
|
signature.update(clearData)
|
||||||
|
return signature.sign()
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.toTypedArray
|
|
||||||
import net.corda.core.internal.cert
|
import net.corda.core.internal.cert
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
import org.bouncycastle.asn1.x509.GeneralName
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
@ -11,35 +11,40 @@ import org.bouncycastle.asn1.x509.NameConstraints
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.cert.*
|
import java.security.cert.CertPathValidator
|
||||||
import java.util.stream.Stream
|
import java.security.cert.CertPathValidatorException
|
||||||
|
import java.security.cert.PKIXParameters
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class X509NameConstraintsTest {
|
class X509NameConstraintsTest {
|
||||||
|
|
||||||
private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair<KeyStore, KeyStore> {
|
private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair<KeyStore, KeyStore> {
|
||||||
val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootKeys)
|
val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val nodeCaCert = X509Utilities.createCertificate(
|
||||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
CertificateType.NODE_CA,
|
||||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB"), intermediateCAKeyPair.public)
|
intermediateCa.certificate,
|
||||||
|
intermediateCa.keyPair,
|
||||||
val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
CordaX500Name("Corda Client CA", "R3 Ltd", "London", "GB"),
|
||||||
val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, CordaX500Name(commonName = "Corda Client CA", organisation = "R3 Ltd", locality = "London", country = "GB"), clientCAKeyPair.public, nameConstraints = nameConstraints)
|
nodeCaKeyPair.public,
|
||||||
|
nameConstraints = nameConstraints)
|
||||||
|
|
||||||
val keyPass = "password"
|
val keyPass = "password"
|
||||||
val trustStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
val trustStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
trustStore.load(null, keyPass.toCharArray())
|
trustStore.load(null, keyPass.toCharArray())
|
||||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
|
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate.cert)
|
||||||
|
|
||||||
val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public)
|
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, subjectName, tlsKeyPair.public)
|
||||||
|
|
||||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
keyStore.load(null, keyPass.toCharArray())
|
keyStore.load(null, keyPass.toCharArray())
|
||||||
keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(),
|
keyStore.addOrReplaceKey(
|
||||||
Stream.of(tlsCert, clientCACert, intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
|
X509Utilities.CORDA_CLIENT_TLS,
|
||||||
|
tlsKeyPair.private,
|
||||||
|
keyPass.toCharArray(),
|
||||||
|
arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCa.certificate))
|
||||||
return Pair(keyStore, trustStore)
|
return Pair(keyStore, trustStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,18 +28,16 @@ Setting your dependencies
|
|||||||
|
|
||||||
Choosing your Corda version
|
Choosing your Corda version
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
The following two lines of the ``build.gradle`` file define the Corda version used to build your CorDapp:
|
``ext.corda_release_version`` and ``ext.corda_gradle_plugins_version`` are used in the ``build.gradle`` to define the
|
||||||
|
versions of Corda and the Corda Gradle Plugins that are used to build your CorDapp.
|
||||||
|
|
||||||
|
For example, to use version 1.0 of Corda and version 1.0 of the Corda gradle plugins, you'd write:
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
ext.corda_release_version = '1.0.0'
|
ext.corda_release_version = '1.0.0'
|
||||||
ext.corda_gradle_plugins_version = '1.0.0'
|
ext.corda_gradle_plugins_version = '1.0.0'
|
||||||
|
|
||||||
In this case, our CorDapp will use:
|
|
||||||
|
|
||||||
* Version 1.0 of Corda
|
|
||||||
* Version 1.0 of the Corda gradle plugins
|
|
||||||
|
|
||||||
You can find the latest published version of both here: https://bintray.com/r3/corda.
|
You can find the latest published version of both here: https://bintray.com/r3/corda.
|
||||||
|
|
||||||
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
|
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
|
||||||
@ -50,25 +48,32 @@ In certain cases, you may also wish to build against the unstable Master branch.
|
|||||||
|
|
||||||
Corda dependencies
|
Corda dependencies
|
||||||
^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^
|
||||||
The ``cordformation`` plugin adds:
|
The ``cordformation`` plugin adds two new gradle configurations:
|
||||||
|
|
||||||
* ``cordaCompile`` as a new configuration that ``compile`` extends from
|
* ``cordaCompile``, which extends ``compile``
|
||||||
* ``cordaRuntime`` which ``runtime`` extends from
|
* ``cordaRuntime``, which extends ``runtime``
|
||||||
|
|
||||||
To build against Corda you must add the following to your ``build.gradle`` file;
|
To build against Corda, you must add the following to your ``build.gradle`` file:
|
||||||
|
|
||||||
* The ``net.corda:corda:<version>`` JAR as a ``cordaRuntime`` dependency
|
* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
|
||||||
* Each compile dependency (eg ``corda-core``) as a ``cordaCompile`` dependency
|
* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency
|
||||||
|
|
||||||
To use Corda's test facilities you must add ``net.corda:corda-test-utils:<version>`` as a ``testCompile`` dependency
|
You may also want to add:
|
||||||
(i.e. a default Java/Kotlin test compile task).
|
|
||||||
|
* ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile`` dependency, in order to use Corda's test
|
||||||
|
frameworks
|
||||||
|
* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's
|
||||||
|
built-in development webserver
|
||||||
|
|
||||||
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
|
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
|
||||||
|
|
||||||
Dependencies on other CorDapps
|
Dependencies on other CorDapps
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
Sometimes, a CorDapp you build will depend on states, contracts or flows defined in another CorDapp. You must include
|
You CorDapp may also depend on classes defined in another CorDapp, such as states, contracts and flows. There are two
|
||||||
the CorDapp your CorDapp depends upon as a ``cordapp`` dependency in your ``build.gradle`` file.
|
ways to add another CorDapp as a dependency in your CorDapp's ``build.gradle`` file:
|
||||||
|
|
||||||
|
* ``cordapp project(":another-cordapp")`` (use this if the other CorDapp is defined in a module in the same project)
|
||||||
|
* ``cordapp "net.corda:another-cordapp:1.0"`` (use this otherwise)
|
||||||
|
|
||||||
Other dependencies
|
Other dependencies
|
||||||
^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -10,7 +10,6 @@ import net.corda.core.messaging.CordaRPCOps
|
|||||||
import net.corda.core.serialization.SerializationWhitelist
|
import net.corda.core.serialization.SerializationWhitelist
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.webserver.services.WebServerPluginRegistry
|
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import javax.ws.rs.GET
|
import javax.ws.rs.GET
|
||||||
import javax.ws.rs.Path
|
import javax.ws.rs.Path
|
||||||
|
@ -67,10 +67,21 @@ The bootstrapper tool can be built with the command:
|
|||||||
|
|
||||||
The resulting jar can be found in ``tools/bootstrapper/build/libs/``.
|
The resulting jar can be found in ``tools/bootstrapper/build/libs/``.
|
||||||
|
|
||||||
To use it, run the following command, specifying the root directory which hosts all the node directories as the argument:
|
To use it, create a directory containing a ``node.conf`` file for each node you want to create. Then run the following command:
|
||||||
|
|
||||||
``java -jar network-bootstrapper.jar <nodes-root-dir>``
|
``java -jar network-bootstrapper.jar <nodes-root-dir>``
|
||||||
|
|
||||||
|
For example running the command on a directory containing these files :
|
||||||
|
|
||||||
|
.. sourcecode:: none
|
||||||
|
|
||||||
|
.
|
||||||
|
├── notary.conf // The notary's node.conf file
|
||||||
|
├── partya.conf // Party A's node.conf file
|
||||||
|
└── partyb.conf // Party B's node.conf file
|
||||||
|
|
||||||
|
Would generate directories containing three nodes: notary, partya and partyb.
|
||||||
|
|
||||||
Starting the nodes
|
Starting the nodes
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -7,25 +7,32 @@
|
|||||||
Shell
|
Shell
|
||||||
=====
|
=====
|
||||||
|
|
||||||
The Corda shell is an embedded command line that allows an administrator to control and monitor the node.
|
.. contents::
|
||||||
Some of its features include:
|
|
||||||
|
|
||||||
* Invoking any of the RPCs the node exposes to applications.
|
The Corda shell is an embedded command line that allows an administrator to control and monitor a node. It is based on
|
||||||
* Starting flows.
|
the `CRaSH`_ shell and supports many of the same features. These features include:
|
||||||
* View a dashboard of threads, heap usage, VM properties.
|
|
||||||
* Uploading and downloading zips from the attachment store.
|
|
||||||
* Issue SQL queries to the underlying database.
|
|
||||||
* View JMX metrics and monitoring exports.
|
|
||||||
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data.
|
|
||||||
|
|
||||||
It is based on the popular `CRaSH`_ shell used in various other projects and supports many of the same features.
|
* Invoking any of the node's RPC methods
|
||||||
|
* Viewing a dashboard of threads, heap usage, VM properties
|
||||||
|
* Uploading and downloading attachments
|
||||||
|
* Issuing SQL queries to the underlying database
|
||||||
|
* Viewing JMX metrics and monitoring exports
|
||||||
|
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data
|
||||||
|
|
||||||
Local terminal shell runs only in development mode. It may be disabled by passing the ``--no-local-shell`` flag to the node.
|
The shell via the local terminal
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
SSH server
|
In development mode, the shell will display in the node's terminal window. It may be disabled by passing the
|
||||||
----------
|
``--no-local-shell`` flag when running the node.
|
||||||
|
|
||||||
Shell can also be accessible via SSH. By default SSH server is *disabled*. To enable it port must be configured - in ``node.conf`` file
|
The shell via SSH
|
||||||
|
-----------------
|
||||||
|
The shell is also accessible via SSH.
|
||||||
|
|
||||||
|
Enabling SSH access
|
||||||
|
*******************
|
||||||
|
|
||||||
|
By default, the SSH server is *disabled*. To enable it, a port must be configured in the node's ``node.conf`` file:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
@ -33,80 +40,159 @@ Shell can also be accessible via SSH. By default SSH server is *disabled*. To en
|
|||||||
port = 2222
|
port = 2222
|
||||||
}
|
}
|
||||||
|
|
||||||
Authentication and authorization
|
Authentication
|
||||||
--------------------------------
|
**************
|
||||||
SSH requires users to login first - using the same users as RPC system. In fact, the shell serves as a proxy to RPC and communicates
|
Users log in to shell via SSH using the same credentials as for RPC. This is because the shell actually communicates
|
||||||
with the node using RPC calls. This also means that RPC permissions are enforced. No permissions are required to allow the connection
|
with the node using RPC calls. No RPC permissions are required to allow the connection and log in.
|
||||||
and log in.
|
|
||||||
Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed`` while starting flows requires
|
|
||||||
``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows`` in addition to a permission for a particular flow.
|
|
||||||
|
|
||||||
Host key
|
The host key is loaded from the ``<node root directory>/sshkey/hostkey.pem`` file. If this file does not exist, it is
|
||||||
--------
|
generated automatically. In development mode, the seed may be specified to give the same results on the same computer
|
||||||
|
in order to avoid host-checking errors.
|
||||||
|
|
||||||
The host key is loaded from ``sshkey/hostkey.pem`` file. If the file does not exist, it will be generated randomly, however
|
Connecting to the shell
|
||||||
in the development mode seed may be tuned to give the same results on the same computer - in order to avoid host checking
|
***********************
|
||||||
errors.
|
|
||||||
|
|
||||||
Connecting
|
Linux and MacOS
|
||||||
----------
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Linux and MacOS computers usually come with SSH client preinstalled. On Windows it usually requires extra download.
|
Run the following command from the terminal:
|
||||||
Usual connection syntax is ``ssh user@host -p 2222`` - where ``user`` is a RPC username, and ``-p`` specifies a port parameters -
|
|
||||||
it's the same as setup in ``node.conf`` file. ``host`` should point to a node hostname, usually ``localhost`` if connecting and
|
|
||||||
running node on the same computer. Password will be asked after establishing connection.
|
|
||||||
|
|
||||||
:note: While developing, checking multiple samples or simply restarting a node frequently host key may be regenerated. SSH usually
|
.. code:: bash
|
||||||
saved once trusted hosts and will refuse to connect in case of a change. Then check may be disabled with extra options
|
|
||||||
``ssh -o StrictHostKeyChecking=no user@host -p2222``. This option should never be used in production environment!
|
|
||||||
|
|
||||||
Getting help
|
ssh -p [portNumber] [host] -l [user]
|
||||||
------------
|
|
||||||
|
|
||||||
You can run ``help`` to list the available commands.
|
Where:
|
||||||
|
|
||||||
The shell has a ``man`` command that can be used to get interactive help on many commands. You can also use the
|
* ``[portNumber]`` is the port number specified in the ``node.conf`` file
|
||||||
``--help`` or ``-h`` flags to a command to get info about what switches it supports.
|
* ``[host]`` is the node's host (e.g. ``localhost`` if running the node locally)
|
||||||
|
* ``[user]`` is the RPC username
|
||||||
|
|
||||||
Commands may have subcommands, in the same style as ``git``. In that case running the command by itself will
|
The RPC password will be requested after a connection is established.
|
||||||
list the supported subcommands.
|
|
||||||
|
|
||||||
Starting flows and performing remote method calls
|
:note: In development mode, restarting a node frequently may cause the host key to be regenerated. SSH usually saves
|
||||||
-------------------------------------------------
|
trusted hosts and will refuse to connect in case of a change. This check can be disabled using the
|
||||||
|
``-o StrictHostKeyChecking=no`` flag. This option should never be used in production environment!
|
||||||
|
|
||||||
**Flows** are the way the ledger is changed. If you aren't familiar with them, please review ":doc:`flow-state-machines`"
|
Windows
|
||||||
first. The ``flow list`` command can be used to list the flows understood by the node, ``flow watch`` shows all the flows
|
^^^^^^^
|
||||||
currently running on the node with the result (or error) information in a user friendly way, ``flow start`` can be
|
|
||||||
used to start flows. The ``flow start`` command takes the class name of a flow, or *any unambiguous substring* and
|
|
||||||
then the data to be passed to the flow constructor. The unambiguous substring feature is helpful for reducing
|
|
||||||
the needed typing. If the match is ambiguous the possible matches will be printed out. If a flow has multiple
|
|
||||||
constructors then the names and types of the arguments will be used to try and determine which to use automatically.
|
|
||||||
If the match against available constructors is unclear, the reasons each available constructor failed to match
|
|
||||||
will be printed out. In the case of an ambiguous match, the first applicable will be used.
|
|
||||||
|
|
||||||
**RPCs** (remote procedure calls) are commands that can be sent to the node to query it, control it and manage it.
|
Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used.
|
||||||
RPCs don't typically do anything that changes the global ledger, but they may change node-specific data in the
|
|
||||||
database. Each RPC is one method on the ``CordaRPCOps`` interface, and may return a stream of events that will
|
|
||||||
be shown on screen until you press Ctrl-C. You perform an RPC by using ``run`` followed by the name.
|
|
||||||
|
|
||||||
.. raw:: html
|
Permissions
|
||||||
|
***********
|
||||||
|
|
||||||
<center><b><a href="api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html">Documentation of available RPCs</a></b><p></center>
|
When accessing the shell via SSH, some additional RPC permissions are required:
|
||||||
|
|
||||||
Whichever form of change is used, there is a need to provide *parameters* to either the RPC or the flow
|
* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed``
|
||||||
constructor. Because parameters can be any arbitrary Java object graph, we need a convenient syntax to express
|
* Starting flows requires ``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows``, as well as a
|
||||||
this sort of data. The shell uses a syntax called `Yaml`_ to do this.
|
permission for the flow being started
|
||||||
|
|
||||||
Data syntax
|
Interacting with the node via the shell
|
||||||
-----------
|
---------------------------------------
|
||||||
|
|
||||||
Yaml (yet another markup language) is a simple JSON-like way to describe object graphs. It has several features
|
The shell interacts with the node by issuing RPCs (remote procedure calls). You make an RPC from the shell by typing
|
||||||
that make it helpful for our use case, like a lightweight syntax and support for "bare words" which mean you can
|
``run`` followed by the name of the desired RPC method. For example, you'd see a list of the registered flows on your
|
||||||
often skip the quotes around strings. Here is an example of how this syntax is used:
|
node by running:
|
||||||
|
|
||||||
|
``run registeredFlows``
|
||||||
|
|
||||||
|
Some RPCs return a stream of events that will be shown on screen until you press Ctrl-C.
|
||||||
|
|
||||||
|
You can find a list of the available RPC methods
|
||||||
|
`here <https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html>`_.
|
||||||
|
|
||||||
|
Flow commands
|
||||||
|
*************
|
||||||
|
|
||||||
|
The shell also has special commands for working with flows:
|
||||||
|
|
||||||
|
* ``flow list`` lists the flows available on the node
|
||||||
|
* ``flow watch`` shows all the flows currently running on the node with result (or error) information
|
||||||
|
* ``flow start`` starts a flow. The ``flow start`` command takes the name of a flow class, or
|
||||||
|
*any unambiguous substring* thereof, as well as the data to be passed to the flow constructor. If there are several
|
||||||
|
matches for a given substring, the possible matches will be printed out. If a flow has multiple constructors then the
|
||||||
|
names and types of the arguments will be used to try and automatically determine which one to use. If the match
|
||||||
|
against available constructors is unclear, the reasons each available constructor failed to match will be printed
|
||||||
|
out. In the case of an ambiguous match, the first applicable constructor will be used
|
||||||
|
|
||||||
|
Parameter syntax
|
||||||
|
****************
|
||||||
|
|
||||||
|
Parameters are passed to RPC or flow commands using a syntax called `Yaml`_ (yet another markup language), a
|
||||||
|
simple JSON-like language. The key features of Yaml are:
|
||||||
|
|
||||||
|
* Parameters are separated by commas
|
||||||
|
* Each parameter is specified as a ``key: value`` pair
|
||||||
|
|
||||||
|
* There **MUST** to be a space after the colon, otherwise you'll get a syntax error
|
||||||
|
|
||||||
|
* Strings do not need to be surrounded by quotes unless they contain commas, colons or embedded quotes
|
||||||
|
* Class names must be fully-qualified (e.g. ``java.lang.String``)
|
||||||
|
|
||||||
|
.. note:: If your CorDapp is written in Java, named arguments won't work unless you compiled the node using the
|
||||||
|
``-parameters`` argument to javac. See :doc:`generating-a-node` for how to specify it via Gradle.
|
||||||
|
|
||||||
|
Creating an instance of a class
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Class instances are created using curly-bracket syntax. For example, if we have a ``Campaign`` class with the following
|
||||||
|
constructor:
|
||||||
|
|
||||||
|
``data class Campaign(val name: String, val target: Int)``
|
||||||
|
|
||||||
|
Then we could create an instance of this class to pass as a parameter as follows:
|
||||||
|
|
||||||
|
``newCampaign: { name: Roger, target: 1000 }``
|
||||||
|
|
||||||
|
Where ``newCampaign`` is a parameter of type ``Campaign``.
|
||||||
|
|
||||||
|
Mappings from strings to types
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Several parameter types can automatically be mapped from strings. See the `defined parsers`_ for more information. We
|
||||||
|
cover the most common types here.
|
||||||
|
|
||||||
|
Amount
|
||||||
|
~~~~~~
|
||||||
|
A parameter of type ``Amount<Currency>`` can be written as either:
|
||||||
|
|
||||||
|
* A dollar ($), pound (£) or euro (€) symbol followed by the amount as a decimal
|
||||||
|
* The amount as a decimal followed by the ISO currency code (e.g. "100.12 CHF")
|
||||||
|
|
||||||
|
OpaqueBytes
|
||||||
|
~~~~~~~~~~~
|
||||||
|
A parameter of type ``OpaqueBytes`` can be provided as a string, which will be automatically converted to
|
||||||
|
``OpaqueBytes``.
|
||||||
|
|
||||||
|
Party
|
||||||
|
~~~~~
|
||||||
|
A parameter of type ``Party`` can be written in several ways:
|
||||||
|
|
||||||
|
* By using the node's full name: ``"O=Monogram Bank,L=Sao Paulo,C=GB"``
|
||||||
|
* By specifying the organisation name only: ``"Monogram Bank"``
|
||||||
|
* By specifying any other non-ambiguous part of the name: ``"Sao Paulo"`` (if only one network node is located in Sao
|
||||||
|
Paulo)
|
||||||
|
|
||||||
|
Instant
|
||||||
|
~~~~~~~
|
||||||
|
A parameter of type ``Instant`` can be written as follows: ``"2017-12-22T00:00:00Z"``.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
Starting a flow
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We would start the ``CashIssue`` flow as follows:
|
||||||
|
|
||||||
``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "O=Bank A,L=London,C=GB", notary: "O=Notary Service,OU=corda,L=London,C=GB"``
|
``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "O=Bank A,L=London,C=GB", notary: "O=Notary Service,OU=corda,L=London,C=GB"``
|
||||||
|
|
||||||
This invokes a constructor of a flow with the following prototype in the code:
|
This breaks down as follows:
|
||||||
|
|
||||||
|
* ``flow start`` is a shell command for starting a flow
|
||||||
|
* ``CashIssue`` is the flow we want to start
|
||||||
|
* Each ``name: value`` pair after that is a flow constructor argument
|
||||||
|
|
||||||
|
This command invokes the following ``CashIssue`` constructor:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -117,50 +203,44 @@ This invokes a constructor of a flow with the following prototype in the code:
|
|||||||
val recipient: Party,
|
val recipient: Party,
|
||||||
val notary: Party) : AbstractCashFlow(progressTracker)
|
val notary: Party) : AbstractCashFlow(progressTracker)
|
||||||
|
|
||||||
Here, everything after ``CashIssue`` is specifying the arguments to the constructor of a flow. In Yaml, an object
|
Querying the vault
|
||||||
is specified as a set of ``key: value`` pairs and in our form, we separate them by commas. There are a few things
|
~~~~~~~~~~~~~~~~~~
|
||||||
to note about this syntax:
|
|
||||||
|
|
||||||
* When a parameter is of type ``Amount<Currency>`` you can write it as either one of the dollar symbol ($),
|
We would query the vault for ``IOUState`` states as follows:
|
||||||
pound (£), euro (€) followed by the amount as a decimal, or as the value followed by the ISO currency code
|
|
||||||
e.g. "100.12 CHF"
|
|
||||||
* ``OpaqueBytes`` is filled with the contents of whatever is provided as a string.
|
|
||||||
* ``Party`` objects are looked up by name.
|
|
||||||
* Strings do not need to be surrounded by quotes unless they contain a comma or embedded quotes. This makes it
|
|
||||||
a lot more convenient to type such strings.
|
|
||||||
|
|
||||||
Other types also have sensible mappings from strings. See `the defined parsers`_ for more information.
|
``run vaultQuery contractStateType: com.template.IOUState``
|
||||||
|
|
||||||
Nested objects can be created using curly braces, as in ``{ a: 1, b: 2}``. This is helpful when no particular
|
This breaks down as follows:
|
||||||
parser is defined for the type you need, for instance, if an API requires a ``Pair<String, Int>``
|
|
||||||
which could be represented as ``{ first: foo, second: 123 }``.
|
|
||||||
|
|
||||||
.. note:: If your CorDapp is written in Java,
|
* ``run`` is a shell command for making an RPC call
|
||||||
named arguments won't work unless you compiled using the ``-parameters`` argument to javac.
|
* ``vaultQuery`` is the RPC call we want to make
|
||||||
See :doc:`generating-a-node` for how to specify it via Gradle.
|
* ``contractStateType: com.template.IOUState`` is the fully-qualified name of the state type we are querying for
|
||||||
|
|
||||||
The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this:
|
|
||||||
|
|
||||||
``run registeredFlows``
|
|
||||||
|
|
||||||
Attachments
|
Attachments
|
||||||
-----------
|
***********
|
||||||
|
|
||||||
The shell can be used to upload and download attachments from the node interactively. To learn more, see
|
The shell can be used to upload and download attachments from the node. To learn more, see the tutorial
|
||||||
the tutorial ":doc:`tutorial-attachments`".
|
":doc:`tutorial-attachments`".
|
||||||
|
|
||||||
|
Getting help
|
||||||
|
************
|
||||||
|
|
||||||
|
You can type ``help`` in the shell to list the available commands, and ``man`` to get interactive help on many
|
||||||
|
commands. You can also pass the ``--help`` or ``-h`` flags to a command to get info about what switches it supports.
|
||||||
|
|
||||||
|
Commands may have subcommands, in the same style as ``git``. In that case, running the command by itself will
|
||||||
|
list the supported subcommands.
|
||||||
|
|
||||||
Extending the shell
|
Extending the shell
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
The shell can be extended using commands written in either Java or `Groovy`_ (Groovy is a scripting language that
|
The shell can be extended using commands written in either Java or `Groovy`_ (a Java-compatible scripting language).
|
||||||
is Java compatible). Such commands have full access to the node internal APIs and thus can be used to achieve
|
These commands have full access to the node's internal APIs and thus can be used to achieve almost anything.
|
||||||
almost anything.
|
|
||||||
|
|
||||||
A full tutorial on how to write such commands is out of scope for this documentation, to learn more please
|
A full tutorial on how to write such commands is out of scope for this documentation. To learn more, please refer to
|
||||||
refer to the `CRaSH`_ documentation. New commands can be placed in the ``shell-commands`` subdirectory in the
|
the `CRaSH`_ documentation. New commands are placed in the ``shell-commands`` subdirectory in the node directory. Edits
|
||||||
node directory. Edits to existing commands will be used automatically, but at this time commands added after the
|
to existing commands will be used automatically, but currently commands added after the node has started won't be
|
||||||
node has started won't be automatically detected. Commands should be named in all lower case with either a
|
automatically detected. Commands must have names all in lower-case with either a ``.java`` or ``.groovy`` extension.
|
||||||
``.java`` or ``.groovy`` extension.
|
|
||||||
|
|
||||||
.. warning:: Commands written in Groovy ignore Java security checks, so have unrestricted access to node and JVM
|
.. warning:: Commands written in Groovy ignore Java security checks, so have unrestricted access to node and JVM
|
||||||
internals regardless of any sandboxing that may be in place. Don't allow untrusted users to edit files in the
|
internals regardless of any sandboxing that may be in place. Don't allow untrusted users to edit files in the
|
||||||
@ -171,14 +251,13 @@ Limitations
|
|||||||
|
|
||||||
The shell will be enhanced over time. The currently known limitations include:
|
The shell will be enhanced over time. The currently known limitations include:
|
||||||
|
|
||||||
* SSH access is currently not available.
|
* There is no command completion for flows or RPCs
|
||||||
* There is no command completion for flows or RPCs.
|
* Command history is not preserved across restarts
|
||||||
* Command history is not preserved across restarts.
|
* The ``jdbc`` command requires you to explicitly log into the database first
|
||||||
* The ``jdbc`` command requires you to explicitly log into the database first.
|
* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted
|
||||||
* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted.
|
* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using
|
||||||
* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using.
|
|
||||||
|
|
||||||
.. _Yaml: http://www.yaml.org/spec/1.2/spec.html
|
.. _Yaml: http://www.yaml.org/spec/1.2/spec.html
|
||||||
.. _the defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html
|
.. _defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html
|
||||||
.. _Groovy: http://groovy-lang.org/
|
.. _Groovy: http://groovy-lang.org/
|
||||||
.. _CRaSH: http://www.crashub.org/
|
.. _CRaSH: http://www.crashub.org/
|
||||||
|
@ -35,8 +35,14 @@ the transaction to the regulator. There are two important aspects to note here:
|
|||||||
If the states define a relational mapping (see :doc:`api-persistence`) then the regulator will be able to query the
|
If the states define a relational mapping (see :doc:`api-persistence`) then the regulator will be able to query the
|
||||||
reports from their database and observe new transactions coming in via RPC.
|
reports from their database and observe new transactions coming in via RPC.
|
||||||
|
|
||||||
.. warning:: Nodes which act as both observers and which directly take part in the ledger are not supported at this
|
Caveats
|
||||||
time. In particular, coin selection may return states which you do not have the private keys to be able to sign
|
-------
|
||||||
for. Future versions of Corda may address this issue, but for now, if you wish to both participate in the ledger
|
|
||||||
and also observe transactions that you can't sign for you will need to run two nodes and have two separate
|
* Nodes which act as both observers and direct participants in the ledger are not supported at this time. In
|
||||||
identities.
|
particular, coin selection may return states which you do not have the private keys to be able to sign for. Future
|
||||||
|
versions of Corda may address this issue, but for now, if you wish to both participate in the ledger and also observe
|
||||||
|
transactions that you can't sign for you will need to run two nodes and have two separate identities
|
||||||
|
|
||||||
|
* Nodes only record each transaction once. If a node has already recorded a transaction in non-observer mode, it cannot
|
||||||
|
later re-record the same transaction as an observer. This issue is tracked here:
|
||||||
|
https://r3-cev.atlassian.net/browse/CORDA-883
|
@ -26,7 +26,7 @@ additional classes are used when the jar is invoked directly. To do this we'll u
|
|||||||
./gradlew experimental:quasar-hook:jar
|
./gradlew experimental:quasar-hook:jar
|
||||||
./gradlew samples:irs-demo:deployNodes
|
./gradlew samples:irs-demo:deployNodes
|
||||||
cd samples/irs-demo/build/nodes/NotaryService
|
cd samples/irs-demo/build/nodes/NotaryService
|
||||||
java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix -jar corda.jar
|
java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix,org.jolokia -jar corda.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the node is started just exit the node.
|
Once the node is started just exit the node.
|
||||||
|
@ -90,8 +90,12 @@ private abstract class JavaCommand(
|
|||||||
add(getJavaPath())
|
add(getJavaPath())
|
||||||
addAll(jvmArgs)
|
addAll(jvmArgs)
|
||||||
add("-Dname=$nodeName")
|
add("-Dname=$nodeName")
|
||||||
null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
|
val jvmArgs: MutableList<String> = mutableListOf()
|
||||||
null != monitoringPort && add("-Dcapsule.jvm.args=-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
|
null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
|
||||||
|
null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
|
||||||
|
if (jvmArgs.isNotEmpty()) {
|
||||||
|
add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}")
|
||||||
|
}
|
||||||
add("-jar")
|
add("-jar")
|
||||||
add(jarName)
|
add(jarName)
|
||||||
init()
|
init()
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
|
import net.corda.core.crypto.CompositeKey
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.cert
|
||||||
|
import net.corda.core.internal.createDirectories
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.internal.toX509CertHolder
|
||||||
|
import net.corda.core.utilities.trace
|
||||||
|
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
||||||
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains utility methods for generating identities for a node.
|
||||||
|
*
|
||||||
|
* WARNING: This is not application for production use.
|
||||||
|
*/
|
||||||
|
object DevIdentityGenerator {
|
||||||
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
// TODO These don't need to be prefixes but can be the full aliases
|
||||||
|
// TODO Move these constants out of here as the node needs access to them
|
||||||
|
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
|
||||||
|
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
|
||||||
|
|
||||||
|
/** Install a node key store for the given node directory using the given legal name. */
|
||||||
|
fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party {
|
||||||
|
val nodeSslConfig = object : NodeSSLConfiguration {
|
||||||
|
override val baseDirectory = nodeDir
|
||||||
|
override val keyStorePassword: String = "cordacadevpass"
|
||||||
|
override val trustStorePassword get() = throw NotImplementedError("Not expected to be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO The passwords for the dev key stores are spread everywhere and should be constants in a single location
|
||||||
|
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
|
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||||
|
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||||
|
|
||||||
|
nodeSslConfig.certificatesDirectory.createDirectories()
|
||||||
|
nodeSslConfig.createDevKeyStores(rootCert.toX509CertHolder(), intermediateCa, legalName)
|
||||||
|
|
||||||
|
val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword)
|
||||||
|
val identity = keyStoreWrapper.storeLegalIdentity(legalName, "$NODE_IDENTITY_ALIAS_PREFIX-private-key", Crypto.generateKeyPair())
|
||||||
|
return identity.party
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateDistributedNotaryIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
||||||
|
require(dirs.isNotEmpty())
|
||||||
|
|
||||||
|
log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||||
|
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
||||||
|
val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
||||||
|
|
||||||
|
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
|
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||||
|
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||||
|
|
||||||
|
keyPairs.zip(dirs) { keyPair, nodeDir ->
|
||||||
|
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
|
||||||
|
X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, notaryName, publicKey)
|
||||||
|
}
|
||||||
|
val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks"
|
||||||
|
val keystore = loadOrCreateKeyStore(distServKeyStoreFile, "cordacadevpass")
|
||||||
|
keystore.setCertificateEntry("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert.cert)
|
||||||
|
keystore.setKeyEntry(
|
||||||
|
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
|
||||||
|
keyPair.private,
|
||||||
|
"cordacadevkeypass".toCharArray(),
|
||||||
|
arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert))
|
||||||
|
keystore.save(distServKeyStoreFile, "cordacadevpass")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Party(notaryName, compositeKey)
|
||||||
|
}
|
||||||
|
}
|
@ -1,66 +0,0 @@
|
|||||||
package net.corda.nodeapi.internal
|
|
||||||
|
|
||||||
import net.corda.core.crypto.CompositeKey
|
|
||||||
import net.corda.core.crypto.generateKeyPair
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.cert
|
|
||||||
import net.corda.core.internal.createDirectories
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
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 IdentityGenerator {
|
|
||||||
private val log = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
|
|
||||||
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
|
|
||||||
|
|
||||||
fun generateNodeIdentity(dir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party {
|
|
||||||
return generateToDisk(listOf(dir), legalName, NODE_IDENTITY_ALIAS_PREFIX, threshold = 1, customRootCert = customRootCert)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateDistributedNotaryIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1, customRootCert: X509Certificate? = null): Party {
|
|
||||||
return generateToDisk(dirs, notaryName, DISTRIBUTED_NOTARY_ALIAS_PREFIX, threshold, customRootCert)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 name The name of the identity.
|
|
||||||
* @param threshold The threshold for the generated group [CompositeKey].
|
|
||||||
* @param customRootCert the certificate to use as the Corda root CA. If not specified the one in
|
|
||||||
* internal/certificates/cordadevcakeys.jks is used.
|
|
||||||
*/
|
|
||||||
private fun generateToDisk(dirs: List<Path>,
|
|
||||||
name: CordaX500Name,
|
|
||||||
aliasPrefix: String,
|
|
||||||
threshold: Int,
|
|
||||||
customRootCert: X509Certificate?): Party {
|
|
||||||
log.trace { "Generating identity \"$name\" for nodes: ${dirs.joinToString()}" }
|
|
||||||
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
|
||||||
val key = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
|
||||||
|
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
|
||||||
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
|
||||||
val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
|
||||||
|
|
||||||
keyPairs.zip(dirs) { keyPair, dir ->
|
|
||||||
val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, keyPair.public)
|
|
||||||
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, key)
|
|
||||||
val certPath = (dir / "certificates").createDirectories() / "distributedService.jks"
|
|
||||||
val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass")
|
|
||||||
keystore.setCertificateEntry("$aliasPrefix-composite-key", compositeKeyCert.cert)
|
|
||||||
keystore.setKeyEntry("$aliasPrefix-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert))
|
|
||||||
keystore.save(certPath, "cordacadevpass")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Party(name, key)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,57 @@
|
|||||||
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.x500Name
|
||||||
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralName
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
|
import org.bouncycastle.asn1.x509.NameConstraints
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using
|
||||||
|
* the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA.
|
||||||
|
*/
|
||||||
|
fun SSLConfiguration.createDevKeyStores(rootCert: X509CertificateHolder, intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name) {
|
||||||
|
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
|
||||||
|
|
||||||
|
loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply {
|
||||||
|
addOrReplaceKey(
|
||||||
|
X509Utilities.CORDA_CLIENT_CA,
|
||||||
|
nodeCaKeyPair.private,
|
||||||
|
keyStorePassword.toCharArray(),
|
||||||
|
arrayOf(nodeCaCert, intermediateCa.certificate, rootCert))
|
||||||
|
save(nodeKeystore, keyStorePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName, tlsKeyPair.public)
|
||||||
|
|
||||||
|
loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply {
|
||||||
|
addOrReplaceKey(
|
||||||
|
X509Utilities.CORDA_CLIENT_TLS,
|
||||||
|
tlsKeyPair.private,
|
||||||
|
keyStorePassword.toCharArray(),
|
||||||
|
arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert))
|
||||||
|
save(sslKeystore, keyStorePassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given
|
||||||
|
* [CordaX500Name] as the cert subject.
|
||||||
|
*/
|
||||||
|
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair {
|
||||||
|
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
||||||
|
val cert = X509Utilities.createCertificate(
|
||||||
|
CertificateType.NODE_CA,
|
||||||
|
intermediateCa.certificate,
|
||||||
|
intermediateCa.keyPair,
|
||||||
|
legalName,
|
||||||
|
keyPair.public,
|
||||||
|
nameConstraints = nameConstraints)
|
||||||
|
return CertificateAndKeyPair(cert, keyPair)
|
||||||
|
}
|
@ -8,6 +8,7 @@ interface SSLConfiguration {
|
|||||||
val trustStorePassword: String
|
val trustStorePassword: String
|
||||||
val certificatesDirectory: Path
|
val certificatesDirectory: Path
|
||||||
val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks"
|
val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks"
|
||||||
|
// TODO This looks like it should be in NodeSSLConfiguration
|
||||||
val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks"
|
val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks"
|
||||||
val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks"
|
val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks"
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,26 @@
|
|||||||
package net.corda.nodeapi.internal.crypto
|
package net.corda.nodeapi.internal.crypto
|
||||||
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.cert
|
import net.corda.core.internal.cert
|
||||||
import net.corda.core.internal.read
|
import net.corda.core.internal.read
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
|
||||||
import java.security.cert.CertPath
|
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
|
||||||
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
||||||
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
|
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
|
||||||
|
|
||||||
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
|
// TODO This method seems misplaced in this class.
|
||||||
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
|
fun storeLegalIdentity(legalName: CordaX500Name, alias: String, keyPair: KeyPair): PartyAndCertificate {
|
||||||
|
val nodeCaCertChain = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
|
||||||
|
val nodeCa = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||||
|
val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCa.certificate, nodeCa.keyPair, legalName, keyPair.public)
|
||||||
|
val identityCertPath = X509CertificateFactory().generateCertPath(identityCert.cert, *nodeCaCertChain)
|
||||||
// Assume key password = store password.
|
// Assume key password = store password.
|
||||||
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
keyStore.addOrReplaceKey(alias, keyPair.private, storePassword.toCharArray(), identityCertPath.certificates.toTypedArray())
|
||||||
// Create new keys and store in keystore.
|
|
||||||
val cert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
|
|
||||||
val certPath = X509CertificateFactory().generateCertPath(cert.cert, *clientCertPath)
|
|
||||||
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
|
|
||||||
// TODO: X509Utilities.validateCertificateChain()
|
|
||||||
return certPath
|
|
||||||
}
|
|
||||||
|
|
||||||
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
|
|
||||||
val certPath = createCertificate(serviceName, keyPair.public)
|
|
||||||
// Assume key password = store password.
|
|
||||||
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
|
|
||||||
keyStore.save(storePath, storePassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
|
|
||||||
val certPath = createCertificate(serviceName, pubKey)
|
|
||||||
// Assume key password = store password.
|
|
||||||
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
|
|
||||||
keyStore.save(storePath, storePassword)
|
keyStore.save(storePath, storePassword)
|
||||||
|
return PartyAndCertificate(identityCertPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
|
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
|
||||||
@ -47,5 +32,5 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St
|
|||||||
|
|
||||||
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
|
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
|
||||||
|
|
||||||
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
|
fun getCertificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
|
||||||
}
|
}
|
@ -4,12 +4,8 @@ import net.corda.core.CordaOID
|
|||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SignatureScheme
|
import net.corda.core.crypto.SignatureScheme
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.internal.CertRole
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.cert
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.reader
|
|
||||||
import net.corda.core.internal.writer
|
|
||||||
import net.corda.core.internal.x500Name
|
|
||||||
import net.corda.core.utilities.days
|
import net.corda.core.utilities.days
|
||||||
import net.corda.core.utilities.millis
|
import net.corda.core.utilities.millis
|
||||||
import org.bouncycastle.asn1.*
|
import org.bouncycastle.asn1.*
|
||||||
@ -43,6 +39,7 @@ object X509Utilities {
|
|||||||
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
|
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
|
||||||
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
|
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
|
||||||
|
|
||||||
|
// TODO This class is more of a general purpose utility class and as such these constants belong elsewhere
|
||||||
// Aliases for private keys and certificates.
|
// Aliases for private keys and certificates.
|
||||||
const val CORDA_ROOT_CA = "cordarootca"
|
const val CORDA_ROOT_CA = "cordarootca"
|
||||||
const val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
|
const val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
|
||||||
|
@ -4,23 +4,28 @@ import com.typesafe.config.ConfigFactory
|
|||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
|
import net.corda.core.internal.concurrent.fork
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||||
import net.corda.core.utilities.ByteSequence
|
import net.corda.core.utilities.ByteSequence
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit.SECONDS
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,13 +53,14 @@ class NetworkBootstrapper {
|
|||||||
fun bootstrap(directory: Path) {
|
fun bootstrap(directory: Path) {
|
||||||
directory.createDirectories()
|
directory.createDirectories()
|
||||||
println("Bootstrapping local network in $directory")
|
println("Bootstrapping local network in $directory")
|
||||||
|
generateDirectoriesIfNeeded(directory)
|
||||||
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
|
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
|
||||||
require(nodeDirs.isNotEmpty()) { "No nodes found" }
|
require(nodeDirs.isNotEmpty()) { "No nodes found" }
|
||||||
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
|
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
|
||||||
val processes = startNodeInfoGeneration(nodeDirs)
|
val processes = startNodeInfoGeneration(nodeDirs)
|
||||||
initialiseSerialization()
|
initialiseSerialization()
|
||||||
try {
|
try {
|
||||||
println("Waiting for all nodes to generate their node-info files")
|
println("Waiting for all nodes to generate their node-info files...")
|
||||||
val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs)
|
val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs)
|
||||||
println("Distributing all node info-files to all nodes")
|
println("Distributing all node info-files to all nodes")
|
||||||
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
||||||
@ -69,6 +75,27 @@ class NetworkBootstrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateDirectoriesIfNeeded(directory: Path) {
|
||||||
|
val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() }
|
||||||
|
if (confFiles.isEmpty()) return
|
||||||
|
println("Node config files found in the root directory - generating node directories")
|
||||||
|
val cordaJar = extractCordaJarTo(directory)
|
||||||
|
for (confFile in confFiles) {
|
||||||
|
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
|
||||||
|
println("Generating directory for $nodeName")
|
||||||
|
val nodeDir = (directory / nodeName).createDirectory()
|
||||||
|
confFile.moveTo(nodeDir / "node.conf")
|
||||||
|
Files.copy(cordaJar, (nodeDir / "corda.jar"))
|
||||||
|
}
|
||||||
|
Files.delete(cordaJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractCordaJarTo(directory: Path): Path {
|
||||||
|
val cordaJarPath = (directory / "corda.jar")
|
||||||
|
Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").copyTo(cordaJarPath)
|
||||||
|
return cordaJarPath
|
||||||
|
}
|
||||||
|
|
||||||
private fun startNodeInfoGeneration(nodeDirs: List<Path>): List<Process> {
|
private fun startNodeInfoGeneration(nodeDirs: List<Path>): List<Process> {
|
||||||
return nodeDirs.map { nodeDir ->
|
return nodeDirs.map { nodeDir ->
|
||||||
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
|
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
|
||||||
@ -82,15 +109,22 @@ class NetworkBootstrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun gatherNodeInfoFiles(processes: List<Process>, nodeDirs: List<Path>): List<Path> {
|
private fun gatherNodeInfoFiles(processes: List<Process>, nodeDirs: List<Path>): List<Path> {
|
||||||
val timeOutInSeconds = 60L
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
return processes.zip(nodeDirs).map { (process, nodeDir) ->
|
|
||||||
check(process.waitFor(timeOutInSeconds, SECONDS)) {
|
val future = executor.fork {
|
||||||
"Node in ${nodeDir.fileName} took longer than ${timeOutInSeconds}s to generate its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
processes.zip(nodeDirs).map { (process, nodeDir) ->
|
||||||
|
check(process.waitFor() == 0) {
|
||||||
|
"Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
||||||
|
}
|
||||||
|
nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
|
||||||
}
|
}
|
||||||
check(process.exitValue() == 0) {
|
}
|
||||||
"Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
|
||||||
}
|
return try {
|
||||||
nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
|
future.getOrThrow(60.seconds)
|
||||||
|
} catch (e: TimeoutException) {
|
||||||
|
println("...still waiting. If this is taking longer than usual, check the node logs.")
|
||||||
|
future.getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,10 +170,10 @@ class NetworkBootstrapper {
|
|||||||
|
|
||||||
private fun NodeInfo.notaryIdentity(): Party {
|
private fun NodeInfo.notaryIdentity(): Party {
|
||||||
return when (legalIdentities.size) {
|
return when (legalIdentities.size) {
|
||||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||||
1 -> legalIdentities[0]
|
1 -> legalIdentities[0]
|
||||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
// 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.
|
// cluster and is shared by all the other members. This is the notary identity.
|
||||||
2 -> legalIdentities[1]
|
2 -> legalIdentities[1]
|
||||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||||
}
|
}
|
||||||
@ -161,6 +195,7 @@ class NetworkBootstrapper {
|
|||||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||||
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||||
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,17 @@ import net.corda.core.serialization.SerializationContext
|
|||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||||
import net.corda.node.services.config.createKeystoreForCordaNode
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
|
import net.corda.nodeapi.internal.createDevKeyStores
|
||||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
|
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
import net.corda.testing.*
|
import net.corda.testing.ALICE_NAME
|
||||||
|
import net.corda.testing.BOB_NAME
|
||||||
|
import net.corda.testing.TestIdentity
|
||||||
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.bouncycastle.asn1.x509.BasicConstraints
|
import org.bouncycastle.asn1.x509.BasicConstraints
|
||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
@ -33,11 +38,8 @@ import java.io.IOException
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.PrivateKey
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import java.security.cert.Certificate
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.stream.Stream
|
import java.util.stream.Stream
|
||||||
@ -52,6 +54,11 @@ class X509UtilitiesTest {
|
|||||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||||
val BOB get() = bob.party
|
val BOB get() = bob.party
|
||||||
val BOB_PUBKEY get() = bob.publicKey
|
val BOB_PUBKEY get() = bob.publicKey
|
||||||
|
val CIPHER_SUITES = arrayOf(
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||||
|
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -155,107 +162,62 @@ class X509UtilitiesTest {
|
|||||||
assertEquals(edDSAKeypair.private, privateKey)
|
assertEquals(edDSAKeypair.private, privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `create full CA keystore`() {
|
|
||||||
val tmpKeyStore = tempFile("keystore.jks")
|
|
||||||
val tmpTrustStore = tempFile("truststore.jks")
|
|
||||||
|
|
||||||
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
|
|
||||||
createCAKeyStoreAndTrustStore(tmpKeyStore, "keystorepass", "keypass", tmpTrustStore, "trustpass")
|
|
||||||
|
|
||||||
// Load back generated root CA Cert and private key from keystore and check against copy in truststore
|
|
||||||
val keyStore = loadKeyStore(tmpKeyStore, "keystorepass")
|
|
||||||
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
|
|
||||||
val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
|
|
||||||
val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey
|
|
||||||
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
|
|
||||||
assertEquals(rootCaCert, rootCaFromTrustStore)
|
|
||||||
rootCaCert.checkValidity(Date())
|
|
||||||
rootCaCert.verify(rootCaCert.publicKey)
|
|
||||||
|
|
||||||
// Now sign something with private key and verify against certificate public key
|
|
||||||
val testData = "12345".toByteArray()
|
|
||||||
val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData)
|
|
||||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) }
|
|
||||||
|
|
||||||
// Load back generated intermediate CA Cert and private key
|
|
||||||
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate
|
|
||||||
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey
|
|
||||||
intermediateCaCert.checkValidity(Date())
|
|
||||||
intermediateCaCert.verify(rootCaCert.publicKey)
|
|
||||||
|
|
||||||
// Now sign something with private key and verify against certificate public key
|
|
||||||
val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData)
|
|
||||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create server certificate in keystore for SSL`() {
|
fun `create server certificate in keystore for SSL`() {
|
||||||
val tmpCAKeyStore = tempFile("keystore.jks")
|
val sslConfig = object : SSLConfiguration {
|
||||||
val tmpTrustStore = tempFile("truststore.jks")
|
override val certificatesDirectory = tempFolder.root.toPath()
|
||||||
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
|
override val keyStorePassword = "serverstorepass"
|
||||||
val tmpServerKeyStore = tempFile("serverkeystore.jks")
|
override val trustStorePassword = "trustpass"
|
||||||
|
}
|
||||||
|
|
||||||
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||||
createCAKeyStoreAndTrustStore(tmpCAKeyStore,
|
|
||||||
"cakeystorepass",
|
|
||||||
"cakeypass",
|
|
||||||
tmpTrustStore,
|
|
||||||
"trustpass")
|
|
||||||
|
|
||||||
// Load signing intermediate CA cert
|
|
||||||
val caKeyStore = loadKeyStore(tmpCAKeyStore, "cakeystorepass")
|
|
||||||
val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass")
|
|
||||||
|
|
||||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||||
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name)
|
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
|
||||||
|
|
||||||
// Load back server certificate
|
// Load back server certificate
|
||||||
val serverKeyStore = loadKeyStore(tmpServerKeyStore, "serverstorepass")
|
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
|
||||||
val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass")
|
val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword)
|
||||||
|
|
||||||
serverCertAndKey.certificate.isValidOn(Date())
|
serverCert.cert.checkValidity()
|
||||||
serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo))
|
serverCert.cert.verify(intermediateCa.certificate.cert.publicKey)
|
||||||
|
assertThat(CordaX500Name.parse(serverCert.subject.toString())).isEqualTo(MEGA_CORP.name)
|
||||||
|
|
||||||
assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
|
// Load back SSL certificate
|
||||||
|
val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
||||||
|
val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword)
|
||||||
|
|
||||||
// Load back server certificate
|
sslCert.cert.checkValidity()
|
||||||
val sslKeyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
|
sslCert.cert.verify(serverCert.cert.publicKey)
|
||||||
val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass")
|
assertThat(CordaX500Name.parse(sslCert.subject.toString())).isEqualTo(MEGA_CORP.name)
|
||||||
|
|
||||||
sslCertAndKey.certificate.isValidOn(Date())
|
|
||||||
sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo))
|
|
||||||
|
|
||||||
assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
|
|
||||||
// Now sign something with private key and verify against certificate public key
|
// Now sign something with private key and verify against certificate public key
|
||||||
val testData = "123456".toByteArray()
|
val testData = "123456".toByteArray()
|
||||||
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData)
|
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverKeyPair.private, testData)
|
||||||
val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo)
|
val publicKey = Crypto.toSupportedPublicKey(serverCert.subjectPublicKeyInfo)
|
||||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
|
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create server cert and use in SSL socket`() {
|
fun `create server cert and use in SSL socket`() {
|
||||||
val tmpCAKeyStore = tempFile("keystore.jks")
|
val sslConfig = object : SSLConfiguration {
|
||||||
val tmpTrustStore = tempFile("truststore.jks")
|
override val certificatesDirectory = tempFolder.root.toPath()
|
||||||
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
|
override val keyStorePassword = "serverstorepass"
|
||||||
val tmpServerKeyStore = tempFile("serverkeystore.jks")
|
override val trustStorePassword = "trustpass"
|
||||||
|
}
|
||||||
|
|
||||||
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||||
val caKeyStore = createCAKeyStoreAndTrustStore(tmpCAKeyStore,
|
|
||||||
"cakeystorepass",
|
|
||||||
"cakeypass",
|
|
||||||
tmpTrustStore,
|
|
||||||
"trustpass")
|
|
||||||
|
|
||||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||||
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name)
|
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
|
||||||
val keyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
|
sslConfig.createTrustStore(rootCa.certificate.cert)
|
||||||
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
|
|
||||||
|
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
||||||
|
val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
|
||||||
|
|
||||||
val context = SSLContext.getInstance("TLS")
|
val context = SSLContext.getInstance("TLS")
|
||||||
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||||
keyManagerFactory.init(keyStore, "serverstorepass".toCharArray())
|
keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray())
|
||||||
val keyManagers = keyManagerFactory.keyManagers
|
val keyManagers = keyManagerFactory.keyManagers
|
||||||
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||||
trustMgrFactory.init(trustStore)
|
trustMgrFactory.init(trustStore)
|
||||||
@ -266,13 +228,7 @@ class X509UtilitiesTest {
|
|||||||
val clientSocketFactory = context.socketFactory
|
val clientSocketFactory = context.socketFactory
|
||||||
|
|
||||||
val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket
|
val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket
|
||||||
val serverParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
val serverParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2"))
|
||||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
||||||
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
|
|
||||||
arrayOf("TLSv1.2"))
|
|
||||||
serverParams.wantClientAuth = true
|
serverParams.wantClientAuth = true
|
||||||
serverParams.needClientAuth = true
|
serverParams.needClientAuth = true
|
||||||
serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
||||||
@ -280,13 +236,7 @@ class X509UtilitiesTest {
|
|||||||
serverSocket.useClientMode = false
|
serverSocket.useClientMode = false
|
||||||
|
|
||||||
val clientSocket = clientSocketFactory.createSocket() as SSLSocket
|
val clientSocket = clientSocketFactory.createSocket() as SSLSocket
|
||||||
val clientParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
val clientParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2"))
|
||||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
||||||
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
|
|
||||||
arrayOf("TLSv1.2"))
|
|
||||||
clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
||||||
clientSocket.sslParameters = clientParams
|
clientSocket.sslParameters = clientParams
|
||||||
clientSocket.useClientMode = true
|
clientSocket.useClientMode = true
|
||||||
@ -344,60 +294,14 @@ class X509UtilitiesTest {
|
|||||||
|
|
||||||
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
|
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
|
||||||
|
|
||||||
/**
|
private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) {
|
||||||
* All in one wrapper to manufacture a root CA cert and an Intermediate CA cert.
|
val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword)
|
||||||
* Normally this would be run once and then the outputs would be re-used repeatedly to manufacture the server certs
|
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||||
* @param keyStoreFilePath The output KeyStore path to publish the private keys of the CA root and intermediate certs into.
|
trustStore.save(trustStoreFile, trustStorePassword)
|
||||||
* @param storePassword The storage password to protect access to the generated KeyStore and public certificates
|
|
||||||
* @param keyPassword The password that protects the CA private keys.
|
|
||||||
* Unlike the SSL libraries that tend to assume the password is the same as the keystore password.
|
|
||||||
* These CA private keys should be protected more effectively with a distinct password.
|
|
||||||
* @param trustStoreFilePath The output KeyStore to place the Root CA public certificate, which can be used as an SSL truststore
|
|
||||||
* @param trustStorePassword The password to protect the truststore
|
|
||||||
* @return The KeyStore object that was saved to file
|
|
||||||
*/
|
|
||||||
private fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path,
|
|
||||||
storePassword: String,
|
|
||||||
keyPassword: String,
|
|
||||||
trustStoreFilePath: Path,
|
|
||||||
trustStorePassword: String
|
|
||||||
): KeyStore {
|
|
||||||
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB")
|
|
||||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey)
|
|
||||||
|
|
||||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val intermediateCACert = X509Utilities.createCertificate(
|
|
||||||
CertificateType.INTERMEDIATE_CA,
|
|
||||||
rootCACert,
|
|
||||||
rootCAKey,
|
|
||||||
baseName.copy(commonName = "Corda Node Intermediate CA"),
|
|
||||||
intermediateCAKeyPair.public)
|
|
||||||
|
|
||||||
val keyPass = keyPassword.toCharArray()
|
|
||||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
|
||||||
|
|
||||||
keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf<Certificate>(rootCACert.cert))
|
|
||||||
|
|
||||||
keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA,
|
|
||||||
intermediateCAKeyPair.private,
|
|
||||||
keyPass,
|
|
||||||
Stream.of(intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
|
|
||||||
|
|
||||||
keyStore.save(keyStoreFilePath, storePassword)
|
|
||||||
|
|
||||||
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
|
|
||||||
|
|
||||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
|
|
||||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert.cert)
|
|
||||||
|
|
||||||
trustStore.save(trustStoreFilePath, trustStorePassword)
|
|
||||||
|
|
||||||
return keyStore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Get correct private key type from Keystore`() {
|
fun `get correct private key type from Keystore`() {
|
||||||
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||||
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
|
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
|
||||||
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
|
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
|
||||||
|
@ -44,7 +44,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
|
|||||||
applicationVersion = corda_release_version
|
applicationVersion = corda_release_version
|
||||||
appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"]
|
appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"]
|
||||||
// See experimental/quasar-hook/README.md for how to generate.
|
// See experimental/quasar-hook/README.md for how to generate.
|
||||||
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
|
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
|
||||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
|
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
|
||||||
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
|
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
|
||||||
minJavaVersion = '1.8.0'
|
minJavaVersion = '1.8.0'
|
||||||
|
@ -14,11 +14,9 @@ import net.corda.testing.IntegrationTestSchemas
|
|||||||
import net.corda.testing.driver.driver
|
import net.corda.testing.driver.driver
|
||||||
import net.corda.testing.toDatabaseSchemaName
|
import net.corda.testing.toDatabaseSchemaName
|
||||||
import org.junit.ClassRule
|
import org.junit.ClassRule
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class NodeKeystoreCheckTest : IntegrationTest() {
|
class NodeKeystoreCheckTest : IntegrationTest() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -26,16 +24,18 @@ class NodeKeystoreCheckTest : IntegrationTest() {
|
|||||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName())
|
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `starting node in non-dev mode with no key store`() {
|
||||||
|
driver(startNodesInProcess = true) {
|
||||||
|
assertThatThrownBy {
|
||||||
|
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||||
|
}.hasMessageContaining("Identity certificate not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `node should throw exception if cert path doesn't chain to the trust root`() {
|
fun `node should throw exception if cert path doesn't chain to the trust root`() {
|
||||||
driver(startNodesInProcess = true) {
|
driver(startNodesInProcess = true) {
|
||||||
// This will fail because there are no keystore configured.
|
|
||||||
assertFailsWith(IllegalArgumentException::class) {
|
|
||||||
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
|
|
||||||
}.apply {
|
|
||||||
assertTrue(message?.startsWith("Identity certificate not found. ") ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create keystores
|
// Create keystores
|
||||||
val keystorePassword = "password"
|
val keystorePassword = "password"
|
||||||
val config = object : SSLConfiguration {
|
val config = object : SSLConfiguration {
|
||||||
@ -46,9 +46,12 @@ class NodeKeystoreCheckTest : IntegrationTest() {
|
|||||||
config.configureDevKeyAndTrustStores(ALICE_NAME)
|
config.configureDevKeyAndTrustStores(ALICE_NAME)
|
||||||
|
|
||||||
// This should pass with correct keystore.
|
// This should pass with correct keystore.
|
||||||
val node = startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false,
|
val node = startNode(
|
||||||
"keyStorePassword" to keystorePassword,
|
providedName = ALICE_NAME,
|
||||||
"trustStorePassword" to keystorePassword)).get()
|
customOverrides = mapOf("devMode" to false,
|
||||||
|
"keyStorePassword" to keystorePassword,
|
||||||
|
"trustStorePassword" to keystorePassword)
|
||||||
|
).getOrThrow()
|
||||||
node.stop()
|
node.stop()
|
||||||
|
|
||||||
// Fiddle with node keystore.
|
// Fiddle with node keystore.
|
||||||
@ -62,11 +65,9 @@ class NodeKeystoreCheckTest : IntegrationTest() {
|
|||||||
keystore.setKeyEntry(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, config.keyStorePassword.toCharArray(), arrayOf(badNodeCACert.cert, badRoot.cert))
|
keystore.setKeyEntry(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, config.keyStorePassword.toCharArray(), arrayOf(badNodeCACert.cert, badRoot.cert))
|
||||||
keystore.save(config.nodeKeystore, config.keyStorePassword)
|
keystore.save(config.nodeKeystore, config.keyStorePassword)
|
||||||
|
|
||||||
assertFailsWith(IllegalArgumentException::class) {
|
assertThatThrownBy {
|
||||||
startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow()
|
startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||||
}.apply {
|
}.hasMessage("Client CA certificate must chain to the trusted root.")
|
||||||
assertEquals("Client CA certificate must chain to the trusted root.", message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
|
|||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.transactions.minClusterSize
|
import net.corda.node.services.transactions.minClusterSize
|
||||||
import net.corda.node.services.transactions.minCorrectReplicas
|
import net.corda.node.services.transactions.minCorrectReplicas
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.nodeapi.internal.network.NotaryInfo
|
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||||
import net.corda.testing.IntegrationTest
|
import net.corda.testing.IntegrationTest
|
||||||
@ -69,7 +69,7 @@ class BFTNotaryServiceTests : IntegrationTest() {
|
|||||||
(Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists?
|
(Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists?
|
||||||
val replicaIds = (0 until clusterSize)
|
val replicaIds = (0 until clusterSize)
|
||||||
|
|
||||||
notary = IdentityGenerator.generateDistributedNotaryIdentity(
|
notary = DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||||
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
|
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
|
||||||
CordaX500Name("BFT", "Zurich", "CH"))
|
CordaX500Name("BFT", "Zurich", "CH"))
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package net.corda.node.services.network
|
package net.corda.node.services.network
|
||||||
|
|
||||||
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.crypto.SignedData
|
import net.corda.core.crypto.SignedData
|
||||||
|
import net.corda.core.crypto.random63BitValue
|
||||||
|
import net.corda.core.internal.concurrent.transpose
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.internal.exists
|
||||||
import net.corda.core.internal.list
|
import net.corda.core.internal.list
|
||||||
import net.corda.core.internal.readAll
|
import net.corda.core.internal.readAll
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
@ -10,6 +15,10 @@ import net.corda.core.utilities.seconds
|
|||||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.ALICE_NAME
|
||||||
|
import net.corda.testing.BOB_NAME
|
||||||
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.driver.NodeHandle
|
import net.corda.testing.driver.NodeHandle
|
||||||
import net.corda.testing.driver.PortAllocation
|
import net.corda.testing.driver.PortAllocation
|
||||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||||
@ -18,6 +27,8 @@ import net.corda.testing.node.internal.network.NetworkMapServer
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.*
|
import org.junit.*
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.time.Instant
|
||||||
|
import kotlin.streams.toList
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NetworkMapTest : IntegrationTest() {
|
class NetworkMapTest : IntegrationTest() {
|
||||||
@ -29,6 +40,7 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule(true)
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
private val cacheTimeout = 1.seconds
|
private val cacheTimeout = 1.seconds
|
||||||
private val portAllocation = PortAllocation.Incremental(10000)
|
private val portAllocation = PortAllocation.Incremental(10000)
|
||||||
|
|
||||||
@ -39,7 +51,9 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
fun start() {
|
fun start() {
|
||||||
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
|
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
|
||||||
val address = networkMapServer.start()
|
val address = networkMapServer.start()
|
||||||
compatibilityZone = CompatibilityZoneParams(URL("http://$address"))
|
compatibilityZone = CompatibilityZoneParams(URL("http://$address"), publishNotaries = {
|
||||||
|
networkMapServer.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -49,25 +63,35 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `node correctly downloads and saves network parameters file on startup`() {
|
fun `node correctly downloads and saves network parameters file on startup`() {
|
||||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
internalDriver(
|
||||||
|
portAllocation = portAllocation,
|
||||||
|
compatibilityZone = compatibilityZone,
|
||||||
|
initialiseSerialization = false,
|
||||||
|
notarySpecs = emptyList()
|
||||||
|
) {
|
||||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||||
val networkParameters = alice.configuration.baseDirectory
|
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
||||||
.list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() }
|
|
||||||
.readAll()
|
.readAll()
|
||||||
.deserialize<SignedData<NetworkParameters>>()
|
.deserialize<SignedData<NetworkParameters>>()
|
||||||
.verified()
|
.verified()
|
||||||
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameters)
|
// We use a random modified time above to make the network parameters unqiue so that we're sure they came
|
||||||
|
// from the server
|
||||||
|
assertEquals(networkMapServer.networkParameters, networkParameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `nodes can see each other using the http network map`() {
|
fun `nodes can see each other using the http network map`() {
|
||||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
internalDriver(
|
||||||
val alice = startNode(providedName = ALICE_NAME)
|
portAllocation = portAllocation,
|
||||||
val bob = startNode(providedName = BOB_NAME)
|
compatibilityZone = compatibilityZone,
|
||||||
val notaryNode = defaultNotaryNode.get()
|
initialiseSerialization = false
|
||||||
val aliceNode = alice.get()
|
) {
|
||||||
val bobNode = bob.get()
|
val (aliceNode, bobNode, notaryNode) = listOf(
|
||||||
|
startNode(providedName = ALICE_NAME),
|
||||||
|
startNode(providedName = BOB_NAME),
|
||||||
|
defaultNotaryNode
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||||
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||||
@ -77,15 +101,20 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `nodes process network map add updates correctly when adding new node to network map`() {
|
fun `nodes process network map add updates correctly when adding new node to network map`() {
|
||||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
internalDriver(
|
||||||
val alice = startNode(providedName = ALICE_NAME)
|
portAllocation = portAllocation,
|
||||||
val notaryNode = defaultNotaryNode.get()
|
compatibilityZone = compatibilityZone,
|
||||||
val aliceNode = alice.get()
|
initialiseSerialization = false
|
||||||
|
) {
|
||||||
|
val (aliceNode, notaryNode) = listOf(
|
||||||
|
startNode(providedName = ALICE_NAME),
|
||||||
|
defaultNotaryNode
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
|
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
|
||||||
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
|
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
|
||||||
val bob = startNode(providedName = BOB_NAME)
|
|
||||||
val bobNode = bob.get()
|
val bobNode = startNode(providedName = BOB_NAME).getOrThrow()
|
||||||
|
|
||||||
// Wait for network map client to poll for the next update.
|
// Wait for network map client to poll for the next update.
|
||||||
Thread.sleep(cacheTimeout.toMillis() * 2)
|
Thread.sleep(cacheTimeout.toMillis() * 2)
|
||||||
@ -98,12 +127,16 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `nodes process network map remove updates correctly`() {
|
fun `nodes process network map remove updates correctly`() {
|
||||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
internalDriver(
|
||||||
val alice = startNode(providedName = ALICE_NAME)
|
portAllocation = portAllocation,
|
||||||
val bob = startNode(providedName = BOB_NAME)
|
compatibilityZone = compatibilityZone,
|
||||||
val notaryNode = defaultNotaryNode.get()
|
initialiseSerialization = false
|
||||||
val aliceNode = alice.get()
|
) {
|
||||||
val bobNode = bob.get()
|
val (aliceNode, bobNode, notaryNode) = listOf(
|
||||||
|
startNode(providedName = ALICE_NAME),
|
||||||
|
startNode(providedName = BOB_NAME),
|
||||||
|
defaultNotaryNode
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||||
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||||
@ -119,5 +152,12 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NodeHandle.onlySees(vararg nodes: NodeInfo) = assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
private fun NodeHandle.onlySees(vararg nodes: NodeInfo) {
|
||||||
|
// Make sure the nodes aren't getting the node infos from their additional directories
|
||||||
|
val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY
|
||||||
|
if (nodeInfosDir.exists()) {
|
||||||
|
assertThat(nodeInfosDir.list { it.toList() }).isEmpty()
|
||||||
|
}
|
||||||
|
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,15 @@ package net.corda.node.utilities.registration
|
|||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.cert
|
import net.corda.core.internal.cert
|
||||||
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.internal.toX509CertHolder
|
import net.corda.core.internal.toX509CertHolder
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.minutes
|
import net.corda.core.utilities.minutes
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
@ -16,11 +21,16 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
|||||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||||
import net.corda.testing.IntegrationTest
|
import net.corda.testing.IntegrationTest
|
||||||
import net.corda.testing.IntegrationTestSchemas
|
import net.corda.testing.IntegrationTestSchemas
|
||||||
|
import net.corda.testing.ROOT_CA
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.driver.PortAllocation
|
import net.corda.testing.driver.PortAllocation
|
||||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||||
import net.corda.testing.node.internal.internalDriver
|
import net.corda.testing.node.internal.internalDriver
|
||||||
import net.corda.testing.node.internal.network.NetworkMapServer
|
import net.corda.testing.node.internal.network.NetworkMapServer
|
||||||
|
import net.corda.testing.singleIdentity
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
@ -31,6 +41,7 @@ import java.io.InputStream
|
|||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
|
import java.security.cert.CertPathValidatorException
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@ -42,20 +53,25 @@ class NodeRegistrationTest : IntegrationTest() {
|
|||||||
companion object {
|
companion object {
|
||||||
@ClassRule @JvmField
|
@ClassRule @JvmField
|
||||||
val databaseSchemas = IntegrationTestSchemas("Alice")
|
val databaseSchemas = IntegrationTestSchemas("Alice")
|
||||||
|
|
||||||
|
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
|
||||||
|
private val aliceName = CordaX500Name("Alice", "London", "GB")
|
||||||
|
private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule(true)
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
private val portAllocation = PortAllocation.Incremental(13000)
|
private val portAllocation = PortAllocation.Incremental(13000)
|
||||||
private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate()
|
private val registrationHandler = RegistrationHandler(ROOT_CA)
|
||||||
private val registrationHandler = RegistrationHandler(rootCertAndKeyPair)
|
|
||||||
|
|
||||||
private lateinit var server: NetworkMapServer
|
private lateinit var server: NetworkMapServer
|
||||||
private lateinit var serverHostAndPort: NetworkHostAndPort
|
private lateinit var serverHostAndPort: NetworkHostAndPort
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun startServer() {
|
fun startServer() {
|
||||||
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), rootCertAndKeyPair, registrationHandler)
|
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), ROOT_CA, "localhost", registrationHandler)
|
||||||
serverHostAndPort = server.start()
|
serverHostAndPort = server.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,50 +80,65 @@ class NodeRegistrationTest : IntegrationTest() {
|
|||||||
server.close()
|
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
|
@Test
|
||||||
fun `node registration correct root cert`() {
|
fun `node registration correct root cert`() {
|
||||||
val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = rootCertAndKeyPair.certificate.cert)
|
val compatibilityZone = CompatibilityZoneParams(
|
||||||
|
URL("http://$serverHostAndPort"),
|
||||||
|
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
||||||
|
rootCert = ROOT_CA.certificate.cert)
|
||||||
internalDriver(
|
internalDriver(
|
||||||
portAllocation = portAllocation,
|
portAllocation = portAllocation,
|
||||||
notarySpecs = emptyList(),
|
|
||||||
compatibilityZone = compatibilityZone,
|
compatibilityZone = compatibilityZone,
|
||||||
initialiseSerialization = false
|
initialiseSerialization = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||||
|
extraCordappPackagesToScan = listOf("net.corda.finance")
|
||||||
) {
|
) {
|
||||||
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
|
val nodes = listOf(
|
||||||
assertThat(registrationHandler.idsPolled).contains("Alice")
|
startNode(providedName = aliceName),
|
||||||
|
startNode(providedName = genevieveName),
|
||||||
|
defaultNotaryNode
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
val (alice, genevieve) = nodes
|
||||||
|
|
||||||
|
assertThat(registrationHandler.idsPolled).containsOnly(
|
||||||
|
aliceName.organisation,
|
||||||
|
genevieveName.organisation,
|
||||||
|
notaryName.organisation)
|
||||||
|
|
||||||
|
// Check the nodes can communicate among themselves (and the notary).
|
||||||
|
val anonymous = false
|
||||||
|
genevieve.rpc.startFlow(
|
||||||
|
::CashIssueAndPaymentFlow,
|
||||||
|
1000.DOLLARS,
|
||||||
|
OpaqueBytes.of(12),
|
||||||
|
alice.nodeInfo.singleIdentity(),
|
||||||
|
anonymous,
|
||||||
|
defaultNotaryIdentity
|
||||||
|
).returnValue.getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `node registration wrong root cert`() {
|
fun `node registration wrong root cert`() {
|
||||||
val someCert = createSelfKeyAndSelfSignedCertificate().certificate.cert
|
val someRootCert = X509Utilities.createSelfSignedCACertificate(
|
||||||
val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someCert)
|
CordaX500Name("Integration Test Corda Node Root CA", "R3 Ltd", "London", "GB"),
|
||||||
|
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
|
||||||
|
val compatibilityZone = CompatibilityZoneParams(
|
||||||
|
URL("http://$serverHostAndPort"),
|
||||||
|
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
||||||
|
rootCert = someRootCert.cert)
|
||||||
internalDriver(
|
internalDriver(
|
||||||
portAllocation = portAllocation,
|
portAllocation = portAllocation,
|
||||||
notarySpecs = emptyList(),
|
|
||||||
compatibilityZone = compatibilityZone,
|
compatibilityZone = compatibilityZone,
|
||||||
// Changing the content of the truststore makes the node fail in a number of ways if started out process.
|
initialiseSerialization = false,
|
||||||
startNodesInProcess = true
|
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||||
|
startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception
|
||||||
) {
|
) {
|
||||||
assertThatThrownBy {
|
assertThatThrownBy {
|
||||||
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
|
defaultNotaryNode.getOrThrow()
|
||||||
}.isInstanceOf(WrongRootCertException::class.java)
|
}.isInstanceOf(CertPathValidatorException::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")
|
@Path("certificate")
|
||||||
@ -124,6 +155,7 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair)
|
|||||||
certificationRequest,
|
certificationRequest,
|
||||||
rootCertAndKeyPair.keyPair,
|
rootCertAndKeyPair.keyPair,
|
||||||
arrayOf(rootCertAndKeyPair.certificate.cert))
|
arrayOf(rootCertAndKeyPair.certificate.cert))
|
||||||
|
require(!name.organisation.contains("\\s".toRegex())) { "Whitespace in the organisation name not supported" }
|
||||||
certPaths[name.organisation] = certPath
|
certPaths[name.organisation] = certPath
|
||||||
return Response.ok(name.organisation).build()
|
return Response.ok(name.organisation).build()
|
||||||
}
|
}
|
||||||
@ -154,13 +186,14 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair)
|
|||||||
caCertPath: Array<Certificate>): Pair<CertPath, CordaX500Name> {
|
caCertPath: Array<Certificate>): Pair<CertPath, CordaX500Name> {
|
||||||
val request = JcaPKCS10CertificationRequest(certificationRequest)
|
val request = JcaPKCS10CertificationRequest(certificationRequest)
|
||||||
val name = CordaX500Name.parse(request.subject.toString())
|
val name = CordaX500Name.parse(request.subject.toString())
|
||||||
val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.NODE_CA,
|
val nodeCaCert = X509Utilities.createCertificate(
|
||||||
|
CertificateType.NODE_CA,
|
||||||
caCertPath.first().toX509CertHolder(),
|
caCertPath.first().toX509CertHolder(),
|
||||||
caKeyPair,
|
caKeyPair,
|
||||||
name,
|
name,
|
||||||
request.publicKey,
|
request.publicKey,
|
||||||
nameConstraints = null)
|
nameConstraints = null)
|
||||||
val certPath = X509CertificateFactory().generateCertPath(x509CertificateHolder.cert, *caCertPath)
|
val certPath = X509CertificateFactory().generateCertPath(nodeCaCert.cert, *caCertPath)
|
||||||
return Pair(certPath, name)
|
return Pair(certPath, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,10 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
|||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.testing.*
|
import net.corda.testing.ALICE_NAME
|
||||||
|
import net.corda.testing.BOB_NAME
|
||||||
|
import net.corda.testing.chooseIdentity
|
||||||
|
import net.corda.testing.internal.configureTestSSL
|
||||||
import net.corda.testing.node.internal.NodeBasedTest
|
import net.corda.testing.node.internal.NodeBasedTest
|
||||||
import net.corda.testing.node.startFlow
|
import net.corda.testing.node.startFlow
|
||||||
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
|
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
|
||||||
|
@ -6,7 +6,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
|||||||
import net.corda.nodeapi.ArtemisTcpTransport
|
import net.corda.nodeapi.ArtemisTcpTransport
|
||||||
import net.corda.nodeapi.ConnectionDirection
|
import net.corda.nodeapi.ConnectionDirection
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
import net.corda.testing.configureTestSSL
|
import net.corda.testing.internal.configureTestSSL
|
||||||
import org.apache.activemq.artemis.api.core.client.*
|
import org.apache.activemq.artemis.api.core.client.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +60,7 @@ import net.corda.node.services.vault.NodeVaultService
|
|||||||
import net.corda.node.services.vault.VaultSoftLockManager
|
import net.corda.node.services.vault.VaultSoftLockManager
|
||||||
import net.corda.node.shell.InteractiveShell
|
import net.corda.node.shell.InteractiveShell
|
||||||
import net.corda.node.utilities.AffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.crypto.KeyStoreWrapper
|
import net.corda.nodeapi.internal.crypto.KeyStoreWrapper
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
@ -108,7 +108,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
|||||||
// In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the
|
// 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.
|
// AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in.
|
||||||
abstract class AbstractNode(val configuration: NodeConfiguration,
|
abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||||
val platformClock: Clock,
|
val platformClock: CordaClock,
|
||||||
protected val versionInfo: VersionInfo,
|
protected val versionInfo: VersionInfo,
|
||||||
protected val cordappLoader: CordappLoader,
|
protected val cordappLoader: CordappLoader,
|
||||||
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
||||||
@ -184,13 +184,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
return SignedNodeInfo(serialised, listOf(signature))
|
return SignedNodeInfo(serialised, listOf(signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun generateNodeInfo() {
|
open fun generateAndSaveNodeInfo(): NodeInfo {
|
||||||
check(started == null) { "Node has already been started" }
|
check(started == null) { "Node has already been started" }
|
||||||
log.info("Generating nodeInfo ...")
|
log.info("Generating nodeInfo ...")
|
||||||
initCertificate()
|
initCertificate()
|
||||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
||||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||||
initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
|
return initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
|
||||||
// TODO The fact that we need to specify an empty list of notaries just to generate our node info looks like
|
// TODO The fact that we need to specify an empty list of notaries just to generate our node info looks like
|
||||||
// a code smell.
|
// a code smell.
|
||||||
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
|
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
|
||||||
@ -200,6 +200,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
privateKey.sign(serialised.bytes)
|
privateKey.sign(serialised.bytes)
|
||||||
}
|
}
|
||||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo)
|
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo)
|
||||||
|
info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,14 +240,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
val notaryService = makeNotaryService(nodeServices, database)
|
val notaryService = makeNotaryService(nodeServices, database)
|
||||||
val smm = makeStateMachineManager(database)
|
val smm = makeStateMachineManager(database)
|
||||||
val flowStarter = FlowStarterImpl(serverThread, smm)
|
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
|
||||||
|
val flowStarter = FlowStarterImpl(serverThread, smm, flowLogicRefFactory)
|
||||||
val schedulerService = NodeSchedulerService(
|
val schedulerService = NodeSchedulerService(
|
||||||
platformClock,
|
platformClock,
|
||||||
database,
|
database,
|
||||||
flowStarter,
|
flowStarter,
|
||||||
transactionStorage,
|
transactionStorage,
|
||||||
unfinishedSchedules = busyNodeLatch,
|
unfinishedSchedules = busyNodeLatch,
|
||||||
serverThread = serverThread)
|
serverThread = serverThread,
|
||||||
|
flowLogicRefFactory = flowLogicRefFactory)
|
||||||
if (serverThread is ExecutorService) {
|
if (serverThread is ExecutorService) {
|
||||||
runOnStop += {
|
runOnStop += {
|
||||||
// We wait here, even though any in-flight messages should have been drained away because the
|
// We wait here, even though any in-flight messages should have been drained away because the
|
||||||
@ -255,7 +258,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
MoreExecutors.shutdownAndAwaitTermination(serverThread as ExecutorService, 50, SECONDS)
|
MoreExecutors.shutdownAndAwaitTermination(serverThread as ExecutorService, 50, SECONDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService)
|
makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService, flowLogicRefFactory)
|
||||||
val rpcOps = makeRPCOps(flowStarter, database, smm)
|
val rpcOps = makeRPCOps(flowStarter, database, smm)
|
||||||
startMessagingService(rpcOps)
|
startMessagingService(rpcOps)
|
||||||
installCoreFlows()
|
installCoreFlows()
|
||||||
@ -263,7 +266,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
tokenizableServices = nodeServices + cordaServices + schedulerService
|
tokenizableServices = nodeServices + cordaServices + schedulerService
|
||||||
registerCordappFlows(smm)
|
registerCordappFlows(smm)
|
||||||
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
|
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
|
||||||
FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader
|
|
||||||
startShell(rpcOps)
|
startShell(rpcOps)
|
||||||
Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
|
Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
|
||||||
}
|
}
|
||||||
@ -581,10 +583,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage = DBTransactionStorage()
|
protected open fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage = DBTransactionStorage()
|
||||||
|
private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService, flowLogicRefFactory: FlowLogicRefFactory) {
|
||||||
private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService) {
|
|
||||||
VaultSoftLockManager.install(services.vaultService, smm)
|
VaultSoftLockManager.install(services.vaultService, smm)
|
||||||
ScheduledActivityObserver.install(services.vaultService, schedulerService)
|
ScheduledActivityObserver.install(services.vaultService, schedulerService, flowLogicRefFactory)
|
||||||
HibernateObserver.install(services.vaultService.rawUpdates, hibernateConfig, schemaService)
|
HibernateObserver.install(services.vaultService.rawUpdates, hibernateConfig, schemaService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -724,7 +725,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
|
val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
|
||||||
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
|
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
|
||||||
val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
||||||
val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
val clientCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||||
val caCertificates = arrayOf(identityCert, clientCa.certificate.cert)
|
val caCertificates = arrayOf(identityCert, clientCa.certificate.cert)
|
||||||
return PersistentIdentityService(trustRoot, *caCertificates)
|
return PersistentIdentityService(trustRoot, *caCertificates)
|
||||||
}
|
}
|
||||||
@ -754,10 +755,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) {
|
val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) {
|
||||||
// Node's main identity or if it's a single node notary
|
// Node's main identity or if it's a single node notary
|
||||||
Pair(IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
||||||
} else {
|
} else {
|
||||||
// The node is part of a distributed notary whose identity must already be generated beforehand.
|
// The node is part of a distributed notary whose identity must already be generated beforehand.
|
||||||
Pair(IdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
||||||
}
|
}
|
||||||
// TODO: Integrate with Key management service?
|
// TODO: Integrate with Key management service?
|
||||||
val privateKeyAlias = "$id-private-key"
|
val privateKeyAlias = "$id-private-key"
|
||||||
@ -767,10 +768,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
"Unable to find in the key store the identity of the distributed notary ($id) the node is part of")
|
"Unable to find in the key store the identity of the distributed notary ($id) the node is part of")
|
||||||
// TODO: Remove use of [IdentityGenerator.generateToDisk].
|
// TODO: Remove use of [IdentityGenerator.generateToDisk].
|
||||||
log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!")
|
log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!")
|
||||||
keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair())
|
keyStore.storeLegalIdentity(singleName, privateKeyAlias, generateKeyPair())
|
||||||
}
|
}
|
||||||
|
|
||||||
val (x509Cert, keyPair) = keyStore.certificateAndKeyPair(privateKeyAlias)
|
val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias)
|
||||||
|
|
||||||
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
|
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
|
||||||
val compositeKeyAlias = "$id-composite-key"
|
val compositeKeyAlias = "$id-composite-key"
|
||||||
@ -848,10 +849,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager) : FlowStarter {
|
internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager, private val flowLogicRefFactory: FlowLogicRefFactory) : FlowStarter {
|
||||||
override fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<FlowStateMachine<T>> {
|
override fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<FlowStateMachine<T>> {
|
||||||
return serverThread.fetchFrom { smm.startFlow(logic, context) }
|
return serverThread.fetchFrom { smm.startFlow(logic, context) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T> invokeFlowAsync(
|
||||||
|
logicType: Class<out FlowLogic<T>>,
|
||||||
|
context: InvocationContext,
|
||||||
|
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
|
||||||
|
val logicRef = flowLogicRefFactory.createForRPC(logicType, *args)
|
||||||
|
val logic: FlowLogic<T> = uncheckedCast(flowLogicRefFactory.toFlowLogic(logicRef))
|
||||||
|
return startFlow(logic, context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfigurationException(message: String) : CordaException(message)
|
class ConfigurationException(message: String) : CordaException(message)
|
||||||
|
@ -22,10 +22,15 @@ abstract class CordaClock : Clock(), SerializeAsToken {
|
|||||||
override fun getZone(): ZoneId = delegateClock.zone
|
override fun getZone(): ZoneId = delegateClock.zone
|
||||||
@Deprecated("Do not use this. Instead seek to use ZonedDateTime methods.", level = DeprecationLevel.ERROR)
|
@Deprecated("Do not use this. Instead seek to use ZonedDateTime methods.", level = DeprecationLevel.ERROR)
|
||||||
override fun withZone(zone: ZoneId) = throw UnsupportedOperationException("Tokenized clock does not support withZone()")
|
override fun withZone(zone: ZoneId) = throw UnsupportedOperationException("Tokenized clock does not support withZone()")
|
||||||
|
|
||||||
|
/** This is an observer on the mutation count of this [Clock], which reflects the occurrence of mutations. */
|
||||||
|
abstract val mutations: Observable<Long>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class SimpleClock(override val delegateClock: Clock) : CordaClock()
|
class SimpleClock(override val delegateClock: Clock) : CordaClock() {
|
||||||
|
override val mutations: Observable<Long> = Observable.never()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract class with helper methods for a type of Clock that might have it's concept of "now" adjusted externally.
|
* An abstract class with helper methods for a type of Clock that might have it's concept of "now" adjusted externally.
|
||||||
@ -38,8 +43,7 @@ abstract class MutableClock(private var _delegateClock: Clock) : CordaClock() {
|
|||||||
_delegateClock = clock
|
_delegateClock = clock
|
||||||
}
|
}
|
||||||
private val _version = AtomicLong(0L)
|
private val _version = AtomicLong(0L)
|
||||||
/** This is an observer on the mutation count of this [Clock], which reflects the occurence of mutations. */
|
override val mutations: Observable<Long> by lazy {
|
||||||
val mutations: Observable<Long> by lazy {
|
|
||||||
Observable.create { subscriber: Subscriber<in Long> ->
|
Observable.create { subscriber: Subscriber<in Long> ->
|
||||||
if (!subscriber.isUnsubscribed) {
|
if (!subscriber.isUnsubscribed) {
|
||||||
mutationObservers.add(subscriber)
|
mutationObservers.add(subscriber)
|
||||||
|
@ -2,7 +2,6 @@ package net.corda.node.internal
|
|||||||
|
|
||||||
import com.codahale.metrics.JmxReporter
|
import com.codahale.metrics.JmxReporter
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.context.AuthServiceId
|
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
import net.corda.core.internal.concurrent.openFuture
|
||||||
import net.corda.core.internal.concurrent.thenMatch
|
import net.corda.core.internal.concurrent.thenMatch
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
@ -20,7 +19,9 @@ import net.corda.node.internal.cordapp.CordappLoader
|
|||||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||||
import net.corda.node.services.api.SchemaService
|
import net.corda.node.services.api.SchemaService
|
||||||
import net.corda.node.services.config.*
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
|
import net.corda.node.services.config.SecurityConfiguration
|
||||||
|
import net.corda.node.services.config.VerifierType
|
||||||
import net.corda.node.services.messaging.*
|
import net.corda.node.services.messaging.*
|
||||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||||
import net.corda.node.utilities.AddressUtils
|
import net.corda.node.utilities.AddressUtils
|
||||||
@ -33,6 +34,7 @@ import net.corda.nodeapi.internal.serialization.*
|
|||||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import rx.Scheduler
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@ -67,7 +69,7 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createClock(configuration: NodeConfiguration): Clock {
|
private fun createClock(configuration: NodeConfiguration): CordaClock {
|
||||||
return (if (configuration.useTestClock) ::DemoClock else ::SimpleClock)(Clock.systemUTC())
|
return (if (configuration.useTestClock) ::DemoClock else ::SimpleClock)(Clock.systemUTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,15 +271,13 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
private val _startupComplete = openFuture<Unit>()
|
private val _startupComplete = openFuture<Unit>()
|
||||||
val startupComplete: CordaFuture<Unit> get() = _startupComplete
|
val startupComplete: CordaFuture<Unit> get() = _startupComplete
|
||||||
|
|
||||||
override fun generateNodeInfo() {
|
override fun generateAndSaveNodeInfo(): NodeInfo {
|
||||||
initialiseSerialization()
|
initialiseSerialization()
|
||||||
super.generateNodeInfo()
|
return super.generateAndSaveNodeInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start(): StartedNode<Node> {
|
override fun start(): StartedNode<Node> {
|
||||||
if (initialiseSerialization) {
|
initialiseSerialization()
|
||||||
initialiseSerialization()
|
|
||||||
}
|
|
||||||
val started: StartedNode<Node> = uncheckedCast(super.start())
|
val started: StartedNode<Node> = uncheckedCast(super.start())
|
||||||
nodeReadyFuture.thenMatch({
|
nodeReadyFuture.thenMatch({
|
||||||
serverThread.execute {
|
serverThread.execute {
|
||||||
@ -310,8 +310,10 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
return started
|
return started
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRxIoScheduler() = Schedulers.io()!!
|
override fun getRxIoScheduler(): Scheduler = Schedulers.io()
|
||||||
|
|
||||||
private fun initialiseSerialization() {
|
private fun initialiseSerialization() {
|
||||||
|
if (!initialiseSerialization) return
|
||||||
val classloader = cordappLoader.appClassLoader
|
val classloader = cordappLoader.appClassLoader
|
||||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||||
SerializationFactoryImpl().apply {
|
SerializationFactoryImpl().apply {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.corda.node.internal
|
package net.corda.node.internal
|
||||||
|
|
||||||
import com.jcabi.manifests.Manifests
|
import com.jcabi.manifests.Manifests
|
||||||
import com.typesafe.config.ConfigException
|
|
||||||
import joptsimple.OptionException
|
import joptsimple.OptionException
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.concurrent.thenMatch
|
import net.corda.core.internal.concurrent.thenMatch
|
||||||
@ -126,7 +125,7 @@ open class NodeStartup(val args: Array<String>) {
|
|||||||
val node = createNode(conf, versionInfo)
|
val node = createNode(conf, versionInfo)
|
||||||
if (cmdlineOptions.justGenerateNodeInfo) {
|
if (cmdlineOptions.justGenerateNodeInfo) {
|
||||||
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
|
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
|
||||||
node.generateNodeInfo()
|
node.generateAndSaveNodeInfo()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (cmdlineOptions.justRunDbMigration) {
|
if (cmdlineOptions.justRunDbMigration) {
|
||||||
|
@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash
|
|||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
import net.corda.core.internal.uncheckedCast
|
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
@ -21,7 +20,6 @@ import net.corda.node.internal.InitiatedFlowFactory
|
|||||||
import net.corda.node.internal.cordapp.CordappProviderInternal
|
import net.corda.node.internal.cordapp.CordappProviderInternal
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.messaging.MessagingService
|
import net.corda.node.services.messaging.MessagingService
|
||||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
|
|
||||||
@ -137,11 +135,7 @@ interface FlowStarter {
|
|||||||
fun <T> invokeFlowAsync(
|
fun <T> invokeFlowAsync(
|
||||||
logicType: Class<out FlowLogic<T>>,
|
logicType: Class<out FlowLogic<T>>,
|
||||||
context: InvocationContext,
|
context: InvocationContext,
|
||||||
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
|
vararg args: Any?): CordaFuture<FlowStateMachine<T>>
|
||||||
val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args)
|
|
||||||
val logic: FlowLogic<T> = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef))
|
|
||||||
return startFlow(logic, context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StartedNodeServices : ServiceHubInternal, FlowStarter
|
interface StartedNodeServices : ServiceHubInternal, FlowStarter
|
||||||
|
@ -3,17 +3,21 @@ package net.corda.node.services.config
|
|||||||
import com.typesafe.config.*
|
import com.typesafe.config.*
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SignatureScheme
|
import net.corda.core.crypto.SignatureScheme
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import com.typesafe.config.ConfigParseOptions
|
||||||
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.createDirectories
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.internal.exists
|
||||||
|
import net.corda.core.internal.toX509CertHolder
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
import net.corda.nodeapi.internal.config.toProperties
|
import net.corda.nodeapi.internal.config.toProperties
|
||||||
|
import net.corda.nodeapi.internal.createDevKeyStores
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
|
||||||
import org.bouncycastle.asn1.x509.NameConstraints
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyStore
|
|
||||||
|
|
||||||
fun configOf(vararg pairs: Pair<String, Any?>): Config = ConfigFactory.parseMap(mapOf(*pairs))
|
fun configOf(vararg pairs: Pair<String, Any?>): Config = ConfigFactory.parseMap(mapOf(*pairs))
|
||||||
operator fun Config.plus(overrides: Map<String, Any?>): Config = ConfigFactory.parseMap(overrides).withFallback(this)
|
operator fun Config.plus(overrides: Map<String, Any?>): Config = ConfigFactory.parseMap(overrides).withFallback(this)
|
||||||
@ -58,8 +62,10 @@ object ConfigHelper {
|
|||||||
* Strictly for dev only automatically construct a server certificate/private key signed from
|
* Strictly for dev only automatically construct a server certificate/private key signed from
|
||||||
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
|
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
|
||||||
*/
|
*/
|
||||||
|
// TODO Move this to KeyStoreConfigHelpers
|
||||||
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
|
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
|
||||||
|
|
||||||
|
// TODO Move this to KeyStoreConfigHelpers
|
||||||
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||||
certificatesDirectory.createDirectories()
|
certificatesDirectory.createDirectories()
|
||||||
if (!trustStoreFile.exists()) {
|
if (!trustStoreFile.exists()) {
|
||||||
@ -67,7 +73,9 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
|||||||
}
|
}
|
||||||
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
|
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
|
||||||
|
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||||
|
createDevKeyStores(rootCert, intermediateCa, myLegalName)
|
||||||
|
|
||||||
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
||||||
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
||||||
@ -86,58 +94,3 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine.
|
|
||||||
* @param sslKeyStorePath KeyStore path to save ssl key and cert to.
|
|
||||||
* @param clientCAKeystorePath KeyStore path to save client CA key and cert to.
|
|
||||||
* @param storePassword access password for KeyStore.
|
|
||||||
* @param keyPassword PrivateKey access password for the generated keys.
|
|
||||||
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
|
|
||||||
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore.
|
|
||||||
* @param caKeyPassword password to unlock private keys in the CA KeyStore.
|
|
||||||
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications.
|
|
||||||
*/
|
|
||||||
fun createKeystoreForCordaNode(sslKeyStorePath: Path,
|
|
||||||
clientCAKeystorePath: Path,
|
|
||||||
storePassword: String,
|
|
||||||
keyPassword: String,
|
|
||||||
caKeyStore: KeyStore,
|
|
||||||
caKeyPassword: String,
|
|
||||||
legalName: CordaX500Name,
|
|
||||||
signatureScheme: SignatureScheme = X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) {
|
|
||||||
|
|
||||||
val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
|
|
||||||
val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword)
|
|
||||||
|
|
||||||
val clientKey = Crypto.generateKeyPair(signatureScheme)
|
|
||||||
|
|
||||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
|
||||||
val clientCACert = X509Utilities.createCertificate(CertificateType.NODE_CA,
|
|
||||||
intermediateCACert,
|
|
||||||
intermediateCAKeyPair,
|
|
||||||
legalName,
|
|
||||||
clientKey.public,
|
|
||||||
nameConstraints = nameConstraints)
|
|
||||||
|
|
||||||
val tlsKey = Crypto.generateKeyPair(signatureScheme)
|
|
||||||
val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public)
|
|
||||||
|
|
||||||
val keyPass = keyPassword.toCharArray()
|
|
||||||
|
|
||||||
val clientCAKeystore = loadOrCreateKeyStore(clientCAKeystorePath, storePassword)
|
|
||||||
clientCAKeystore.addOrReplaceKey(
|
|
||||||
X509Utilities.CORDA_CLIENT_CA,
|
|
||||||
clientKey.private,
|
|
||||||
keyPass,
|
|
||||||
arrayOf(clientCACert, intermediateCACert, rootCACert))
|
|
||||||
clientCAKeystore.save(clientCAKeystorePath, storePassword)
|
|
||||||
|
|
||||||
val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword)
|
|
||||||
tlsKeystore.addOrReplaceKey(
|
|
||||||
X509Utilities.CORDA_CLIENT_TLS,
|
|
||||||
tlsKey.private,
|
|
||||||
keyPass,
|
|
||||||
arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))
|
|
||||||
tlsKeystore.save(sslKeyStorePath, storePassword)
|
|
||||||
}
|
|
||||||
|
@ -10,6 +10,7 @@ import net.corda.core.contracts.ScheduledStateRef
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowLogicRefFactory
|
||||||
import net.corda.core.internal.ThreadBox
|
import net.corda.core.internal.ThreadBox
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.internal.concurrent.flatMap
|
import net.corda.core.internal.concurrent.flatMap
|
||||||
@ -19,10 +20,10 @@ import net.corda.core.schemas.PersistentStateRef
|
|||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
|
import net.corda.node.internal.CordaClock
|
||||||
import net.corda.node.internal.MutableClock
|
import net.corda.node.internal.MutableClock
|
||||||
import net.corda.node.services.api.FlowStarter
|
import net.corda.node.services.api.FlowStarter
|
||||||
import net.corda.node.services.api.SchedulerService
|
import net.corda.node.services.api.SchedulerService
|
||||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
|
||||||
import net.corda.node.utilities.AffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor
|
||||||
import net.corda.node.utilities.PersistentMap
|
import net.corda.node.utilities.PersistentMap
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
@ -55,13 +56,14 @@ import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture
|
|||||||
* activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on.
|
* activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class NodeSchedulerService(private val clock: Clock,
|
class NodeSchedulerService(private val clock: CordaClock,
|
||||||
private val database: CordaPersistence,
|
private val database: CordaPersistence,
|
||||||
private val flowStarter: FlowStarter,
|
private val flowStarter: FlowStarter,
|
||||||
private val stateLoader: StateLoader,
|
private val stateLoader: StateLoader,
|
||||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
||||||
private val unfinishedSchedules: ReusableLatch = ReusableLatch(),
|
private val unfinishedSchedules: ReusableLatch = ReusableLatch(),
|
||||||
private val serverThread: AffinityExecutor)
|
private val serverThread: AffinityExecutor,
|
||||||
|
private val flowLogicRefFactory: FlowLogicRefFactory)
|
||||||
: SchedulerService, SingletonSerializeAsToken() {
|
: SchedulerService, SingletonSerializeAsToken() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -78,16 +80,12 @@ class NodeSchedulerService(private val clock: Clock,
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
// We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name
|
// We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name
|
||||||
fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create<Any>()): Boolean {
|
fun awaitWithDeadline(clock: CordaClock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create<Any>()): Boolean {
|
||||||
var nanos: Long
|
var nanos: Long
|
||||||
do {
|
do {
|
||||||
val originalFutureCompleted = makeStrandFriendlySettableFuture(future)
|
val originalFutureCompleted = makeStrandFriendlySettableFuture(future)
|
||||||
val subscription = if (clock is MutableClock) {
|
val subscription = clock.mutations.first().subscribe {
|
||||||
clock.mutations.first().subscribe {
|
originalFutureCompleted.set(false)
|
||||||
originalFutureCompleted.set(false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
nanos = (clock.instant() until deadline).toNanos()
|
nanos = (clock.instant() until deadline).toNanos()
|
||||||
if (nanos > 0) {
|
if (nanos > 0) {
|
||||||
@ -102,7 +100,7 @@ class NodeSchedulerService(private val clock: Clock,
|
|||||||
// No need to take action as will fall out of the loop due to future.isDone
|
// No need to take action as will fall out of the loop due to future.isDone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subscription?.unsubscribe()
|
subscription.unsubscribe()
|
||||||
originalFutureCompleted.cancel(false)
|
originalFutureCompleted.cancel(false)
|
||||||
} while (nanos > 0 && !future.isDone)
|
} while (nanos > 0 && !future.isDone)
|
||||||
return future.isDone
|
return future.isDone
|
||||||
@ -279,7 +277,7 @@ class NodeSchedulerService(private val clock: Clock,
|
|||||||
scheduledStatesQueue.remove(scheduledState)
|
scheduledStatesQueue.remove(scheduledState)
|
||||||
scheduledStatesQueue.add(newState)
|
scheduledStatesQueue.add(newState)
|
||||||
} else {
|
} else {
|
||||||
val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef)
|
val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef)
|
||||||
log.trace { "Scheduler starting FlowLogic $flowLogic" }
|
log.trace { "Scheduler starting FlowLogic $flowLogic" }
|
||||||
scheduledFlow = flowLogic
|
scheduledFlow = flowLogic
|
||||||
scheduledStates.remove(scheduledState.ref)
|
scheduledStates.remove(scheduledState.ref)
|
||||||
@ -297,7 +295,7 @@ class NodeSchedulerService(private val clock: Clock,
|
|||||||
val state = txState.data as SchedulableState
|
val state = txState.data as SchedulableState
|
||||||
return try {
|
return try {
|
||||||
// This can throw as running contract code.
|
// This can throw as running contract code.
|
||||||
state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl)
|
state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
|
log.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
|
||||||
null
|
null
|
||||||
|
@ -4,19 +4,19 @@ import net.corda.core.contracts.ContractState
|
|||||||
import net.corda.core.contracts.SchedulableState
|
import net.corda.core.contracts.SchedulableState
|
||||||
import net.corda.core.contracts.ScheduledStateRef
|
import net.corda.core.contracts.ScheduledStateRef
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.flows.FlowLogicRefFactory
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.node.services.api.SchedulerService
|
import net.corda.node.services.api.SchedulerService
|
||||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This observes the vault and schedules and unschedules activities appropriately based on state production and
|
* This observes the vault and schedules and unschedules activities appropriately based on state production and
|
||||||
* consumption.
|
* consumption.
|
||||||
*/
|
*/
|
||||||
class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService) {
|
class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService, private val FlowLogicRefFactory: FlowLogicRefFactory) {
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun install(vaultService: VaultService, schedulerService: SchedulerService) {
|
fun install(vaultService: VaultService, schedulerService: SchedulerService, flowLogicRefFactory: FlowLogicRefFactory) {
|
||||||
val observer = ScheduledActivityObserver(schedulerService)
|
val observer = ScheduledActivityObserver(schedulerService, flowLogicRefFactory)
|
||||||
vaultService.rawUpdates.subscribe { (consumed, produced) ->
|
vaultService.rawUpdates.subscribe { (consumed, produced) ->
|
||||||
consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) }
|
consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) }
|
||||||
produced.forEach { observer.scheduleStateActivity(it) }
|
produced.forEach { observer.scheduleStateActivity(it) }
|
||||||
@ -32,7 +32,7 @@ class ScheduledActivityObserver private constructor(private val schedulerService
|
|||||||
private fun scheduleStateActivity(produced: StateAndRef<ContractState>) {
|
private fun scheduleStateActivity(produced: StateAndRef<ContractState>) {
|
||||||
val producedState = produced.state.data
|
val producedState = produced.state.data
|
||||||
if (producedState is SchedulableState) {
|
if (producedState is SchedulableState) {
|
||||||
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return
|
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactory)?.scheduledAt } ?: return
|
||||||
schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt))
|
schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,8 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String,
|
|||||||
* measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital"
|
* measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital"
|
||||||
* in response to a potential malicious use or buggy update to an app etc.
|
* in response to a potential malicious use or buggy update to an app etc.
|
||||||
*/
|
*/
|
||||||
object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory {
|
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
|
||||||
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
|
class FlowLogicRefFactoryImpl(private val classloader: ClassLoader) : SingletonSerializeAsToken(), FlowLogicRefFactory {
|
||||||
var classloader: ClassLoader = javaClass.classLoader
|
|
||||||
|
|
||||||
override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||||
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
|
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
|
||||||
throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
|
throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
|
||||||
@ -42,7 +40,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
|
|||||||
return createForRPC(flowClass, *args)
|
return createForRPC(flowClass, *args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
override fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||||
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
|
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
|
||||||
// to avoid requiring only a single constructor.
|
// to avoid requiring only a single constructor.
|
||||||
val argTypes = args.map { it?.javaClass }
|
val argTypes = args.map { it?.javaClass }
|
||||||
@ -81,7 +79,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
|
|||||||
return FlowLogicRefImpl(type.name, args)
|
return FlowLogicRefImpl(type.name, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
||||||
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
|
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
|
||||||
val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java)
|
val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java)
|
||||||
return createConstructor(klass, ref.args)()
|
return createConstructor(klass, ref.args)()
|
||||||
|
@ -12,10 +12,10 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
|||||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||||
import org.bouncycastle.util.io.pem.PemObject
|
import org.bouncycastle.util.io.pem.PemObject
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.nio.file.Path
|
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for managing the node registration process, which checks for any existing certificates and requests them if
|
* Helper for managing the node registration process, which checks for any existing certificates and requests them if
|
||||||
@ -32,7 +32,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
|||||||
// TODO: Use different password for private key.
|
// TODO: Use different password for private key.
|
||||||
private val privateKeyPassword = config.keyStorePassword
|
private val privateKeyPassword = config.keyStorePassword
|
||||||
private val trustStore: KeyStore
|
private val trustStore: KeyStore
|
||||||
private val rootCert: Certificate
|
private val rootCert: X509Certificate
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(config.trustStoreFile.exists()) {
|
require(config.trustStoreFile.exists()) {
|
||||||
@ -46,7 +46,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
|||||||
"This file must contain the root CA cert of your compatibility zone. " +
|
"This file must contain the root CA cert of your compatibility zone. " +
|
||||||
"Please contact your CZ operator."
|
"Please contact your CZ operator."
|
||||||
}
|
}
|
||||||
this.rootCert = rootCert
|
this.rootCert = rootCert as X509Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,11 +94,8 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
|||||||
caKeyStore.save(config.nodeKeystore, keystorePassword)
|
caKeyStore.save(config.nodeKeystore, keystorePassword)
|
||||||
println("Node private key and certificate stored in ${config.nodeKeystore}.")
|
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.
|
println("Checking root of the certificate path is what we expect.")
|
||||||
if (rootCert != certificates.last()) {
|
X509Utilities.validateCertificateChain(rootCert, *certificates)
|
||||||
// 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.")
|
println("Generating SSL certificate for node messaging service.")
|
||||||
val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
@ -168,17 +165,3 @@ 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())
|
|
||||||
|
@ -48,13 +48,15 @@ public class FlowLogicRefFromJavaTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final FlowLogicRefFactoryImpl flowLogicRefFactory = new FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl.class.getClassLoader());
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void test() {
|
||||||
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
flowLogicRefFactory.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNoArg() {
|
public void testNoArg() {
|
||||||
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class);
|
flowLogicRefFactory.createForRPC(JavaNoArgFlowLogic.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,47 +34,48 @@ class FlowLogicRefTest {
|
|||||||
override fun call() = Unit
|
override fun call() = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader)
|
||||||
@Test
|
@Test
|
||||||
fun `create kotlin no arg`() {
|
fun `create kotlin no arg`() {
|
||||||
FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java)
|
flowLogicRefFactory.createForRPC(KotlinNoArgFlowLogic::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create kotlin`() {
|
fun `create kotlin`() {
|
||||||
val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack")))
|
val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack")))
|
||||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create primary`() {
|
fun `create primary`() {
|
||||||
FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
flowLogicRefFactory.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create kotlin void`() {
|
fun `create kotlin void`() {
|
||||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap())
|
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create kotlin non primary`() {
|
fun `create kotlin non primary`() {
|
||||||
val args = mapOf(Pair("C", ParamType2("Hello Jack")))
|
val args = mapOf(Pair("C", ParamType2("Hello Jack")))
|
||||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create java primitive no registration required`() {
|
fun `create java primitive no registration required`() {
|
||||||
val args = mapOf(Pair("primitive", "A string"))
|
val args = mapOf(Pair("primitive", "A string"))
|
||||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create kotlin primitive no registration required`() {
|
fun `create kotlin primitive no registration required`() {
|
||||||
val args = mapOf(Pair("kotlinType", 3))
|
val args = mapOf(Pair("kotlinType", 3))
|
||||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalFlowLogicException::class)
|
@Test(expected = IllegalFlowLogicException::class)
|
||||||
fun `create for non-schedulable flow logic`() {
|
fun `create for non-schedulable flow logic`() {
|
||||||
FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java)
|
flowLogicRefFactory.create(NonSchedulableFlow::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
|||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule(true)
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader)
|
||||||
private val realClock: Clock = Clock.systemUTC()
|
private val realClock: Clock = Clock.systemUTC()
|
||||||
private val stoppedClock: Clock = Clock.fixed(realClock.instant(), realClock.zone)
|
private val stoppedClock: Clock = Clock.fixed(realClock.instant(), realClock.zone)
|
||||||
private val testClock = TestClock(stoppedClock)
|
private val testClock = TestClock(stoppedClock)
|
||||||
@ -122,7 +123,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
|||||||
}
|
}
|
||||||
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
||||||
mockSMM = StateMachineManagerImpl(services, DBCheckpointStorage(), smmExecutor, database, newSecureRandom())
|
mockSMM = StateMachineManagerImpl(services, DBCheckpointStorage(), smmExecutor, database, newSecureRandom())
|
||||||
scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor)
|
scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM, flowLogicRefFactory), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor, flowLogicRefFactory = flowLogicRefFactory)
|
||||||
mockSMM.changes.subscribe { change ->
|
mockSMM.changes.subscribe { change ->
|
||||||
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
|
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
|
||||||
smmHasRemovedAllFlows.countDown()
|
smmHasRemovedAllFlows.countDown()
|
||||||
@ -305,7 +306,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
apply {
|
apply {
|
||||||
val freshKey = kms.freshKey()
|
val freshKey = kms.freshKey()
|
||||||
val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party)
|
val state = TestState(flowLogicRefFactory.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party)
|
||||||
val builder = TransactionBuilder(null).apply {
|
val builder = TransactionBuilder(null).apply {
|
||||||
addOutputState(state, DummyContract.PROGRAM_ID, DUMMY_NOTARY)
|
addOutputState(state, DummyContract.PROGRAM_ID, DUMMY_NOTARY)
|
||||||
addCommand(Command(), freshKey)
|
addCommand(Command(), freshKey)
|
||||||
|
@ -70,7 +70,7 @@ class NetworkMapClientTest {
|
|||||||
// The test server returns same network parameter for any hash.
|
// The test server returns same network parameter for any hash.
|
||||||
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
|
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
|
||||||
assertNotNull(networkParameter)
|
assertNotNull(networkParameter)
|
||||||
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter)
|
assertEquals(server.networkParameters, networkParameter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -8,6 +8,8 @@ import com.google.common.util.concurrent.SettableFuture
|
|||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.hours
|
import net.corda.core.utilities.hours
|
||||||
import net.corda.core.utilities.minutes
|
import net.corda.core.utilities.minutes
|
||||||
|
import net.corda.node.internal.CordaClock
|
||||||
|
import net.corda.node.internal.SimpleClock
|
||||||
import net.corda.node.services.events.NodeSchedulerService
|
import net.corda.node.services.events.NodeSchedulerService
|
||||||
import net.corda.testing.node.TestClock
|
import net.corda.testing.node.TestClock
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -25,13 +27,13 @@ import kotlin.test.fail
|
|||||||
class ClockUtilsTest {
|
class ClockUtilsTest {
|
||||||
|
|
||||||
lateinit var realClock: Clock
|
lateinit var realClock: Clock
|
||||||
lateinit var stoppedClock: Clock
|
lateinit var stoppedClock: CordaClock
|
||||||
lateinit var executor: ExecutorService
|
lateinit var executor: ExecutorService
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
realClock = Clock.systemUTC()
|
realClock = Clock.systemUTC()
|
||||||
stoppedClock = Clock.fixed(realClock.instant(), realClock.zone)
|
stoppedClock = SimpleClock(Clock.fixed(realClock.instant(), realClock.zone))
|
||||||
executor = Executors.newSingleThreadExecutor()
|
executor = Executors.newSingleThreadExecutor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,12 +14,14 @@ import net.corda.core.internal.createDirectories
|
|||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import net.corda.testing.ALICE_NAME
|
import net.corda.testing.ALICE_NAME
|
||||||
|
import net.corda.testing.internal.createDevNodeCaCertPath
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.security.cert.CertPathValidatorException
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
@ -29,16 +31,19 @@ class NetworkRegistrationHelperTest {
|
|||||||
private val fs = Jimfs.newFileSystem(unix())
|
private val fs = Jimfs.newFileSystem(unix())
|
||||||
private val requestId = SecureHash.randomSHA256().toString()
|
private val requestId = SecureHash.randomSHA256().toString()
|
||||||
private val nodeLegalName = ALICE_NAME
|
private val nodeLegalName = ALICE_NAME
|
||||||
private val intermediateCaName = CordaX500Name("CORDA_INTERMEDIATE_CA", "R3 Ltd", "London", "GB")
|
|
||||||
private val rootCaName = CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB")
|
|
||||||
private val nodeCaCert = createCaCert(nodeLegalName)
|
|
||||||
private val intermediateCaCert = createCaCert(intermediateCaName)
|
|
||||||
private val rootCaCert = createCaCert(rootCaName)
|
|
||||||
|
|
||||||
|
private lateinit var rootCaCert: X509Certificate
|
||||||
|
private lateinit var intermediateCaCert: X509Certificate
|
||||||
|
private lateinit var nodeCaCert: X509Certificate
|
||||||
private lateinit var config: NodeConfiguration
|
private lateinit var config: NodeConfiguration
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun init() {
|
fun init() {
|
||||||
|
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(nodeLegalName)
|
||||||
|
this.rootCaCert = rootCa.certificate.cert
|
||||||
|
this.intermediateCaCert = intermediateCa.certificate.cert
|
||||||
|
this.nodeCaCert = nodeCa.certificate.cert
|
||||||
|
|
||||||
val baseDirectory = fs.getPath("/baseDir").createDirectories()
|
val baseDirectory = fs.getPath("/baseDir").createDirectories()
|
||||||
abstract class AbstractNodeConfiguration : NodeConfiguration
|
abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||||
config = rigorousMock<AbstractNodeConfiguration>().also {
|
config = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
@ -108,11 +113,13 @@ class NetworkRegistrationHelperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `wrong root cert in truststore`() {
|
fun `wrong root cert in truststore`() {
|
||||||
saveTrustStoreWithRootCa(createCaCert(CordaX500Name("Foo", "MU", "GB")))
|
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val rootCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), rootKeyPair)
|
||||||
|
saveTrustStoreWithRootCa(rootCert.cert)
|
||||||
val registrationHelper = createRegistrationHelper()
|
val registrationHelper = createRegistrationHelper()
|
||||||
assertThatThrownBy {
|
assertThatThrownBy {
|
||||||
registrationHelper.buildKeystore()
|
registrationHelper.buildKeystore()
|
||||||
}.isInstanceOf(WrongRootCertException::class.java)
|
}.isInstanceOf(CertPathValidatorException::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRegistrationHelper(): NetworkRegistrationHelper {
|
private fun createRegistrationHelper(): NetworkRegistrationHelper {
|
||||||
@ -123,15 +130,11 @@ class NetworkRegistrationHelperTest {
|
|||||||
return NetworkRegistrationHelper(config, certService)
|
return NetworkRegistrationHelper(config, certService)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTrustStoreWithRootCa(rootCa: X509Certificate) {
|
private fun saveTrustStoreWithRootCa(rootCert: X509Certificate) {
|
||||||
config.trustStoreFile.parent.createDirectories()
|
config.certificatesDirectory.createDirectories()
|
||||||
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
|
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
|
||||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa)
|
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||||
it.save(config.trustStoreFile, config.trustStorePassword)
|
it.save(config.trustStoreFile, config.trustStorePassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCaCert(name: CordaX500Name): X509Certificate {
|
|
||||||
return X509Utilities.createSelfSignedCACertificate(name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ define([
|
|||||||
$scope.formError = resp.data;
|
$scope.formError = resp.data;
|
||||||
}, handleHttpFail);
|
}, handleHttpFail);
|
||||||
};
|
};
|
||||||
$('input.percent').mask("9.999999%", {placeholder: "", autoclear: false});
|
$('input.percent').mask("9.999999", {placeholder: "", autoclear: false});
|
||||||
$('#swapirscolumns').click(() => {
|
$('#swapirscolumns').click(() => {
|
||||||
let first = $('#irscolumns .irscolumn:eq( 0 )');
|
let first = $('#irscolumns .irscolumn:eq( 0 )');
|
||||||
let last = $('#irscolumns .irscolumn:eq( 1 )');
|
let last = $('#irscolumns .irscolumn:eq( 1 )');
|
||||||
|
@ -8,7 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
|||||||
import net.corda.node.services.config.BFTSMaRtConfiguration
|
import net.corda.node.services.config.BFTSMaRtConfiguration
|
||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.transactions.minCorrectReplicas
|
import net.corda.node.services.transactions.minCorrectReplicas
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.testing.node.internal.demorun.*
|
import net.corda.testing.node.internal.demorun.*
|
||||||
import net.corda.testing.ALICE_NAME
|
import net.corda.testing.ALICE_NAME
|
||||||
import net.corda.testing.BOB_NAME
|
import net.corda.testing.BOB_NAME
|
||||||
@ -62,7 +62,7 @@ class BFTNotaryCordform : CordformDefinition() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setup(context: CordformContext) {
|
override fun setup(context: CordformContext) {
|
||||||
IdentityGenerator.generateDistributedNotaryIdentity(
|
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||||
notaryNames.map { context.baseDirectory(it.toString()) },
|
notaryNames.map { context.baseDirectory(it.toString()) },
|
||||||
clusterName,
|
clusterName,
|
||||||
minCorrectReplicas(clusterSize)
|
minCorrectReplicas(clusterSize)
|
||||||
|
@ -4,11 +4,10 @@ import net.corda.cordform.CordformContext
|
|||||||
import net.corda.cordform.CordformDefinition
|
import net.corda.cordform.CordformDefinition
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.node.services.NotaryService
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.config.RaftConfig
|
import net.corda.node.services.config.RaftConfig
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.testing.node.internal.demorun.*
|
import net.corda.testing.node.internal.demorun.*
|
||||||
import net.corda.testing.ALICE_NAME
|
import net.corda.testing.ALICE_NAME
|
||||||
import net.corda.testing.BOB_NAME
|
import net.corda.testing.BOB_NAME
|
||||||
@ -59,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setup(context: CordformContext) {
|
override fun setup(context: CordformContext) {
|
||||||
IdentityGenerator.generateDistributedNotaryIdentity(
|
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||||
notaryNames.map { context.baseDirectory(it.toString()) },
|
notaryNames.map { context.baseDirectory(it.toString()) },
|
||||||
clusterName
|
clusterName
|
||||||
)
|
)
|
||||||
|
@ -72,6 +72,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
|||||||
notary = [validating : true]
|
notary = [validating : true]
|
||||||
p2pPort 10002
|
p2pPort 10002
|
||||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||||
|
extraConfig = [
|
||||||
|
jvmArgs : [ "-Xmx1g"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
node {
|
node {
|
||||||
name "O=Bank A,L=London,C=GB"
|
name "O=Bank A,L=London,C=GB"
|
||||||
@ -80,6 +83,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
|||||||
rpcPort 10006
|
rpcPort 10006
|
||||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||||
rpcUsers = ext.rpcUsers
|
rpcUsers = ext.rpcUsers
|
||||||
|
extraConfig = [
|
||||||
|
jvmArgs : [ "-Xmx1g"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
node {
|
node {
|
||||||
name "O=Bank B,L=New York,C=US"
|
name "O=Bank B,L=New York,C=US"
|
||||||
@ -88,6 +94,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
|||||||
rpcPort 10009
|
rpcPort 10009
|
||||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||||
rpcUsers = ext.rpcUsers
|
rpcUsers = ext.rpcUsers
|
||||||
|
extraConfig = [
|
||||||
|
jvmArgs : [ "-Xmx1g"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
node {
|
node {
|
||||||
name "O=Bank C,L=Tokyo,C=JP"
|
name "O=Bank C,L=Tokyo,C=JP"
|
||||||
@ -96,6 +105,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
|||||||
rpcPort 10012
|
rpcPort 10012
|
||||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||||
rpcUsers = ext.rpcUsers
|
rpcUsers = ext.rpcUsers
|
||||||
|
extraConfig = [
|
||||||
|
jvmArgs : [ "-Xmx1g"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,8 @@ class SellerFlow(private val otherParty: Party,
|
|||||||
progressTracker.currentStep = SELF_ISSUING
|
progressTracker.currentStep = SELF_ISSUING
|
||||||
|
|
||||||
val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||||
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java).states.first()
|
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java)
|
||||||
|
.states.firstOrNull() ?: throw IllegalStateException("No commercial paper found. Please check if you issued the papers first, follow the README for instructions.")
|
||||||
|
|
||||||
progressTracker.currentStep = TRADING
|
progressTracker.currentStep = TRADING
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package net.corda.testing.driver
|
|||||||
|
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.CertRole
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.internal.list
|
import net.corda.core.internal.list
|
||||||
import net.corda.core.internal.readLines
|
import net.corda.core.internal.readLines
|
||||||
@ -47,12 +48,28 @@ class DriverTests : IntegrationTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `simple node startup and shutdown`() {
|
fun `simple node startup and shutdown`() {
|
||||||
val handle = driver {
|
val handle = driver {
|
||||||
val regulator = startNode(providedName = DUMMY_REGULATOR_NAME)
|
val node = startNode(providedName = DUMMY_REGULATOR_NAME)
|
||||||
nodeMustBeUp(regulator)
|
nodeMustBeUp(node)
|
||||||
}
|
}
|
||||||
nodeMustBeDown(handle)
|
nodeMustBeDown(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `starting with default notary`() {
|
||||||
|
driver {
|
||||||
|
// Make sure the default is a single-node notary
|
||||||
|
val notary = defaultNotaryNode.getOrThrow()
|
||||||
|
val notaryIdentities = notary.nodeInfo.legalIdentitiesAndCerts
|
||||||
|
// Make sure the notary node has only one identity
|
||||||
|
assertThat(notaryIdentities).hasSize(1)
|
||||||
|
val identity = notaryIdentities[0]
|
||||||
|
// Make sure this identity is a legal identity, like it is for normal nodes.
|
||||||
|
assertThat(CertRole.extract(identity.certificate)).isEqualTo(CertRole.LEGAL_IDENTITY)
|
||||||
|
// And make sure this identity is published as the notary identity (via the network parameters)
|
||||||
|
assertThat(notary.rpc.notaryIdentities()).containsOnly(identity.party)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `random free port allocation`() {
|
fun `random free port allocation`() {
|
||||||
val nodeHandle = driver(portAllocation = PortAllocation.RandomFree) {
|
val nodeHandle = driver(portAllocation = PortAllocation.RandomFree) {
|
||||||
|
@ -14,11 +14,11 @@ import net.corda.node.internal.StartedNode
|
|||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.config.VerifierType
|
import net.corda.node.services.config.VerifierType
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
|
import net.corda.testing.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
import net.corda.testing.node.internal.DriverDSLImpl
|
import net.corda.testing.node.internal.DriverDSLImpl
|
||||||
import net.corda.testing.node.internal.genericDriver
|
import net.corda.testing.node.internal.genericDriver
|
||||||
import net.corda.testing.node.internal.getTimestampAsDirectoryName
|
import net.corda.testing.node.internal.getTimestampAsDirectoryName
|
||||||
import net.corda.testing.DUMMY_NOTARY_NAME
|
|
||||||
import net.corda.testing.node.NotarySpec
|
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
@ -38,7 +38,7 @@ import net.corda.node.services.transactions.BFTSMaRt
|
|||||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||||
import net.corda.node.utilities.AffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor
|
||||||
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.nodeapi.internal.network.NotaryInfo
|
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||||
@ -236,7 +236,7 @@ open class MockNetwork(private val cordappPackages: List<String>,
|
|||||||
|
|
||||||
private fun generateNotaryIdentities(): List<NotaryInfo> {
|
private fun generateNotaryIdentities(): List<NotaryInfo> {
|
||||||
return notarySpecs.mapIndexed { index, (name, validating) ->
|
return notarySpecs.mapIndexed { index, (name, validating) ->
|
||||||
val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(nextNodeId + index), name)
|
val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(nextNodeId + index), name)
|
||||||
NotaryInfo(identity, validating)
|
NotaryInfo(identity, validating)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,15 +10,12 @@ import net.corda.cordform.CordformContext
|
|||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.concurrent.firstOf
|
import net.corda.core.concurrent.firstOf
|
||||||
import net.corda.core.crypto.random63BitValue
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.ThreadBox
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.concurrent.*
|
import net.corda.core.internal.concurrent.*
|
||||||
import net.corda.core.internal.copyTo
|
|
||||||
import net.corda.core.internal.createDirectories
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
@ -31,7 +28,8 @@ import net.corda.node.services.Permissions
|
|||||||
import net.corda.node.services.config.*
|
import net.corda.node.services.config.*
|
||||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||||
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.addShutdownHook
|
import net.corda.nodeapi.internal.addShutdownHook
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.config.parseAs
|
import net.corda.nodeapi.internal.config.parseAs
|
||||||
@ -68,7 +66,7 @@ import java.nio.file.StandardCopyOption
|
|||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset.UTC
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -96,15 +94,16 @@ class DriverDSLImpl(
|
|||||||
private var _shutdownManager: ShutdownManager? = null
|
private var _shutdownManager: ShutdownManager? = null
|
||||||
override val shutdownManager get() = _shutdownManager!!
|
override val shutdownManager get() = _shutdownManager!!
|
||||||
private val cordappPackages = extraCordappPackagesToScan + getCallerPackage()
|
private val cordappPackages = extraCordappPackagesToScan + getCallerPackage()
|
||||||
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
|
|
||||||
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
|
|
||||||
// Investigate whether we can avoid that.
|
|
||||||
private var nodeInfoFilesCopier: NodeInfoFilesCopier? = null
|
|
||||||
// Map from a nodes legal name to an observable emitting the number of nodes in its network map.
|
// Map from a nodes legal name to an observable emitting the number of nodes in its network map.
|
||||||
private val countObservables = mutableMapOf<CordaX500Name, Observable<Int>>()
|
private val countObservables = mutableMapOf<CordaX500Name, Observable<Int>>()
|
||||||
private lateinit var _notaries: List<NotaryHandle>
|
/**
|
||||||
override val notaryHandles: List<NotaryHandle> get() = _notaries
|
* Future which completes when the network map is available, whether a local one or one from the CZ. This future acts
|
||||||
private var networkParameters: NetworkParametersCopier? = null
|
* as a gate to prevent nodes from starting too early. The value of the future is a [LocalNetworkMap] object, which
|
||||||
|
* is null if the network map is being provided by the CZ.
|
||||||
|
*/
|
||||||
|
private lateinit var networkMapAvailability: CordaFuture<LocalNetworkMap?>
|
||||||
|
private lateinit var _notaries: CordaFuture<List<NotaryHandle>>
|
||||||
|
override val notaryHandles: List<NotaryHandle> get() = _notaries.getOrThrow()
|
||||||
|
|
||||||
class State {
|
class State {
|
||||||
val processes = ArrayList<Process>()
|
val processes = ArrayList<Process>()
|
||||||
@ -145,12 +144,12 @@ class DriverDSLImpl(
|
|||||||
_executorService?.shutdownNow()
|
_executorService?.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
|
private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
|
||||||
val rpcAddress = config.rpcAddress!!
|
val rpcAddress = config.corda.rpcAddress!!
|
||||||
val client = CordaRPCClient(rpcAddress)
|
val client = CordaRPCClient(rpcAddress)
|
||||||
val connectionFuture = poll(executorService, "RPC connection") {
|
val connectionFuture = poll(executorService, "RPC connection") {
|
||||||
try {
|
try {
|
||||||
client.start(config.rpcUsers[0].username, config.rpcUsers[0].password)
|
config.corda.rpcUsers[0].run { client.start(username, password) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (processDeathFuture.isDone) throw e
|
if (processDeathFuture.isDone) throw e
|
||||||
log.error("Exception $e, Retrying RPC connection at $rpcAddress")
|
log.error("Exception $e, Retrying RPC connection at $rpcAddress")
|
||||||
@ -178,65 +177,74 @@ class DriverDSLImpl(
|
|||||||
): CordaFuture<NodeHandle> {
|
): CordaFuture<NodeHandle> {
|
||||||
val p2pAddress = portAllocation.nextHostAndPort()
|
val p2pAddress = portAllocation.nextHostAndPort()
|
||||||
// TODO: Derive name from the full picked name, don't just wrap the common name
|
// 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 name = providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB")
|
||||||
|
|
||||||
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
||||||
nodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url)
|
// We don't need the network map to be available to be able to register the node
|
||||||
|
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url)
|
||||||
} else {
|
} else {
|
||||||
doneFuture(Unit)
|
doneFuture(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return registrationFuture.flatMap {
|
return registrationFuture.flatMap {
|
||||||
val rpcAddress = portAllocation.nextHostAndPort()
|
networkMapAvailability.flatMap {
|
||||||
val webAddress = portAllocation.nextHostAndPort()
|
// But starting the node proper does require the network map
|
||||||
val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) }
|
startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<Unit> {
|
private fun startRegisteredNode(name: CordaX500Name,
|
||||||
|
localNetworkMap: LocalNetworkMap?,
|
||||||
|
rpcUsers: List<User>,
|
||||||
|
verifierType: VerifierType,
|
||||||
|
customOverrides: Map<String, Any?>,
|
||||||
|
startInSameProcess: Boolean? = null,
|
||||||
|
maximumHeapSize: String = "200m",
|
||||||
|
p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort()): CordaFuture<NodeHandle> {
|
||||||
|
val rpcAddress = portAllocation.nextHostAndPort()
|
||||||
|
val webAddress = portAllocation.nextHostAndPort()
|
||||||
|
val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) }
|
||||||
|
val czUrlConfig = if (compatibilityZone != null) mapOf("compatibilityZoneURL" to compatibilityZone.url.toString()) else emptyMap()
|
||||||
|
val config = NodeConfig(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
|
||||||
|
) + czUrlConfig + customOverrides
|
||||||
|
))
|
||||||
|
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NodeConfig> {
|
||||||
val baseDirectory = baseDirectory(providedName).createDirectories()
|
val baseDirectory = baseDirectory(providedName).createDirectories()
|
||||||
val config = ConfigHelper.loadConfig(
|
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||||
baseDirectory = baseDirectory,
|
baseDirectory = baseDirectory,
|
||||||
allowMissingConfig = true,
|
allowMissingConfig = true,
|
||||||
configOverrides = configOf(
|
configOverrides = configOf(
|
||||||
"p2pAddress" to "localhost:1222", // required argument, not really used
|
"p2pAddress" to "localhost:1222", // required argument, not really used
|
||||||
"compatibilityZoneURL" to compatibilityZoneURL.toString(),
|
"compatibilityZoneURL" to compatibilityZoneURL.toString(),
|
||||||
"myLegalName" to providedName.toString())
|
"myLegalName" to providedName.toString())
|
||||||
)
|
))
|
||||||
val configuration = config.parseAsNodeConfiguration()
|
|
||||||
|
|
||||||
configuration.trustStoreFile.parent.createDirectories()
|
config.corda.certificatesDirectory.createDirectories()
|
||||||
loadOrCreateKeyStore(configuration.trustStoreFile, configuration.trustStorePassword).also {
|
loadOrCreateKeyStore(config.corda.trustStoreFile, config.corda.trustStorePassword).apply {
|
||||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||||
it.save(configuration.trustStoreFile, configuration.trustStorePassword)
|
save(config.corda.trustStoreFile, config.corda.trustStorePassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (startNodesInProcess) {
|
return if (startNodesInProcess) {
|
||||||
// This is a bit cheating, we're not starting a full node, we're just calling the code nodes call
|
executorService.fork {
|
||||||
// when registering.
|
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore()
|
||||||
NetworkRegistrationHelper(configuration, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore()
|
config
|
||||||
doneFuture(Unit)
|
}
|
||||||
} else {
|
} else {
|
||||||
startOutOfProcessNodeRegistration(config, configuration)
|
startOutOfProcessMiniNode(config, "--initial-registration").map { config }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +255,9 @@ class DriverDSLImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun startCordformNodes(cordforms: List<CordformNode>): CordaFuture<*> {
|
internal fun startCordformNodes(cordforms: List<CordformNode>): CordaFuture<*> {
|
||||||
check(compatibilityZone == null) { "Cordform nodes should be run without compatibilityZone configuration" }
|
check(notarySpecs.isEmpty()) { "Specify notaries in the CordformDefinition" }
|
||||||
|
check(compatibilityZone == null) { "Cordform nodes cannot be run with compatibilityZoneURL" }
|
||||||
|
|
||||||
val clusterNodes = HashMultimap.create<ClusterType, CordaX500Name>()
|
val clusterNodes = HashMultimap.create<ClusterType, CordaX500Name>()
|
||||||
val notaryInfos = ArrayList<NotaryInfo>()
|
val notaryInfos = ArrayList<NotaryInfo>()
|
||||||
|
|
||||||
@ -266,23 +276,23 @@ class DriverDSLImpl(
|
|||||||
clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name)
|
clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name)
|
||||||
} else {
|
} else {
|
||||||
// We have all we need here to generate the identity for single node notaries
|
// We have all we need here to generate the identity for single node notaries
|
||||||
val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(name), legalName = name)
|
val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(name), legalName = name)
|
||||||
notaryInfos += NotaryInfo(identity, notaryConfig.validating)
|
notaryInfos += NotaryInfo(identity, notaryConfig.validating)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterNodes.asMap().forEach { type, nodeNames ->
|
clusterNodes.asMap().forEach { type, nodeNames ->
|
||||||
val identity = IdentityGenerator.generateDistributedNotaryIdentity(
|
val identity = DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||||
dirs = nodeNames.map { baseDirectory(it) },
|
dirs = nodeNames.map { baseDirectory(it) },
|
||||||
notaryName = type.clusterName
|
notaryName = type.clusterName
|
||||||
)
|
)
|
||||||
notaryInfos += NotaryInfo(identity, type.validating)
|
notaryInfos += NotaryInfo(identity, type.validating)
|
||||||
}
|
}
|
||||||
|
|
||||||
networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos))
|
val localNetworkMap = LocalNetworkMap(notaryInfos)
|
||||||
|
|
||||||
return cordforms.map {
|
return cordforms.map {
|
||||||
val startedNode = startCordformNode(it)
|
val startedNode = startCordformNode(it, localNetworkMap)
|
||||||
if (it.webAddress != null) {
|
if (it.webAddress != null) {
|
||||||
// Start a webserver if an address for it was specified
|
// Start a webserver if an address for it was specified
|
||||||
startedNode.flatMap { startWebserver(it) }
|
startedNode.flatMap { startWebserver(it) }
|
||||||
@ -292,21 +302,21 @@ class DriverDSLImpl(
|
|||||||
}.transpose()
|
}.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startCordformNode(cordform: CordformNode): CordaFuture<NodeHandle> {
|
private fun startCordformNode(cordform: CordformNode, localNetworkMap: LocalNetworkMap): CordaFuture<NodeHandle> {
|
||||||
val name = CordaX500Name.parse(cordform.name)
|
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
|
// 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()
|
val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap()
|
||||||
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
|
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
|
||||||
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
|
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
|
||||||
val rpcUsers = cordform.rpcUsers
|
val rpcUsers = cordform.rpcUsers
|
||||||
val config = ConfigHelper.loadConfig(
|
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||||
baseDirectory = baseDirectory(name),
|
baseDirectory = baseDirectory(name),
|
||||||
allowMissingConfig = true,
|
allowMissingConfig = true,
|
||||||
configOverrides = cordform.config + rpcAddress + notary + mapOf(
|
configOverrides = cordform.config + rpcAddress + notary + mapOf(
|
||||||
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
|
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
return startNodeInternal(config, webAddress, null, "200m")
|
return startNodeInternal(config, webAddress, null, "200m", localNetworkMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
|
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
|
||||||
@ -340,31 +350,80 @@ class DriverDSLImpl(
|
|||||||
}
|
}
|
||||||
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
|
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
|
||||||
_shutdownManager = ShutdownManager(executorService)
|
_shutdownManager = ShutdownManager(executorService)
|
||||||
if (compatibilityZone == null) {
|
val notaryInfosFuture = 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
|
// If no CZ is specified then the driver does the generation of the network parameters and the copying of the
|
||||||
nodeInfoFilesCopier = NodeInfoFilesCopier().also {
|
// node info files.
|
||||||
shutdownManager.registerShutdown(it::close)
|
startNotaryIdentityGeneration().map { notaryInfos -> Pair(notaryInfos, LocalNetworkMap(notaryInfos)) }
|
||||||
|
} else {
|
||||||
|
// Otherwise it's the CZ's job to distribute thse via the HTTP network map, as that is what the nodes will be expecting.
|
||||||
|
val notaryInfosFuture = if (compatibilityZone.rootCert == null) {
|
||||||
|
// No root cert specified so we use the dev root cert to generate the notary identities.
|
||||||
|
startNotaryIdentityGeneration()
|
||||||
|
} else {
|
||||||
|
// With a root cert specified we delegate generation of the notary identities to the CZ.
|
||||||
|
startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.url)
|
||||||
|
}
|
||||||
|
notaryInfosFuture.map { notaryInfos ->
|
||||||
|
compatibilityZone.publishNotaries(notaryInfos)
|
||||||
|
Pair(notaryInfos, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
networkMapAvailability = notaryInfosFuture.map { it.second }
|
||||||
|
|
||||||
|
_notaries = notaryInfosFuture.map { (notaryInfos, localNetworkMap) ->
|
||||||
|
val listOfFutureNodeHandles = startNotaries(localNetworkMap)
|
||||||
|
notaryInfos.zip(listOfFutureNodeHandles) { (identity, validating), nodeHandlesFuture ->
|
||||||
|
NotaryHandle(identity, validating, nodeHandlesFuture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val notaryInfos = generateNotaryIdentities()
|
|
||||||
// The network parameters must be serialised before starting any of the nodes
|
|
||||||
if (compatibilityZone == null) networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos))
|
|
||||||
val nodeHandles = startNotaries()
|
|
||||||
_notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateNotaryIdentities(): List<NotaryInfo> {
|
private fun startNotaryIdentityGeneration(): CordaFuture<List<NotaryInfo>> {
|
||||||
return notarySpecs.map { spec ->
|
return executorService.fork {
|
||||||
val identity = if (spec.cluster == null) {
|
notarySpecs.map { spec ->
|
||||||
IdentityGenerator.generateNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert)
|
val identity = if (spec.cluster == null) {
|
||||||
} else {
|
DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name)
|
||||||
IdentityGenerator.generateDistributedNotaryIdentity(
|
} else {
|
||||||
dirs = generateNodeNames(spec).map { baseDirectory(it) },
|
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||||
notaryName = spec.name,
|
dirs = generateNodeNames(spec).map { baseDirectory(it) },
|
||||||
customRootCert = compatibilityZone?.rootCert
|
notaryName = spec.name
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
NotaryInfo(identity, spec.validating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<List<NotaryInfo>> {
|
||||||
|
// Start the registration process for all the notaries together then wait for their responses.
|
||||||
|
return notarySpecs.map { spec ->
|
||||||
|
require(spec.cluster == null) { "Registering distributed notaries not supported" }
|
||||||
|
startNotaryRegistration(spec, rootCert, compatibilityZoneURL)
|
||||||
|
}.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NotaryInfo> {
|
||||||
|
return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config ->
|
||||||
|
// Node registration only gives us the node CA cert, not the identity cert. That is only created on first
|
||||||
|
// startup or when the node is told to just generate its node info file. We do that here.
|
||||||
|
if (startNodesInProcess) {
|
||||||
|
executorService.fork {
|
||||||
|
val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo()
|
||||||
|
NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper
|
||||||
|
// This causes two node info files to be generated.
|
||||||
|
startOutOfProcessMiniNode(config, "--just-generate-node-info").map {
|
||||||
|
// Once done we have to read the signed node info file that's been generated
|
||||||
|
val nodeInfoFile = config.corda.baseDirectory.list { paths ->
|
||||||
|
paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
|
||||||
|
}
|
||||||
|
val nodeInfo = nodeInfoFile.readAll().deserialize<SignedNodeInfo>().verified()
|
||||||
|
NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NotaryInfo(identity, spec.validating)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,11 +431,11 @@ class DriverDSLImpl(
|
|||||||
return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") }
|
return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNotaries(): List<CordaFuture<List<NodeHandle>>> {
|
private fun startNotaries(localNetworkMap: LocalNetworkMap?): List<CordaFuture<List<NodeHandle>>> {
|
||||||
return notarySpecs.map {
|
return notarySpecs.map {
|
||||||
when {
|
when {
|
||||||
it.cluster == null -> startSingleNotary(it)
|
it.cluster == null -> startSingleNotary(it, localNetworkMap)
|
||||||
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it)
|
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it, localNetworkMap)
|
||||||
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
|
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -386,16 +445,17 @@ class DriverDSLImpl(
|
|||||||
// generating the configs for the nodes, probably making use of Any.toConfig()
|
// generating the configs for the nodes, probably making use of Any.toConfig()
|
||||||
private fun NotaryConfig.toConfigMap(): Map<String, Any> = mapOf("notary" to toConfig().root().unwrapped())
|
private fun NotaryConfig.toConfigMap(): Map<String, Any> = mapOf("notary" to toConfig().root().unwrapped())
|
||||||
|
|
||||||
private fun startSingleNotary(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
|
private fun startSingleNotary(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture<List<NodeHandle>> {
|
||||||
return startNode(
|
return startRegisteredNode(
|
||||||
providedName = spec.name,
|
spec.name,
|
||||||
rpcUsers = spec.rpcUsers,
|
localNetworkMap,
|
||||||
verifierType = spec.verifierType,
|
spec.rpcUsers,
|
||||||
|
spec.verifierType,
|
||||||
customOverrides = NotaryConfig(spec.validating).toConfigMap()
|
customOverrides = NotaryConfig(spec.validating).toConfigMap()
|
||||||
).map { listOf(it) }
|
).map { listOf(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
|
private fun startRaftNotaryCluster(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture<List<NodeHandle>> {
|
||||||
fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map<String, Any> {
|
fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map<String, Any> {
|
||||||
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
|
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
|
||||||
val config = NotaryConfig(
|
val config = NotaryConfig(
|
||||||
@ -408,20 +468,22 @@ class DriverDSLImpl(
|
|||||||
val clusterAddress = portAllocation.nextHostAndPort()
|
val clusterAddress = portAllocation.nextHostAndPort()
|
||||||
|
|
||||||
// Start the first node that will bootstrap the cluster
|
// Start the first node that will bootstrap the cluster
|
||||||
val firstNodeFuture = startNode(
|
val firstNodeFuture = startRegisteredNode(
|
||||||
providedName = nodeNames[0],
|
nodeNames[0],
|
||||||
rpcUsers = spec.rpcUsers,
|
localNetworkMap,
|
||||||
verifierType = spec.verifierType,
|
spec.rpcUsers,
|
||||||
|
spec.verifierType,
|
||||||
customOverrides = notaryConfig(clusterAddress)
|
customOverrides = notaryConfig(clusterAddress)
|
||||||
)
|
)
|
||||||
|
|
||||||
// All other nodes will join the cluster
|
// All other nodes will join the cluster
|
||||||
val restNodeFutures = nodeNames.drop(1).map {
|
val restNodeFutures = nodeNames.drop(1).map {
|
||||||
val nodeAddress = portAllocation.nextHostAndPort()
|
val nodeAddress = portAllocation.nextHostAndPort()
|
||||||
startNode(
|
startRegisteredNode(
|
||||||
providedName = it,
|
it,
|
||||||
rpcUsers = spec.rpcUsers,
|
localNetworkMap,
|
||||||
verifierType = spec.verifierType,
|
spec.rpcUsers,
|
||||||
|
spec.verifierType,
|
||||||
customOverrides = notaryConfig(nodeAddress, clusterAddress)
|
customOverrides = notaryConfig(nodeAddress, clusterAddress)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -436,8 +498,6 @@ class DriverDSLImpl(
|
|||||||
return driverDirectory / nodeDirectoryName
|
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 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.
|
* @param networkMapCacheChangeObservable an observable returning the updates to the node network map.
|
||||||
@ -447,7 +507,7 @@ class DriverDSLImpl(
|
|||||||
private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable<NetworkMapCache.MapChange>):
|
private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable<NetworkMapCache.MapChange>):
|
||||||
ConnectableObservable<Int> {
|
ConnectableObservable<Int> {
|
||||||
val count = AtomicInteger(initial)
|
val count = AtomicInteger(initial)
|
||||||
return networkMapCacheChangeObservable.map { it ->
|
return networkMapCacheChangeObservable.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkMapCache.MapChange.Added -> count.incrementAndGet()
|
is NetworkMapCache.MapChange.Added -> count.incrementAndGet()
|
||||||
is NetworkMapCache.MapChange.Removed -> count.decrementAndGet()
|
is NetworkMapCache.MapChange.Removed -> count.decrementAndGet()
|
||||||
@ -482,31 +542,44 @@ class DriverDSLImpl(
|
|||||||
return future
|
return future
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOutOfProcessNodeRegistration(config: Config, configuration: NodeConfiguration): CordaFuture<Unit> {
|
/**
|
||||||
|
* Start the node with the given flag which is expected to start the node for some function, which once complete will
|
||||||
|
* terminate the node.
|
||||||
|
*/
|
||||||
|
private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: String): CordaFuture<Unit> {
|
||||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||||
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
|
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
|
||||||
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort,
|
val process = startOutOfProcessNode(
|
||||||
systemProperties, cordappPackages, "200m", initialRegistration = true)
|
config,
|
||||||
|
quasarJarPath,
|
||||||
|
debugPort,
|
||||||
|
jolokiaJarPath,
|
||||||
|
monitorPort,
|
||||||
|
systemProperties,
|
||||||
|
cordappPackages,
|
||||||
|
"200m",
|
||||||
|
extraCmdLineFlag
|
||||||
|
)
|
||||||
|
|
||||||
return poll(executorService, "node registration (${configuration.myLegalName})") {
|
return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") {
|
||||||
if (process.isAlive) null else Unit
|
if (process.isAlive) null else Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNodeInternal(config: Config,
|
private fun startNodeInternal(config: NodeConfig,
|
||||||
webAddress: NetworkHostAndPort,
|
webAddress: NetworkHostAndPort,
|
||||||
startInProcess: Boolean?,
|
startInProcess: Boolean?,
|
||||||
maximumHeapSize: String): CordaFuture<NodeHandle> {
|
maximumHeapSize: String,
|
||||||
val configuration = config.parseAsNodeConfiguration()
|
localNetworkMap: LocalNetworkMap?): CordaFuture<NodeHandle> {
|
||||||
val baseDirectory = configuration.baseDirectory.createDirectories()
|
val baseDirectory = config.corda.baseDirectory.createDirectories()
|
||||||
nodeInfoFilesCopier?.addConfig(baseDirectory)
|
localNetworkMap?.networkParametersCopier?.install(baseDirectory)
|
||||||
networkParameters?.install(baseDirectory)
|
localNetworkMap?.nodeInfosCopier?.addConfig(baseDirectory)
|
||||||
val onNodeExit: () -> Unit = {
|
val onNodeExit: () -> Unit = {
|
||||||
nodeInfoFilesCopier?.removeConfig(baseDirectory)
|
localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory)
|
||||||
countObservables.remove(configuration.myLegalName)
|
countObservables.remove(config.corda.myLegalName)
|
||||||
}
|
}
|
||||||
if (startInProcess ?: startNodesInProcess) {
|
if (startInProcess ?: startNodesInProcess) {
|
||||||
val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages)
|
val nodeAndThreadFuture = startInProcessNode(executorService, config, cordappPackages)
|
||||||
shutdownManager.registerShutdown(
|
shutdownManager.registerShutdown(
|
||||||
nodeAndThreadFuture.map { (node, thread) ->
|
nodeAndThreadFuture.map { (node, thread) ->
|
||||||
{
|
{
|
||||||
@ -516,16 +589,16 @@ class DriverDSLImpl(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return nodeAndThreadFuture.flatMap { (node, thread) ->
|
return nodeAndThreadFuture.flatMap { (node, thread) ->
|
||||||
establishRpc(configuration, openFuture()).flatMap { rpc ->
|
establishRpc(config, openFuture()).flatMap { rpc ->
|
||||||
allNodesConnected(rpc).map {
|
allNodesConnected(rpc).map {
|
||||||
NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit)
|
NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, node, thread, onNodeExit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||||
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.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, initialRegistration = false)
|
val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, null)
|
||||||
if (waitForNodesToFinish) {
|
if (waitForNodesToFinish) {
|
||||||
state.locked {
|
state.locked {
|
||||||
processes += process
|
processes += process
|
||||||
@ -533,22 +606,21 @@ class DriverDSLImpl(
|
|||||||
} else {
|
} else {
|
||||||
shutdownManager.registerProcessShutdown(process)
|
shutdownManager.registerProcessShutdown(process)
|
||||||
}
|
}
|
||||||
val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process)
|
val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process)
|
||||||
return p2pReadyFuture.flatMap {
|
return p2pReadyFuture.flatMap {
|
||||||
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${configuration.myLegalName})") {
|
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") {
|
||||||
if (process.isAlive) null else process
|
if (process.isAlive) null else process
|
||||||
}
|
}
|
||||||
establishRpc(configuration, processDeathFuture).flatMap { rpc ->
|
establishRpc(config, processDeathFuture).flatMap { rpc ->
|
||||||
// Check for all nodes to have all other nodes in background in case RPC is failing over:
|
// Check for all nodes to have all other nodes in background in case RPC is failing over:
|
||||||
val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it }
|
val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it }
|
||||||
firstOf(processDeathFuture, networkMapFuture) {
|
firstOf(processDeathFuture, networkMapFuture) {
|
||||||
if (it == processDeathFuture) {
|
if (it == processDeathFuture) {
|
||||||
throw ListenProcessDeathException(configuration.p2pAddress, process)
|
throw ListenProcessDeathException(config.corda.p2pAddress, process)
|
||||||
}
|
}
|
||||||
processDeathFuture.cancel(false)
|
processDeathFuture.cancel(false)
|
||||||
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
|
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
|
||||||
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process,
|
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, debugPort, process, onNodeExit)
|
||||||
onNodeExit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -561,6 +633,25 @@ class DriverDSLImpl(
|
|||||||
return pollFuture
|
return pollFuture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local version of the network map, which is a bunch of classes that copy the relevant files to the node directories.
|
||||||
|
*/
|
||||||
|
private inner class LocalNetworkMap(notaryInfos: List<NotaryInfo>) {
|
||||||
|
val networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaryInfos))
|
||||||
|
// 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.
|
||||||
|
val nodeInfosCopier = NodeInfoFilesCopier().also { shutdownManager.registerShutdown(it::close) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration].
|
||||||
|
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
|
||||||
|
*/
|
||||||
|
private class NodeConfig(val typesafe: Config) {
|
||||||
|
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal val log = contextLogger()
|
internal val log = contextLogger()
|
||||||
|
|
||||||
@ -588,28 +679,26 @@ class DriverDSLImpl(
|
|||||||
|
|
||||||
private fun startInProcessNode(
|
private fun startInProcessNode(
|
||||||
executorService: ScheduledExecutorService,
|
executorService: ScheduledExecutorService,
|
||||||
nodeConf: NodeConfiguration,
|
config: NodeConfig,
|
||||||
config: Config,
|
|
||||||
cordappPackages: List<String>
|
cordappPackages: List<String>
|
||||||
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
|
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
|
||||||
return executorService.fork {
|
return executorService.fork {
|
||||||
log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}")
|
log.info("Starting in-process Node ${config.corda.myLegalName.organisation}")
|
||||||
// Write node.conf
|
// Write node.conf
|
||||||
writeConfig(nodeConf.baseDirectory, "node.conf", config)
|
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
|
||||||
// TODO pass the version in?
|
// TODO pass the version in?
|
||||||
val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start()
|
val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start()
|
||||||
val nodeThread = thread(name = nodeConf.myLegalName.organisation) {
|
val nodeThread = thread(name = config.corda.myLegalName.organisation) {
|
||||||
node.internals.run()
|
node.internals.run()
|
||||||
}
|
}
|
||||||
node to nodeThread
|
node to nodeThread
|
||||||
}.flatMap { nodeAndThread ->
|
}.flatMap { nodeAndThread ->
|
||||||
addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread }
|
addressMustBeBoundFuture(executorService, config.corda.p2pAddress).map { nodeAndThread }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOutOfProcessNode(
|
private fun startOutOfProcessNode(
|
||||||
nodeConf: NodeConfiguration,
|
config: NodeConfig,
|
||||||
config: Config,
|
|
||||||
quasarJarPath: String,
|
quasarJarPath: String,
|
||||||
debugPort: Int?,
|
debugPort: Int?,
|
||||||
jolokiaJarPath: String,
|
jolokiaJarPath: String,
|
||||||
@ -617,15 +706,17 @@ class DriverDSLImpl(
|
|||||||
overriddenSystemProperties: Map<String, String>,
|
overriddenSystemProperties: Map<String, String>,
|
||||||
cordappPackages: List<String>,
|
cordappPackages: List<String>,
|
||||||
maximumHeapSize: String,
|
maximumHeapSize: String,
|
||||||
initialRegistration: Boolean
|
extraCmdLineFlag: String?
|
||||||
): Process {
|
): Process {
|
||||||
log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled") + ", jolokia monitoring port is " + (monitorPort ?: "not enabled"))
|
log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " +
|
||||||
|
"debug port is " + (debugPort ?: "not enabled") + ", " +
|
||||||
|
"jolokia monitoring port is " + (monitorPort ?: "not enabled"))
|
||||||
// Write node.conf
|
// Write node.conf
|
||||||
writeConfig(nodeConf.baseDirectory, "node.conf", config)
|
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
|
||||||
|
|
||||||
val systemProperties = mutableMapOf(
|
val systemProperties = mutableMapOf(
|
||||||
"name" to nodeConf.myLegalName,
|
"name" to config.corda.myLegalName,
|
||||||
"visualvm.display.name" to "corda-${nodeConf.myLegalName}",
|
"visualvm.display.name" to "corda-${config.corda.myLegalName}",
|
||||||
"java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process
|
"java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process
|
||||||
"log4j2.debug" to if(debugPort != null) "true" else "false"
|
"log4j2.debug" to if(debugPort != null) "true" else "false"
|
||||||
)
|
)
|
||||||
@ -643,18 +734,18 @@ class DriverDSLImpl(
|
|||||||
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
|
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
|
||||||
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
|
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
|
||||||
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
|
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
|
||||||
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
|
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;)"
|
||||||
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
|
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
|
||||||
"-javaagent:$quasarJarPath=$excludePattern"
|
"-javaagent:$quasarJarPath=$excludePattern"
|
||||||
val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" }
|
val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" }
|
||||||
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
|
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
|
||||||
|
|
||||||
val arguments = mutableListOf(
|
val arguments = mutableListOf(
|
||||||
"--base-directory=${nodeConf.baseDirectory}",
|
"--base-directory=${config.corda.baseDirectory}",
|
||||||
"--logging-level=$loggingLevel",
|
"--logging-level=$loggingLevel",
|
||||||
"--no-local-shell").also {
|
"--no-local-shell").also {
|
||||||
if (initialRegistration) {
|
if (extraCmdLineFlag != null) {
|
||||||
it += "--initial-registration"
|
it += extraCmdLineFlag
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
|
|
||||||
@ -663,8 +754,8 @@ class DriverDSLImpl(
|
|||||||
arguments = arguments,
|
arguments = arguments,
|
||||||
jdwpPort = debugPort,
|
jdwpPort = debugPort,
|
||||||
extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent),
|
extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent),
|
||||||
errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
|
errorLogPath = config.corda.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
|
||||||
workingDirectory = nodeConf.baseDirectory,
|
workingDirectory = config.corda.baseDirectory,
|
||||||
maximumHeapSize = maximumHeapSize
|
maximumHeapSize = maximumHeapSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -831,11 +922,16 @@ fun <DI : DriverDSL, D : InternalDriverDSL, A> genericDriver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Internal API to enable testing of the network map service and node registration process using the internal driver.
|
||||||
* @property url The base CZ URL for registration and network map updates
|
* @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
|
* @property publishNotaries Hook for a network map server to capture the generated [NotaryInfo] objects needed for
|
||||||
* to be rooted at this cert.
|
* creating the network parameters. This is needed as the network map server is expected to distribute it. The callback
|
||||||
|
* will occur on a different thread to the driver-calling thread.
|
||||||
|
* @property rootCert If specified then the nodes will register themselves with the doorman service using [url] and expect
|
||||||
|
* the registration response to be rooted at this cert. If not specified then no registration is performed and the dev
|
||||||
|
* root cert is used as normal.
|
||||||
*/
|
*/
|
||||||
data class CompatibilityZoneParams(val url: URL, val rootCert: X509Certificate? = null)
|
data class CompatibilityZoneParams(val url: URL, val publishNotaries: (List<NotaryInfo>) -> Unit, val rootCert: X509Certificate? = null)
|
||||||
|
|
||||||
fun <A> internalDriver(
|
fun <A> internalDriver(
|
||||||
isDebug: Boolean = DriverParameters().isDebug,
|
isDebug: Boolean = DriverParameters().isDebug,
|
||||||
@ -875,8 +971,7 @@ fun <A> internalDriver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getTimestampAsDirectoryName(): String {
|
fun getTimestampAsDirectoryName(): String {
|
||||||
// Add a random number in case 2 tests are started in the same instant.
|
return DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(UTC).format(Instant.now())
|
||||||
return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) + random63BitValue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun writeConfig(path: Path, filename: String, config: Config) {
|
fun writeConfig(path: Path, filename: String, config: Config) {
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
package net.corda.testing.node.internal.network
|
package net.corda.testing.node.internal.network
|
||||||
|
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.cert
|
import net.corda.core.internal.cert
|
||||||
import net.corda.core.internal.toX509CertHolder
|
import net.corda.core.internal.toX509CertHolder
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
||||||
import net.corda.nodeapi.internal.*
|
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
|
||||||
import net.corda.testing.ROOT_CA
|
import net.corda.testing.ROOT_CA
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.eclipse.jetty.server.Server
|
import org.eclipse.jetty.server.Server
|
||||||
import org.eclipse.jetty.server.ServerConnector
|
import org.eclipse.jetty.server.ServerConnector
|
||||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||||
@ -36,26 +36,36 @@ import javax.ws.rs.core.Response.ok
|
|||||||
|
|
||||||
class NetworkMapServer(cacheTimeout: Duration,
|
class NetworkMapServer(cacheTimeout: Duration,
|
||||||
hostAndPort: NetworkHostAndPort,
|
hostAndPort: NetworkHostAndPort,
|
||||||
root_ca: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing.
|
rootCa: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing.
|
||||||
|
private val myHostNameValue: String = "test.host.name",
|
||||||
vararg additionalServices: Any) : Closeable {
|
vararg additionalServices: Any) : Closeable {
|
||||||
companion object {
|
companion object {
|
||||||
val stubNetworkParameter = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
|
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
|
||||||
private val serializedParameters = stubNetworkParameter.serialize()
|
|
||||||
|
|
||||||
private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair {
|
private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair {
|
||||||
val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
val networkMapCert = X509Utilities.createCertificate(
|
val networkMapCert = X509Utilities.createCertificate(
|
||||||
CertificateType.INTERMEDIATE_CA,
|
CertificateType.NETWORK_MAP,
|
||||||
rootCAKeyAndCert.certificate,
|
rootCAKeyAndCert.certificate,
|
||||||
rootCAKeyAndCert.keyPair,
|
rootCAKeyAndCert.keyPair,
|
||||||
X500Name("CN=Corda Network Map,L=London"),
|
CordaX500Name("Corda Network Map", "R3 Ltd", "London","GB"),
|
||||||
networkMapKey.public).cert
|
networkMapKey.public).cert
|
||||||
|
// Check that the certificate validates. Nodes will perform this check upon receiving a network map,
|
||||||
|
// it's better to fail here than there.
|
||||||
|
X509Utilities.validateCertificateChain(rootCAKeyAndCert.certificate.cert, networkMapCert)
|
||||||
return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey)
|
return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val server: Server
|
private val server: Server
|
||||||
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(root_ca))
|
var networkParameters: NetworkParameters = stubNetworkParameters
|
||||||
|
set(networkParameters) {
|
||||||
|
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
|
||||||
|
field = networkParameters
|
||||||
|
}
|
||||||
|
private val serializedParameters get() = networkParameters.serialize()
|
||||||
|
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(rootCa))
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
||||||
@ -95,13 +105,13 @@ class NetworkMapServer(cacheTimeout: Duration,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Path("network-map")
|
@Path("network-map")
|
||||||
class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) {
|
inner class InMemoryNetworkMapService(private val cacheTimeout: Duration,
|
||||||
|
private val networkMapKeyAndCert: CertificateAndKeyPair) {
|
||||||
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
|
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
|
||||||
private val parametersHash = serializedParameters.hash
|
private val parametersHash by lazy { serializedParameters.hash }
|
||||||
private val signedParameters = SignedData(
|
private val signedParameters by lazy { SignedData(
|
||||||
serializedParameters,
|
serializedParameters,
|
||||||
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))
|
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))) }
|
||||||
)
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("publish")
|
@Path("publish")
|
||||||
@ -151,7 +161,7 @@ class NetworkMapServer(cacheTimeout: Duration,
|
|||||||
@GET
|
@GET
|
||||||
@Path("my-hostname")
|
@Path("my-hostname")
|
||||||
fun getHostName(): Response {
|
fun getHostName(): Response {
|
||||||
return Response.ok("test.host.name").build()
|
return Response.ok(myHostNameValue).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,27 +5,24 @@ package net.corda.testing
|
|||||||
|
|
||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.cert
|
import net.corda.core.internal.cert
|
||||||
import net.corda.core.internal.unspecifiedCountry
|
import net.corda.core.internal.unspecifiedCountry
|
||||||
import net.corda.core.internal.x500Name
|
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
import net.corda.nodeapi.internal.createDevNodeCa
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
|
||||||
import org.bouncycastle.asn1.x509.NameConstraints
|
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.nio.file.Files
|
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@ -78,29 +75,11 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<NetworkHostAnd
|
|||||||
return (0 until numberToAlloc).map { NetworkHostAndPort(hostName, freePort + it) }
|
return (0 until numberToAlloc).map { NetworkHostAndPort(hostName, freePort + it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration = object : SSLConfiguration {
|
|
||||||
override val certificatesDirectory = Files.createTempDirectory("certs")
|
|
||||||
override val keyStorePassword: String get() = "cordacadevpass"
|
|
||||||
override val trustStorePassword: String get() = "trustpass"
|
|
||||||
|
|
||||||
init {
|
|
||||||
configureDevKeyAndTrustStores(legalName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTestPartyAndCertificate(party: Party): PartyAndCertificate {
|
fun getTestPartyAndCertificate(party: Party): PartyAndCertificate {
|
||||||
val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT
|
val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT
|
||||||
val intermediate: CertificateAndKeyPair = DEV_CA
|
val intermediate: CertificateAndKeyPair = DEV_CA
|
||||||
|
|
||||||
|
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediate, party.name)
|
||||||
val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val nodeCaCert = X509Utilities.createCertificate(
|
|
||||||
CertificateType.NODE_CA,
|
|
||||||
intermediate.certificate,
|
|
||||||
intermediate.keyPair,
|
|
||||||
party.name,
|
|
||||||
nodeCaKeyPair.public,
|
|
||||||
nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf()))
|
|
||||||
|
|
||||||
val identityCert = X509Utilities.createCertificate(
|
val identityCert = X509Utilities.createCertificate(
|
||||||
CertificateType.LEGAL_IDENTITY,
|
CertificateType.LEGAL_IDENTITY,
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
package net.corda.testing.internal
|
package net.corda.testing.internal
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||||
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
|
import net.corda.nodeapi.internal.createDevNodeCa
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED
|
import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.internal.stubbing.answers.ThrowsException
|
import org.mockito.internal.stubbing.answers.ThrowsException
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
|
import java.nio.file.Files
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@ -42,3 +51,56 @@ fun <T> rigorousMock(clazz: Class<T>): T = Mockito.mock(clazz) {
|
|||||||
it.callRealMethod()
|
it.callRealMethod()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration {
|
||||||
|
return object : SSLConfiguration {
|
||||||
|
override val certificatesDirectory = Files.createTempDirectory("certs")
|
||||||
|
override val keyStorePassword: String get() = "cordacadevpass"
|
||||||
|
override val trustStorePassword: String get() = "trustpass"
|
||||||
|
|
||||||
|
init {
|
||||||
|
configureDevKeyAndTrustStores(legalName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val defaultRootCaName = CordaX500Name("Corda Root CA", "R3 Ltd", "London", "GB")
|
||||||
|
private val defaultIntermediateCaName = CordaX500Name("Corda Intermediate CA", "R3 Ltd", "London", "GB")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a pair of [CertificateAndKeyPair]s, the first being the root CA and the second the intermediate CA.
|
||||||
|
* @param rootCaName The subject name for the root CA cert.
|
||||||
|
* @param intermediateCaName The subject name for the intermediate CA cert.
|
||||||
|
*/
|
||||||
|
fun createDevIntermediateCaCertPath(
|
||||||
|
rootCaName: CordaX500Name = defaultRootCaName,
|
||||||
|
intermediateCaName: CordaX500Name = defaultIntermediateCaName
|
||||||
|
): Pair<CertificateAndKeyPair, CertificateAndKeyPair> {
|
||||||
|
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val rootCert = X509Utilities.createSelfSignedCACertificate(rootCaName, rootKeyPair)
|
||||||
|
|
||||||
|
val intermediateCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val intermediateCaCert = X509Utilities.createCertificate(
|
||||||
|
CertificateType.INTERMEDIATE_CA,
|
||||||
|
rootCert,
|
||||||
|
rootKeyPair,
|
||||||
|
intermediateCaName,
|
||||||
|
intermediateCaKeyPair.public)
|
||||||
|
|
||||||
|
return Pair(CertificateAndKeyPair(rootCert, rootKeyPair), CertificateAndKeyPair(intermediateCaCert, intermediateCaKeyPair))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a triple of [CertificateAndKeyPair]s, the first being the root CA, the second the intermediate CA and the third
|
||||||
|
* the node CA.
|
||||||
|
* @param legalName The subject name for the node CA cert.
|
||||||
|
*/
|
||||||
|
fun createDevNodeCaCertPath(
|
||||||
|
legalName: CordaX500Name,
|
||||||
|
rootCaName: CordaX500Name = defaultRootCaName,
|
||||||
|
intermediateCaName: CordaX500Name = defaultIntermediateCaName
|
||||||
|
): Triple<CertificateAndKeyPair, CertificateAndKeyPair, CertificateAndKeyPair> {
|
||||||
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath(rootCaName, intermediateCaName)
|
||||||
|
val nodeCa = createDevNodeCa(intermediateCa, legalName)
|
||||||
|
return Triple(rootCa, intermediateCa, nodeCa)
|
||||||
|
}
|
||||||
|
@ -6,7 +6,10 @@ configurations {
|
|||||||
runtimeArtifacts
|
runtimeArtifacts
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Fix SLF4J warnings that occur when running the bootstrapper
|
dependencies {
|
||||||
|
compile "org.slf4j:slf4j-nop:$slf4j_version"
|
||||||
|
}
|
||||||
|
|
||||||
task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) {
|
task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) {
|
||||||
applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper'
|
applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper'
|
||||||
archiveName "network-bootstrapper.jar"
|
archiveName "network-bootstrapper.jar"
|
||||||
@ -16,6 +19,9 @@ task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').comp
|
|||||||
minJavaVersion = '1.8.0'
|
minJavaVersion = '1.8.0'
|
||||||
jvmArgs = ['-XX:+UseG1GC']
|
jvmArgs = ['-XX:+UseG1GC']
|
||||||
}
|
}
|
||||||
|
from(project(':node:capsule').tasks['buildCordaJAR']) {
|
||||||
|
rename 'corda-(.*)', 'corda.jar'
|
||||||
|
}
|
||||||
applicationSource = files(
|
applicationSource = files(
|
||||||
project(':node-api').configurations.runtime,
|
project(':node-api').configurations.runtime,
|
||||||
project(':node-api').jar
|
project(':node-api').jar
|
||||||
|
@ -13,7 +13,7 @@ import net.corda.demobench.pty.R3Pty
|
|||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.nodeapi.internal.network.NotaryInfo
|
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||||
import net.corda.nodeapi.internal.IdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.management.ManagementFactory
|
import java.lang.management.ManagementFactory
|
||||||
@ -153,7 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
|
|
||||||
// Generate notary identity and save it into node's directory. This identity will be used in network parameters.
|
// Generate notary identity and save it into node's directory. This identity will be used in network parameters.
|
||||||
private fun getNotaryIdentity(config: NodeConfigWrapper): Party {
|
private fun getNotaryIdentity(config: NodeConfigWrapper): Party {
|
||||||
return IdentityGenerator.generateNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName)
|
return DevIdentityGenerator.installKeyStoreWithNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
|
Loading…
Reference in New Issue
Block a user