mirror of
https://github.com/corda/corda.git
synced 2025-06-16 14:18:20 +00:00
Merge branch 'tudor_merge_os_master' into feature/ENT-2222/constraints_propagation_private
# Conflicts: # node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt # testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt
This commit is contained in:
@ -13,8 +13,8 @@ import net.corda.node.internal.DataSourceFactory
|
||||
import net.corda.node.internal.NodeWithInfo
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.config.PasswordEncryption
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.apache.shiro.authc.credential.DefaultPasswordService
|
||||
import org.junit.After
|
||||
@ -33,7 +33,6 @@ import kotlin.test.assertFailsWith
|
||||
*/
|
||||
@RunWith(Parameterized::class)
|
||||
class AuthDBTests : NodeBasedTest() {
|
||||
|
||||
private lateinit var node: NodeWithInfo
|
||||
private lateinit var client: CordaRPCClient
|
||||
private lateinit var db: UsersDB
|
||||
|
@ -12,10 +12,15 @@ import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.nodeapi.exceptions.InternalNodeException
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.startNode
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.io.*
|
||||
@ -24,12 +29,22 @@ import kotlin.test.assertEquals
|
||||
class BootTests {
|
||||
@Test
|
||||
fun `java deserialization is disabled`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>()))
|
||||
val future = CordaRPCClient(startNode(rpcUsers = listOf(user)).getOrThrow().rpcAddress).
|
||||
start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue
|
||||
assertThatThrownBy { future.getOrThrow() }
|
||||
.isInstanceOf(CordaRuntimeException::class.java)
|
||||
val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>()))
|
||||
val devParams = NodeParameters(providedName = BOB_NAME, rpcUsers = listOf(user))
|
||||
val params = NodeParameters(rpcUsers = listOf(user))
|
||||
|
||||
fun NodeHandle.attemptJavaDeserialization() {
|
||||
CordaRPCClient(rpcAddress).use(user.username, user.password) { connection ->
|
||||
connection.proxy
|
||||
rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
driver {
|
||||
val devModeNode = startNode(devParams).getOrThrow()
|
||||
val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()
|
||||
|
||||
assertThatThrownBy { devModeNode.attemptJavaDeserialization() }.isInstanceOf(CordaRuntimeException::class.java)
|
||||
assertThatThrownBy { node.attemptJavaDeserialization() }.isInstanceOf(InternalNodeException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,15 +4,14 @@ import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
class NodeKeystoreCheckTest {
|
||||
@ -21,22 +20,20 @@ class NodeKeystoreCheckTest {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
assertThatThrownBy {
|
||||
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||
}.hasMessageContaining("Identity certificate not found")
|
||||
}.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `node should throw exception if cert path doesn't chain to the trust root`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
// Create keystores
|
||||
// Create keystores.
|
||||
val keystorePassword = "password"
|
||||
val config = object : SSLConfiguration {
|
||||
override val keyStorePassword: String = keystorePassword
|
||||
override val trustStorePassword: String = keystorePassword
|
||||
override val certificatesDirectory: Path = baseDirectory(ALICE_NAME) / "certificates"
|
||||
override val crlCheckSoftFail: Boolean = true
|
||||
}
|
||||
config.configureDevKeyAndTrustStores(ALICE_NAME)
|
||||
val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates"
|
||||
val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, keystorePassword)
|
||||
val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = keystorePassword, trustStorePassword = keystorePassword)
|
||||
|
||||
p2pSslConfig.configureDevKeyAndTrustStores(ALICE_NAME, signingCertStore, certificatesDirectory)
|
||||
|
||||
// This should pass with correct keystore.
|
||||
val node = startNode(
|
||||
@ -48,8 +45,8 @@ class NodeKeystoreCheckTest {
|
||||
node.stop()
|
||||
|
||||
// Fiddle with node keystore.
|
||||
config.loadNodeKeyStore().update {
|
||||
// Self signed root
|
||||
signingCertStore.get().update {
|
||||
// Self signed root.
|
||||
val badRootKeyPair = Crypto.generateKeyPair()
|
||||
val badRoot = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Bad Root,L=Lodnon,C=GB"), badRootKeyPair)
|
||||
val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
|
@ -19,6 +19,7 @@ import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.MAX_MESSAGE_SIZE
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
|
||||
import org.apache.activemq.artemis.api.core.RoutingType
|
||||
@ -27,7 +28,6 @@ import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.security.KeyStore
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -168,44 +168,54 @@ class AMQPBridgeTest {
|
||||
}
|
||||
|
||||
private fun createArtemis(sourceQueueName: String?): Triple<ArtemisMessagingServer, ArtemisMessagingClient, BridgeManager> {
|
||||
val baseDir = temporaryFolder.root.toPath() / "artemis"
|
||||
val certificatesDirectory = baseDir / "certificates"
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
|
||||
doReturn(baseDir).whenever(it).baseDirectory
|
||||
doReturn(ALICE_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(artemisAddress).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisAddress.copy(host = "0.0.0.0"), MAX_MESSAGE_SIZE)
|
||||
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
|
||||
val artemisClient = ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE)
|
||||
artemisServer.start()
|
||||
artemisClient.start()
|
||||
val bridgeManager = AMQPBridgeManager(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
|
||||
val bridgeManager = AMQPBridgeManager(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE)
|
||||
bridgeManager.start()
|
||||
val artemis = artemisClient.started!!
|
||||
if (sourceQueueName != null) {
|
||||
// Local queue for outgoing messages
|
||||
artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true)
|
||||
bridgeManager.deployBridge(sourceQueueName, amqpAddress, setOf(BOB.name))
|
||||
bridgeManager.deployBridge(sourceQueueName, listOf(amqpAddress), setOf(BOB.name))
|
||||
}
|
||||
return Triple(artemisServer, artemisClient, bridgeManager)
|
||||
}
|
||||
|
||||
private fun createAMQPServer(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer {
|
||||
val baseDir = temporaryFolder.root.toPath() / "server"
|
||||
val certificatesDirectory = baseDir / "certificates"
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
|
||||
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
}
|
||||
serverConfig.configureWithDevSSLCertificate()
|
||||
|
||||
val keyStore = serverConfig.p2pSslOptions.keyStore.get()
|
||||
val amqpConfig = object : AMQPConfiguration {
|
||||
override val keyStore: KeyStore = serverConfig.loadSslKeyStore().internal
|
||||
override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray()
|
||||
override val trustStore: KeyStore = serverConfig.loadTrustStore().internal
|
||||
override val keyStore = keyStore
|
||||
override val trustStore = serverConfig.p2pSslOptions.trustStore.get()
|
||||
override val trace: Boolean = true
|
||||
override val maxMessageSize: Int = maxMessageSize
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.CertificateStoreSupplier
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
|
||||
@ -27,6 +28,7 @@ import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.DEV_INTERMEDIATE_CA
|
||||
import net.corda.testing.internal.DEV_ROOT_CA
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter
|
||||
@ -50,7 +52,6 @@ import java.io.Closeable
|
||||
import java.math.BigInteger
|
||||
import java.net.InetSocketAddress
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.Security
|
||||
import java.security.cert.X509CRL
|
||||
@ -61,6 +62,7 @@ import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.Response
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CertificateRevocationListNodeTests {
|
||||
@Rule
|
||||
@ -80,6 +82,32 @@ class CertificateRevocationListNodeTests {
|
||||
|
||||
private abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||
|
||||
companion object {
|
||||
fun createRevocationList(clrServer: CrlServer, signatureAlgorithm: String, caCertificate: X509Certificate,
|
||||
caPrivateKey: PrivateKey,
|
||||
endpoint: String,
|
||||
indirect: Boolean,
|
||||
vararg serialNumbers: BigInteger): X509CRL {
|
||||
println("Generating CRL for $endpoint")
|
||||
val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis()))
|
||||
val extensionUtils = JcaX509ExtensionUtils()
|
||||
builder.addExtension(Extension.authorityKeyIdentifier,
|
||||
false, extensionUtils.createAuthorityKeyIdentifier(caCertificate))
|
||||
val issuingDistPointName = GeneralName(
|
||||
GeneralName.uniformResourceIdentifier,
|
||||
"http://${clrServer.hostAndPort.host}:${clrServer.hostAndPort.port}/crl/$endpoint")
|
||||
// This is required and needs to match the certificate settings with respect to being indirect
|
||||
val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false)
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint)
|
||||
builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis()))
|
||||
serialNumbers.forEach {
|
||||
builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold)
|
||||
}
|
||||
val signer = JcaContentSignerBuilder(signatureAlgorithm).setProvider(Crypto.findProvider("BC")).build(caPrivateKey)
|
||||
return JcaX509CRLConverter().setProvider(Crypto.findProvider("BC")).getCRL(builder.build(signer))
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
@ -330,22 +358,25 @@ class CertificateRevocationListNodeTests {
|
||||
nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl",
|
||||
tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl",
|
||||
maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<AMQPClient, X509Certificate> {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "client"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
|
||||
}
|
||||
clientConfig.configureWithDevSSLCertificate()
|
||||
val nodeCert = clientConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint)
|
||||
val clientTruststore = clientConfig.loadTrustStore().internal
|
||||
val clientKeystore = clientConfig.loadSslKeyStore().internal
|
||||
val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint)
|
||||
val keyStore = clientConfig.p2pSslOptions.keyStore.get()
|
||||
|
||||
val amqpConfig = object : AMQPConfiguration {
|
||||
override val keyStore: KeyStore = clientKeystore
|
||||
override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray()
|
||||
override val trustStore: KeyStore = clientTruststore
|
||||
override val keyStore = keyStore
|
||||
override val trustStore = clientConfig.p2pSslOptions.trustStore.get()
|
||||
override val crlCheckSoftFail: Boolean = crlCheckSoftFail
|
||||
override val maxMessageSize: Int = maxMessageSize
|
||||
}
|
||||
@ -360,21 +391,24 @@ class CertificateRevocationListNodeTests {
|
||||
nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl",
|
||||
tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl",
|
||||
maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<AMQPServer, X509Certificate> {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "server"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(name).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
|
||||
}
|
||||
serverConfig.configureWithDevSSLCertificate()
|
||||
val nodeCert = serverConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint)
|
||||
val serverTruststore = serverConfig.loadTrustStore().internal
|
||||
val serverKeystore = serverConfig.loadSslKeyStore().internal
|
||||
val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint)
|
||||
val keyStore = serverConfig.p2pSslOptions.keyStore.get()
|
||||
val amqpConfig = object : AMQPConfiguration {
|
||||
override val keyStore: KeyStore = serverKeystore
|
||||
override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray()
|
||||
override val trustStore: KeyStore = serverTruststore
|
||||
override val keyStore = keyStore
|
||||
override val trustStore = serverConfig.p2pSslOptions.trustStore.get()
|
||||
override val crlCheckSoftFail: Boolean = crlCheckSoftFail
|
||||
override val maxMessageSize: Int = maxMessageSize
|
||||
}
|
||||
@ -384,22 +418,28 @@ class CertificateRevocationListNodeTests {
|
||||
amqpConfig), nodeCert)
|
||||
}
|
||||
|
||||
private fun SSLConfiguration.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate {
|
||||
val nodeKeyStore = loadNodeKeyStore()
|
||||
val (nodeCert, nodeKeys) = nodeKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
private fun Pair<CertificateStoreSupplier, MutualSslConfiguration>.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate {
|
||||
|
||||
val signingCertificateStore = first
|
||||
val p2pSslConfiguration = second
|
||||
val nodeKeyStore = signingCertificateStore.get()
|
||||
val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) }
|
||||
val newNodeCert = replaceCrlDistPointCaCertificate(nodeCert, CertificateType.NODE_CA, INTERMEDIATE_CA.keyPair, nodeCaCrlDistPoint)
|
||||
val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(2).toTypedArray())
|
||||
nodeKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA)
|
||||
nodeKeyStore.save()
|
||||
val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.drop(2).toTypedArray())
|
||||
nodeKeyStore.update {
|
||||
internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA)
|
||||
}
|
||||
nodeKeyStore.update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain)
|
||||
}
|
||||
val sslKeyStore = loadSslKeyStore()
|
||||
val (tlsCert, tlsKeys) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS)
|
||||
val sslKeyStore = p2pSslConfiguration.keyStore.get()
|
||||
val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) }
|
||||
val newTlsCert = replaceCrlDistPointCaCertificate(tlsCert, CertificateType.TLS, nodeKeys, tlsCrlDistPoint, X500Name.getInstance(ROOT_CA.certificate.subjectX500Principal.encoded))
|
||||
val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(3).toTypedArray())
|
||||
sslKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS)
|
||||
sslKeyStore.save()
|
||||
val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.drop(3).toTypedArray())
|
||||
|
||||
sslKeyStore.update {
|
||||
internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS)
|
||||
}
|
||||
sslKeyStore.update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain)
|
||||
}
|
||||
@ -442,12 +482,15 @@ class CertificateRevocationListNodeTests {
|
||||
@Path("node.crl")
|
||||
@Produces("application/pkcs7-crl")
|
||||
fun getNodeCRL(): Response {
|
||||
return Response.ok(createRevocationList(
|
||||
return Response.ok(CertificateRevocationListNodeTests.createRevocationList(
|
||||
server,
|
||||
SIGNATURE_ALGORITHM,
|
||||
INTERMEDIATE_CA.certificate,
|
||||
INTERMEDIATE_CA.keyPair.private,
|
||||
NODE_CRL,
|
||||
false,
|
||||
*revokedNodeCerts.toTypedArray()).encoded).build()
|
||||
*revokedNodeCerts.toTypedArray()).encoded)
|
||||
.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -455,11 +498,14 @@ class CertificateRevocationListNodeTests {
|
||||
@Produces("application/pkcs7-crl")
|
||||
fun getIntermediateCRL(): Response {
|
||||
return Response.ok(createRevocationList(
|
||||
server,
|
||||
SIGNATURE_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
INTEMEDIATE_CRL,
|
||||
false,
|
||||
*revokedIntermediateCerts.toTypedArray()).encoded).build()
|
||||
*revokedIntermediateCerts.toTypedArray()).encoded)
|
||||
.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -467,33 +513,13 @@ class CertificateRevocationListNodeTests {
|
||||
@Produces("application/pkcs7-crl")
|
||||
fun getEmptyCRL(): Response {
|
||||
return Response.ok(createRevocationList(
|
||||
server,
|
||||
SIGNATURE_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
EMPTY_CRL, true).encoded).build()
|
||||
}
|
||||
|
||||
private fun createRevocationList(caCertificate: X509Certificate,
|
||||
caPrivateKey: PrivateKey,
|
||||
endpoint: String,
|
||||
indirect: Boolean,
|
||||
vararg serialNumbers: BigInteger): X509CRL {
|
||||
println("Generating CRL for $endpoint")
|
||||
val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis()))
|
||||
val extensionUtils = JcaX509ExtensionUtils()
|
||||
builder.addExtension(Extension.authorityKeyIdentifier,
|
||||
false, extensionUtils.createAuthorityKeyIdentifier(caCertificate))
|
||||
val issuingDistPointName = GeneralName(
|
||||
GeneralName.uniformResourceIdentifier,
|
||||
"http://${server.hostAndPort.host}:${server.hostAndPort.port}/crl/$endpoint")
|
||||
// This is required and needs to match the certificate settings with respect to being indirect
|
||||
val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false)
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint)
|
||||
builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis()))
|
||||
serialNumbers.forEach {
|
||||
builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold)
|
||||
}
|
||||
val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(caPrivateKey)
|
||||
return JcaX509CRLConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCRL(builder.build(signer))
|
||||
EMPTY_CRL,
|
||||
true).encoded)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ -533,4 +559,32 @@ class CertificateRevocationListNodeTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify CRL algorithms`() {
|
||||
val ECDSA_ALGORITHM = "SHA256withECDSA"
|
||||
val EC_ALGORITHM = "EC"
|
||||
val EMPTY_CRL = "empty.crl"
|
||||
|
||||
val crl = createRevocationList(
|
||||
server,
|
||||
ECDSA_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
EMPTY_CRL,
|
||||
true)
|
||||
// This should pass.
|
||||
crl.verify(ROOT_CA.keyPair.public)
|
||||
|
||||
// Try changing the algorithm to EC will fail.
|
||||
assertFailsWith<IllegalArgumentException>("Unknown signature type requested: EC") {
|
||||
createRevocationList(
|
||||
server,
|
||||
EC_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
EMPTY_CRL,
|
||||
true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,16 +12,18 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.CIPHER_SUITES
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.createDevKeyStores
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.init
|
||||
import net.corda.nodeapi.internal.registerDevP2pCertificates
|
||||
import net.corda.nodeapi.internal.registerDevSigningCertificates
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
@ -29,13 +31,13 @@ import net.corda.testing.core.MAX_MESSAGE_SIZE
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import org.apache.activemq.artemis.api.core.RoutingType
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.*
|
||||
import kotlin.concurrent.thread
|
||||
@ -87,6 +89,22 @@ class ProtonWrapperTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AMPQ Client fails to connect when crl soft fail check is disabled`() {
|
||||
val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"),
|
||||
maxMessageSize = MAX_MESSAGE_SIZE, crlCheckSoftFail = false)
|
||||
amqpServer.use {
|
||||
amqpServer.start()
|
||||
val amqpClient = createClient()
|
||||
amqpClient.use {
|
||||
val clientConnected = amqpClient.onConnection.toFuture()
|
||||
amqpClient.start()
|
||||
val clientConnect = clientConnected.get()
|
||||
assertEquals(false, clientConnect.connected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AMPQ Client refuses to connect to unexpected server`() {
|
||||
val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"))
|
||||
@ -102,34 +120,30 @@ class ProtonWrapperTests {
|
||||
}
|
||||
}
|
||||
|
||||
private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) {
|
||||
val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword)
|
||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||
trustStore.save(trustStoreFile, trustStorePassword)
|
||||
}
|
||||
private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) {
|
||||
|
||||
trustStore.get(true)[X509Utilities.CORDA_ROOT_CA] = rootCert
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test AMQP Client with invalid root certificate`() {
|
||||
val sslConfig = object : SSLConfiguration {
|
||||
override val certificatesDirectory = temporaryFolder.root.toPath()
|
||||
override val keyStorePassword = "serverstorepass"
|
||||
override val trustStorePassword = "trustpass"
|
||||
override val crlCheckSoftFail: Boolean = true
|
||||
}
|
||||
val certificatesDirectory = temporaryFolder.root.toPath()
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "serverstorepass")
|
||||
val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = "serverstorepass")
|
||||
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
|
||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||
sslConfig.createDevKeyStores(ALICE_NAME, rootCa.certificate, intermediateCa)
|
||||
signingCertificateStore.get(true).also { it.registerDevSigningCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) }
|
||||
sslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) }
|
||||
sslConfig.createTrustStore(rootCa.certificate)
|
||||
|
||||
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
||||
val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
|
||||
val keyStore = sslConfig.keyStore.get()
|
||||
val trustStore = sslConfig.trustStore.get()
|
||||
|
||||
val context = SSLContext.getInstance("TLS")
|
||||
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray())
|
||||
keyManagerFactory.init(keyStore)
|
||||
val keyManagers = keyManagerFactory.keyManagers
|
||||
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustMgrFactory.init(trustStore)
|
||||
@ -139,7 +153,7 @@ class ProtonWrapperTests {
|
||||
val serverSocketFactory = context.serverSocketFactory
|
||||
|
||||
val serverSocket = serverSocketFactory.createServerSocket(serverPort) as SSLServerSocket
|
||||
val serverParams = SSLParameters(CIPHER_SUITES.toTypedArray(),
|
||||
val serverParams = SSLParameters(ArtemisTcpTransport.CIPHER_SUITES.toTypedArray(),
|
||||
arrayOf("TLSv1.2"))
|
||||
serverParams.wantClientAuth = true
|
||||
serverParams.needClientAuth = true
|
||||
@ -388,11 +402,16 @@ class ProtonWrapperTests {
|
||||
}
|
||||
|
||||
private fun createArtemisServerAndClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "artemis"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(CHARLIE_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
@ -400,28 +419,32 @@ class ProtonWrapperTests {
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
|
||||
val server = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), maxMessageSize)
|
||||
val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), maxMessageSize)
|
||||
val client = ArtemisMessagingClient(artemisConfig.p2pSslOptions, NetworkHostAndPort("localhost", artemisPort), maxMessageSize)
|
||||
server.start()
|
||||
client.start()
|
||||
return Pair(server, client)
|
||||
}
|
||||
|
||||
private fun createClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "client"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
}
|
||||
clientConfig.configureWithDevSSLCertificate()
|
||||
|
||||
val clientTruststore = clientConfig.loadTrustStore().internal
|
||||
val clientKeystore = clientConfig.loadSslKeyStore().internal
|
||||
val clientTruststore = clientConfig.p2pSslOptions.trustStore.get()
|
||||
val clientKeystore = clientConfig.p2pSslOptions.keyStore.get()
|
||||
val amqpConfig = object : AMQPConfiguration {
|
||||
override val keyStore: KeyStore = clientKeystore
|
||||
override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray()
|
||||
override val trustStore: KeyStore = clientTruststore
|
||||
override val keyStore = clientKeystore
|
||||
override val trustStore = clientTruststore
|
||||
override val trace: Boolean = true
|
||||
override val maxMessageSize: Int = maxMessageSize
|
||||
}
|
||||
@ -434,21 +457,25 @@ class ProtonWrapperTests {
|
||||
}
|
||||
|
||||
private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "client_%$id"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
}
|
||||
clientConfig.configureWithDevSSLCertificate()
|
||||
|
||||
val clientTruststore = clientConfig.loadTrustStore().internal
|
||||
val clientKeystore = clientConfig.loadSslKeyStore().internal
|
||||
val clientTruststore = clientConfig.p2pSslOptions.trustStore.get()
|
||||
val clientKeystore = clientConfig.p2pSslOptions.keyStore.get()
|
||||
val amqpConfig = object : AMQPConfiguration {
|
||||
override val keyStore: KeyStore = clientKeystore
|
||||
override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray()
|
||||
override val trustStore: KeyStore = clientTruststore
|
||||
override val keyStore = clientKeystore
|
||||
override val trustStore = clientTruststore
|
||||
override val trace: Boolean = true
|
||||
override val maxMessageSize: Int = maxMessageSize
|
||||
}
|
||||
@ -459,22 +486,29 @@ class ProtonWrapperTests {
|
||||
sharedThreadPool = sharedEventGroup)
|
||||
}
|
||||
|
||||
private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer {
|
||||
private fun createServer(port: Int,
|
||||
name: CordaX500Name = ALICE_NAME,
|
||||
maxMessageSize: Int = MAX_MESSAGE_SIZE,
|
||||
crlCheckSoftFail: Boolean = true): AMQPServer {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "server"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(name).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
|
||||
}
|
||||
serverConfig.configureWithDevSSLCertificate()
|
||||
|
||||
val serverTruststore = serverConfig.loadTrustStore().internal
|
||||
val serverKeystore = serverConfig.loadSslKeyStore().internal
|
||||
val serverTruststore = serverConfig.p2pSslOptions.trustStore.get()
|
||||
val serverKeystore = serverConfig.p2pSslOptions.keyStore.get()
|
||||
val amqpConfig = object : AMQPConfiguration {
|
||||
override val keyStore: KeyStore = serverKeystore
|
||||
override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray()
|
||||
override val trustStore: KeyStore = serverTruststore
|
||||
override val keyStore = serverKeystore
|
||||
override val trustStore = serverTruststore
|
||||
override val trace: Boolean = true
|
||||
override val maxMessageSize: Int = maxMessageSize
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import net.corda.core.internal.packageName
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.TestCorDapp
|
||||
@ -48,8 +50,8 @@ class AsymmetricCorDappsTests {
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = emptySet())) {
|
||||
|
||||
val nodeA = startNode(additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java)))).getOrThrow()
|
||||
val nodeB = startNode(additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java, Pong::class.java)))).getOrThrow()
|
||||
val nodeA = startNode(providedName = ALICE_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java)))).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java, Pong::class.java)))).getOrThrow()
|
||||
nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
@ -63,7 +65,7 @@ class AsymmetricCorDappsTests {
|
||||
val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java))
|
||||
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = setOf(sharedCordapp))) {
|
||||
|
||||
val (nodeA, nodeB) = listOf(startNode(), startNode(additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow()
|
||||
val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow()
|
||||
nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
@ -77,7 +79,7 @@ class AsymmetricCorDappsTests {
|
||||
val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java))
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = setOf(sharedCordapp))) {
|
||||
|
||||
val (nodeA, nodeB) = listOf(startNode(), startNode(additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow()
|
||||
val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow()
|
||||
nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
@ -38,8 +40,8 @@ class FlowRetryTest {
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
notarySpecs = emptyList()
|
||||
)) {
|
||||
val nodeAHandle = startNode(rpcUsers = listOf(user)).getOrThrow()
|
||||
val nodeBHandle = startNode(rpcUsers = listOf(user)).getOrThrow()
|
||||
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
val result = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::InitiatorFlow, numSessions, numIterations, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||
@ -66,7 +68,7 @@ class InitiatorFlow(private val sessionsCount: Int, private val iterationsCount:
|
||||
|
||||
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||
|
||||
val seen = Collections.synchronizedSet(HashSet<Visited>())
|
||||
val seen: MutableSet<Visited> = Collections.synchronizedSet(HashSet<Visited>())
|
||||
|
||||
fun visit(sessionNum: Int, iterationNum: Int, step: Step) {
|
||||
val visited = Visited(sessionNum, iterationNum, step)
|
||||
@ -117,7 +119,7 @@ class InitiatedFlow(val session: FlowSession) : FlowLogic<Any>() {
|
||||
|
||||
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||
|
||||
val seen = Collections.synchronizedSet(HashSet<Visited>())
|
||||
val seen: MutableSet<Visited> = Collections.synchronizedSet(HashSet<Visited>())
|
||||
|
||||
fun visit(sessionNum: Int, iterationNum: Int, step: Step) {
|
||||
val visited = Visited(sessionNum, iterationNum, step)
|
||||
@ -154,4 +156,4 @@ data class SessionInfo(val sessionNum: Int, val iterationsCount: Int)
|
||||
|
||||
enum class Step { First, BeforeInitiate, AfterInitiate, AfterInitiateSendReceive, BeforeSend, AfterSend, BeforeReceive, AfterReceive }
|
||||
|
||||
data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step)
|
||||
data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step)
|
||||
|
@ -1,15 +1,19 @@
|
||||
package net.corda.node.modes.draining
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.client.rpc.internal.drainAndShutdown
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.nodeapi.internal.hasCancelledDrainingShutdown
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
@ -18,6 +22,7 @@ import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.chooseIdentity
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.waitForShutdown
|
||||
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -29,6 +34,9 @@ import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.fail
|
||||
|
||||
class P2PFlowsDrainingModeTest {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val user = User("mark", "dadada", setOf(Permissions.all()))
|
||||
@ -36,10 +44,6 @@ class P2PFlowsDrainingModeTest {
|
||||
|
||||
private var executor: ScheduledExecutorService? = null
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<P2PFlowsDrainingModeTest>()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
executor = Executors.newSingleThreadScheduledExecutor()
|
||||
@ -81,24 +85,74 @@ class P2PFlowsDrainingModeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clean shutdown by draining`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
fun `terminate node waiting for pending flows`() {
|
||||
|
||||
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow()
|
||||
var successful = false
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(true)
|
||||
IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) }
|
||||
|
||||
nodeA.rpc.drainAndShutdown()
|
||||
.doOnError { error ->
|
||||
error.printStackTrace()
|
||||
successful = false
|
||||
}
|
||||
.doOnCompleted { successful = true }
|
||||
.doAfterTerminate { latch.countDown() }
|
||||
.subscribe()
|
||||
nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.terminate(true)
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(false)
|
||||
|
||||
latch.await()
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `terminate resets persistent draining mode property when waiting for pending flows`() {
|
||||
|
||||
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
var successful = false
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
// This would not be needed, as `terminate(true)` sets draining mode anyway, but it's here to ensure that it removes the persistent value anyway.
|
||||
nodeA.rpc.setFlowsDrainingModeEnabled(true)
|
||||
nodeA.rpc.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted(nodeA::stop).doOnCompleted {
|
||||
val nodeARestarted = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
successful = !nodeARestarted.rpc.isFlowsDrainingModeEnabled()
|
||||
}.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.terminate(true)
|
||||
|
||||
latch.await()
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabling draining mode cancels draining shutdown`() {
|
||||
|
||||
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow()
|
||||
var successful = false
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(true)
|
||||
IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) }
|
||||
|
||||
nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doAfterTerminate { successful = false }.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.terminate(true)
|
||||
nodeA.rpc.hasCancelledDrainingShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.setFlowsDrainingModeEnabled(false)
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(false)
|
||||
|
||||
latch.await()
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
|
@ -23,8 +23,10 @@ import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
@ -50,7 +52,7 @@ class AttachmentLoadingTests {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val attachments = MockAttachmentStorage()
|
||||
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments).apply {
|
||||
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
|
||||
start(testNetworkParameters().whitelistedContractImplementations)
|
||||
}
|
||||
private val cordapp get() = provider.cordapps.first()
|
||||
|
@ -116,4 +116,4 @@ class ScheduledFlowIntegrationTests {
|
||||
assertEquals(aliceSpentStates.count(), bobSpentStates.count())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.services.config.FlowTimeoutConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.network.NetworkMapCacheImpl
|
||||
import net.corda.node.services.network.PersistentNetworkMapCache
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
||||
import net.corda.node.utilities.TestingNamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -21,6 +23,7 @@ import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.internal.MOCK_VERSION_INFO
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException
|
||||
@ -64,24 +67,30 @@ class ArtemisMessagingTest {
|
||||
private var messagingClient: P2PMessagingClient? = null
|
||||
private var messagingServer: ArtemisMessagingServer? = null
|
||||
|
||||
private lateinit var networkMapCache: NetworkMapCacheImpl
|
||||
private lateinit var networkMapCache: PersistentNetworkMapCache
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||
|
||||
val baseDirectory = temporaryFolder.root.toPath()
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
|
||||
config = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(temporaryFolder.root.toPath()).whenever(it).baseDirectory
|
||||
doReturn(ALICE_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(FlowTimeoutConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).flowTimeout
|
||||
}
|
||||
LogHelper.setLevel(PersistentUniquenessProvider::class)
|
||||
database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null })
|
||||
val persistentNetworkMapCache = PersistentNetworkMapCache(database, ALICE_NAME).apply { start(emptyList()) }
|
||||
networkMapCache = NetworkMapCacheImpl(persistentNetworkMapCache, rigorousMock(), database).apply { start() }
|
||||
networkMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, rigorousMock()).apply { start(emptyList()) }
|
||||
}
|
||||
|
||||
@After
|
||||
@ -216,6 +225,8 @@ class ArtemisMessagingTest {
|
||||
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
|
||||
database,
|
||||
networkMapCache,
|
||||
MetricRegistry(),
|
||||
TestingNamedCacheFactory(),
|
||||
isDrainingModeOn = { false },
|
||||
drainingModeWasChangedEvents = PublishSubject.create<Pair<Boolean, Boolean>>()).apply {
|
||||
config.configureWithDevSSLCertificate()
|
||||
|
@ -1,8 +1,6 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.ParametersUpdateInfo
|
||||
@ -10,9 +8,7 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||
@ -242,19 +238,3 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
||||
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DriverDSLImpl.startNode(providedName: CordaX500Name, devMode: Boolean): CordaFuture<NodeHandle> {
|
||||
var customOverrides = emptyMap<String, String>()
|
||||
if (!devMode) {
|
||||
val nodeDir = baseDirectory(providedName)
|
||||
val nodeSslConfig = object : NodeSSLConfiguration {
|
||||
override val baseDirectory = nodeDir
|
||||
override val keyStorePassword = "cordacadevpass"
|
||||
override val trustStorePassword = "trustpass"
|
||||
override val crlCheckSoftFail = true
|
||||
}
|
||||
nodeSslConfig.configureDevKeyAndTrustStores(providedName)
|
||||
customOverrides = mapOf("devMode" to "false")
|
||||
}
|
||||
return startNode(providedName = providedName, customOverrides = customOverrides)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.internal.schemas.NodeInfoSchemaV1
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.utilities.TestingNamedCacheFactory
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
@ -18,7 +20,6 @@ class PersistentNetworkMapCacheTest {
|
||||
private companion object {
|
||||
val ALICE = TestIdentity(ALICE_NAME, 70)
|
||||
val BOB = TestIdentity(BOB_NAME, 80)
|
||||
val CHARLIE = TestIdentity(CHARLIE_NAME, 90)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@ -27,7 +28,7 @@ class PersistentNetworkMapCacheTest {
|
||||
|
||||
private var portCounter = 1000
|
||||
private val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null })
|
||||
private val charlieNetMapCache = PersistentNetworkMapCache(database, CHARLIE.name)
|
||||
private val charlieNetMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate))
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
@ -38,7 +39,6 @@ class PersistentNetworkMapCacheTest {
|
||||
fun addNode() {
|
||||
val alice = createNodeInfo(listOf(ALICE))
|
||||
charlieNetMapCache.addNode(alice)
|
||||
assertThat(charlieNetMapCache.nodeReady).isDone()
|
||||
val fromDb = database.transaction {
|
||||
session.createQuery(
|
||||
"from ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name}",
|
||||
@ -48,32 +48,6 @@ class PersistentNetworkMapCacheTest {
|
||||
assertThat(fromDb).containsOnly(alice)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `adding the node's own node-info doesn't complete the nodeReady future`() {
|
||||
val charlie = createNodeInfo(listOf(CHARLIE))
|
||||
charlieNetMapCache.addNode(charlie)
|
||||
assertThat(charlieNetMapCache.nodeReady).isNotDone()
|
||||
assertThat(charlieNetMapCache.getNodeByLegalName(CHARLIE.name)).isEqualTo(charlie)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `starting with just the node's own node-info in the db`() {
|
||||
val charlie = createNodeInfo(listOf(CHARLIE))
|
||||
saveNodeInfoIntoDb(charlie)
|
||||
assertThat(charlieNetMapCache.allNodes).containsOnly(charlie)
|
||||
charlieNetMapCache.start(emptyList())
|
||||
assertThat(charlieNetMapCache.nodeReady).isNotDone()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `starting with another node-info in the db`() {
|
||||
val alice = createNodeInfo(listOf(ALICE))
|
||||
saveNodeInfoIntoDb(alice)
|
||||
assertThat(charlieNetMapCache.allNodes).containsOnly(alice)
|
||||
charlieNetMapCache.start(emptyList())
|
||||
assertThat(charlieNetMapCache.nodeReady).isDone()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown legal name`() {
|
||||
charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE)))
|
||||
@ -135,19 +109,4 @@ class PersistentNetworkMapCacheTest {
|
||||
serial = 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveNodeInfoIntoDb(nodeInfo: NodeInfo) {
|
||||
database.transaction {
|
||||
session.save(NodeInfoSchemaV1.PersistentNodeInfo(
|
||||
id = 0,
|
||||
hash = nodeInfo.serialize().hash.toString(),
|
||||
addresses = nodeInfo.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) },
|
||||
legalIdentitiesAndCerts = nodeInfo.legalIdentitiesAndCerts.mapIndexed { idx, elem ->
|
||||
NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0)
|
||||
},
|
||||
platformVersion = nodeInfo.platformVersion,
|
||||
serial = nodeInfo.serial
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,13 +17,13 @@ import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
||||
import net.corda.node.utilities.saveToKeyStore
|
||||
import net.corda.node.utilities.saveToTrustStore
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.createNodeSslConfig
|
||||
import net.corda.testing.internal.p2pSslOptions
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -58,12 +58,12 @@ class ArtemisRpcTests {
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert)
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
|
||||
testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_ssl_disabled() {
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), null, false, null)
|
||||
testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), null, false, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -73,7 +73,7 @@ class ArtemisRpcTests {
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
// here client sslOptions are passed null (as in, do not use SSL)
|
||||
assertThatThrownBy {
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, null)
|
||||
testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, null)
|
||||
}.isInstanceOf(ActiveMQConnectionTimedOutException::class.java)
|
||||
}
|
||||
|
||||
@ -91,11 +91,11 @@ class ArtemisRpcTests {
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
|
||||
assertThatThrownBy {
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
|
||||
testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
|
||||
}.isInstanceOf(RPCException::class.java)
|
||||
}
|
||||
|
||||
private fun testSslCommunication(nodeSSlconfig: SSLConfiguration,
|
||||
private fun testSslCommunication(nodeSSlconfig: MutualSslConfiguration,
|
||||
brokerSslOptions: BrokerRpcSslOptions?,
|
||||
useSslForBroker: Boolean,
|
||||
clientSslOptions: ClientRpcSslOptions?,
|
||||
@ -140,7 +140,7 @@ class ArtemisRpcTests {
|
||||
class TestRpcOpsImpl : TestRpcOps {
|
||||
override fun greet(name: String): String = "Oh, hello $name!"
|
||||
|
||||
override val protocolVersion: Int = 1
|
||||
override val protocolVersion: Int = 1000
|
||||
}
|
||||
|
||||
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
|
||||
|
@ -9,13 +9,13 @@ import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.nodeapi.exceptions.InternalNodeException
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.driver.*
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.startNode
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
|
||||
import org.hibernate.exception.GenericJDBCException
|
||||
@ -23,75 +23,95 @@ import org.junit.Test
|
||||
import java.sql.SQLException
|
||||
|
||||
class RpcExceptionHandlingTest {
|
||||
|
||||
private val user = User("mark", "dadada", setOf(Permissions.all()))
|
||||
private val users = listOf(user)
|
||||
|
||||
@Test
|
||||
fun `rpc client handles exceptions thrown on node side`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
fun `rpc client receive client-relevant exceptions regardless of devMode`() {
|
||||
val params = NodeParameters(rpcUsers = users)
|
||||
val clientRelevantMessage = "This is for the players!"
|
||||
|
||||
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow()
|
||||
|
||||
assertThatThrownBy { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
|
||||
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
}
|
||||
fun NodeHandle.throwExceptionFromFlow() {
|
||||
rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rpc client handles client-relevant exceptions thrown on node side`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
|
||||
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow()
|
||||
val clientRelevantMessage = "This is for the players!"
|
||||
|
||||
assertThatThrownBy { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
|
||||
fun assertThatThrownExceptionIsReceivedUnwrapped(node: NodeHandle) {
|
||||
assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception ->
|
||||
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
assertThat(exception.message).isEqualTo(clientRelevantMessage)
|
||||
}
|
||||
}
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val devModeNode = startNode(params, BOB_NAME).getOrThrow()
|
||||
val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()
|
||||
|
||||
assertThatThrownExceptionIsReceivedUnwrapped(devModeNode)
|
||||
assertThatThrownExceptionIsReceivedUnwrapped(node)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FlowException is received by the RPC client`() {
|
||||
fun `FlowException is received by the RPC client only if in devMode`() {
|
||||
val params = NodeParameters(rpcUsers = users)
|
||||
val expectedMessage = "Flow error!"
|
||||
val expectedErrorId = 123L
|
||||
|
||||
fun NodeHandle.throwExceptionFromFlow() {
|
||||
rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow()
|
||||
val exceptionMessage = "Flow error!"
|
||||
assertThatThrownBy { node.rpc.startFlow(::FlowExceptionFlow, exceptionMessage).returnValue.getOrThrow() }
|
||||
.isInstanceOfSatisfying(FlowException::class.java) { exception ->
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
assertThat(exception.message).isEqualTo(exceptionMessage)
|
||||
}
|
||||
val devModeNode = startNode(params, BOB_NAME).getOrThrow()
|
||||
val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()
|
||||
|
||||
assertThatThrownBy { devModeNode.throwExceptionFromFlow() }.isInstanceOfSatisfying(FlowException::class.java) { exception ->
|
||||
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
assertThat(exception.message).isEqualTo(expectedMessage)
|
||||
assertThat(exception.errorId).isEqualTo(expectedErrorId)
|
||||
}
|
||||
assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception ->
|
||||
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
assertThat(exception.message).isEqualTo(InternalNodeException.message)
|
||||
assertThat(exception.errorId).isEqualTo(expectedErrorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rpc client handles exceptions thrown on counter-party side`() {
|
||||
val params = NodeParameters(rpcUsers = users)
|
||||
|
||||
fun DriverDSL.scenario(devMode: Boolean) {
|
||||
|
||||
val nodeA = startNode(ALICE_NAME, devMode, params).getOrThrow()
|
||||
val nodeB = startNode(BOB_NAME, devMode, params).getOrThrow()
|
||||
|
||||
nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(NodeParameters(providedName = ALICE_NAME, rpcUsers = users)).getOrThrow()
|
||||
val nodeB = startNode(NodeParameters(providedName = BOB_NAME, rpcUsers = users)).getOrThrow()
|
||||
|
||||
assertThatThrownBy { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
|
||||
assertThatThrownBy { scenario(true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
|
||||
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
assertThatThrownBy { scenario(false) }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception ->
|
||||
|
||||
@StartableByRPC
|
||||
class Flow : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
throw GenericJDBCException("Something went wrong!", SQLException("Oops!"))
|
||||
assertThat(exception).hasNoCause()
|
||||
assertThat(exception.stackTrace).isEmpty()
|
||||
assertThat(exception.message).isEqualTo(InternalNodeException.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +141,11 @@ class ClientRelevantErrorFlow(private val message: String) : FlowLogic<String>()
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class FlowExceptionFlow(private val message: String) : FlowLogic<String>() {
|
||||
class FlowExceptionFlow(private val message: String, private val errorId: Long? = null) : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String = throw FlowException(message)
|
||||
override fun call(): String {
|
||||
val exception = FlowException(message)
|
||||
errorId?.let { exception.originalErrorId = it }
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,10 @@ import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class HardRestartTest {
|
||||
companion object {
|
||||
val logConfigFile = ProjectStructure.projectRootDir / "config" / "dev" / "log4j2.xml"
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class Ping(private val pongParty: Party, val times: Int) : FlowLogic<Unit>() {
|
||||
@ -54,10 +58,6 @@ class HardRestartTest {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val logConfigFile = ProjectStructure.projectRootDir / "config" / "dev" / "log4j2.xml"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restartShortPingPongFlowRandomly() {
|
||||
val demoUser = User("demo", "demo", setOf(Permissions.startFlow<Ping>(), Permissions.all()))
|
||||
@ -257,4 +257,4 @@ class HardRestartTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.utilities.TestingNamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -155,7 +156,7 @@ class RaftTransactionCommitLogTests {
|
||||
val address = Address(myAddress.host, myAddress.port)
|
||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(includeNotarySchemas = true))
|
||||
databases.add(database)
|
||||
val stateMachineFactory = { RaftTransactionCommitLog(database, Clock.systemUTC(), RaftUniquenessProvider.Companion::createMap) }
|
||||
val stateMachineFactory = { RaftTransactionCommitLog(database, Clock.systemUTC(), { RaftUniquenessProvider.createMap(TestingNamedCacheFactory()) }) }
|
||||
|
||||
val server = CopycatServer.builder(address)
|
||||
.withStateMachine(stateMachineFactory)
|
||||
|
@ -0,0 +1,70 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_B_NAME
|
||||
import net.corda.testing.core.expect
|
||||
import net.corda.testing.core.expectEvents
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
|
||||
class AdditionP2PAddressModeTest {
|
||||
private val portAllocation = PortAllocation.Incremental(27182)
|
||||
@Test
|
||||
fun `runs nodes with one configured to use additionalP2PAddresses`() {
|
||||
val testUser = User("test", "test", setOf(all()))
|
||||
driver(DriverParameters(startNodesInProcess = true, inMemoryDB = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) {
|
||||
val mainAddress = portAllocation.nextHostAndPort().toString()
|
||||
val altAddress = portAllocation.nextHostAndPort().toString()
|
||||
val haConfig = mutableMapOf<String, Any?>()
|
||||
haConfig["detectPublicIp"] = false
|
||||
haConfig["p2pAddress"] = mainAddress //advertise this as primary
|
||||
haConfig["messagingServerAddress"] = altAddress // but actually host on the alternate address
|
||||
haConfig["messagingServerExternal"] = false
|
||||
haConfig["additionalP2PAddresses"] = ConfigValueFactory.fromIterable(listOf(altAddress)) // advertise this secondary address
|
||||
|
||||
val (nodeA, nodeB) = listOf(
|
||||
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(testUser), customOverrides = haConfig),
|
||||
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(testUser), customOverrides = mapOf("p2pAddress" to portAllocation.nextHostAndPort().toString()))
|
||||
).map { it.getOrThrow() }
|
||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
||||
val client = CordaRPCClient(it.rpcAddress)
|
||||
client.start(testUser.username, testUser.password).proxy
|
||||
}
|
||||
|
||||
val nodeBVaultUpdates = nodeBRpc.vaultTrack(Cash.State::class.java).updates
|
||||
|
||||
val issueRef = OpaqueBytes.of(0.toByte())
|
||||
nodeARpc.startFlowDynamic(
|
||||
CashIssueAndPaymentFlow::class.java,
|
||||
DOLLARS(1234),
|
||||
issueRef,
|
||||
nodeB.nodeInfo.legalIdentities.get(0),
|
||||
true,
|
||||
defaultNotaryIdentity
|
||||
).returnValue.getOrThrow()
|
||||
nodeBVaultUpdates.expectEvents {
|
||||
expect { update ->
|
||||
println("Bob got vault update of $update")
|
||||
val amount: Amount<Issued<Currency>> = update.produced.first().state.data.amount
|
||||
assertEquals(1234.DOLLARS, amount.withoutIssuer())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package net.corda.services.messaging
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.toX500Name
|
||||
@ -11,9 +10,10 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_U
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.loadDevCaTrustStore
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
|
||||
@ -84,57 +84,35 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() {
|
||||
|
||||
@Test
|
||||
fun `login with invalid certificate chain`() {
|
||||
val sslConfig = object : SSLConfiguration {
|
||||
override val certificatesDirectory = Files.createTempDirectory("certs")
|
||||
override val keyStorePassword: String get() = "cordacadevpass"
|
||||
override val trustStorePassword: String get() = "trustpass"
|
||||
override val crlCheckSoftFail: Boolean = true
|
||||
val certsDir = Files.createTempDirectory("certs")
|
||||
certsDir.createDirectories()
|
||||
val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certsDir)
|
||||
val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certsDir)
|
||||
|
||||
init {
|
||||
val legalName = CordaX500Name("MegaCorp", "London", "GB")
|
||||
certificatesDirectory.createDirectories()
|
||||
if (!trustStoreFile.exists()) {
|
||||
javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").use { it.copyTo(trustStoreFile) }
|
||||
}
|
||||
|
||||
val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
// Set name constrain to the legal name.
|
||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf())
|
||||
val clientCACert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
DEV_INTERMEDIATE_CA.certificate,
|
||||
DEV_INTERMEDIATE_CA.keyPair,
|
||||
legalName.x500Principal,
|
||||
clientKeyPair.public,
|
||||
nameConstraints = nameConstraints)
|
||||
|
||||
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
// Using different x500 name in the TLS cert which is not allowed in the name constraints.
|
||||
val clientTLSCert = X509Utilities.createCertificate(
|
||||
CertificateType.TLS,
|
||||
clientCACert,
|
||||
clientKeyPair,
|
||||
CordaX500Name("MiniCorp", "London", "GB").x500Principal,
|
||||
tlsKeyPair.public)
|
||||
|
||||
loadNodeKeyStore(createNew = true).update {
|
||||
setPrivateKey(
|
||||
X509Utilities.CORDA_CLIENT_CA,
|
||||
clientKeyPair.private,
|
||||
listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||
}
|
||||
|
||||
loadSslKeyStore(createNew = true).update {
|
||||
setPrivateKey(
|
||||
X509Utilities.CORDA_CLIENT_TLS,
|
||||
tlsKeyPair.private,
|
||||
listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||
}
|
||||
}
|
||||
val legalName = CordaX500Name("MegaCorp", "London", "GB")
|
||||
if (!p2pSslConfig.trustStore.path.exists()) {
|
||||
val trustStore = p2pSslConfig.trustStore.get(true)
|
||||
loadDevCaTrustStore().copyTo(trustStore)
|
||||
}
|
||||
|
||||
val attacker = clientTo(alice.node.configuration.p2pAddress, sslConfig)
|
||||
val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
// Set name constrain to the legal name.
|
||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf())
|
||||
val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, DEV_INTERMEDIATE_CA.certificate, DEV_INTERMEDIATE_CA.keyPair, legalName.x500Principal, clientKeyPair.public, nameConstraints = nameConstraints)
|
||||
|
||||
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
// Using different x500 name in the TLS cert which is not allowed in the name constraints.
|
||||
val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKeyPair, CordaX500Name("MiniCorp", "London", "GB").x500Principal, tlsKeyPair.public)
|
||||
|
||||
signingCertStore.get(createNew = true).update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||
}
|
||||
|
||||
p2pSslConfig.keyStore.get(createNew = true).update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||
}
|
||||
|
||||
val attacker = clientTo(alice.node.configuration.p2pAddress, p2pSslConfig)
|
||||
assertThatExceptionOfType(ActiveMQNotConnectedException::class.java).isThrownBy {
|
||||
attacker.start(PEER_USER, PEER_USER)
|
||||
}
|
||||
|
@ -18,12 +18,12 @@ import net.corda.node.internal.NodeWithInfo
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.internal.configureTestSSL
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
|
||||
@ -94,7 +94,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
assertAllQueueCreationAttacksFail(randomQueue)
|
||||
}
|
||||
|
||||
fun clientTo(target: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient {
|
||||
fun clientTo(target: NetworkHostAndPort, sslConfiguration: MutualSslConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient {
|
||||
val client = SimpleMQClient(target, sslConfiguration)
|
||||
clients += client
|
||||
return client
|
||||
|
@ -38,7 +38,10 @@ class P2PMessagingTest {
|
||||
}
|
||||
|
||||
private fun startDriverWithDistributedService(dsl: DriverDSL.(List<InProcess>) -> Unit) {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2))))) {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))
|
||||
)) {
|
||||
dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) })
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package net.corda.services.messaging
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.testing.internal.configureTestSSL
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
|
||||
@ -12,7 +12,7 @@ import org.apache.activemq.artemis.api.core.client.*
|
||||
* As the name suggests this is a simple client for connecting to MQ brokers.
|
||||
*/
|
||||
class SimpleMQClient(val target: NetworkHostAndPort,
|
||||
private val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) {
|
||||
private val config: MutualSslConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) {
|
||||
companion object {
|
||||
val DEFAULT_MQ_LEGAL_NAME = CordaX500Name(organisation = "SimpleMQClient", locality = "London", country = "GB")
|
||||
}
|
||||
@ -22,7 +22,7 @@ class SimpleMQClient(val target: NetworkHostAndPort,
|
||||
lateinit var producer: ClientProducer
|
||||
|
||||
fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) {
|
||||
val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(target, config, enableSSL = enableSSL)
|
||||
val tcpTransport = p2pConnectorTcpTransport(target, config, enableSSL = enableSSL)
|
||||
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
|
||||
isBlockOnNonDurableSend = true
|
||||
threadPoolMaxSize = 1
|
||||
|
@ -21,16 +21,8 @@ public class CordaCaplet extends Capsule {
|
||||
|
||||
private Config parseConfigFile(List<String> args) {
|
||||
String baseDirOption = getOption(args, "--base-directory");
|
||||
// Ensure consistent behaviour with NodeArgsParser.kt, see CORDA-1598.
|
||||
if (null == baseDirOption || baseDirOption.isEmpty()) {
|
||||
baseDirOption = getOption(args, "-base-directory");
|
||||
}
|
||||
this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString();
|
||||
String config = getOption(args, "--config-file");
|
||||
// Same as for baseDirOption.
|
||||
if (null == config || config.isEmpty()) {
|
||||
config = getOption(args, "-config-file");
|
||||
}
|
||||
File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config);
|
||||
try {
|
||||
ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false);
|
||||
|
@ -3,12 +3,12 @@
|
||||
|
||||
package net.corda.node
|
||||
|
||||
import net.corda.cliutils.start
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass.
|
||||
// It will exit the process in case of startup failure and is not intended to be used by embedders. If you want
|
||||
// to embed Node in your own container, instantiate it directly and set up the configuration objects yourself.
|
||||
exitProcess(if (NodeStartup(args).run()) 0 else 1)
|
||||
}
|
||||
NodeStartup().start(args)
|
||||
}
|
||||
|
@ -1,142 +0,0 @@
|
||||
package net.corda.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import joptsimple.OptionSet
|
||||
import joptsimple.util.EnumConverter
|
||||
import joptsimple.util.PathConverter
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||
import net.corda.node.utilities.AbstractArgsParser
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import org.slf4j.event.Level
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup.
|
||||
class NodeArgsParser : AbstractArgsParser<CmdLineOptions>() {
|
||||
// The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
|
||||
// Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
|
||||
private val baseDirectoryArg = optionParser
|
||||
.accepts("base-directory", "The node working directory where all the files are kept")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter())
|
||||
.defaultsTo(Paths.get("."))
|
||||
private val configFileArg = optionParser
|
||||
.accepts("config-file", "The path to the config file")
|
||||
.withRequiredArg()
|
||||
.defaultsTo("node.conf")
|
||||
private val loggerLevel = optionParser
|
||||
.accepts("logging-level", "Enable logging at this level and higher")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(object : EnumConverter<Level>(Level::class.java) {})
|
||||
.defaultsTo(Level.INFO)
|
||||
private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
|
||||
private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.")
|
||||
private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.")
|
||||
private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
|
||||
private val networkRootTrustStorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter())
|
||||
.defaultsTo((Paths.get("certificates") / "network-root-truststore.jks"))
|
||||
private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
|
||||
.withRequiredArg()
|
||||
private val unknownConfigKeysPolicy = optionParser.accepts("on-unknown-config-keys", "How to behave on unknown node configuration.")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(object : EnumConverter<UnknownConfigKeysPolicy>(UnknownConfigKeysPolicy::class.java) {})
|
||||
.defaultsTo(UnknownConfigKeysPolicy.FAIL)
|
||||
private val devModeArg = optionParser.accepts("dev-mode", "Run the node in developer mode. Unsafe for production.")
|
||||
|
||||
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
|
||||
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
|
||||
"Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit")
|
||||
private val justGenerateRpcSslCertsArg = optionParser.accepts("just-generate-rpc-ssl-settings",
|
||||
"Generate the ssl keystore and truststore for a secure RPC connection.")
|
||||
private val bootstrapRaftClusterArg = optionParser.accepts("bootstrap-raft-cluster", "Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster.")
|
||||
private val clearNetworkMapCache = optionParser.accepts("clear-network-map-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.")
|
||||
|
||||
override fun doParse(optionSet: OptionSet): CmdLineOptions {
|
||||
require(optionSet.nonOptionArguments().isEmpty()) { "Unrecognized argument(s): ${optionSet.nonOptionArguments().joinToString(separator = ", ")}"}
|
||||
|
||||
val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
|
||||
val configFilePath = Paths.get(optionSet.valueOf(configFileArg))
|
||||
val configFile = if (configFilePath.isAbsolute) configFilePath else baseDirectory / configFilePath.toString()
|
||||
val loggingLevel = optionSet.valueOf(loggerLevel)
|
||||
val logToConsole = optionSet.has(logToConsoleArg)
|
||||
val isRegistration = optionSet.has(isRegistrationArg)
|
||||
val isVersion = optionSet.has(isVersionArg)
|
||||
val noLocalShell = optionSet.has(noLocalShellArg)
|
||||
val sshdServer = optionSet.has(sshdServerArg)
|
||||
val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg)
|
||||
val justGenerateRpcSslCerts = optionSet.has(justGenerateRpcSslCertsArg)
|
||||
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
|
||||
val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg)
|
||||
val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg)
|
||||
val unknownConfigKeysPolicy = optionSet.valueOf(unknownConfigKeysPolicy)
|
||||
val devMode = optionSet.has(devModeArg)
|
||||
val clearNetworkMapCache = optionSet.has(clearNetworkMapCache)
|
||||
|
||||
val registrationConfig = if (isRegistration) {
|
||||
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
|
||||
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return CmdLineOptions(baseDirectory,
|
||||
configFile,
|
||||
loggingLevel,
|
||||
logToConsole,
|
||||
registrationConfig,
|
||||
isVersion,
|
||||
noLocalShell,
|
||||
sshdServer,
|
||||
justGenerateNodeInfo,
|
||||
justGenerateRpcSslCerts,
|
||||
bootstrapRaftCluster,
|
||||
unknownConfigKeysPolicy,
|
||||
devMode,
|
||||
clearNetworkMapCache)
|
||||
}
|
||||
}
|
||||
|
||||
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
|
||||
|
||||
data class CmdLineOptions(val baseDirectory: Path,
|
||||
val configFile: Path,
|
||||
val loggingLevel: Level,
|
||||
val logToConsole: Boolean,
|
||||
val nodeRegistrationOption: NodeRegistrationOption?,
|
||||
val isVersion: Boolean,
|
||||
val noLocalShell: Boolean,
|
||||
val sshdServer: Boolean,
|
||||
val justGenerateNodeInfo: Boolean,
|
||||
val justGenerateRpcSslCerts: Boolean,
|
||||
val bootstrapRaftCluster: Boolean,
|
||||
val unknownConfigKeysPolicy: UnknownConfigKeysPolicy,
|
||||
val devMode: Boolean,
|
||||
val clearNetworkMapCache: Boolean) {
|
||||
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
|
||||
val rawConfig = ConfigHelper.loadConfig(
|
||||
baseDirectory,
|
||||
configFile,
|
||||
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
|
||||
if (devMode) mapOf("devMode" to this.devMode) else emptyMap<String, Any>())
|
||||
)
|
||||
return rawConfig to Try.on {
|
||||
rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
if (nodeRegistrationOption != null) {
|
||||
require(!config.devMode) { "registration cannot occur in devMode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
134
node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt
Normal file
134
node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt
Normal file
@ -0,0 +1,134 @@
|
||||
package net.corda.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import picocli.CommandLine.Option
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class NodeCmdLineOptions {
|
||||
@Option(
|
||||
names = ["-b", "--base-directory"],
|
||||
description = ["The node working directory where all the files are kept."]
|
||||
)
|
||||
var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize()
|
||||
|
||||
@Option(
|
||||
names = ["-f", "--config-file"],
|
||||
description = ["The path to the config file. By default this is node.conf in the base directory."]
|
||||
)
|
||||
private var _configFile: Path? = null
|
||||
val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf")
|
||||
|
||||
@Option(
|
||||
names = ["--sshd"],
|
||||
description = ["If set, enables SSH server for node administration."]
|
||||
)
|
||||
var sshdServer: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--sshd-port"],
|
||||
description = ["The port to start the SSH server on, if enabled."]
|
||||
)
|
||||
var sshdServerPort: Int = 2222
|
||||
|
||||
@Option(
|
||||
names = ["-n", "--no-local-shell"],
|
||||
description = ["Do not start the embedded shell locally."]
|
||||
)
|
||||
var noLocalShell: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--initial-registration"],
|
||||
description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."]
|
||||
)
|
||||
var isRegistration: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-t", "--network-root-truststore"],
|
||||
description = ["Network root trust store obtained from network operator."]
|
||||
)
|
||||
private var _networkRootTrustStorePath: Path? = null
|
||||
val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks"
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--network-root-truststore-password"],
|
||||
description = ["Network root trust store password obtained from network operator."]
|
||||
)
|
||||
var networkRootTrustStorePassword: String? = null
|
||||
|
||||
@Option(
|
||||
names = ["--on-unknown-config-keys"],
|
||||
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
|
||||
)
|
||||
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--dev-mode"],
|
||||
description = ["Run the node in developer mode. Unsafe for production."]
|
||||
)
|
||||
var devMode: Boolean? = null
|
||||
|
||||
@Option(
|
||||
names = ["--just-generate-node-info"],
|
||||
description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"]
|
||||
)
|
||||
var justGenerateNodeInfo: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--just-generate-rpc-ssl-settings"],
|
||||
description = ["Generate the SSL key and trust stores for a secure RPC connection."]
|
||||
)
|
||||
var justGenerateRpcSslCerts: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--bootstrap-raft-cluster"],
|
||||
description = ["Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster."]
|
||||
)
|
||||
var bootstrapRaftCluster: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-c", "--clear-network-map-cache"],
|
||||
description = ["Clears local copy of network map, on node startup it will be restored from server or file system."]
|
||||
)
|
||||
var clearNetworkMapCache: Boolean = false
|
||||
|
||||
val nodeRegistrationOption: NodeRegistrationOption? by lazy {
|
||||
if (isRegistration) {
|
||||
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
|
||||
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
|
||||
val rawConfig = ConfigHelper.loadConfig(
|
||||
baseDirectory,
|
||||
configFile,
|
||||
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
|
||||
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
|
||||
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
|
||||
)
|
||||
return rawConfig to Try.on {
|
||||
rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
if (nodeRegistrationOption != null) {
|
||||
require(!config.devMode) { "Registration cannot occur in devMode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
|
@ -1,5 +1,7 @@
|
||||
package net.corda.node
|
||||
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
|
||||
/**
|
||||
* Encapsulates various pieces of version information of the node.
|
||||
*/
|
||||
@ -14,4 +16,9 @@ data class VersionInfo(
|
||||
/** The exact version control commit ID of the node build. */
|
||||
val revision: String,
|
||||
/** The node vendor */
|
||||
val vendor: String)
|
||||
val vendor: String) {
|
||||
|
||||
companion object {
|
||||
val UNKNOWN = VersionInfo(PLATFORM_VERSION, "Unknown", "Unknown", "Unknown")
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.node.cordapp
|
||||
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
|
||||
/**
|
||||
@ -12,7 +13,7 @@ interface CordappLoader {
|
||||
/**
|
||||
* Returns all [Cordapp]s found.
|
||||
*/
|
||||
val cordapps: List<Cordapp>
|
||||
val cordapps: List<CordappImpl>
|
||||
|
||||
/**
|
||||
* Returns a [ClassLoader] containing all types from all [Cordapp]s.
|
||||
|
@ -38,12 +38,14 @@ import net.corda.node.internal.cordapp.CordappConfigFileProvider
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.node.internal.cordapp.CordappProviderInternal
|
||||
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
|
||||
import net.corda.node.internal.rpc.proxies.ExceptionMaskingRpcOpsProxy
|
||||
import net.corda.node.internal.rpc.proxies.ExceptionSerialisingRpcOpsProxy
|
||||
import net.corda.node.services.ContractUpgradeHandler
|
||||
import net.corda.node.services.FinalityHandler
|
||||
import net.corda.node.services.NotaryChangeHandler
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
import net.corda.node.services.config.shell.toShellConfig
|
||||
import net.corda.node.services.events.NodeSchedulerService
|
||||
import net.corda.node.services.events.ScheduledActivityObserver
|
||||
@ -52,21 +54,26 @@ import net.corda.node.services.keys.KeyManagementServiceInternal
|
||||
import net.corda.node.services.keys.PersistentKeyManagementService
|
||||
import net.corda.node.services.messaging.DeduplicationHandler
|
||||
import net.corda.node.services.messaging.MessagingService
|
||||
import net.corda.node.services.network.*
|
||||
import net.corda.node.services.network.NetworkMapClient
|
||||
import net.corda.node.services.network.NetworkMapUpdater
|
||||
import net.corda.node.services.network.NodeInfoWatcher
|
||||
import net.corda.node.services.network.PersistentNetworkMapCache
|
||||
import net.corda.node.services.persistence.*
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.statemachine.*
|
||||
import net.corda.node.services.transactions.*
|
||||
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.JVMAgentRegistry
|
||||
import net.corda.node.utilities.NamedThreadFactory
|
||||
import net.corda.node.utilities.NodeBuildProperties
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX
|
||||
import net.corda.nodeapi.internal.persistence.*
|
||||
import net.corda.nodeapi.internal.storeLegalIdentity
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
@ -106,6 +113,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
// TODO Log warning if this node is a notary but not one of the ones specified in the network parameters, both for core and custom
|
||||
abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val platformClock: CordaClock,
|
||||
cacheFactoryPrototype: NamedCacheFactory,
|
||||
protected val versionInfo: VersionInfo,
|
||||
protected val cordappLoader: CordappLoader,
|
||||
protected val serverThread: AffinityExecutor.ServiceAffinityExecutor,
|
||||
@ -115,6 +123,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
|
||||
|
||||
protected val metricRegistry = MetricRegistry()
|
||||
protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize()
|
||||
val monitoringService = MonitoringService(metricRegistry).tokenize()
|
||||
|
||||
protected val runOnStop = ArrayList<() -> Any?>()
|
||||
|
||||
init {
|
||||
@ -129,7 +142,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null).tokenize()
|
||||
val identityService = PersistentIdentityService().tokenize()
|
||||
val identityService = PersistentIdentityService(cacheFactory).tokenize()
|
||||
val database: CordaPersistence = createCordaPersistence(
|
||||
configuration.database,
|
||||
identityService::wellKnownPartyFromX500Name,
|
||||
@ -141,14 +154,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
// TODO Break cyclic dependency
|
||||
identityService.database = database
|
||||
}
|
||||
private val persistentNetworkMapCache = PersistentNetworkMapCache(database, configuration.myLegalName)
|
||||
val networkMapCache = NetworkMapCacheImpl(persistentNetworkMapCache, identityService, database).tokenize()
|
||||
|
||||
val networkMapCache = PersistentNetworkMapCache(cacheFactory, database, identityService).tokenize()
|
||||
val checkpointStorage = DBCheckpointStorage()
|
||||
@Suppress("LeakingThis")
|
||||
val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize()
|
||||
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL) }
|
||||
private val metricRegistry = MetricRegistry()
|
||||
val attachments = NodeAttachmentService(metricRegistry, database, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound).tokenize()
|
||||
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
|
||||
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize()
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
|
||||
@Suppress("LeakingThis")
|
||||
val keyManagementService = makeKeyManagementService(identityService).tokenize()
|
||||
@ -157,7 +169,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
|
||||
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database)
|
||||
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
|
||||
val monitoringService = MonitoringService(metricRegistry).tokenize()
|
||||
val networkMapUpdater = NetworkMapUpdater(
|
||||
networkMapCache,
|
||||
NodeInfoWatcher(
|
||||
@ -204,7 +215,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
/**
|
||||
* Completes once the node has successfully registered with the network map service
|
||||
* or has loaded network map data from local database
|
||||
* or has loaded network map data from local database.
|
||||
*/
|
||||
val nodeReadyFuture: CordaFuture<Unit> get() = networkMapCache.nodeReady.map { Unit }
|
||||
|
||||
@ -213,7 +224,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
/** Set to non-null once [start] has been successfully called. */
|
||||
open val started get() = _started
|
||||
open val started: S? get() = _started
|
||||
@Volatile
|
||||
private var _started: S? = null
|
||||
|
||||
@ -229,30 +240,34 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
/** The implementation of the [CordaRPCOps] interface used by this node. */
|
||||
open fun makeRPCOps(): CordaRPCOps {
|
||||
val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit { stop() } }
|
||||
val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit { stop() } }.also { it.closeOnStop() }
|
||||
val proxies = mutableListOf<(CordaRPCOps) -> CordaRPCOps>()
|
||||
// Mind that order is relevant here.
|
||||
val proxies = listOf<(CordaRPCOps) -> CordaRPCOps>(::AuthenticatedRpcOpsProxy, { ExceptionSerialisingRpcOpsProxy(it, true) })
|
||||
proxies += ::AuthenticatedRpcOpsProxy
|
||||
if (!configuration.devMode) {
|
||||
proxies += { it -> ExceptionMaskingRpcOpsProxy(it, true) }
|
||||
}
|
||||
proxies += { it -> ExceptionSerialisingRpcOpsProxy(it, configuration.devMode) }
|
||||
return proxies.fold(ops) { delegate, decorate -> decorate(delegate) }
|
||||
}
|
||||
|
||||
private fun initKeyStore(): X509Certificate {
|
||||
private fun initKeyStores(): X509Certificate {
|
||||
if (configuration.devMode) {
|
||||
configuration.configureWithDevSSLCertificate()
|
||||
}
|
||||
return validateKeyStore()
|
||||
return validateKeyStores()
|
||||
}
|
||||
|
||||
open fun generateAndSaveNodeInfo(): NodeInfo {
|
||||
check(started == null) { "Node has already been started" }
|
||||
log.info("Generating nodeInfo ...")
|
||||
val trustRoot = initKeyStore()
|
||||
val trustRoot = initKeyStores()
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
startDatabase()
|
||||
val nodeCa = configuration.loadNodeKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_CA)
|
||||
val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA]
|
||||
identityService.start(trustRoot, listOf(identity.certificate, nodeCa))
|
||||
return database.use {
|
||||
it.transaction {
|
||||
persistentNetworkMapCache.start(notaries = emptyList())
|
||||
val (_, nodeInfoAndSigned) = updateNodeInfo(identity, identityKeyPair, publish = false)
|
||||
nodeInfoAndSigned.nodeInfo
|
||||
}
|
||||
@ -264,8 +279,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
log.info("Starting clearing of network map cache entries...")
|
||||
startDatabase()
|
||||
database.use {
|
||||
persistentNetworkMapCache.start(notaries = emptyList())
|
||||
persistentNetworkMapCache.clearNetworkMapCache()
|
||||
networkMapCache.clearNetworkMapCache()
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,8 +291,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
log.info("Node starting up ...")
|
||||
|
||||
val trustRoot = initKeyStore()
|
||||
val nodeCa = configuration.loadNodeKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_CA)
|
||||
val trustRoot = initKeyStores()
|
||||
val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA]
|
||||
initialiseJVMAgents()
|
||||
|
||||
schemaService.mappedSchemasWarnings().forEach {
|
||||
@ -300,14 +314,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
"Node's platform version is lower than network's required minimumPlatformVersion"
|
||||
}
|
||||
servicesForResolution.start(netParams)
|
||||
networkMapCache.start(netParams.notaries)
|
||||
|
||||
startDatabase()
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
identityService.start(trustRoot, listOf(identity.certificate, nodeCa))
|
||||
|
||||
val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction {
|
||||
persistentNetworkMapCache.start(netParams.notaries)
|
||||
networkMapCache.start()
|
||||
updateNodeInfo(identity, identityKeyPair, publish = true)
|
||||
}
|
||||
|
||||
@ -350,7 +363,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
/** Subclasses must override this to create a [StartedNode] of the desired type, using the provided machinery. */
|
||||
/** Subclasses must override this to create a "started" node of the desired type, using the provided machinery. */
|
||||
abstract fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): S
|
||||
|
||||
private fun verifyCheckpointsCompatible(tokenizableServices: List<Any>) {
|
||||
@ -368,10 +381,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
open fun startShell() {
|
||||
if (configuration.shouldInitCrashShell()) {
|
||||
val shellConfiguration = configuration.toShellConfig()
|
||||
shellConfiguration.sshHostKeyDirectory?.let {
|
||||
shellConfiguration.sshdPort?.let {
|
||||
log.info("Binding Shell SSHD server on port $it.")
|
||||
}
|
||||
InteractiveShell.startShellInternal(shellConfiguration, cordappLoader.appClassLoader)
|
||||
InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader)
|
||||
}
|
||||
}
|
||||
|
||||
@ -455,7 +468,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
} else {
|
||||
1.days
|
||||
}
|
||||
val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater", Executors.defaultThreadFactory()))
|
||||
val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater"))
|
||||
executor.submit(object : Runnable {
|
||||
override fun run() {
|
||||
val republishInterval = try {
|
||||
@ -516,13 +529,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
/**
|
||||
* If the [serviceClass] is a notary service, it will only be enable if the "custom" flag is set in
|
||||
* If the [serviceClass] is a notary service, it will only be enabled if the "custom" flag is set in
|
||||
* the notary configuration.
|
||||
*/
|
||||
private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass)
|
||||
|
||||
/**
|
||||
* This customizes the ServiceHub for each CordaService that is initiating flows
|
||||
* This customizes the ServiceHub for each CordaService that is initiating flows.
|
||||
*/
|
||||
// TODO Move this into its own file
|
||||
private class AppServiceHubImpl<T : SerializeAsToken>(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter) : AppServiceHub, ServiceHub by serviceHub {
|
||||
@ -563,28 +576,38 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
private fun <T : SerializeAsToken> installCordaService(flowStarter: FlowStarter, serviceClass: Class<T>, myNotaryIdentity: PartyAndCertificate?) {
|
||||
serviceClass.requireAnnotation<CordaService>()
|
||||
|
||||
val service = try {
|
||||
val serviceContext = AppServiceHubImpl<T>(services, flowStarter)
|
||||
if (isNotaryService(serviceClass)) {
|
||||
myNotaryIdentity ?: throw IllegalStateException("Trying to install a notary service but no notary identity specified")
|
||||
val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
serviceContext.serviceInstance = constructor.newInstance(serviceContext, myNotaryIdentity.owningKey)
|
||||
serviceContext.serviceInstance
|
||||
} else {
|
||||
try {
|
||||
val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true }
|
||||
serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext)
|
||||
serviceContext.serviceInstance
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true }
|
||||
log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " +
|
||||
"Upgrade to an AppServiceHub parameter to enable updated API features.")
|
||||
constructor.newInstance(services)
|
||||
}
|
||||
}
|
||||
if (isNotaryService(serviceClass)) {
|
||||
myNotaryIdentity ?: throw IllegalStateException("Trying to install a notary service but no notary identity specified")
|
||||
try {
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
constructor.newInstance(services, myNotaryIdentity.owningKey )
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
val serviceContext = AppServiceHubImpl<T>(services, flowStarter)
|
||||
val service = constructor.newInstance(serviceContext, myNotaryIdentity.owningKey)
|
||||
serviceContext.serviceInstance = service
|
||||
service
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val serviceContext = AppServiceHubImpl<T>(services, flowStarter)
|
||||
val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true }
|
||||
val service = extendedServiceConstructor.newInstance(serviceContext)
|
||||
serviceContext.serviceInstance = service
|
||||
service
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true }
|
||||
log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " +
|
||||
"Upgrade to an AppServiceHub parameter to enable updated API features.")
|
||||
constructor.newInstance(services)
|
||||
}
|
||||
}
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw ServiceInstantiationException(e.cause)
|
||||
}
|
||||
|
||||
cordappServices.putInstance(serviceClass, service)
|
||||
|
||||
if (service is NotaryService) handleCustomNotaryService(service)
|
||||
@ -687,36 +710,56 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
|
||||
return DBTransactionStorage(transactionCacheSizeBytes, database)
|
||||
return DBTransactionStorage(database, cacheFactory)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected open fun acceptableLiveFiberCountOnStop(): Int = 0
|
||||
|
||||
private fun validateKeyStore(): X509Certificate {
|
||||
val containCorrectKeys = try {
|
||||
// This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect.
|
||||
val sslKeystore = configuration.loadSslKeyStore()
|
||||
val identitiesKeystore = configuration.loadNodeKeyStore()
|
||||
X509Utilities.CORDA_CLIENT_TLS in sslKeystore && X509Utilities.CORDA_CLIENT_CA in identitiesKeystore
|
||||
private fun getCertificateStores(): AllCertificateStores? {
|
||||
return try {
|
||||
// The following will throw IOException if key file not found or KeyStoreException if keystore password is incorrect.
|
||||
val sslKeyStore = configuration.p2pSslOptions.keyStore.get()
|
||||
val identitiesKeyStore = configuration.signingCertificateStore.get()
|
||||
val trustStore = configuration.p2pSslOptions.trustStore.get()
|
||||
AllCertificateStores(trustStore, sslKeyStore, identitiesKeyStore)
|
||||
} catch (e: KeyStoreException) {
|
||||
log.warn("Certificate key store found but key store password does not match configuration.")
|
||||
false
|
||||
log.warn("At least one of the keystores or truststore passwords does not match configuration.")
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
log.error("IO exception while trying to validate keystore", e)
|
||||
false
|
||||
log.error("IO exception while trying to validate keystores and truststore", e)
|
||||
null
|
||||
}
|
||||
require(containCorrectKeys) {
|
||||
"Identity certificate not found. " +
|
||||
"Please either copy your existing identity key and certificate from another node, " +
|
||||
}
|
||||
|
||||
private data class AllCertificateStores(val trustStore: CertificateStore, val sslKeyStore: CertificateStore, val identitiesKeyStore: CertificateStore)
|
||||
|
||||
private fun validateKeyStores(): X509Certificate {
|
||||
// Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist.
|
||||
val certStores = requireNotNull(getCertificateStores()) {
|
||||
"One or more keyStores (identity or TLS) or trustStore not found. " +
|
||||
"Please either copy your existing keys and certificates from another node, " +
|
||||
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
|
||||
"Read more at: https://docs.corda.net/permissioning.html"
|
||||
}
|
||||
// Step 2. Check that trustStore contains the correct key-alias entry.
|
||||
require(CORDA_ROOT_CA in certStores.trustStore) {
|
||||
"Alias for trustRoot key not found. Please ensure you have an updated trustStore file."
|
||||
}
|
||||
// Step 3. Check that tls keyStore contains the correct key-alias entry.
|
||||
require(CORDA_CLIENT_TLS in certStores.sslKeyStore) {
|
||||
"Alias for TLS key not found. Please ensure you have an updated TLS keyStore file."
|
||||
}
|
||||
|
||||
// Check all cert path chain to the trusted root
|
||||
val sslCertChainRoot = configuration.loadSslKeyStore().getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).last()
|
||||
val nodeCaCertChainRoot = configuration.loadNodeKeyStore().getCertificateChain(X509Utilities.CORDA_CLIENT_CA).last()
|
||||
val trustRoot = configuration.loadTrustStore().getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||
// Step 4. Check that identity keyStores contain the correct key-alias entry for Node CA.
|
||||
require(CORDA_CLIENT_CA in certStores.identitiesKeyStore) {
|
||||
"Alias for Node CA key not found. Please ensure you have an updated identity keyStore file."
|
||||
}
|
||||
|
||||
// Step 5. Check all cert paths chain to the trusted root.
|
||||
val trustRoot = certStores.trustStore[CORDA_ROOT_CA]
|
||||
val sslCertChainRoot = certStores.sslKeyStore.query { getCertificateChain(CORDA_CLIENT_TLS) }.last()
|
||||
val nodeCaCertChainRoot = certStores.identitiesKeyStore.query { getCertificateChain(CORDA_CLIENT_CA) }.last()
|
||||
|
||||
require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." }
|
||||
require(nodeCaCertChainRoot == trustRoot) { "Client CA certificate must chain to the trusted root." }
|
||||
@ -725,12 +768,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
// Specific class so that MockNode can catch it.
|
||||
class DatabaseConfigurationException(msg: String) : CordaException(msg)
|
||||
class DatabaseConfigurationException(message: String) : CordaException(message)
|
||||
|
||||
protected open fun startDatabase() {
|
||||
val props = configuration.dataSourceProperties
|
||||
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
|
||||
database.startHikariPool(props, configuration.database, schemaService.internalSchemas())
|
||||
database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry)
|
||||
// Now log the vendor string as this will also cause a connection to be tested eagerly.
|
||||
logVendorString(database, log)
|
||||
}
|
||||
@ -751,7 +794,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
|
||||
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
|
||||
// the identity key. But the infrastructure to make that easy isn't here yet.
|
||||
return PersistentKeyManagementService(identityService, database)
|
||||
return PersistentKeyManagementService(cacheFactory, identityService, database)
|
||||
}
|
||||
|
||||
private fun makeCoreNotaryService(notaryConfig: NotaryConfig, myNotaryIdentity: PartyAndCertificate?): NotaryService {
|
||||
@ -760,7 +803,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
return notaryConfig.run {
|
||||
when {
|
||||
raft != null -> {
|
||||
val uniquenessProvider = RaftUniquenessProvider(configuration, database, platformClock, monitoringService.metrics, raft)
|
||||
val uniquenessProvider = RaftUniquenessProvider(configuration.baseDirectory, configuration.p2pSslOptions, database, platformClock, monitoringService.metrics, cacheFactory, raft)
|
||||
(if (validating) ::RaftValidatingNotaryService else ::RaftNonValidatingNotaryService)(services, notaryKey, uniquenessProvider)
|
||||
}
|
||||
bftSMaRt != null -> {
|
||||
@ -787,7 +830,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
// Meanwhile, we let the remote service send us updates until the acknowledgment buffer overflows and it
|
||||
// unsubscribes us forcibly, rather than blocking the shutdown process.
|
||||
|
||||
// Run shutdown hooks in opposite order to starting
|
||||
// Run shutdown hooks in opposite order to starting.
|
||||
for (toRun in runOnStop.reversed()) {
|
||||
toRun()
|
||||
}
|
||||
@ -804,40 +847,40 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
networkParameters: NetworkParameters)
|
||||
|
||||
private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair<PartyAndCertificate, KeyPair> {
|
||||
val keyStore = configuration.loadNodeKeyStore()
|
||||
val keyStore = configuration.signingCertificateStore.get()
|
||||
|
||||
val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) {
|
||||
// Node's main identity or if it's a single node notary
|
||||
Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
||||
// Node's main identity or if it's a single node notary.
|
||||
Pair(NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
||||
} else {
|
||||
// The node is part of a distributed notary whose identity must already be generated beforehand.
|
||||
Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
||||
Pair(DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
||||
}
|
||||
// TODO: Integrate with Key management service?
|
||||
val privateKeyAlias = "$id-private-key"
|
||||
|
||||
if (privateKeyAlias !in keyStore) {
|
||||
singleName ?: throw IllegalArgumentException(
|
||||
"Unable to find in the key store the identity of the distributed notary the node is part of")
|
||||
log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!")
|
||||
// TODO This check shouldn't be needed
|
||||
check(singleName == configuration.myLegalName)
|
||||
// We shouldn't have a distributed notary at this stage, so singleName should NOT be null.
|
||||
requireNotNull(singleName) {
|
||||
"Unable to find in the key store the identity of the distributed notary the node is part of"
|
||||
}
|
||||
log.info("$privateKeyAlias not found in key store, generating fresh key!")
|
||||
keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair())
|
||||
}
|
||||
|
||||
val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias)
|
||||
val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) }
|
||||
|
||||
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
|
||||
val compositeKeyAlias = "$id-composite-key"
|
||||
val certificates = if (compositeKeyAlias in keyStore) {
|
||||
// Use composite key instead if it exists
|
||||
val certificate = keyStore.getCertificate(compositeKeyAlias)
|
||||
// Use composite key instead if it exists.
|
||||
val certificate = keyStore[compositeKeyAlias]
|
||||
// We have to create the certificate chain for the composite key manually, this is because we don't have a keystore
|
||||
// provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate +
|
||||
// the tail of the private key certificates, as they are both signed by the same certificate chain.
|
||||
listOf(certificate) + keyStore.getCertificateChain(privateKeyAlias).drop(1)
|
||||
listOf(certificate) + keyStore.query { getCertificateChain(privateKeyAlias) }.drop(1)
|
||||
} else {
|
||||
keyStore.getCertificateChain(privateKeyAlias).let {
|
||||
keyStore.query { getCertificateChain(privateKeyAlias) }.let {
|
||||
check(it[0] == x509Cert) { "Certificates from key store do not line up!" }
|
||||
it
|
||||
}
|
||||
@ -901,6 +944,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
override val clock: Clock get() = platformClock
|
||||
override val configuration: NodeConfiguration get() = this@AbstractNode.configuration
|
||||
override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater
|
||||
override val cacheFactory: NamedCacheFactory get() = this@AbstractNode.cacheFactory
|
||||
|
||||
private lateinit var _myInfo: NodeInfo
|
||||
override val myInfo: NodeInfo get() = _myInfo
|
||||
@ -1013,9 +1057,9 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters)
|
||||
}
|
||||
|
||||
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>) {
|
||||
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null) {
|
||||
try {
|
||||
val dataSource = DataSourceFactory.createDataSource(hikariProperties)
|
||||
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
|
||||
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig)
|
||||
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L })
|
||||
start(dataSource)
|
||||
@ -1027,4 +1071,13 @@ fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfi
|
||||
else -> throw CouldNotCreateDataSourceException("Could not create the DataSource: ${ex.message}", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSslOptions? {
|
||||
|
||||
if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) {
|
||||
return null
|
||||
}
|
||||
// Here we're using the node's RPC key store as the RPC client's trust store.
|
||||
return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword)
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
|
||||
import net.corda.core.serialization.internal.checkpointDeserialize
|
||||
import net.corda.node.services.api.CheckpointStorage
|
||||
import net.corda.node.services.statemachine.SubFlow
|
||||
import net.corda.node.services.statemachine.SubFlowVersion
|
||||
import net.corda.serialization.internal.SerializeAsTokenContextImpl
|
||||
import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl
|
||||
import net.corda.serialization.internal.withTokenContext
|
||||
|
||||
object CheckpointVerifier {
|
||||
@ -19,13 +20,13 @@ object CheckpointVerifier {
|
||||
* @throws CheckpointIncompatibleException if any offending checkpoint is found.
|
||||
*/
|
||||
fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any>) {
|
||||
val checkpointSerializationContext = SerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
|
||||
SerializeAsTokenContextImpl(tokenizableServices, SerializationDefaults.SERIALIZATION_FACTORY, SerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
|
||||
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
)
|
||||
checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) ->
|
||||
|
||||
val checkpoint = try {
|
||||
serializedCheckpoint.deserialize(context = checkpointSerializationContext)
|
||||
serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext)
|
||||
} catch (e: Exception) {
|
||||
throw CheckpointIncompatibleException.CannotBeDeserialisedException(e)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import net.corda.core.internal.RPC_UPLOADER
|
||||
import net.corda.core.internal.STRUCTURAL_STEP_PREFIX
|
||||
import net.corda.core.internal.sign
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
@ -27,17 +28,21 @@ import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.messaging.context
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.nodeapi.exceptions.NonRpcFlowException
|
||||
import net.corda.nodeapi.exceptions.RejectedCommandException
|
||||
import net.corda.nodeapi.internal.pendingFlowsCount
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import java.io.InputStream
|
||||
import java.net.ConnectException
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
|
||||
@ -48,13 +53,38 @@ internal class CordaRPCOpsImpl(
|
||||
private val smm: StateMachineManager,
|
||||
private val flowStarter: FlowStarter,
|
||||
private val shutdownNode: () -> Unit
|
||||
) : CordaRPCOps {
|
||||
) : CordaRPCOps, AutoCloseable {
|
||||
|
||||
private companion object {
|
||||
private val logger = loggerFor<CordaRPCOpsImpl>()
|
||||
}
|
||||
|
||||
private val drainingShutdownHook = AtomicReference<Subscription?>()
|
||||
|
||||
init {
|
||||
services.nodeProperties.flowsDrainingMode.values.filter { it.isDisabled() }.subscribe({
|
||||
cancelDrainingShutdownHook()
|
||||
}, {
|
||||
// Nothing to do in case of errors here.
|
||||
})
|
||||
}
|
||||
|
||||
private fun Pair<Boolean, Boolean>.isDisabled(): Boolean = first && !second
|
||||
|
||||
/**
|
||||
* Returns the RPC protocol version, which is the same the node's platform Version. Exists since version 1 so guaranteed
|
||||
* to be present.
|
||||
*/
|
||||
override val protocolVersion: Int get() = nodeInfo().platformVersion
|
||||
|
||||
override fun networkMapSnapshot(): List<NodeInfo> {
|
||||
val (snapshot, updates) = networkMapFeed()
|
||||
updates.notUsed()
|
||||
return snapshot
|
||||
}
|
||||
|
||||
override val networkParameters: NetworkParameters get() = services.networkParameters
|
||||
|
||||
override fun networkParametersFeed(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> {
|
||||
return services.networkMapUpdater.trackParametersUpdate()
|
||||
}
|
||||
@ -213,7 +243,7 @@ internal class CordaRPCOpsImpl(
|
||||
return services.networkMapCache.getNodeByLegalIdentity(party)
|
||||
}
|
||||
|
||||
override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted()
|
||||
override fun registeredFlows(): List<String> = services.rpcFlows.asSequence().map(Class<*>::getName).sorted().toList()
|
||||
|
||||
override fun clearNetworkMapCache() {
|
||||
services.networkMapCache.clearNetworkMapCache()
|
||||
@ -262,18 +292,46 @@ internal class CordaRPCOpsImpl(
|
||||
return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
|
||||
override fun setFlowsDrainingModeEnabled(enabled: Boolean) {
|
||||
services.nodeProperties.flowsDrainingMode.setEnabled(enabled)
|
||||
override fun setFlowsDrainingModeEnabled(enabled: Boolean) = setPersistentDrainingModeProperty(enabled, propagateChange = true)
|
||||
|
||||
override fun isFlowsDrainingModeEnabled() = services.nodeProperties.flowsDrainingMode.isEnabled()
|
||||
|
||||
override fun shutdown() = terminate(false)
|
||||
|
||||
override fun terminate(drainPendingFlows: Boolean) {
|
||||
|
||||
if (drainPendingFlows) {
|
||||
logger.info("Waiting for pending flows to complete before shutting down.")
|
||||
setFlowsDrainingModeEnabled(true)
|
||||
drainingShutdownHook.set(pendingFlowsCount().updates.doOnNext {(completed, total) ->
|
||||
logger.info("Pending flows progress before shutdown: $completed / $total.")
|
||||
}.doOnCompleted { setPersistentDrainingModeProperty(false, false) }.doOnCompleted(::cancelDrainingShutdownHook).doOnCompleted { logger.info("No more pending flows to drain. Shutting down.") }.doOnCompleted(shutdownNode::invoke).subscribe({
|
||||
// Nothing to do on each update here, only completion matters.
|
||||
}, { error ->
|
||||
logger.error("Error while waiting for pending flows to drain in preparation for shutdown. Cause was: ${error.message}", error)
|
||||
}))
|
||||
} else {
|
||||
shutdownNode.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isFlowsDrainingModeEnabled(): Boolean {
|
||||
return services.nodeProperties.flowsDrainingMode.isEnabled()
|
||||
override fun isWaitingForShutdown() = drainingShutdownHook.get() != null
|
||||
|
||||
override fun close() {
|
||||
|
||||
cancelDrainingShutdownHook()
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
shutdownNode.invoke()
|
||||
private fun cancelDrainingShutdownHook() {
|
||||
|
||||
drainingShutdownHook.getAndSet(null)?.let {
|
||||
it.unsubscribe()
|
||||
logger.info("Cancelled draining shutdown hook.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPersistentDrainingModeProperty(enabled: Boolean, propagateChange: Boolean) = services.nodeProperties.flowsDrainingMode.setEnabled(enabled, propagateChange)
|
||||
|
||||
private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo {
|
||||
return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.context.toFlowInitiator(), flowLogic.track(), flowLogic.stateMachine.context)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import com.zaxxer.hikari.util.PropertyElf
|
||||
@ -32,10 +33,14 @@ object DataSourceFactory {
|
||||
}.set(null, SynchronizedGetPutRemove<String, Database>())
|
||||
}
|
||||
|
||||
fun createDataSource(hikariProperties: Properties, pool: Boolean = true): DataSource {
|
||||
fun createDataSource(hikariProperties: Properties, pool: Boolean = true, metricRegistry: MetricRegistry? = null): DataSource {
|
||||
val config = HikariConfig(hikariProperties)
|
||||
return if (pool) {
|
||||
HikariDataSource(config)
|
||||
val dataSource = HikariDataSource(config)
|
||||
if (metricRegistry != null) {
|
||||
dataSource.metricRegistry = metricRegistry
|
||||
}
|
||||
dataSource
|
||||
} else {
|
||||
// Basic init for the one test that wants to go via this API but without starting a HikariPool:
|
||||
(Class.forName(hikariProperties.getProperty("dataSourceClassName")).newInstance() as DataSource).also {
|
||||
|
@ -12,4 +12,3 @@ sealed class InitiatedFlowFactory<out F : FlowLogic<*>> {
|
||||
val appName: String,
|
||||
override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationFactory
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
@ -37,29 +38,24 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser
|
||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
|
||||
import net.corda.node.serialization.kryo.KryoServerSerializationScheme
|
||||
import net.corda.node.serialization.kryo.KryoSerializationScheme
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.SecurityConfiguration
|
||||
import net.corda.node.services.config.shouldInitCrashShell
|
||||
import net.corda.node.services.config.shouldStartLocalShell
|
||||
import net.corda.node.services.config.JmxReporterType
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.rpc.ArtemisRpcBroker
|
||||
import net.corda.node.utilities.AddressUtils
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.DemoClock
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
|
||||
import net.corda.nodeapi.internal.ShutdownHook
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import net.corda.nodeapi.internal.bridging.BridgeControlListener
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.serialization.internal.*
|
||||
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
||||
import net.corda.serialization.internal.*
|
||||
import org.apache.commons.lang.SystemUtils
|
||||
import org.h2.jdbc.JdbcSQLException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -71,10 +67,10 @@ import java.net.InetAddress
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.management.ObjectName
|
||||
import kotlin.system.exitProcess
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NodeWithInfo(val node: Node, val info: NodeInfo) {
|
||||
val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {}
|
||||
@ -92,10 +88,12 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) {
|
||||
open class Node(configuration: NodeConfiguration,
|
||||
versionInfo: VersionInfo,
|
||||
private val initialiseSerialization: Boolean = true,
|
||||
cordappLoader: CordappLoader = makeCordappLoader(configuration)
|
||||
cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo),
|
||||
cacheFactoryPrototype: NamedCacheFactory = DefaultNamedCacheFactory()
|
||||
) : AbstractNode<NodeInfo>(
|
||||
configuration,
|
||||
createClock(configuration),
|
||||
cacheFactoryPrototype,
|
||||
versionInfo,
|
||||
cordappLoader,
|
||||
// Under normal (non-test execution) it will always be "1"
|
||||
@ -134,11 +132,33 @@ open class Node(configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private val sameVmNodeCounter = AtomicInteger()
|
||||
private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader {
|
||||
return JarScanningCordappLoader.fromDirectories(configuration.cordappDirectories)
|
||||
|
||||
private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
|
||||
return JarScanningCordappLoader.fromDirectories(configuration.cordappDirectories, versionInfo)
|
||||
}
|
||||
|
||||
// TODO: make this configurable.
|
||||
const val MAX_RPC_MESSAGE_SIZE = 10485760
|
||||
|
||||
fun isValidJavaVersion(): Boolean {
|
||||
if (!hasMinimumJavaVersion()) {
|
||||
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
|
||||
println("Corda will now exit...")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasMinimumJavaVersion(): Boolean {
|
||||
// when the ext.java8_minUpdateVersion gradle constant changes, so must this check
|
||||
val major = SystemUtils.JAVA_VERSION_FLOAT
|
||||
return try {
|
||||
val update = SystemUtils.JAVA_VERSION.substringAfter("_").toLong()
|
||||
major == 1.8F && update >= 171
|
||||
} catch (e: NumberFormatException) { // custom JDKs may not have the update version (e.g. 1.8.0-adoptopenjdk)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val log: Logger get() = staticLog
|
||||
@ -196,7 +216,9 @@ open class Node(configuration: NodeConfiguration,
|
||||
database = database,
|
||||
networkMap = networkMapCache,
|
||||
isDrainingModeOn = nodeProperties.flowsDrainingMode::isEnabled,
|
||||
drainingModeWasChangedEvents = nodeProperties.flowsDrainingMode.values
|
||||
drainingModeWasChangedEvents = nodeProperties.flowsDrainingMode.values,
|
||||
metricRegistry = metricRegistry,
|
||||
cacheFactory = cacheFactory
|
||||
)
|
||||
}
|
||||
|
||||
@ -227,12 +249,12 @@ open class Node(configuration: NodeConfiguration,
|
||||
startLocalRpcBroker(securityManager)
|
||||
}
|
||||
|
||||
val bridgeControlListener = BridgeControlListener(configuration, network.serverAddress, networkParameters.maxMessageSize)
|
||||
val bridgeControlListener = BridgeControlListener(configuration.p2pSslOptions, network.serverAddress, networkParameters.maxMessageSize)
|
||||
|
||||
printBasicNodeInfo("Advertised P2P messaging addresses", nodeInfo.addresses.joinToString())
|
||||
val rpcServerConfiguration = RPCServerConfiguration.DEFAULT
|
||||
rpcServerAddresses?.let {
|
||||
internalRpcMessagingClient = InternalRPCMessagingClient(configuration, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.loadSslKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal), rpcServerConfiguration)
|
||||
internalRpcMessagingClient = InternalRPCMessagingClient(configuration.p2pSslOptions, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.p2pSslOptions.keyStore.get()[X509Utilities.CORDA_CLIENT_TLS].subjectX500Principal), rpcServerConfiguration)
|
||||
printBasicNodeInfo("RPC connection address", it.primary.toString())
|
||||
printBasicNodeInfo("RPC admin connection address", it.admin.toString())
|
||||
}
|
||||
@ -271,18 +293,17 @@ open class Node(configuration: NodeConfiguration,
|
||||
val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc"
|
||||
with(rpcOptions) {
|
||||
rpcBroker = if (useSsl) {
|
||||
ArtemisRpcBroker.withSsl(configuration, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell())
|
||||
ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell())
|
||||
} else {
|
||||
ArtemisRpcBroker.withoutSsl(configuration, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell())
|
||||
ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell())
|
||||
}
|
||||
}
|
||||
rpcBroker!!.closeOnStop()
|
||||
rpcBroker!!.addresses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun myAddresses(): List<NetworkHostAndPort> = listOf(getAdvertisedAddress())
|
||||
override fun myAddresses(): List<NetworkHostAndPort> = listOf(getAdvertisedAddress()) + configuration.additionalP2PAddresses
|
||||
|
||||
private fun getAdvertisedAddress(): NetworkHostAndPort {
|
||||
return with(configuration) {
|
||||
@ -420,12 +441,13 @@ open class Node(configuration: NodeConfiguration,
|
||||
// https://jolokia.org/agent/jvm.html
|
||||
JmxReporter.forRegistry(registry).inDomain("net.corda").createsObjectNamesWith { _, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val category = name.substringBefore('.').substringBeforeLast('/')
|
||||
val component = name.substringBefore('.').substringAfterLast('/', "")
|
||||
val subName = name.substringAfter('.', "")
|
||||
if (subName == "")
|
||||
ObjectName("$domain:name=$category")
|
||||
(if (subName == "")
|
||||
ObjectName("$domain:name=$category${if (component.isNotEmpty()) ",component=$component," else ""}")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
ObjectName("$domain:type=$category,${if (component.isNotEmpty()) "component=$component," else ""}name=$subName"))
|
||||
}.build().start()
|
||||
}
|
||||
|
||||
@ -452,8 +474,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps))
|
||||
registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps))
|
||||
registerScheme(KryoServerSerializationScheme())
|
||||
},
|
||||
checkpointSerializationFactory = CheckpointSerializationFactory(KryoSerializationScheme),
|
||||
p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
|
||||
rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader),
|
||||
storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
|
||||
@ -489,4 +511,4 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
log.info("Shutdown complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import io.netty.channel.unix.Errors
|
||||
import joptsimple.OptionParser
|
||||
import joptsimple.util.PathConverter
|
||||
import net.corda.cliutils.CordaCliWrapper
|
||||
import net.corda.cliutils.CordaVersionProvider
|
||||
import net.corda.cliutils.ExitCodes
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.errors.AddressBindingException
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.*
|
||||
import net.corda.node.internal.Node.Companion.isValidJavaVersion
|
||||
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfigurationImpl
|
||||
@ -22,18 +24,19 @@ import net.corda.node.services.config.shouldStartSSHDaemon
|
||||
import net.corda.node.services.transactions.bftSMaRtSerialFilter
|
||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NodeRegistrationHelper
|
||||
import net.corda.node.utilities.registration.NodeRegistrationException
|
||||
import net.corda.node.utilities.registration.NodeRegistrationHelper
|
||||
import net.corda.node.utilities.saveToKeyStore
|
||||
import net.corda.node.utilities.saveToTrustStore
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
|
||||
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.fusesource.jansi.AnsiConsole
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||
import picocli.CommandLine.Mixin
|
||||
import sun.misc.VMSupport
|
||||
import java.io.Console
|
||||
import java.io.File
|
||||
@ -42,14 +45,13 @@ import java.io.RandomAccessFile
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.net.InetAddress
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.DayOfWeek
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/** This class is responsible for starting a Node from command line arguments. */
|
||||
open class NodeStartup(val args: Array<String>) {
|
||||
open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
|
||||
const val LOGS_DIRECTORY_NAME = "logs"
|
||||
@ -57,72 +59,84 @@ open class NodeStartup(val args: Array<String>) {
|
||||
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
|
||||
}
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = NodeCmdLineOptions()
|
||||
|
||||
/**
|
||||
* @return true if the node startup was successful. This value is intended to be the exit code of the process.
|
||||
* @return exit code based on the success of the node startup. This value is intended to be the exit code of the process.
|
||||
*/
|
||||
open fun run(): Boolean {
|
||||
override fun runProgram(): Int {
|
||||
val startTime = System.currentTimeMillis()
|
||||
if (!canNormalizeEmptyPath()) {
|
||||
println("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.")
|
||||
println("Corda will now exit...")
|
||||
return false
|
||||
}
|
||||
// Step 1. Check for supported Java version.
|
||||
if (!isValidJavaVersion()) return ExitCodes.FAILURE
|
||||
|
||||
val registrationMode = checkRegistrationMode()
|
||||
val cmdlineOptions: CmdLineOptions = if (registrationMode && !args.contains("--initial-registration")) {
|
||||
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
|
||||
// Pretend that the node was started with `--initial-registration` to help prevent user error.
|
||||
NodeArgsParser().parseOrExit(*args.plus("--initial-registration"))
|
||||
} else {
|
||||
NodeArgsParser().parseOrExit(*args)
|
||||
}
|
||||
// We do the single node check before we initialise logging so that in case of a double-node start it
|
||||
// Step 2. We do the single node check before we initialise logging so that in case of a double-node start it
|
||||
// doesn't mess with the running node's logs.
|
||||
enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory)
|
||||
enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory)
|
||||
|
||||
initLogging(cmdlineOptions)
|
||||
// Register all cryptography [Provider]s.
|
||||
// Step 3. Initialise logging.
|
||||
initLogging()
|
||||
|
||||
// Step 4. Register all cryptography [Provider]s.
|
||||
// Required to install our [SecureRandom] before e.g., UUID asks for one.
|
||||
// This needs to go after initLogging(netty clashes with our logging).
|
||||
Crypto.registerProviders()
|
||||
|
||||
// Step 5. Print banner and basic node info.
|
||||
val versionInfo = getVersionInfo()
|
||||
|
||||
if (cmdlineOptions.isVersion) {
|
||||
println("${versionInfo.vendor} ${versionInfo.releaseVersion}")
|
||||
println("Revision ${versionInfo.revision}")
|
||||
println("Platform Version ${versionInfo.platformVersion}")
|
||||
return true
|
||||
}
|
||||
|
||||
drawBanner(versionInfo)
|
||||
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
|
||||
|
||||
val configuration = (attempt { loadConfiguration(cmdlineOptions) }.doOnException(handleConfigurationLoadingError(cmdlineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return false
|
||||
|
||||
// Step 6. Load and validate node configuration.
|
||||
val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return ExitCodes.FAILURE
|
||||
val errors = configuration.validate()
|
||||
if (errors.isNotEmpty()) {
|
||||
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
|
||||
return false
|
||||
return ExitCodes.FAILURE
|
||||
}
|
||||
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return false
|
||||
// Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE
|
||||
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return false
|
||||
// Step 8. Any actions required before starting up the Corda network layer.
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
|
||||
cmdlineOptions.nodeRegistrationOption?.let {
|
||||
// Step 9. Check if in registration mode.
|
||||
checkAndRunRegistrationMode(configuration, versionInfo)?.let {
|
||||
return if (it) ExitCodes.SUCCESS
|
||||
else ExitCodes.FAILURE
|
||||
}
|
||||
|
||||
// Step 10. Log startup info.
|
||||
logStartupInfo(versionInfo, configuration)
|
||||
|
||||
// Step 11. Start node: create the node, check for other command-line options, add extra logging etc.
|
||||
attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
}
|
||||
|
||||
private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? {
|
||||
checkUnfinishedRegistration()
|
||||
cmdLineOptions.nodeRegistrationOption?.let {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
attempt { registerWithNetwork(configuration, versionInfo, cmdlineOptions.nodeRegistrationOption) }.doOnException(handleRegistrationError) as? Try.Success ?: return false
|
||||
|
||||
attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success
|
||||
?: return false
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory)
|
||||
deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory)
|
||||
return true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
logStartupInfo(versionInfo, cmdlineOptions, configuration)
|
||||
|
||||
return attempt { startNode(configuration, versionInfo, startTime, cmdlineOptions) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError).isSuccess
|
||||
// TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration.
|
||||
// There might be cases where the node user should investigate what went wrong before registering again.
|
||||
private fun checkUnfinishedRegistration() {
|
||||
if (checkRegistrationMode() && !cmdLineOptions.isRegistration) {
|
||||
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
|
||||
// Pretend that the node was started with `--initial-registration` to help prevent user error.
|
||||
cmdLineOptions.isRegistration = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
|
||||
@ -131,27 +145,12 @@ open class NodeStartup(val args: Array<String>) {
|
||||
|
||||
private val startNodeExpectedErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
|
||||
|
||||
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print("$message [errorCode=${errorCode()}]")
|
||||
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
|
||||
|
||||
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""} [errorCode=${errorCode()}]", error)
|
||||
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
|
||||
|
||||
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
|
||||
|
||||
private fun Exception.errorCode(): String {
|
||||
|
||||
val hash = staticLocationBasedHash()
|
||||
return Integer.toOctalString(hash)
|
||||
}
|
||||
|
||||
private fun Throwable.staticLocationBasedHash(visited: Set<Throwable> = setOf(this)): Int {
|
||||
|
||||
val cause = this.cause
|
||||
return when {
|
||||
cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace, cause.staticLocationBasedHash(visited + cause))
|
||||
else -> Objects.hash(this::class.java.name, stackTrace)
|
||||
}
|
||||
}
|
||||
|
||||
private val handleRegistrationError = { error: Exception ->
|
||||
when (error) {
|
||||
is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.")
|
||||
@ -186,14 +185,13 @@ open class NodeStartup(val args: Array<String>) {
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun loadConfiguration(cmdlineOptions: CmdLineOptions): NodeConfiguration {
|
||||
|
||||
val (rawConfig, configurationResult) = loadConfigFile(cmdlineOptions)
|
||||
if (cmdlineOptions.devMode) {
|
||||
private fun loadConfiguration(): NodeConfiguration {
|
||||
val (rawConfig, configurationResult) = loadConfigFile()
|
||||
if (cmdLineOptions.devMode == true) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
val configuration = configurationResult.getOrThrow()
|
||||
return if (cmdlineOptions.bootstrapRaftCluster) {
|
||||
return if (cmdLineOptions.bootstrapRaftCluster) {
|
||||
println("Bootstrapping raft cluster (starting up as seed node).")
|
||||
// Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining.
|
||||
(configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList())))
|
||||
@ -203,31 +201,14 @@ open class NodeStartup(val args: Array<String>) {
|
||||
}
|
||||
|
||||
private fun checkRegistrationMode(): Boolean {
|
||||
// Parse the command line args just to get the base directory. The base directory is needed to determine
|
||||
// if the node registration marker file exists, _before_ we call NodeArgsParser.parse().
|
||||
// If it does exist, we call NodeArgsParser with `--initial-registration` added to the argument list. This way
|
||||
// we make sure that the initial registration is completed, even if the node was restarted before the first
|
||||
// attempt to register succeeded and the node administrator forgets to specify `--initial-registration` upon
|
||||
// restart.
|
||||
val optionParser = OptionParser()
|
||||
optionParser.allowsUnrecognizedOptions()
|
||||
val baseDirectoryArg = optionParser
|
||||
.accepts("base-directory", "The node working directory where all the files are kept")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter())
|
||||
.defaultsTo(Paths.get("."))
|
||||
val isRegistrationArg =
|
||||
optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
|
||||
val optionSet = optionParser.parse(*args)
|
||||
val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
|
||||
// If the node was started with `--initial-registration`, create marker file.
|
||||
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
|
||||
val marker = File((baseDirectory / INITIAL_REGISTRATION_MARKER).toUri())
|
||||
if (!optionSet.has(isRegistrationArg) && !marker.exists()) {
|
||||
val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER
|
||||
if (!cmdLineOptions.isRegistration && !marker.exists()) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
marker.createNewFile()
|
||||
marker.createFile()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not create marker file for `--initial-registration`.", e)
|
||||
}
|
||||
@ -249,80 +230,20 @@ open class NodeStartup(val args: Array<String>) {
|
||||
|
||||
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
|
||||
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) {
|
||||
|
||||
cmdlineOptions.baseDirectory.createDirectories()
|
||||
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
val node = createNode(conf, versionInfo)
|
||||
if (cmdlineOptions.clearNetworkMapCache) {
|
||||
if (cmdLineOptions.clearNetworkMapCache) {
|
||||
node.clearNetworkMapCache()
|
||||
return
|
||||
}
|
||||
if (cmdlineOptions.justGenerateNodeInfo) {
|
||||
if (cmdLineOptions.justGenerateNodeInfo) {
|
||||
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
|
||||
node.generateAndSaveNodeInfo()
|
||||
return
|
||||
}
|
||||
if (cmdlineOptions.justGenerateRpcSslCerts) {
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
|
||||
|
||||
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
|
||||
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
|
||||
|
||||
if (keyStorePath.exists() || trustStorePath.exists()) {
|
||||
println("Found existing RPC SSL keystores. Command was already run. Exiting..")
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
val console: Console? = System.console()
|
||||
|
||||
when (console) {
|
||||
// In this case, the JVM is not connected to the console so we need to exit
|
||||
null -> {
|
||||
println("Not connected to console. Exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
// Otherwise we can proceed normally
|
||||
else -> {
|
||||
while (true) {
|
||||
val keystorePassword1 = console.readPassword("Enter the keystore password => ")
|
||||
val keystorePassword2 = console.readPassword("Re-enter the keystore password => ")
|
||||
if (!keystorePassword1.contentEquals(keystorePassword2)) {
|
||||
println("The keystore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
|
||||
println("The keystore was saved to: $keyStorePath .")
|
||||
break
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val trustStorePassword1 = console.readPassword("Enter the truststore password => ")
|
||||
val trustStorePassword2 = console.readPassword("Re-enter the truststore password => ")
|
||||
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
|
||||
println("The truststore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
|
||||
println("The truststore was saved to: $trustStorePath .")
|
||||
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
|
||||
break
|
||||
}
|
||||
|
||||
val dollar = '$'
|
||||
println("""
|
||||
|
|
||||
|The SSL certificates were generated successfully.
|
||||
|
|
||||
|Add this snippet to the "rpcSettings" section of your node.conf:
|
||||
| useSsl=true
|
||||
| ssl {
|
||||
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
|
||||
| keyStorePassword=the_above_password
|
||||
| }
|
||||
|""".trimMargin())
|
||||
}
|
||||
}
|
||||
if (cmdLineOptions.justGenerateRpcSslCerts) {
|
||||
generateRpcSslCertificates(conf)
|
||||
return
|
||||
}
|
||||
|
||||
@ -335,8 +256,10 @@ open class NodeStartup(val args: Array<String>) {
|
||||
}
|
||||
|
||||
val nodeInfo = node.start()
|
||||
Node.printBasicNodeInfo("Loaded CorDapps", node.services.cordappProvider.cordapps.joinToString { it.name })
|
||||
logLoadedCorDapps(node.services.cordappProvider.cordapps)
|
||||
|
||||
node.nodeReadyFuture.thenMatch({
|
||||
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
|
||||
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
|
||||
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
|
||||
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
|
||||
@ -361,7 +284,83 @@ open class NodeStartup(val args: Array<String>) {
|
||||
node.run()
|
||||
}
|
||||
|
||||
protected open fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: NodeConfiguration) {
|
||||
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
|
||||
|
||||
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
|
||||
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
|
||||
|
||||
if (keyStorePath.exists() || trustStorePath.exists()) {
|
||||
println("Found existing RPC SSL keystores. Command was already run. Exiting..")
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
val console: Console? = System.console()
|
||||
|
||||
when (console) {
|
||||
// In this case, the JVM is not connected to the console so we need to exit.
|
||||
null -> {
|
||||
println("Not connected to console. Exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
// Otherwise we can proceed normally.
|
||||
else -> {
|
||||
while (true) {
|
||||
val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (keystorePassword1.isEmpty()) {
|
||||
println("The RPC keystore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ")
|
||||
if (!keystorePassword1.contentEquals(keystorePassword2)) {
|
||||
println("The RPC keystore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
|
||||
println("The RPC keystore was saved to: $keyStorePath .")
|
||||
break
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (trustStorePassword1.isEmpty()) {
|
||||
println("The RPC truststore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ")
|
||||
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
|
||||
println("The RPC truststore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
|
||||
println("The RPC truststore was saved to: $trustStorePath .")
|
||||
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
|
||||
break
|
||||
}
|
||||
|
||||
val dollar = '$'
|
||||
println("""
|
||||
|
|
||||
|The SSL certificates for RPC were generated successfully.
|
||||
|
|
||||
|Add this snippet to the "rpcSettings" section of your node.conf:
|
||||
| useSsl=true
|
||||
| ssl {
|
||||
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
|
||||
| keyStorePassword=the_above_password
|
||||
| }
|
||||
|""".trimMargin())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) {
|
||||
logger.info("Vendor: ${versionInfo.vendor}")
|
||||
logger.info("Release: ${versionInfo.releaseVersion}")
|
||||
logger.info("Platform Version: ${versionInfo.platformVersion}")
|
||||
@ -370,12 +369,11 @@ open class NodeStartup(val args: Array<String>) {
|
||||
logger.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this
|
||||
logger.info("Main class: ${NodeConfiguration::class.java.location.toURI().path}")
|
||||
logger.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
|
||||
logger.info("Application Args: ${args.joinToString(" ")}")
|
||||
logger.info("bootclasspath: ${info.bootClassPath}")
|
||||
logger.info("classpath: ${info.classPath}")
|
||||
logger.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
|
||||
logger.info("Machine: ${lookupMachineNameAndMaybeWarn()}")
|
||||
logger.info("Working Directory: ${cmdlineOptions.baseDirectory}")
|
||||
logger.info("Working Directory: ${cmdLineOptions.baseDirectory}")
|
||||
val agentProperties = VMSupport.getAgentProperties()
|
||||
if (agentProperties.containsKey("sun.jdwp.listenerAddress")) {
|
||||
logger.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}")
|
||||
@ -403,24 +401,32 @@ open class NodeStartup(val args: Array<String>) {
|
||||
println("Corda node will now terminate.")
|
||||
}
|
||||
|
||||
protected open fun loadConfigFile(cmdlineOptions: CmdLineOptions): Pair<Config, Try<NodeConfiguration>> = cmdlineOptions.loadConfig()
|
||||
protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig()
|
||||
|
||||
protected open fun banJavaSerialisation(conf: NodeConfiguration) {
|
||||
SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter)
|
||||
}
|
||||
|
||||
protected open fun getVersionInfo(): VersionInfo {
|
||||
// Manifest properties are only available if running from the corda jar
|
||||
fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
|
||||
|
||||
return VersionInfo(
|
||||
manifestValue("Corda-Platform-Version")?.toInt() ?: 1,
|
||||
manifestValue("Corda-Release-Version") ?: "Unknown",
|
||||
manifestValue("Corda-Revision") ?: "Unknown",
|
||||
manifestValue("Corda-Vendor") ?: "Unknown"
|
||||
PLATFORM_VERSION,
|
||||
CordaVersionProvider.releaseVersion,
|
||||
CordaVersionProvider.revision,
|
||||
CordaVersionProvider.vendor
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun logLoadedCorDapps(corDapps: List<CordappImpl>) {
|
||||
fun CordappImpl.Info.description() = "$shortName version $version by $vendor"
|
||||
|
||||
Node.printBasicNodeInfo("Loaded ${corDapps.size} CorDapp(s)", corDapps.map { it.info }.joinToString(", ", transform = CordappImpl.Info::description))
|
||||
corDapps.map { it.info }.filter { it.hasUnknownFields() }.let { malformed ->
|
||||
if (malformed.isNotEmpty()) {
|
||||
logger.warn("Found ${malformed.size} CorDapp(s) with unknown information. They will be unable to run on Corda in the future.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enforceSingleNodeIsRunning(baseDirectory: Path) {
|
||||
// Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
|
||||
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
|
||||
@ -454,14 +460,14 @@ open class NodeStartup(val args: Array<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun initLogging(cmdlineOptions: CmdLineOptions) {
|
||||
val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH)
|
||||
override fun initLogging() {
|
||||
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
|
||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||
if (cmdlineOptions.logToConsole) {
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLogLevel", loggingLevel)
|
||||
Node.renderBasicInfoToConsole = false
|
||||
}
|
||||
System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
|
||||
System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
||||
SLF4JBridgeHandler.install()
|
||||
}
|
||||
@ -491,20 +497,7 @@ open class NodeStartup(val args: Array<String>) {
|
||||
return hostName
|
||||
}
|
||||
|
||||
private fun canNormalizeEmptyPath(): Boolean {
|
||||
// Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83
|
||||
return try {
|
||||
Paths.get("").normalize()
|
||||
true
|
||||
} catch (e: ArrayIndexOutOfBoundsException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
open fun drawBanner(versionInfo: VersionInfo) {
|
||||
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
|
||||
AnsiConsole.systemInstall()
|
||||
|
||||
Emoji.renderIfSupported {
|
||||
val messages = arrayListOf(
|
||||
"The only distributed ledger that pays\nhomage to Pac Man in its logo.",
|
||||
@ -581,3 +574,4 @@ open class NodeStartup(val args: Array<String>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.cordapp.CordappContext
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.createCordappContext
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
@ -34,7 +35,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
|
||||
/**
|
||||
* Current known CorDapps loaded on this node
|
||||
*/
|
||||
override val cordapps get() = cordappLoader.cordapps
|
||||
override val cordapps: List<CordappImpl> get() = cordappLoader.cordapps
|
||||
|
||||
fun start(whitelistedContractImplementations: Map<String, List<AttachmentId>>) {
|
||||
cordappAttachments.putAll(loadContractsIntoAttachmentStore())
|
||||
|
@ -3,8 +3,9 @@ package net.corda.node.internal.cordapp
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
|
||||
interface CordappProviderInternal : CordappProvider {
|
||||
val cordapps: List<Cordapp>
|
||||
val cordapps: List<CordappImpl>
|
||||
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
|
||||
}
|
||||
|
@ -9,12 +9,14 @@ import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.nodeapi.internal.coreContractClasses
|
||||
@ -25,6 +27,7 @@ import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.streams.toList
|
||||
|
||||
@ -33,9 +36,10 @@ import kotlin.streams.toList
|
||||
*
|
||||
* @property cordappJarPaths The classpath of cordapp JARs
|
||||
*/
|
||||
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>) : CordappLoaderTemplate() {
|
||||
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() {
|
||||
|
||||
override val cordapps: List<Cordapp> by lazy { loadCordapps() + coreCordapp }
|
||||
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + coreCordapp }
|
||||
|
||||
override val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader)
|
||||
|
||||
@ -55,10 +59,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
*
|
||||
* @param corDappDirectories Directories used to scan for CorDapp JARs.
|
||||
*/
|
||||
fun fromDirectories(corDappDirectories: Iterable<Path>): CordappLoader {
|
||||
|
||||
fun fromDirectories(corDappDirectories: Iterable<Path>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader {
|
||||
logger.info("Looking for CorDapps in ${corDappDirectories.distinct().joinToString(", ", "[", "]")}")
|
||||
return JarScanningCordappLoader(corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() })
|
||||
return JarScanningCordappLoader(corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }, versionInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,7 +69,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
*
|
||||
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
|
||||
*/
|
||||
fun fromJarUrls(scanJars: List<URL>) = JarScanningCordappLoader(scanJars.map { it.restricted() })
|
||||
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader {
|
||||
return JarScanningCordappLoader(scanJars.map { it.restricted() }, versionInfo)
|
||||
}
|
||||
|
||||
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
|
||||
|
||||
@ -87,31 +92,42 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
ContractUpgradeFlow.Initiate::class.java,
|
||||
ContractUpgradeFlow.Authorise::class.java,
|
||||
ContractUpgradeFlow.Deauthorise::class.java)
|
||||
|
||||
/** A Cordapp representing the core package which is not scanned automatically. */
|
||||
@VisibleForTesting
|
||||
internal val coreCordapp = CordappImpl(
|
||||
contractClassNames = listOf(),
|
||||
initiatedFlows = listOf(),
|
||||
rpcFlows = coreRPCFlows,
|
||||
serviceFlows = listOf(),
|
||||
schedulableFlows = listOf(),
|
||||
services = listOf(),
|
||||
serializationWhitelists = listOf(),
|
||||
serializationCustomSerializers = listOf(),
|
||||
customSchemas = setOf(),
|
||||
allFlows = listOf(),
|
||||
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
|
||||
jarHash = SecureHash.allOnesHash
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadCordapps(): List<Cordapp> {
|
||||
return cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
|
||||
/** A Cordapp representing the core package which is not scanned automatically. */
|
||||
@VisibleForTesting
|
||||
internal val coreCordapp = CordappImpl(
|
||||
contractClassNames = listOf(),
|
||||
initiatedFlows = listOf(),
|
||||
rpcFlows = coreRPCFlows,
|
||||
serviceFlows = listOf(),
|
||||
schedulableFlows = listOf(),
|
||||
services = listOf(),
|
||||
serializationWhitelists = listOf(),
|
||||
serializationCustomSerializers = listOf(),
|
||||
customSchemas = setOf(),
|
||||
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion),
|
||||
allFlows = listOf(),
|
||||
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
|
||||
jarHash = SecureHash.allOnesHash
|
||||
)
|
||||
|
||||
private fun loadCordapps(): List<CordappImpl> {
|
||||
val cordapps = cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
|
||||
.filter {
|
||||
if (it.info.minimumPlatformVersion > versionInfo.platformVersion) {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) }
|
||||
return cordapps
|
||||
}
|
||||
|
||||
private fun RestrictedScanResult.toCordapp(url: RestrictedURL): Cordapp {
|
||||
|
||||
private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl {
|
||||
val info = url.url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(CordappImpl.jarName(url.url))
|
||||
return CordappImpl(
|
||||
findContractClassNames(this),
|
||||
findInitiatedFlows(this),
|
||||
@ -124,6 +140,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
findCustomSchemas(this),
|
||||
findAllFlows(this),
|
||||
url.url,
|
||||
info,
|
||||
getJarHash(url.url)
|
||||
)
|
||||
}
|
||||
@ -273,26 +290,23 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
class MultipleCordappsForFlowException(message: String) : Exception(message)
|
||||
|
||||
abstract class CordappLoaderTemplate : CordappLoader {
|
||||
|
||||
override val flowCordappMap: Map<Class<out FlowLogic<*>>, Cordapp> by lazy {
|
||||
cordapps.flatMap { corDapp -> corDapp.allFlows.map { flow -> flow to corDapp } }
|
||||
.groupBy { it.first }
|
||||
.mapValues {
|
||||
if (it.value.size > 1) {
|
||||
throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].")
|
||||
.mapValues { entry ->
|
||||
if (entry.value.size > 1) {
|
||||
throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow " +
|
||||
"${entry.value.first().first.name}: [ ${entry.value.joinToString { it.second.name }} ].")
|
||||
}
|
||||
it.value.single().second
|
||||
entry.value.single().second
|
||||
}
|
||||
}
|
||||
|
||||
override val cordappSchemas: Set<MappedSchema> by lazy {
|
||||
|
||||
cordapps.flatMap { it.customSchemas }.toSet()
|
||||
}
|
||||
|
||||
override val appClassLoader: ClassLoader by lazy {
|
||||
|
||||
URLClassLoader(cordapps.stream().map { it.jarPath }.toTypedArray(), javaClass.classLoader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import java.util.*
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.Manifest
|
||||
|
||||
fun createTestManifest(name: String, title: String, version: String, vendor: String): Manifest {
|
||||
|
||||
val manifest = Manifest()
|
||||
|
||||
// Mandatory manifest attribute. If not present, all other entries are silently skipped.
|
||||
@ -27,21 +24,22 @@ fun createTestManifest(name: String, title: String, version: String, vendor: Str
|
||||
}
|
||||
|
||||
operator fun Manifest.set(key: String, value: String) {
|
||||
|
||||
mainAttributes.putValue(key, value)
|
||||
}
|
||||
|
||||
internal fun Manifest?.toCordappInfo(defaultShortName: String): Cordapp.Info {
|
||||
|
||||
var unknown = CordappImpl.Info.UNKNOWN
|
||||
fun Manifest?.toCordappInfo(defaultShortName: String): CordappImpl.Info {
|
||||
var info = CordappImpl.Info.UNKNOWN
|
||||
(this?.mainAttributes?.getValue("Name") ?: defaultShortName).let { shortName ->
|
||||
unknown = unknown.copy(shortName = shortName)
|
||||
info = info.copy(shortName = shortName)
|
||||
}
|
||||
this?.mainAttributes?.getValue("Implementation-Vendor")?.let { vendor ->
|
||||
unknown = unknown.copy(vendor = vendor)
|
||||
info = info.copy(vendor = vendor)
|
||||
}
|
||||
this?.mainAttributes?.getValue("Implementation-Version")?.let { version ->
|
||||
unknown = unknown.copy(version = version)
|
||||
info = info.copy(version = version)
|
||||
}
|
||||
return unknown
|
||||
val minPlatformVersion = this?.mainAttributes?.getValue("Min-Platform-Version")?.toInt() ?: 1
|
||||
val targetPlatformVersion = this?.mainAttributes?.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion
|
||||
info = info.copy(minimumPlatformVersion = minPlatformVersion, targetPlatformVersion = targetPlatformVersion)
|
||||
return info
|
||||
}
|
@ -16,6 +16,8 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor
|
||||
/**
|
||||
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
|
||||
* to be present.
|
||||
*
|
||||
* TODO: Why is this logic duplicated vs the actual implementation?
|
||||
*/
|
||||
override val protocolVersion: Int get() = delegate.nodeInfo().platformVersion
|
||||
|
||||
@ -31,7 +33,6 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor
|
||||
|
||||
private companion object {
|
||||
private fun proxy(delegate: CordaRPCOps, context: () -> RpcAuthContext): CordaRPCOps {
|
||||
|
||||
val handler = PermissionsEnforcingInvocationHandler(delegate, context)
|
||||
return Proxy.newProxyInstance(delegate::class.java.classLoader, arrayOf(CordaRPCOps::class.java), handler) as CordaRPCOps
|
||||
}
|
||||
|
@ -0,0 +1,125 @@
|
||||
package net.corda.node.internal.rpc.proxies
|
||||
|
||||
import net.corda.core.ClientRelevantError
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.doOnError
|
||||
import net.corda.core.flows.IdentifiableException
|
||||
import net.corda.core.internal.concurrent.doOnError
|
||||
import net.corda.core.internal.concurrent.mapError
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.mapErrors
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.messaging.FlowHandle
|
||||
import net.corda.core.messaging.FlowHandleImpl
|
||||
import net.corda.core.messaging.FlowProgressHandle
|
||||
import net.corda.core.messaging.FlowProgressHandleImpl
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.InvocationHandlerTemplate
|
||||
import net.corda.nodeapi.exceptions.InternalNodeException
|
||||
import rx.Observable
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy.newProxyInstance
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal class ExceptionMaskingRpcOpsProxy(private val delegate: CordaRPCOps, doLog: Boolean) : CordaRPCOps by proxy(delegate, doLog) {
|
||||
private companion object {
|
||||
private val logger = loggerFor<ExceptionMaskingRpcOpsProxy>()
|
||||
|
||||
private val whitelist = setOf(
|
||||
ClientRelevantError::class,
|
||||
TransactionVerificationException::class
|
||||
)
|
||||
|
||||
private fun proxy(delegate: CordaRPCOps, doLog: Boolean): CordaRPCOps {
|
||||
val handler = ErrorObfuscatingInvocationHandler(delegate, whitelist, doLog)
|
||||
return newProxyInstance(delegate::class.java.classLoader, arrayOf(CordaRPCOps::class.java), handler) as CordaRPCOps
|
||||
}
|
||||
}
|
||||
|
||||
private class ErrorObfuscatingInvocationHandler(override val delegate: CordaRPCOps, private val whitelist: Set<KClass<*>>, private val doLog: Boolean) : InvocationHandlerTemplate {
|
||||
override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): Any? {
|
||||
try {
|
||||
val result = super.invoke(proxy, method, arguments)
|
||||
return result?.let { obfuscateResult(it) }
|
||||
} catch (exception: Exception) {
|
||||
// In this special case logging and re-throwing is the right approach.
|
||||
log(exception)
|
||||
throw obfuscate(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <RESULT : Any> obfuscateResult(result: RESULT): Any {
|
||||
return when (result) {
|
||||
is CordaFuture<*> -> wrapFuture(result)
|
||||
is DataFeed<*, *> -> wrapFeed(result)
|
||||
is FlowProgressHandle<*> -> wrapFlowProgressHandle(result)
|
||||
is FlowHandle<*> -> wrapFlowHandle(result)
|
||||
is Observable<*> -> wrapObservable(result)
|
||||
else -> result
|
||||
}
|
||||
}
|
||||
|
||||
private fun wrapFlowProgressHandle(handle: FlowProgressHandle<*>): FlowProgressHandle<*> {
|
||||
val returnValue = wrapFuture(handle.returnValue)
|
||||
val progress = wrapObservable(handle.progress)
|
||||
val stepsTreeIndexFeed = handle.stepsTreeIndexFeed?.let { wrapFeed(it) }
|
||||
val stepsTreeFeed = handle.stepsTreeFeed?.let { wrapFeed(it) }
|
||||
|
||||
return FlowProgressHandleImpl(handle.id, returnValue, progress, stepsTreeIndexFeed, stepsTreeFeed)
|
||||
}
|
||||
|
||||
private fun wrapFlowHandle(handle: FlowHandle<*>): FlowHandle<*> {
|
||||
return FlowHandleImpl(handle.id, wrapFuture(handle.returnValue))
|
||||
}
|
||||
|
||||
private fun <ELEMENT> wrapObservable(observable: Observable<ELEMENT>): Observable<ELEMENT> {
|
||||
return observable.doOnError(::log).mapErrors(::obfuscate)
|
||||
}
|
||||
|
||||
private fun <SNAPSHOT, ELEMENT> wrapFeed(feed: DataFeed<SNAPSHOT, ELEMENT>): DataFeed<SNAPSHOT, ELEMENT> {
|
||||
return feed.doOnError(::log).mapErrors(::obfuscate)
|
||||
}
|
||||
|
||||
private fun <RESULT> wrapFuture(future: CordaFuture<RESULT>): CordaFuture<RESULT> {
|
||||
return future.doOnError(::log).mapError(::obfuscate)
|
||||
}
|
||||
|
||||
private fun log(error: Throwable) {
|
||||
if (doLog) {
|
||||
logger.error("Error during RPC invocation", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun obfuscate(error: Throwable): Throwable {
|
||||
val exposed = if (error.isWhitelisted()) error else InternalNodeException((error as? IdentifiableException)?.errorId)
|
||||
removeDetails(exposed)
|
||||
return exposed
|
||||
}
|
||||
|
||||
private fun removeDetails(error: Throwable) {
|
||||
error.stackTrace = arrayOf<StackTraceElement>()
|
||||
error.declaredField<Any?>("cause").value = null
|
||||
error.declaredField<Any?>("suppressedExceptions").value = null
|
||||
when (error) {
|
||||
is CordaException -> error.setCause(null)
|
||||
is CordaRuntimeException -> error.setCause(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.isWhitelisted(): Boolean {
|
||||
return whitelist.any { it.isInstance(this) }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ErrorObfuscatingInvocationHandler(whitelist=$whitelist)"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ExceptionMaskingRpcOpsProxy"
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.node.serialization.amqp
|
||||
|
||||
import net.corda.core.context.Trace
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.messaging.ObservableContextInterface
|
||||
import net.corda.node.services.messaging.ObservableSubscription
|
||||
@ -30,8 +31,9 @@ class RpcServerObservableSerializer : CustomSerializer.Implements<Observable<*>>
|
||||
fun createContext(
|
||||
serializationContext: SerializationContext,
|
||||
observableContext: ObservableContextInterface
|
||||
) = serializationContext.withProperty(
|
||||
RpcServerObservableSerializer.RpcObservableContextKey, observableContext)
|
||||
) = serializationContext.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext)
|
||||
|
||||
val log = contextLogger()
|
||||
}
|
||||
|
||||
override val schemaForDocumentation = Schema(
|
||||
@ -136,5 +138,6 @@ class RpcServerObservableSerializer : CustomSerializer.Implements<Observable<*>>
|
||||
}
|
||||
}
|
||||
observableContext.observableMap.put(observableId, observableWithSubscription)
|
||||
log.trace("Serialized observable $observableId of type $obj")
|
||||
}
|
||||
}
|
@ -8,8 +8,8 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import com.esotericsoftware.kryo.util.Util
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.internal.writer
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.serialization.internal.MutableClassWhitelist
|
||||
@ -25,7 +25,7 @@ import java.util.*
|
||||
/**
|
||||
* Corda specific class resolver which enables extra customisation for the purposes of serialization using Kryo
|
||||
*/
|
||||
class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() {
|
||||
class CordaClassResolver(serializationContext: CheckpointSerializationContext) : DefaultClassResolver() {
|
||||
val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist)
|
||||
|
||||
// These classes are assignment-compatible Java equivalents of Kotlin classes.
|
||||
|
@ -14,12 +14,11 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Storage
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.serialization.internal.checkUseCase
|
||||
import net.corda.serialization.internal.serializationContextKey
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -275,16 +274,9 @@ object SignedTransactionSerializer : Serializer<SignedTransaction>() {
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UseCaseSerializer<T>(private val allowedUseCases: EnumSet<SerializationContext.UseCase>) : Serializer<T>() {
|
||||
protected fun checkUseCase() {
|
||||
net.corda.serialization.internal.checkUseCase(allowedUseCases)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object PrivateKeySerializer : UseCaseSerializer<PrivateKey>(EnumSet.of(Storage, Checkpoint)) {
|
||||
object PrivateKeySerializer : Serializer<PrivateKey>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: PrivateKey) {
|
||||
checkUseCase()
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
|
||||
|
@ -10,10 +10,9 @@ import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import com.esotericsoftware.kryo.serializers.ClosureSerializer
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationScheme
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.serialization.internal.*
|
||||
import java.security.PublicKey
|
||||
@ -32,46 +31,30 @@ private object AutoCloseableSerialisationDetector : Serializer<AutoCloseable>()
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<AutoCloseable>) = throw IllegalStateException("Should not reach here!")
|
||||
}
|
||||
|
||||
abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
object KryoSerializationScheme : CheckpointSerializationScheme {
|
||||
private val kryoPoolsForContexts = ConcurrentHashMap<Pair<ClassWhitelist, ClassLoader>, KryoPool>()
|
||||
|
||||
protected abstract fun rpcClientKryoPool(context: SerializationContext): KryoPool
|
||||
protected abstract fun rpcServerKryoPool(context: SerializationContext): KryoPool
|
||||
|
||||
// this can be overridden in derived serialization schemes
|
||||
protected open val publicKeySerializer: Serializer<PublicKey> = PublicKeySerializer
|
||||
|
||||
private fun getPool(context: SerializationContext): KryoPool {
|
||||
private fun getPool(context: CheckpointSerializationContext): KryoPool {
|
||||
return kryoPoolsForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) {
|
||||
when (context.useCase) {
|
||||
SerializationContext.UseCase.Checkpoint ->
|
||||
KryoPool.Builder {
|
||||
val serializer = Fiber.getFiberSerializer(false) as KryoSerializer
|
||||
val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) }
|
||||
// TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that
|
||||
val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true }
|
||||
serializer.kryo.apply {
|
||||
field.set(this, classResolver)
|
||||
// don't allow overriding the public key serializer for checkpointing
|
||||
DefaultKryoCustomizer.customize(this)
|
||||
addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector)
|
||||
register(ClosureSerializer.Closure::class.java, CordaClosureSerializer)
|
||||
classLoader = it.second
|
||||
}
|
||||
}.build()
|
||||
SerializationContext.UseCase.RPCClient ->
|
||||
rpcClientKryoPool(context)
|
||||
SerializationContext.UseCase.RPCServer ->
|
||||
rpcServerKryoPool(context)
|
||||
else ->
|
||||
KryoPool.Builder {
|
||||
DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(context)), publicKeySerializer).apply { classLoader = it.second }
|
||||
}.build()
|
||||
}
|
||||
KryoPool.Builder {
|
||||
val serializer = Fiber.getFiberSerializer(false) as KryoSerializer
|
||||
val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) }
|
||||
// TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that
|
||||
val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true }
|
||||
serializer.kryo.apply {
|
||||
field.set(this, classResolver)
|
||||
// don't allow overriding the public key serializer for checkpointing
|
||||
DefaultKryoCustomizer.customize(this)
|
||||
addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector)
|
||||
register(ClosureSerializer.Closure::class.java, CordaClosureSerializer)
|
||||
classLoader = it.second
|
||||
}
|
||||
}.build()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> SerializationContext.kryo(task: Kryo.() -> T): T {
|
||||
private fun <T : Any> CheckpointSerializationContext.kryo(task: Kryo.() -> T): T {
|
||||
return getPool(this).run { kryo ->
|
||||
kryo.context.ensureCapacity(properties.size)
|
||||
properties.forEach { kryo.context.put(it.key, it.value) }
|
||||
@ -83,7 +66,7 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: CheckpointSerializationContext): T {
|
||||
val dataBytes = kryoMagic.consume(byteSequence)
|
||||
?: throw KryoException("Serialized bytes header does not match expected format.")
|
||||
return context.kryo {
|
||||
@ -111,7 +94,7 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
override fun <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> {
|
||||
return context.kryo {
|
||||
SerializedBytes(kryoOutput {
|
||||
kryoMagic.writeTo(this)
|
||||
@ -131,13 +114,11 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
}
|
||||
}
|
||||
|
||||
val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(
|
||||
kryoMagic,
|
||||
val KRYO_CHECKPOINT_CONTEXT = CheckpointSerializationContextImpl(
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
QuasarWhitelist,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Checkpoint,
|
||||
null,
|
||||
AlwaysAcceptEncodingWhitelist
|
||||
)
|
||||
|
@ -1,14 +0,0 @@
|
||||
package net.corda.node.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.CordaSerializationMagic
|
||||
|
||||
class KryoServerSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
|
||||
return magic == kryoMagic && target == SerializationContext.UseCase.Checkpoint
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
|
||||
override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
|
||||
}
|
@ -8,7 +8,7 @@ interface NodePropertiesStore {
|
||||
|
||||
interface FlowsDrainingModeOperations {
|
||||
|
||||
fun setEnabled(enabled: Boolean)
|
||||
fun setEnabled(enabled: Boolean, propagateChange: Boolean = true)
|
||||
|
||||
fun isEnabled(): Boolean
|
||||
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.concurrent.OpenFuture
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -24,10 +25,13 @@ import net.corda.node.services.network.NetworkMapUpdater
|
||||
import net.corda.node.services.persistence.AttachmentStorageInternal
|
||||
import net.corda.node.services.statemachine.ExternalEvent
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import java.security.PublicKey
|
||||
|
||||
interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase {
|
||||
override val nodeReady: OpenFuture<Void?>
|
||||
|
||||
interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBaseInternal
|
||||
interface NetworkMapCacheBaseInternal : NetworkMapCacheBase {
|
||||
val allNodeHashes: List<SecureHash>
|
||||
|
||||
fun getNodeByHash(nodeHash: SecureHash): NodeInfo?
|
||||
@ -129,6 +133,7 @@ interface ServiceHubInternal : ServiceHub {
|
||||
}
|
||||
|
||||
fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
|
||||
val cacheFactory: NamedCacheFactory
|
||||
}
|
||||
|
||||
interface FlowStarter {
|
||||
|
@ -3,16 +3,16 @@ package net.corda.node.services.config
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import net.corda.cliutils.CordaSystemUtils
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.nodeapi.internal.*
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.toProperties
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.nodeapi.internal.crypto.save
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
|
||||
@ -68,22 +68,33 @@ object ConfigHelper {
|
||||
* 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() = p2pSslOptions.configureDevKeyAndTrustStores(myLegalName, signingCertificateStore, certificatesDirectory)
|
||||
|
||||
// TODO Move this to KeyStoreConfigHelpers
|
||||
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||
fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path) {
|
||||
|
||||
val specifiedTrustStore = trustStore.getOptional()
|
||||
|
||||
val specifiedKeyStore = keyStore.getOptional()
|
||||
val specifiedSigningStore = signingCertificateStore.getOptional()
|
||||
|
||||
if (specifiedTrustStore != null && specifiedKeyStore != null && specifiedSigningStore != null) return
|
||||
certificatesDirectory.createDirectories()
|
||||
if (!trustStoreFile.exists()) {
|
||||
loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), DEV_CA_TRUST_STORE_PASS).save(trustStoreFile, trustStorePassword)
|
||||
|
||||
if (specifiedTrustStore == null) {
|
||||
loadDevCaTrustStore().copyTo(trustStore.get(true))
|
||||
}
|
||||
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
||||
val (nodeKeyStore) = createDevKeyStores(myLegalName)
|
||||
|
||||
if (keyStore.getOptional() == null || signingCertificateStore.getOptional() == null) {
|
||||
val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.password).get(true).also { it.registerDevSigningCertificates(myLegalName) }
|
||||
|
||||
FileBasedCertificateStoreSupplier(keyStore.path, keyStore.password).get(true).also { it.registerDevP2pCertificates(myLegalName) }
|
||||
|
||||
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
||||
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
||||
if (distributedServiceKeystore.exists()) {
|
||||
val serviceKeystore = X509KeyStore.fromFile(distributedServiceKeystore, DEV_CA_KEY_STORE_PASS)
|
||||
nodeKeyStore.update {
|
||||
signingKeyStore.update {
|
||||
serviceKeystore.aliases().forEach {
|
||||
if (serviceKeystore.internal.isKeyEntry(it)) {
|
||||
setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_PRIVATE_KEY_PASS), serviceKeystore.getCertificateChain(it))
|
||||
@ -95,15 +106,3 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** This is generally covered by commons-lang. */
|
||||
object CordaSystemUtils {
|
||||
const val OS_NAME = "os.name"
|
||||
|
||||
const val MAC_PREFIX = "Mac"
|
||||
const val WIN_PREFIX = "Windows"
|
||||
|
||||
fun isOsMac() = getOsName().startsWith(MAC_PREFIX)
|
||||
fun isOsWindows() = getOsName().startsWith(WIN_PREFIX)
|
||||
fun getOsName() = System.getProperty(OS_NAME)
|
||||
}
|
@ -11,7 +11,9 @@ import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||
import net.corda.nodeapi.internal.config.SslConfiguration
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
@ -30,7 +32,7 @@ private val DEFAULT_FLOW_MONITOR_PERIOD_MILLIS: Duration = Duration.ofMinutes(1)
|
||||
private val DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS: Duration = Duration.ofMinutes(1)
|
||||
private const val CORDAPPS_DIR_NAME_DEFAULT = "cordapps"
|
||||
|
||||
interface NodeConfiguration : NodeSSLConfiguration {
|
||||
interface NodeConfiguration {
|
||||
val myLegalName: CordaX500Name
|
||||
val emailAddress: String
|
||||
val jmxMonitoringHttpPort: Int?
|
||||
@ -48,6 +50,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val notary: NotaryConfig?
|
||||
val additionalNodeInfoPollingFrequencyMsec: Long
|
||||
val p2pAddress: NetworkHostAndPort
|
||||
val additionalP2PAddresses: List<NetworkHostAndPort>
|
||||
val rpcOptions: NodeRpcOptions
|
||||
val messagingServerAddress: NetworkHostAndPort?
|
||||
val messagingServerExternal: Boolean
|
||||
@ -69,9 +72,16 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val effectiveH2Settings: NodeH2Settings?
|
||||
val flowMonitorPeriodMillis: Duration get() = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS
|
||||
val flowMonitorSuspensionLoggingThresholdMillis: Duration get() = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS
|
||||
val cordappDirectories: List<Path> get() = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT)
|
||||
val crlCheckSoftFail: Boolean
|
||||
val jmxReporterType : JmxReporterType? get() = defaultJmxReporterType
|
||||
|
||||
val baseDirectory: Path
|
||||
val certificatesDirectory: Path
|
||||
val signingCertificateStore: FileBasedCertificateStoreSupplier
|
||||
val p2pSslOptions: MutualSslConfiguration
|
||||
|
||||
val cordappDirectories: List<Path>
|
||||
|
||||
fun validate(): List<String>
|
||||
|
||||
companion object {
|
||||
@ -176,8 +186,8 @@ data class NodeConfigurationImpl(
|
||||
override val myLegalName: CordaX500Name,
|
||||
override val jmxMonitoringHttpPort: Int? = null,
|
||||
override val emailAddress: String,
|
||||
override val keyStorePassword: String,
|
||||
override val trustStorePassword: String,
|
||||
private val keyStorePassword: String,
|
||||
private val trustStorePassword: String,
|
||||
override val crlCheckSoftFail: Boolean,
|
||||
override val dataSourceProperties: Properties,
|
||||
override val compatibilityZoneURL: URL? = null,
|
||||
@ -189,6 +199,7 @@ data class NodeConfigurationImpl(
|
||||
override val verifierType: VerifierType,
|
||||
override val flowTimeout: FlowTimeoutConfiguration,
|
||||
override val p2pAddress: NetworkHostAndPort,
|
||||
override val additionalP2PAddresses: List<NetworkHostAndPort> = emptyList(),
|
||||
private val rpcAddress: NetworkHostAndPort? = null,
|
||||
private val rpcSettings: NodeRpcSettings,
|
||||
override val messagingServerAddress: NetworkHostAndPort?,
|
||||
@ -243,6 +254,17 @@ data class NodeConfigurationImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override val certificatesDirectory = baseDirectory / "certificates"
|
||||
|
||||
private val signingCertificateStorePath = certificatesDirectory / "nodekeystore.jks"
|
||||
override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword)
|
||||
|
||||
private val p2pKeystorePath: Path get() = certificatesDirectory / "sslkeystore.jks"
|
||||
private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword)
|
||||
private val p2pTrustStoreFilePath: Path get() = certificatesDirectory / "truststore.jks"
|
||||
private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword)
|
||||
override val p2pSslOptions: MutualSslConfiguration = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore)
|
||||
|
||||
override val rpcOptions: NodeRpcOptions
|
||||
get() {
|
||||
return actualRpcSettings.asOptions()
|
||||
@ -356,8 +378,6 @@ data class NodeConfigurationImpl(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
data class NodeRpcSettings(
|
||||
val address: NetworkHostAndPort?,
|
||||
val adminAddress: NetworkHostAndPort?,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.config.shell
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.internal.clientSslOptionsCompatibleWith
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
|
||||
import net.corda.tools.shell.ShellConfiguration
|
||||
@ -14,8 +15,8 @@ fun NodeConfiguration.toShellConfig() = ShellConfiguration(
|
||||
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
|
||||
user = INTERNAL_SHELL_USER,
|
||||
password = INTERNAL_SHELL_USER,
|
||||
hostAndPort = this.rpcOptions.adminAddress,
|
||||
nodeSslConfig = this,
|
||||
hostAndPort = this.rpcOptions.address,
|
||||
ssl = clientSslOptionsCompatibleWith(this.rpcOptions),
|
||||
sshdPort = this.sshd?.port,
|
||||
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
|
||||
noLocalShell = this.noLocalShell)
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -29,13 +30,14 @@ import javax.persistence.Lob
|
||||
* cached for efficient lookup.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class PersistentIdentityService : SingletonSerializeAsToken(), IdentityServiceInternal {
|
||||
class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
fun createPKMap(): AppendOnlyPersistentMap<SecureHash, PartyAndCertificate, PersistentIdentity, String> {
|
||||
fun createPKMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<SecureHash, PartyAndCertificate, PersistentIdentity, String> {
|
||||
return AppendOnlyPersistentMap(
|
||||
"PersistentIdentityService_partyByKey",
|
||||
cacheFactory = cacheFactory,
|
||||
name = "PersistentIdentityService_partyByKey",
|
||||
toPersistentEntityKey = { it.toString() },
|
||||
fromPersistentEntity = {
|
||||
Pair(
|
||||
@ -50,9 +52,10 @@ class PersistentIdentityService : SingletonSerializeAsToken(), IdentityServiceIn
|
||||
)
|
||||
}
|
||||
|
||||
fun createX500Map(): AppendOnlyPersistentMap<CordaX500Name, SecureHash, PersistentIdentityNames, String> {
|
||||
fun createX500Map(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<CordaX500Name, SecureHash, PersistentIdentityNames, String> {
|
||||
return AppendOnlyPersistentMap(
|
||||
"PersistentIdentityService_partyByName",
|
||||
cacheFactory = cacheFactory,
|
||||
name = "PersistentIdentityService_partyByName",
|
||||
toPersistentEntityKey = { it.toString() },
|
||||
fromPersistentEntity = { Pair(CordaX500Name.parse(it.name), SecureHash.parse(it.publicKeyHash)) },
|
||||
toPersistentEntity = { key: CordaX500Name, value: SecureHash ->
|
||||
@ -101,8 +104,8 @@ class PersistentIdentityService : SingletonSerializeAsToken(), IdentityServiceIn
|
||||
// CordaPersistence is not a c'tor parameter to work around the cyclic dependency
|
||||
lateinit var database: CordaPersistence
|
||||
|
||||
private val keyToParties = createPKMap()
|
||||
private val principalToParties = createX500Map()
|
||||
private val keyToParties = createPKMap(cacheFactory)
|
||||
private val principalToParties = createX500Map(cacheFactory)
|
||||
|
||||
fun start(trustRoot: X509Certificate, caCertificates: List<X509Certificate> = emptyList()) {
|
||||
_trustRoot = trustRoot
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
@ -25,12 +26,11 @@ import javax.persistence.Lob
|
||||
*
|
||||
* This class needs database transactions to be in-flight during method calls and init.
|
||||
*/
|
||||
class PersistentKeyManagementService(val identityService: PersistentIdentityService,
|
||||
class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService,
|
||||
private val database: CordaPersistence) : SingletonSerializeAsToken(), KeyManagementServiceInternal {
|
||||
@Entity
|
||||
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs")
|
||||
class PersistentKey(
|
||||
|
||||
@Id
|
||||
@Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false)
|
||||
var publicKeyHash: String,
|
||||
@ -47,12 +47,15 @@ class PersistentKeyManagementService(val identityService: PersistentIdentityServ
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun createKeyMap(): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> {
|
||||
fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> {
|
||||
return AppendOnlyPersistentMap(
|
||||
"PersistentKeyManagementService_keys",
|
||||
cacheFactory = cacheFactory,
|
||||
name = "PersistentKeyManagementService_keys",
|
||||
toPersistentEntityKey = { it.toStringShort() },
|
||||
fromPersistentEntity = { Pair(Crypto.decodePublicKey(it.publicKey), Crypto.decodePrivateKey(
|
||||
it.privateKey)) },
|
||||
fromPersistentEntity = {
|
||||
Pair(Crypto.decodePublicKey(it.publicKey),
|
||||
Crypto.decodePrivateKey(it.privateKey))
|
||||
},
|
||||
toPersistentEntity = { key: PublicKey, value: PrivateKey ->
|
||||
PersistentKey(key, value)
|
||||
},
|
||||
@ -61,7 +64,7 @@ class PersistentKeyManagementService(val identityService: PersistentIdentityServ
|
||||
}
|
||||
}
|
||||
|
||||
private val keysMap = createKeyMap()
|
||||
private val keysMap = createKeyMap(cacheFactory)
|
||||
|
||||
override fun start(initialKeyPairs: Set<KeyPair>) {
|
||||
initialKeyPairs.forEach { keysMap.addWithDuplicatesAllowed(it.public, it.private) }
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import io.netty.channel.unix.Errors
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -12,13 +11,13 @@ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_P2P_
|
||||
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.PEER_ROLE
|
||||
import net.corda.core.internal.errors.AddressBindingException
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport
|
||||
import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor
|
||||
import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport
|
||||
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
@ -120,7 +119,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
bindingsDirectory = (artemisDir / "bindings").toString()
|
||||
journalDirectory = (artemisDir / "journal").toString()
|
||||
largeMessagesDirectory = (artemisDir / "large-messages").toString()
|
||||
acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config))
|
||||
acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config.p2pSslOptions))
|
||||
// Enable built in message deduplication. Note we still have to do our own as the delayed commits
|
||||
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
|
||||
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
|
||||
@ -162,8 +161,8 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
|
||||
@Throws(IOException::class, KeyStoreException::class)
|
||||
private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager {
|
||||
val keyStore = config.loadSslKeyStore().internal
|
||||
val trustStore = config.loadTrustStore().internal
|
||||
val keyStore = config.p2pSslOptions.keyStore.get().value.internal
|
||||
val trustStore = config.p2pSslOptions.trustStore.get().value.internal
|
||||
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
// Override to make it work with our login module
|
||||
|
@ -6,9 +6,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ServerLocator
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
@ -16,7 +16,7 @@ import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
/**
|
||||
* Used by the Node to communicate with the RPC broker.
|
||||
*/
|
||||
class InternalRPCMessagingClient(val sslConfig: SSLConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name, val rpcServerConfiguration: RPCServerConfiguration) : SingletonSerializeAsToken(), AutoCloseable {
|
||||
class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name, val rpcServerConfiguration: RPCServerConfiguration) : SingletonSerializeAsToken(), AutoCloseable {
|
||||
private var locator: ServerLocator? = null
|
||||
private var rpcServer: RPCServer? = null
|
||||
|
||||
|
@ -4,6 +4,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.node.services.statemachine.DeduplicationId
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import java.time.Instant
|
||||
@ -15,17 +16,18 @@ import javax.persistence.Id
|
||||
/**
|
||||
* Encapsulate the de-duplication logic.
|
||||
*/
|
||||
class P2PMessageDeduplicator(private val database: CordaPersistence) {
|
||||
class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val database: CordaPersistence) {
|
||||
// A temporary in-memory set of deduplication IDs and associated high water mark details.
|
||||
// When we receive a message we don't persist the ID immediately,
|
||||
// so we store the ID here in the meantime (until the persisting db tx has committed). This is because Artemis may
|
||||
// redeliver messages to the same consumer if they weren't ACKed.
|
||||
private val beingProcessedMessages = ConcurrentHashMap<DeduplicationId, MessageMeta>()
|
||||
private val processedMessages = createProcessedMessages()
|
||||
private val processedMessages = createProcessedMessages(cacheFactory)
|
||||
|
||||
private fun createProcessedMessages(): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> {
|
||||
private fun createProcessedMessages(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> {
|
||||
return AppendOnlyPersistentMap(
|
||||
"P2PMessageDeduplicator_processedMessages",
|
||||
cacheFactory = cacheFactory,
|
||||
name = "P2PMessageDeduplicator_processedMessages",
|
||||
toPersistentEntityKey = { it.toString },
|
||||
fromPersistentEntity = { Pair(DeduplicationId(it.id), MessageMeta(it.insertionTime, it.hash, it.seqNo)) },
|
||||
toPersistentEntity = { key: DeduplicationId, value: MessageMeta ->
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.ThreadBox
|
||||
@ -26,7 +27,7 @@ import net.corda.node.services.statemachine.DeduplicationId
|
||||
import net.corda.node.services.statemachine.ExternalEvent
|
||||
import net.corda.node.services.statemachine.SenderDeduplicationId
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.*
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
|
||||
@ -34,6 +35,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
|
||||
import net.corda.nodeapi.internal.bridging.BridgeControl
|
||||
import net.corda.nodeapi.internal.bridging.BridgeEntry
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -81,6 +83,9 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
|
||||
private val database: CordaPersistence,
|
||||
private val networkMap: NetworkMapCacheInternal,
|
||||
@Suppress("UNUSED")
|
||||
private val metricRegistry: MetricRegistry,
|
||||
cacheFactory: NamedCacheFactory,
|
||||
private val isDrainingModeOn: () -> Boolean,
|
||||
private val drainingModeWasChangedEvents: Observable<Pair<Boolean, Boolean>>
|
||||
) : SingletonSerializeAsToken(), MessagingService, AddressToArtemisQueueResolver {
|
||||
@ -120,7 +125,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
private lateinit var advertisedAddress: NetworkHostAndPort
|
||||
private var maxMessageSize: Int = -1
|
||||
|
||||
override val myAddress: SingleMessageRecipient get() = NodeAddress(myIdentity, advertisedAddress)
|
||||
override val myAddress: SingleMessageRecipient get() = NodeAddress(myIdentity)
|
||||
override val ourSenderUUID = UUID.randomUUID().toString()
|
||||
|
||||
private val state = ThreadBox(InnerState())
|
||||
@ -129,7 +134,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
|
||||
private val handlers = ConcurrentHashMap<String, MessageHandler>()
|
||||
|
||||
private val deduplicator = P2PMessageDeduplicator(database)
|
||||
private val deduplicator = P2PMessageDeduplicator(cacheFactory, database)
|
||||
internal var messagingExecutor: MessagingExecutor? = null
|
||||
|
||||
/**
|
||||
@ -149,7 +154,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
started = true
|
||||
log.info("Connecting to message broker: $serverAddress")
|
||||
// TODO Add broker CN to config for host verification in case the embedded broker isn't used
|
||||
val tcpTransport = p2pConnectorTcpTransport(serverAddress, config)
|
||||
val tcpTransport = p2pConnectorTcpTransport(serverAddress, config.p2pSslOptions)
|
||||
locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
|
||||
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
|
||||
// would be the default and the two lines below can be deleted.
|
||||
@ -233,7 +238,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
fun gatherAddresses(node: NodeInfo): Sequence<BridgeEntry> {
|
||||
return state.locked {
|
||||
node.legalIdentitiesAndCerts.map {
|
||||
val messagingAddress = NodeAddress(it.party.owningKey, node.addresses.first())
|
||||
val messagingAddress = NodeAddress(it.party.owningKey)
|
||||
BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name })
|
||||
}.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence()
|
||||
}
|
||||
@ -242,14 +247,14 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
fun deployBridges(node: NodeInfo) {
|
||||
gatherAddresses(node)
|
||||
.forEach {
|
||||
sendBridgeControl(BridgeControl.Create(myIdentity.toStringShort(), it))
|
||||
sendBridgeControl(BridgeControl.Create(config.myLegalName.toString(), it))
|
||||
}
|
||||
}
|
||||
|
||||
fun destroyBridges(node: NodeInfo) {
|
||||
gatherAddresses(node)
|
||||
.forEach {
|
||||
sendBridgeControl(BridgeControl.Delete(myIdentity.toStringShort(), it))
|
||||
sendBridgeControl(BridgeControl.Delete(config.myLegalName.toString(), it))
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +294,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
delayStartQueues += queue.toString()
|
||||
}
|
||||
}
|
||||
val startupMessage = BridgeControl.NodeToBridgeSnapshot(myIdentity.toStringShort(), inboxes, requiredBridges)
|
||||
val startupMessage = BridgeControl.NodeToBridgeSnapshot(config.myLegalName.toString(), inboxes, requiredBridges)
|
||||
sendBridgeControl(startupMessage)
|
||||
}
|
||||
|
||||
@ -310,12 +315,9 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
return
|
||||
}
|
||||
eventsSubscription = p2pConsumer!!.messages
|
||||
.doOnError { error -> throw error }
|
||||
.doOnNext { message -> deliver(message) }
|
||||
// this `run()` method is semantically meant to block until the message consumption runs, hence the latch here
|
||||
.doOnCompleted(latch::countDown)
|
||||
.doOnError { error -> throw error }
|
||||
.subscribe()
|
||||
.subscribe({ message -> deliver(message) }, { error -> throw error })
|
||||
p2pConsumer!!
|
||||
}
|
||||
consumer.start()
|
||||
@ -495,7 +497,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
val peers = networkMap.getNodesByOwningKeyIndex(keyHash)
|
||||
for (node in peers) {
|
||||
val bridge = BridgeEntry(queueName, node.addresses, node.legalIdentities.map { it.name })
|
||||
val createBridgeMessage = BridgeControl.Create(myIdentity.toStringShort(), bridge)
|
||||
val createBridgeMessage = BridgeControl.Create(config.myLegalName.toString(), bridge)
|
||||
sendBridgeControl(createBridgeMessage)
|
||||
}
|
||||
}
|
||||
@ -540,7 +542,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
|
||||
override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients {
|
||||
return when (partyInfo) {
|
||||
is PartyInfo.SingleNode -> NodeAddress(partyInfo.party.owningKey, partyInfo.addresses.single())
|
||||
is PartyInfo.SingleNode -> NodeAddress(partyInfo.party.owningKey)
|
||||
is PartyInfo.DistributedNode -> ServiceAddress(partyInfo.party.owningKey)
|
||||
}
|
||||
}
|
||||
|
@ -336,6 +336,7 @@ class RPCServer(
|
||||
context.invocation.pushToLoggingContext()
|
||||
when (arguments) {
|
||||
is Try.Success -> {
|
||||
log.debug { "Arguments: ${arguments.value.toTypedArray().contentDeepToString()}" }
|
||||
rpcExecutor!!.submit {
|
||||
val result = invokeRpc(context, clientToServer.methodName, arguments.value)
|
||||
sendReply(clientToServer.replyId, clientToServer.clientAddress, result)
|
||||
|
@ -1,51 +0,0 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.api.NetworkMapCacheBaseInternal
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
|
||||
class NetworkMapCacheImpl(
|
||||
private val networkMapCacheBase: NetworkMapCacheBaseInternal,
|
||||
private val identityService: IdentityService,
|
||||
private val database: CordaPersistence
|
||||
) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
for (nodeInfo in networkMapCacheBase.allNodes) {
|
||||
for (identity in nodeInfo.legalIdentitiesAndCerts) {
|
||||
identityService.verifyAndRegisterIdentity(identity)
|
||||
}
|
||||
}
|
||||
networkMapCacheBase.changed.subscribe { mapChange ->
|
||||
// TODO how should we handle network map removal
|
||||
if (mapChange is NetworkMapCache.MapChange.Added) {
|
||||
mapChange.node.legalIdentitiesAndCerts.forEach {
|
||||
try {
|
||||
identityService.verifyAndRegisterIdentity(it)
|
||||
} catch (ignore: Exception) {
|
||||
// Log a warning to indicate node info is not added to the network map cache.
|
||||
logger.warn("Node info for :'${it.name}' is not added to the network map due to verification error.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? {
|
||||
return database.transaction {
|
||||
val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party)
|
||||
wellKnownParty?.let {
|
||||
getNodesByLegalIdentityKey(it.owningKey).firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.utilities.registration.cacheControl
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
@ -23,7 +24,7 @@ import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
class NetworkMapClient(compatibilityZoneURL: URL) {
|
||||
class NetworkMapClient(compatibilityZoneURL: URL, private val versionInfo: VersionInfo) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
@ -38,14 +39,18 @@ class NetworkMapClient(compatibilityZoneURL: URL) {
|
||||
fun publish(signedNodeInfo: SignedNodeInfo) {
|
||||
val publishURL = URL("$networkMapUrl/publish")
|
||||
logger.trace { "Publishing NodeInfo to $publishURL." }
|
||||
publishURL.post(signedNodeInfo.serialize())
|
||||
publishURL.post(signedNodeInfo.serialize(),
|
||||
"Platform-Version" to "${versionInfo.platformVersion}",
|
||||
"Client-Version" to versionInfo.releaseVersion)
|
||||
logger.trace { "Published NodeInfo to $publishURL successfully." }
|
||||
}
|
||||
|
||||
fun ackNetworkParametersUpdate(signedParametersHash: SignedData<SecureHash>) {
|
||||
val ackURL = URL("$networkMapUrl/ack-parameters")
|
||||
logger.trace { "Sending network parameters with hash ${signedParametersHash.raw.deserialize()} approval to $ackURL." }
|
||||
ackURL.post(signedParametersHash.serialize())
|
||||
ackURL.post(signedParametersHash.serialize(),
|
||||
"Platform-Version" to "${versionInfo.platformVersion}",
|
||||
"Client-Version" to versionInfo.releaseVersion)
|
||||
logger.trace { "Sent network parameters approval to $ackURL successfully." }
|
||||
}
|
||||
|
||||
|
@ -24,13 +24,12 @@ import java.nio.file.StandardCopyOption
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
private val fileWatcher: NodeInfoWatcher,
|
||||
private val nodeInfoWatcher: NodeInfoWatcher,
|
||||
private val networkMapClient: NetworkMapClient?,
|
||||
private val baseDirectory: Path,
|
||||
private val extraNetworkMapKeys: List<UUID>
|
||||
@ -40,8 +39,10 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
private val defaultRetryInterval = 1.minutes
|
||||
}
|
||||
|
||||
private val parametersUpdatesTrack: PublishSubject<ParametersUpdateInfo> = PublishSubject.create<ParametersUpdateInfo>()
|
||||
private val executor = ScheduledThreadPoolExecutor(1, NamedThreadFactory("Network Map Updater Thread", Executors.defaultThreadFactory()))
|
||||
private val parametersUpdatesTrack = PublishSubject.create<ParametersUpdateInfo>()
|
||||
private val networkMapPoller = ScheduledThreadPoolExecutor(1, NamedThreadFactory("Network Map Updater Thread")).apply {
|
||||
executeExistingDelayedTasksAfterShutdownPolicy = false
|
||||
}
|
||||
private var newNetworkParameters: Pair<ParametersUpdate, SignedNetworkParameters>? = null
|
||||
private var fileWatcherSubscription: Subscription? = null
|
||||
private lateinit var trustRoot: X509Certificate
|
||||
@ -50,7 +51,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
|
||||
override fun close() {
|
||||
fileWatcherSubscription?.unsubscribe()
|
||||
MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS)
|
||||
MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
fun start(trustRoot: X509Certificate, currentParametersHash: SecureHash, ourNodeInfoHash: SecureHash) {
|
||||
@ -58,26 +59,38 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
this.trustRoot = trustRoot
|
||||
this.currentParametersHash = currentParametersHash
|
||||
this.ourNodeInfoHash = ourNodeInfoHash
|
||||
// Subscribe to file based networkMap
|
||||
fileWatcherSubscription = fileWatcher.nodeInfoUpdates().subscribe {
|
||||
when (it) {
|
||||
is NodeInfoUpdate.Add -> {
|
||||
networkMapCache.addNode(it.nodeInfo)
|
||||
}
|
||||
is NodeInfoUpdate.Remove -> {
|
||||
if (it.hash != ourNodeInfoHash) {
|
||||
val nodeInfo = networkMapCache.getNodeByHash(it.hash)
|
||||
nodeInfo?.let { networkMapCache.removeNode(it) }
|
||||
watchForNodeInfoFiles()
|
||||
if (networkMapClient != null) {
|
||||
watchHttpNetworkMap()
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchForNodeInfoFiles() {
|
||||
nodeInfoWatcher
|
||||
.nodeInfoUpdates()
|
||||
.subscribe {
|
||||
for (update in it) {
|
||||
when (update) {
|
||||
is NodeInfoUpdate.Add -> networkMapCache.addNode(update.nodeInfo)
|
||||
is NodeInfoUpdate.Remove -> {
|
||||
if (update.hash != ourNodeInfoHash) {
|
||||
val nodeInfo = networkMapCache.getNodeByHash(update.hash)
|
||||
nodeInfo?.let(networkMapCache::removeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (networkMapClient == null) {
|
||||
// Mark the network map cache as ready on a successful poll of the node infos dir if not using
|
||||
// the HTTP network map even if there aren't any node infos
|
||||
networkMapCache.nodeReady.set(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (networkMapClient == null) return
|
||||
|
||||
// Subscribe to remote network map if configured.
|
||||
executor.executeExistingDelayedTasksAfterShutdownPolicy = false
|
||||
executor.submit(object : Runnable {
|
||||
private fun watchHttpNetworkMap() {
|
||||
// The check may be expensive, so always run it in the background even the first time.
|
||||
networkMapPoller.submit(object : Runnable {
|
||||
override fun run() {
|
||||
val nextScheduleDelay = try {
|
||||
updateNetworkMapCache()
|
||||
@ -86,9 +99,9 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
defaultRetryInterval
|
||||
}
|
||||
// Schedule the next update.
|
||||
executor.schedule(this, nextScheduleDelay.toMillis(), TimeUnit.MILLISECONDS)
|
||||
networkMapPoller.schedule(this, nextScheduleDelay.toMillis(), TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}) // The check may be expensive, so always run it in the background even the first time.
|
||||
})
|
||||
}
|
||||
|
||||
fun trackParametersUpdate(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> {
|
||||
@ -99,9 +112,13 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
}
|
||||
|
||||
fun updateNetworkMapCache(): Duration {
|
||||
if (networkMapClient == null) throw CordaRuntimeException("Network map cache can be updated only if network map/compatibility zone URL is specified")
|
||||
if (networkMapClient == null) {
|
||||
throw CordaRuntimeException("Network map cache can be updated only if network map/compatibility zone URL is specified")
|
||||
}
|
||||
|
||||
val (globalNetworkMap, cacheTimeout) = networkMapClient.getNetworkMap()
|
||||
globalNetworkMap.parametersUpdate?.let { handleUpdateNetworkParameters(networkMapClient, it) }
|
||||
|
||||
val additionalHashes = extraNetworkMapKeys.flatMap {
|
||||
try {
|
||||
networkMapClient.getNetworkMap(it).payload.nodeInfoHashes
|
||||
@ -111,6 +128,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
emptyList<SecureHash>()
|
||||
}
|
||||
}
|
||||
|
||||
val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet()
|
||||
|
||||
if (currentParametersHash != globalNetworkMap.networkParameterHash) {
|
||||
@ -120,12 +138,9 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
val currentNodeHashes = networkMapCache.allNodeHashes
|
||||
|
||||
// Remove node info from network map.
|
||||
(currentNodeHashes - allHashesFromNetworkMap - fileWatcher.processedNodeInfoHashes)
|
||||
.mapNotNull {
|
||||
if (it != ourNodeInfoHash) {
|
||||
networkMapCache.getNodeByHash(it)
|
||||
} else null
|
||||
}.forEach(networkMapCache::removeNode)
|
||||
(currentNodeHashes - allHashesFromNetworkMap - nodeInfoWatcher.processedNodeInfoHashes)
|
||||
.mapNotNull { if (it != ourNodeInfoHash) networkMapCache.getNodeByHash(it) else null }
|
||||
.forEach(networkMapCache::removeNode)
|
||||
|
||||
(allHashesFromNetworkMap - currentNodeHashes).mapNotNull {
|
||||
// Download new node info from network map
|
||||
@ -141,6 +156,10 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
networkMapCache.addNode(it)
|
||||
}
|
||||
|
||||
// Mark the network map cache as ready on a successful poll of the HTTP network map, even on the odd chance that
|
||||
// it's empty
|
||||
networkMapCache.nodeReady.set(null)
|
||||
|
||||
return cacheTimeout
|
||||
}
|
||||
|
||||
|
@ -3,23 +3,16 @@ package net.corda.node.services.network
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
|
||||
import net.corda.serialization.internal.SerializationFactoryImpl
|
||||
import rx.Observable
|
||||
import rx.Scheduler
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.time.Duration
|
||||
@ -63,7 +56,7 @@ class NodeInfoWatcher(private val nodePath: Path,
|
||||
}
|
||||
}
|
||||
|
||||
internal data class NodeInfoFromFile(val nodeInfohash: SecureHash, val lastModified: FileTime)
|
||||
private data class NodeInfoFromFile(val nodeInfohash: SecureHash, val lastModified: FileTime)
|
||||
|
||||
private val nodeInfosDir = nodePath / NODE_INFO_DIRECTORY
|
||||
private val nodeInfoFilesMap = HashMap<Path, NodeInfoFromFile>()
|
||||
@ -75,20 +68,16 @@ class NodeInfoWatcher(private val nodePath: Path,
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all the files contained in [nodePath] / [NODE_INFO_DIRECTORY] and keep watching
|
||||
* the folder for further updates.
|
||||
* Read all the files contained in [nodePath] / [NODE_INFO_DIRECTORY] and keep watching the folder for further updates.
|
||||
*
|
||||
* We simply list the directory content every 5 seconds, the Java implementation of WatchService has been proven to
|
||||
* be unreliable on MacOs and given the fairly simple use case we have, this simple implementation should do.
|
||||
*
|
||||
* @return an [Observable] returning [NodeInfoUpdate]s, at most one [NodeInfo] is returned for each processed file.
|
||||
* @return an [Observable] that emits lists of [NodeInfoUpdate]s. Each emitted list is a poll event of the folder and
|
||||
* may be empty if no changes were detected.
|
||||
*/
|
||||
fun nodeInfoUpdates(): Observable<NodeInfoUpdate> {
|
||||
return Observable.interval(0, pollInterval.toMillis(), TimeUnit.MILLISECONDS, scheduler)
|
||||
.flatMapIterable { loadFromDirectory() }
|
||||
fun nodeInfoUpdates(): Observable<List<NodeInfoUpdate>> {
|
||||
return Observable.interval(0, pollInterval.toMillis(), TimeUnit.MILLISECONDS, scheduler).map { pollDirectory() }
|
||||
}
|
||||
|
||||
private fun loadFromDirectory(): List<NodeInfoUpdate> {
|
||||
private fun pollDirectory(): List<NodeInfoUpdate> {
|
||||
val processedPaths = HashSet<Path>()
|
||||
val result = nodeInfosDir.list { paths ->
|
||||
paths
|
||||
@ -122,14 +111,3 @@ class NodeInfoWatcher(private val nodePath: Path,
|
||||
return result.map { NodeInfoUpdate.Add(it.nodeInfo) } + removedHashes
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Remove this once we have a tool that can read AMQP serialised files
|
||||
fun main(args: Array<String>) {
|
||||
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
},
|
||||
AMQP_P2P_CONTEXT)
|
||||
)
|
||||
println(Paths.get(args[0]).readObject<SignedNodeInfo>().verified())
|
||||
}
|
||||
|
@ -1,25 +1,29 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.internal.concurrent.OpenFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.schemas.NodeInfoSchemaV1
|
||||
import net.corda.node.services.api.NetworkMapCacheBaseInternal
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.node.utilities.NonInvalidatingCache
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
|
||||
@ -33,8 +37,9 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/** Database-based network map cache. */
|
||||
@ThreadSafe
|
||||
open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
private val myLegalName: CordaX500Name) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal {
|
||||
open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
||||
private val database: CordaPersistence,
|
||||
private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
@ -44,10 +49,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
override val changed: Observable<MapChange> = _changed.wrapWithDatabaseTransaction()
|
||||
private val changePublisher: rx.Observer<MapChange> get() = _changed.bufferUntilDatabaseCommit()
|
||||
|
||||
// TODO revisit the logic under which nodeReady and loadDBSuccess are set.
|
||||
// with the NetworkMapService redesign their meaning is not too well defined.
|
||||
private val _nodeReady = openFuture<Void?>()
|
||||
override val nodeReady: CordaFuture<Void?> = _nodeReady
|
||||
override val nodeReady: OpenFuture<Void?> = openFuture()
|
||||
|
||||
private lateinit var notaries: List<NotaryInfo>
|
||||
|
||||
override val notaryIdentities: List<Party> get() = notaries.map { it.identity }
|
||||
@ -67,14 +70,14 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
|
||||
fun start(notaries: List<NotaryInfo>) {
|
||||
this.notaries = notaries
|
||||
val otherNodeInfoCount = database.transaction {
|
||||
session.createQuery(
|
||||
"select count(*) from ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n join n.legalIdentitiesAndCerts i where i.name != :myLegalName")
|
||||
.setParameter("myLegalName", myLegalName.toString())
|
||||
.singleResult as Long
|
||||
}
|
||||
if (otherNodeInfoCount > 0) {
|
||||
_nodeReady.set(null)
|
||||
}
|
||||
|
||||
override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? {
|
||||
return database.transaction {
|
||||
val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party)
|
||||
wellKnownParty?.let {
|
||||
getNodesByLegalIdentityKey(it.owningKey).firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +126,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List<NodeInfo> = nodesByKeyCache[identityKey]!!
|
||||
|
||||
private val nodesByKeyCache = NonInvalidatingCache<PublicKey, List<NodeInfo>>(
|
||||
"PersistentNetworkMap_nodesByKey",
|
||||
1024) { key ->
|
||||
cacheFactory = cacheFactory,
|
||||
name = "PersistentNetworkMap_nodesByKey") { key ->
|
||||
database.transaction { queryByIdentityKey(session, key) }
|
||||
}
|
||||
|
||||
@ -143,8 +146,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
}
|
||||
|
||||
private val identityByLegalNameCache = NonInvalidatingCache<CordaX500Name, Optional<PartyAndCertificate>>(
|
||||
"PersistentNetworkMap_idByLegalName",
|
||||
1024) { name ->
|
||||
cacheFactory = cacheFactory,
|
||||
name = "PersistentNetworkMap_idByLegalName") { name ->
|
||||
Optional.ofNullable(database.transaction { queryIdentityByLegalName(session, name) })
|
||||
}
|
||||
|
||||
@ -160,6 +163,7 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
val previousNode = getNodesByLegalIdentityKey(node.legalIdentities.first().owningKey).firstOrNull()
|
||||
if (previousNode == null) {
|
||||
logger.info("No previous node found")
|
||||
if (!verifyAndRegisterIdentities(node)) return
|
||||
database.transaction {
|
||||
updateInfoDB(node, session)
|
||||
changePublisher.onNext(MapChange.Added(node))
|
||||
@ -169,6 +173,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
return
|
||||
} else if (previousNode != node) {
|
||||
logger.info("Previous node was found as: $previousNode")
|
||||
// TODO We should be adding any new identities as well
|
||||
if (!verifyIdentities(node)) return
|
||||
database.transaction {
|
||||
updateInfoDB(node, session)
|
||||
changePublisher.onNext(MapChange.Modified(node, previousNode))
|
||||
@ -177,12 +183,30 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence,
|
||||
logger.info("Previous node was identical to incoming one - doing nothing")
|
||||
}
|
||||
}
|
||||
if (node.legalIdentities[0].name != myLegalName) {
|
||||
_nodeReady.set(null)
|
||||
}
|
||||
logger.debug { "Done adding node with info: $node" }
|
||||
}
|
||||
|
||||
private fun verifyIdentities(node: NodeInfo): Boolean {
|
||||
val failures = node.legalIdentitiesAndCerts.mapNotNull { Try.on { it.verify(identityService.trustAnchor) } as? Try.Failure }
|
||||
if (failures.isNotEmpty()) {
|
||||
logger.warn("$node has ${failures.size} invalid identities:")
|
||||
failures.forEach { logger.warn("", it) }
|
||||
}
|
||||
return failures.isEmpty()
|
||||
}
|
||||
|
||||
private fun verifyAndRegisterIdentities(node: NodeInfo): Boolean {
|
||||
// First verify all the node's identities are valid before registering any of them
|
||||
return if (verifyIdentities(node)) {
|
||||
for (identity in node.legalIdentitiesAndCerts) {
|
||||
identityService.verifyAndRegisterIdentity(identity)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeNode(node: NodeInfo) {
|
||||
logger.info("Removing node with info: $node")
|
||||
synchronized(_changed) {
|
||||
|
@ -65,7 +65,7 @@ class DBCheckpointStorage : CheckpointStorage {
|
||||
|
||||
override fun getCheckpoint(id: StateMachineRunId): SerializedBytes<Checkpoint>? {
|
||||
val bytes = currentDBSession().get(DBCheckpoint::class.java, id.uuid.toString())?.checkpoint ?: return null
|
||||
return SerializedBytes<Checkpoint>(bytes)
|
||||
return SerializedBytes(bytes)
|
||||
}
|
||||
|
||||
override fun getAllCheckpoints(): Stream<Pair<StateMachineRunId, SerializedBytes<Checkpoint>>> {
|
||||
@ -78,8 +78,8 @@ class DBCheckpointStorage : CheckpointStorage {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCheckpointCount(connection: Connection): Long =
|
||||
try {
|
||||
override fun getCheckpointCount(connection: Connection): Long {
|
||||
return try {
|
||||
connection.prepareStatement("select count(*) from node_checkpoints").use { ps ->
|
||||
ps.executeQuery().use { rs ->
|
||||
rs.next()
|
||||
@ -90,5 +90,5 @@ class DBCheckpointStorage : CheckpointStorage {
|
||||
// Happens when the table was not created yet.
|
||||
0L
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.api.WritableTransactionStorage
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMapBase
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.node.utilities.WeightBasedAppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
@ -31,7 +32,7 @@ typealias TxCacheValue = Pair<SerializedBytes<CoreTransaction>, List<Transaction
|
||||
fun TxCacheValue.toSignedTx() = SignedTransaction(this.first, this.second)
|
||||
fun SignedTransaction.toTxCacheValue() = TxCacheValue(this.txBits, this.sigs)
|
||||
|
||||
class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPersistence) : WritableTransactionStorage, SingletonSerializeAsToken() {
|
||||
class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: NamedCacheFactory) : WritableTransactionStorage, SingletonSerializeAsToken() {
|
||||
|
||||
@Entity
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}transactions")
|
||||
@ -49,9 +50,10 @@ class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPers
|
||||
)
|
||||
|
||||
private companion object {
|
||||
fun createTransactionsMap(maxSizeInBytes: Long)
|
||||
fun createTransactionsMap(cacheFactory: NamedCacheFactory)
|
||||
: AppendOnlyPersistentMapBase<SecureHash, TxCacheValue, DBTransaction, String> {
|
||||
return WeightBasedAppendOnlyPersistentMap<SecureHash, TxCacheValue, DBTransaction, String>(
|
||||
cacheFactory = cacheFactory,
|
||||
name = "DBTransactionStorage_transactions",
|
||||
toPersistentEntityKey = { it.toString() },
|
||||
fromPersistentEntity = {
|
||||
@ -67,7 +69,6 @@ class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPers
|
||||
}
|
||||
},
|
||||
persistentEntityClass = DBTransaction::class.java,
|
||||
maxWeight = maxSizeInBytes,
|
||||
weighingFunc = { hash, tx -> hash.size + weighTx(tx) }
|
||||
)
|
||||
}
|
||||
@ -86,7 +87,7 @@ class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPers
|
||||
}
|
||||
}
|
||||
|
||||
private val txStorage = ThreadBox(createTransactionsMap(cacheSizeBytes))
|
||||
private val txStorage = ThreadBox(createTransactionsMap(cacheFactory))
|
||||
|
||||
override fun addTransaction(transaction: SignedTransaction): Boolean = database.transaction {
|
||||
txStorage.locked {
|
||||
|
@ -18,8 +18,8 @@ import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.node.utilities.NonInvalidatingCache
|
||||
import net.corda.node.utilities.NonInvalidatingWeightBasedCache
|
||||
import net.corda.nodeapi.exceptions.DuplicateAttachmentException
|
||||
@ -43,9 +43,8 @@ import javax.persistence.*
|
||||
@ThreadSafe
|
||||
class NodeAttachmentService(
|
||||
metrics: MetricRegistry,
|
||||
private val database: CordaPersistence,
|
||||
attachmentContentCacheSize: Long = NodeConfiguration.defaultAttachmentContentCacheSize,
|
||||
attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound
|
||||
cacheFactory: NamedCacheFactory,
|
||||
private val database: CordaPersistence
|
||||
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
@ -206,8 +205,8 @@ class NodeAttachmentService(
|
||||
// a problem somewhere else or this needs to be revisited.
|
||||
|
||||
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
|
||||
cacheFactory = cacheFactory,
|
||||
name = "NodeAttachmentService_attachmentContent",
|
||||
maxWeight = attachmentContentCacheSize,
|
||||
weigher = Weigher<SecureHash, Optional<Pair<Attachment, ByteArray>>> { key, value -> key.size + if (value.isPresent) value.get().second.size else 0 },
|
||||
loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) }
|
||||
)
|
||||
@ -228,10 +227,9 @@ class NodeAttachmentService(
|
||||
}
|
||||
|
||||
private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>(
|
||||
"NodeAttachmentService_attachemnt",
|
||||
attachmentCacheBound) { key ->
|
||||
Optional.ofNullable(createAttachment(key))
|
||||
}
|
||||
cacheFactory = cacheFactory,
|
||||
name = "NodeAttachmentService_attachmentPresence",
|
||||
loadFunction = { key -> Optional.ofNullable(createAttachment(key)) })
|
||||
|
||||
private fun createAttachment(key: SecureHash): Attachment? {
|
||||
val content = attachmentContentCache.get(key)!!
|
||||
|
@ -57,12 +57,13 @@ class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private
|
||||
|
||||
override val values = PublishSubject.create<Pair<Boolean, Boolean>>()!!
|
||||
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
var oldValue: Boolean? = null
|
||||
persistence.transaction {
|
||||
oldValue = map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false
|
||||
override fun setEnabled(enabled: Boolean, propagateChange: Boolean) {
|
||||
val oldValue = persistence.transaction {
|
||||
map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false
|
||||
}
|
||||
if (propagateChange) {
|
||||
values.onNext(oldValue to enabled)
|
||||
}
|
||||
values.onNext(oldValue!! to enabled)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
|
@ -8,7 +8,7 @@ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECU
|
||||
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||
@ -28,17 +28,17 @@ class ArtemisRpcBroker internal constructor(
|
||||
private val maxMessageSize: Int,
|
||||
private val jmxEnabled: Boolean = false,
|
||||
private val baseDirectory: Path,
|
||||
private val nodeConfiguration: SSLConfiguration,
|
||||
private val nodeConfiguration: MutualSslConfiguration,
|
||||
private val shouldStartLocalShell: Boolean) : ArtemisBroker {
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<ArtemisRpcBroker>()
|
||||
|
||||
fun withSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
|
||||
fun withSsl(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
|
||||
return ArtemisRpcBroker(address, adminAddress, sslOptions, true, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell)
|
||||
}
|
||||
|
||||
fun withoutSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
|
||||
fun withoutSsl(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
|
||||
return ArtemisRpcBroker(address, adminAddress, null, false, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell)
|
||||
}
|
||||
}
|
||||
@ -83,14 +83,14 @@ class ArtemisRpcBroker internal constructor(
|
||||
|
||||
@Throws(IOException::class, KeyStoreException::class)
|
||||
private fun createArtemisSecurityManager(loginListener: LoginListener): ActiveMQJAASSecurityManager {
|
||||
val keyStore = nodeConfiguration.loadSslKeyStore().internal
|
||||
val trustStore = nodeConfiguration.loadTrustStore().internal
|
||||
val keyStore = nodeConfiguration.keyStore.get()
|
||||
val trustStore = nodeConfiguration.trustStore.get()
|
||||
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(
|
||||
RPC_SECURITY_CONFIG to RPCJaasConfig(securityManager, loginListener, useSsl),
|
||||
NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
|
||||
NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore.value.internal, trustStore.value.internal)
|
||||
)
|
||||
return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options))
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.artemis.BrokerJaasLoginModule
|
||||
import net.corda.node.internal.artemis.SecureArtemisConfiguration
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
|
||||
import org.apache.activemq.artemis.core.security.Role
|
||||
@ -17,7 +17,7 @@ import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy
|
||||
import org.apache.activemq.artemis.core.settings.impl.AddressSettings
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: SSLConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() {
|
||||
internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: MutualSslConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() {
|
||||
val loginListener: (String) -> Unit
|
||||
|
||||
init {
|
||||
|
@ -69,7 +69,7 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
|
||||
if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap()
|
||||
|
||||
fun internalSchemas() = requiredSchemas.keys + extraSchemas.filter { schema -> // when mapped schemas from the finance module are present, they are considered as internal ones
|
||||
schema::class.simpleName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.simpleName == "net.corda.finance.schemas.CommercialPaperSchemaV1" }
|
||||
schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" }
|
||||
|
||||
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })
|
||||
|
||||
|
@ -4,9 +4,9 @@ import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.codahale.metrics.*
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.checkpointSerialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.api.CheckpointStorage
|
||||
@ -27,7 +27,7 @@ class ActionExecutorImpl(
|
||||
private val checkpointStorage: CheckpointStorage,
|
||||
private val flowMessaging: FlowMessaging,
|
||||
private val stateMachineManager: StateMachineManagerInternal,
|
||||
private val checkpointSerializationContext: SerializationContext,
|
||||
private val checkpointSerializationContext: CheckpointSerializationContext,
|
||||
metrics: MetricRegistry
|
||||
) : ActionExecutor {
|
||||
|
||||
@ -237,7 +237,7 @@ class ActionExecutorImpl(
|
||||
}
|
||||
|
||||
private fun serializeCheckpoint(checkpoint: Checkpoint): SerializedBytes<Checkpoint> {
|
||||
return checkpoint.serialize(context = checkpointSerializationContext)
|
||||
return checkpoint.checkpointSerialize(context = checkpointSerializationContext)
|
||||
}
|
||||
|
||||
private fun cancelFlowTimeout(action: Action.CancelFlowTimeout) {
|
||||
|
@ -12,8 +12,8 @@ import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.checkpointSerialize
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
@ -69,7 +69,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
val actionExecutor: ActionExecutor,
|
||||
val stateMachine: StateMachine,
|
||||
val serviceHub: ServiceHubInternal,
|
||||
val checkpointSerializationContext: SerializationContext,
|
||||
val checkpointSerializationContext: CheckpointSerializationContext,
|
||||
val unfinishedFibers: ReusableLatch
|
||||
)
|
||||
|
||||
@ -369,7 +369,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
Event.Suspend(
|
||||
ioRequest = ioRequest,
|
||||
maySkipCheckpoint = skipPersistingCheckpoint,
|
||||
fiber = this.serialize(context = serializationContext.value)
|
||||
fiber = this.checkpointSerialize(context = serializationContext.value)
|
||||
)
|
||||
} catch (throwable: Throwable) {
|
||||
Event.Error(throwable)
|
||||
|
@ -5,4 +5,4 @@ import net.corda.core.CordaException
|
||||
/**
|
||||
* An exception propagated and thrown in case a session initiation fails.
|
||||
*/
|
||||
class SessionRejectException(reason: String) : CordaException(reason)
|
||||
class SessionRejectException(message: String) : CordaException(message)
|
||||
|
@ -19,6 +19,10 @@ import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
|
||||
import net.corda.core.serialization.internal.checkpointDeserialize
|
||||
import net.corda.core.serialization.internal.checkpointSerialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
@ -36,7 +40,7 @@ import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.injectOldProgressTracker
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
|
||||
import net.corda.serialization.internal.SerializeAsTokenContextImpl
|
||||
import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl
|
||||
import net.corda.serialization.internal.withTokenContext
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import rx.Observable
|
||||
@ -103,7 +107,7 @@ class SingleThreadedStateMachineManager(
|
||||
private val transitionExecutor = makeTransitionExecutor()
|
||||
private val ourSenderUUID = serviceHub.networkService.ourSenderUUID
|
||||
|
||||
private var checkpointSerializationContext: SerializationContext? = null
|
||||
private var checkpointSerializationContext: CheckpointSerializationContext? = null
|
||||
private var actionExecutor: ActionExecutor? = null
|
||||
|
||||
override val allStateMachines: List<FlowLogic<*>>
|
||||
@ -122,8 +126,8 @@ class SingleThreadedStateMachineManager(
|
||||
|
||||
override fun start(tokenizableServices: List<Any>) {
|
||||
checkQuasarJavaAgentPresence()
|
||||
val checkpointSerializationContext = SerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
|
||||
SerializeAsTokenContextImpl(tokenizableServices, SerializationDefaults.SERIALIZATION_FACTORY, SerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
|
||||
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
)
|
||||
this.checkpointSerializationContext = checkpointSerializationContext
|
||||
this.actionExecutor = makeActionExecutor(checkpointSerializationContext)
|
||||
@ -134,6 +138,7 @@ class SingleThreadedStateMachineManager(
|
||||
(fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable)
|
||||
}
|
||||
serviceHub.networkMapCache.nodeReady.then {
|
||||
logger.info("Node ready, info: ${serviceHub.myInfo}")
|
||||
resumeRestoredFlows(fibers)
|
||||
flowMessaging.start { _, deduplicationHandler ->
|
||||
executor.execute {
|
||||
@ -530,7 +535,7 @@ class SingleThreadedStateMachineManager(
|
||||
val resultFuture = openFuture<Any?>()
|
||||
flowStateMachineImpl.transientValues = TransientReference(createTransientValues(flowId, resultFuture))
|
||||
flowLogic.stateMachine = flowStateMachineImpl
|
||||
val frozenFlowLogic = (flowLogic as FlowLogic<*>).serialize(context = checkpointSerializationContext!!)
|
||||
val frozenFlowLogic = (flowLogic as FlowLogic<*>).checkpointSerialize(context = checkpointSerializationContext!!)
|
||||
|
||||
val flowCorDappVersion = createSubFlowVersion(serviceHub.cordappProvider.getCordappForFlow(flowLogic), serviceHub.myInfo.platformVersion)
|
||||
|
||||
@ -612,7 +617,7 @@ class SingleThreadedStateMachineManager(
|
||||
|
||||
private fun deserializeCheckpoint(serializedCheckpoint: SerializedBytes<Checkpoint>): Checkpoint? {
|
||||
return try {
|
||||
serializedCheckpoint.deserialize(context = checkpointSerializationContext!!)
|
||||
serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext!!)
|
||||
} catch (exception: Throwable) {
|
||||
logger.error("Encountered unrestorable checkpoint!", exception)
|
||||
null
|
||||
@ -657,7 +662,7 @@ class SingleThreadedStateMachineManager(
|
||||
val resultFuture = openFuture<Any?>()
|
||||
val fiber = when (flowState) {
|
||||
is FlowState.Unstarted -> {
|
||||
val logic = flowState.frozenFlowLogic.deserialize(context = checkpointSerializationContext!!)
|
||||
val logic = flowState.frozenFlowLogic.checkpointDeserialize(context = checkpointSerializationContext!!)
|
||||
val state = StateMachineState(
|
||||
checkpoint = checkpoint,
|
||||
pendingDeduplicationHandlers = initialDeduplicationHandler?.let { listOf(it) } ?: emptyList(),
|
||||
@ -676,7 +681,7 @@ class SingleThreadedStateMachineManager(
|
||||
fiber
|
||||
}
|
||||
is FlowState.Started -> {
|
||||
val fiber = flowState.frozenFiber.deserialize(context = checkpointSerializationContext!!)
|
||||
val fiber = flowState.frozenFiber.checkpointDeserialize(context = checkpointSerializationContext!!)
|
||||
val state = StateMachineState(
|
||||
checkpoint = checkpoint,
|
||||
pendingDeduplicationHandlers = initialDeduplicationHandler?.let { listOf(it) } ?: emptyList(),
|
||||
@ -741,7 +746,7 @@ class SingleThreadedStateMachineManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeActionExecutor(checkpointSerializationContext: SerializationContext): ActionExecutor {
|
||||
private fun makeActionExecutor(checkpointSerializationContext: CheckpointSerializationContext): ActionExecutor {
|
||||
return ActionExecutorImpl(
|
||||
serviceHub,
|
||||
checkpointStorage,
|
||||
|
@ -2,9 +2,9 @@ package net.corda.node.services.statemachine.interceptors
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.checkpointDeserialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.statemachine.ActionExecutor
|
||||
import net.corda.node.services.statemachine.Event
|
||||
@ -68,7 +68,7 @@ class FiberDeserializationChecker {
|
||||
private val jobQueue = LinkedBlockingQueue<Job>()
|
||||
private var foundUnrestorableFibers: Boolean = false
|
||||
|
||||
fun start(checkpointSerializationContext: SerializationContext) {
|
||||
fun start(checkpointSerializationContext: CheckpointSerializationContext) {
|
||||
require(checkerThread == null)
|
||||
checkerThread = thread(name = "FiberDeserializationChecker") {
|
||||
while (true) {
|
||||
@ -76,7 +76,7 @@ class FiberDeserializationChecker {
|
||||
when (job) {
|
||||
is Job.Check -> {
|
||||
try {
|
||||
job.serializedFiber.deserialize(context = checkpointSerializationContext)
|
||||
job.serializedFiber.checkpointDeserialize(context = checkpointSerializationContext)
|
||||
} catch (throwable: Throwable) {
|
||||
log.error("Encountered unrestorable checkpoint!", throwable)
|
||||
foundUnrestorableFibers = true
|
||||
|
@ -102,7 +102,8 @@ class BFTNonValidatingNotaryService(
|
||||
|
||||
private fun createMap(): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> {
|
||||
return AppendOnlyPersistentMap(
|
||||
"BFTNonValidatingNotaryService_transactions",
|
||||
cacheFactory = services.cacheFactory,
|
||||
name = "BFTNonValidatingNotaryService_transactions",
|
||||
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
|
||||
fromPersistentEntity = {
|
||||
//TODO null check will become obsolete after making DB/JPA columns not nullable
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -8,9 +9,10 @@ import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.concurrent.OpenFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.notary.AsyncUniquenessProvider
|
||||
import net.corda.core.internal.notary.NotaryInternalException
|
||||
import net.corda.core.internal.notary.UniquenessProvider
|
||||
import net.corda.core.internal.notary.isConsumedByTheSameTx
|
||||
import net.corda.core.internal.notary.validateTimeWindow
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
@ -20,17 +22,22 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.persistence.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/** A RDBMS backed Uniqueness provider */
|
||||
@ThreadSafe
|
||||
class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||
class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersistence, cacheFactory: NamedCacheFactory) : AsyncUniquenessProvider, SingletonSerializeAsToken() {
|
||||
|
||||
@MappedSuperclass
|
||||
class BaseComittedState(
|
||||
@EmbeddedId
|
||||
@ -63,21 +70,41 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl
|
||||
var requestDate: Instant
|
||||
)
|
||||
|
||||
private data class CommitRequest(
|
||||
val states: List<StateRef>,
|
||||
val txId: SecureHash,
|
||||
val callerIdentity: Party,
|
||||
val requestSignature: NotarisationRequestSignature,
|
||||
val timeWindow: TimeWindow?,
|
||||
val references: List<StateRef>,
|
||||
val future: OpenFuture<AsyncUniquenessProvider.Result>)
|
||||
|
||||
@Entity
|
||||
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_committed_states")
|
||||
class CommittedState(id: PersistentStateRef, consumingTxHash: String) : BaseComittedState(id, consumingTxHash)
|
||||
|
||||
private class InnerState {
|
||||
val commitLog = createMap()
|
||||
private val commitLog = createMap(cacheFactory)
|
||||
|
||||
private val requestQueue = LinkedBlockingQueue<CommitRequest>(requestQueueSize)
|
||||
|
||||
/** A request processor thread. */
|
||||
private val processorThread = thread(name = "Notary request queue processor", isDaemon = true) {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
processRequest(requestQueue.take())
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
}
|
||||
log.debug { "Shutting down with ${requestQueue.size} in-flight requests unprocessed." }
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
companion object {
|
||||
private const val requestQueueSize = 100_000
|
||||
private val log = contextLogger()
|
||||
fun createMap(): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> =
|
||||
fun createMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> =
|
||||
AppendOnlyPersistentMap(
|
||||
"PersistentUniquenessProvider_transactions",
|
||||
cacheFactory = cacheFactory,
|
||||
name = "PersistentUniquenessProvider_transactions",
|
||||
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
|
||||
fromPersistentEntity = {
|
||||
//TODO null check will become obsolete after making DB/JPA columns not nullable
|
||||
@ -99,23 +126,25 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl
|
||||
)
|
||||
}
|
||||
|
||||
override fun commit(
|
||||
|
||||
/**
|
||||
* Generates and adds a [CommitRequest] to the request queue. If the request queue is full, this method will block
|
||||
* until space is available.
|
||||
*
|
||||
* Returns a future that will complete once the request is processed, containing the commit [Result].
|
||||
*/
|
||||
override fun commitAsync(
|
||||
states: List<StateRef>,
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>
|
||||
) {
|
||||
mutex.locked {
|
||||
logRequest(txId, callerIdentity, requestSignature)
|
||||
val conflictingStates = findAlreadyCommitted(states, references, commitLog)
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
handleConflicts(txId, conflictingStates)
|
||||
} else {
|
||||
handleNoConflicts(timeWindow, states, txId, commitLog)
|
||||
}
|
||||
}
|
||||
): CordaFuture<AsyncUniquenessProvider.Result> {
|
||||
val future = openFuture<AsyncUniquenessProvider.Result>()
|
||||
val request = CommitRequest(states, txId, callerIdentity, requestSignature, timeWindow, references, future)
|
||||
requestQueue.put(request)
|
||||
return future
|
||||
}
|
||||
|
||||
private fun logRequest(txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature) {
|
||||
@ -149,6 +178,25 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl
|
||||
return conflictingStates
|
||||
}
|
||||
|
||||
private fun commitOne(
|
||||
states: List<StateRef>,
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>
|
||||
) {
|
||||
database.transaction {
|
||||
logRequest(txId, callerIdentity, requestSignature)
|
||||
val conflictingStates = findAlreadyCommitted(states, references, commitLog)
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
handleConflicts(txId, conflictingStates)
|
||||
} else {
|
||||
handleNoConflicts(timeWindow, states, txId, commitLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
|
||||
if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
|
||||
log.debug { "Transaction $txId already notarised" }
|
||||
@ -171,4 +219,26 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl
|
||||
throw NotaryInternalException(outsideTimeWindowError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRequest(request: CommitRequest) {
|
||||
try {
|
||||
commitOne(request.states, request.txId, request.callerIdentity, request.requestSignature, request.timeWindow, request.references)
|
||||
respondWithSuccess(request)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Error processing commit request", e)
|
||||
respondWithError(request, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun respondWithError(request: CommitRequest, exception: Exception) {
|
||||
if (exception is NotaryInternalException) {
|
||||
request.future.set(AsyncUniquenessProvider.Result.Failure(exception.error))
|
||||
} else {
|
||||
request.future.setException(NotaryInternalException(NotaryError.General(Exception("Internal service error."))))
|
||||
}
|
||||
}
|
||||
|
||||
private fun respondWithSuccess(request: CommitRequest) {
|
||||
request.future.set(AsyncUniquenessProvider.Result.Success)
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,10 @@ import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.notary.isConsumedByTheSameTx
|
||||
import net.corda.core.internal.notary.validateTimeWindow
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationFactory
|
||||
import net.corda.core.serialization.internal.checkpointSerialize
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
@ -200,11 +200,11 @@ class RaftTransactionCommitLog<E, EK>(
|
||||
}
|
||||
|
||||
class CordaKryoSerializer<T : Any> : TypeSerializer<T> {
|
||||
private val context = SerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY)
|
||||
private val factory = SerializationFactory.defaultFactory
|
||||
private val context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY)
|
||||
private val factory = CheckpointSerializationFactory.defaultFactory
|
||||
|
||||
override fun write(obj: T, buffer: BufferOutput<*>, serializer: Serializer) {
|
||||
val serialized = obj.serialize(context = context)
|
||||
val serialized = obj.checkpointSerialize(context = context)
|
||||
buffer.writeInt(serialized.size)
|
||||
buffer.write(serialized.bytes)
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.config.RaftConfig
|
||||
import net.corda.node.services.transactions.RaftTransactionCommitLog.Commands.CommitTransaction
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import java.nio.file.Path
|
||||
@ -51,17 +51,20 @@ import javax.persistence.Table
|
||||
*/
|
||||
@ThreadSafe
|
||||
class RaftUniquenessProvider(
|
||||
private val transportConfiguration: NodeSSLConfiguration,
|
||||
private val storagePath: Path,
|
||||
private val transportConfiguration: MutualSslConfiguration,
|
||||
private val db: CordaPersistence,
|
||||
private val clock: Clock,
|
||||
private val metrics: MetricRegistry,
|
||||
private val cacheFactory: NamedCacheFactory,
|
||||
private val raftConfig: RaftConfig
|
||||
) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
fun createMap(): AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, CommittedState, PersistentStateRef> =
|
||||
fun createMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, CommittedState, PersistentStateRef> =
|
||||
AppendOnlyPersistentMap(
|
||||
"RaftUniquenessProvider_transactions",
|
||||
cacheFactory = cacheFactory,
|
||||
name = "RaftUniquenessProvider_transactions",
|
||||
toPersistentEntityKey = { PersistentStateRef(it) },
|
||||
fromPersistentEntity = {
|
||||
val txId = it.id.txId
|
||||
@ -96,8 +99,6 @@ class RaftUniquenessProvider(
|
||||
var index: Long = 0
|
||||
)
|
||||
|
||||
/** Directory storing the Raft log and state machine snapshots */
|
||||
private val storagePath: Path = transportConfiguration.baseDirectory
|
||||
private lateinit var _clientFuture: CompletableFuture<CopycatClient>
|
||||
private lateinit var server: CopycatServer
|
||||
|
||||
@ -111,7 +112,7 @@ class RaftUniquenessProvider(
|
||||
fun start() {
|
||||
log.info("Creating Copycat server, log stored in: ${storagePath.toAbsolutePath()}")
|
||||
val stateMachineFactory = {
|
||||
RaftTransactionCommitLog(db, clock, RaftUniquenessProvider.Companion::createMap)
|
||||
RaftTransactionCommitLog(db, clock, { createMap(cacheFactory) })
|
||||
}
|
||||
val address = raftConfig.nodeAddress.let { Address(it.host, it.port) }
|
||||
val storage = buildStorage(storagePath)
|
||||
@ -155,14 +156,14 @@ class RaftUniquenessProvider(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildTransport(config: SSLConfiguration): Transport? {
|
||||
private fun buildTransport(config: MutualSslConfiguration): Transport? {
|
||||
return NettyTransport.builder()
|
||||
.withSsl()
|
||||
.withSslProtocol(SslProtocol.TLSv1_2)
|
||||
.withKeyStorePath(config.sslKeystore.toString())
|
||||
.withKeyStorePassword(config.keyStorePassword)
|
||||
.withTrustStorePath(config.trustStoreFile.toString())
|
||||
.withTrustStorePassword(config.trustStorePassword)
|
||||
.withKeyStorePath(config.keyStore.path.toString())
|
||||
.withKeyStorePassword(config.keyStore.password)
|
||||
.withTrustStorePath(config.trustStore.path.toString())
|
||||
.withTrustStorePassword(config.trustStore.password)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -8,10 +8,10 @@ import java.security.PublicKey
|
||||
|
||||
/** A simple Notary service that does not perform transaction validation */
|
||||
class SimpleNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock)
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = NonValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ import java.security.PublicKey
|
||||
|
||||
/** A Notary service that validates the transaction chain of the submitted transaction before committing it */
|
||||
class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock)
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = ValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,13 @@ import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultQueryException
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.node.services.vault.BinaryComparisonOperator.*
|
||||
import net.corda.core.node.services.vault.CollectionOperator.*
|
||||
import net.corda.core.node.services.vault.ColumnPredicate.*
|
||||
import net.corda.core.node.services.vault.EqualityComparisonOperator.*
|
||||
import net.corda.core.node.services.vault.LikenessOperator.*
|
||||
import net.corda.core.node.services.vault.NullOperator.IS_NULL
|
||||
import net.corda.core.node.services.vault.NullOperator.NOT_NULL
|
||||
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
@ -54,54 +61,93 @@ abstract class AbstractQueryCriteriaParser<Q : GenericQueryCriteria<Q,P>, in P:
|
||||
|
||||
protected fun columnPredicateToPredicate(column: Path<out Any?>, columnPredicate: ColumnPredicate<*>): Predicate {
|
||||
return when (columnPredicate) {
|
||||
is ColumnPredicate.EqualityComparison -> {
|
||||
val literal = columnPredicate.rightLiteral
|
||||
when (columnPredicate.operator) {
|
||||
EqualityComparisonOperator.EQUAL -> criteriaBuilder.equal(column, literal)
|
||||
EqualityComparisonOperator.NOT_EQUAL -> criteriaBuilder.notEqual(column, literal)
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.BinaryComparison -> {
|
||||
val literal: Comparable<Any?>? = uncheckedCast(columnPredicate.rightLiteral)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<Comparable<Any?>?>
|
||||
when (columnPredicate.operator) {
|
||||
BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal)
|
||||
BinaryComparisonOperator.GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal)
|
||||
BinaryComparisonOperator.LESS_THAN -> criteriaBuilder.lessThan(column, literal)
|
||||
BinaryComparisonOperator.LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal)
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.Likeness -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<String?>
|
||||
when (columnPredicate.operator) {
|
||||
LikenessOperator.LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral)
|
||||
LikenessOperator.NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral)
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.CollectionExpression -> {
|
||||
when (columnPredicate.operator) {
|
||||
CollectionOperator.IN -> column.`in`(columnPredicate.rightLiteral)
|
||||
CollectionOperator.NOT_IN -> criteriaBuilder.not(column.`in`(columnPredicate.rightLiteral))
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.Between -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<Comparable<Any?>?>
|
||||
val fromLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightFromLiteral)
|
||||
val toLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightToLiteral)
|
||||
criteriaBuilder.between(column, fromLiteral, toLiteral)
|
||||
}
|
||||
is ColumnPredicate.NullExpression -> {
|
||||
when (columnPredicate.operator) {
|
||||
NullOperator.IS_NULL -> criteriaBuilder.isNull(column)
|
||||
NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column)
|
||||
}
|
||||
}
|
||||
is EqualityComparison -> equalityComparisonToPredicate(column, columnPredicate)
|
||||
is BinaryComparison -> binaryComparisonToPredicate(column, columnPredicate)
|
||||
is Likeness -> likeComparisonToPredicate(column, columnPredicate)
|
||||
is CollectionExpression -> collectionComparisonToPredicate(column, columnPredicate)
|
||||
is Between -> betweenComparisonToPredicate(column, columnPredicate)
|
||||
is NullExpression -> nullComparisonToPredicate(column, columnPredicate)
|
||||
else -> throw VaultQueryException("Not expecting $columnPredicate")
|
||||
}
|
||||
}
|
||||
|
||||
private fun equalityComparisonToPredicate(column: Path<out Any?>, columnPredicate: EqualityComparison<*>): Predicate {
|
||||
val literal = columnPredicate.rightLiteral
|
||||
return if (literal is String) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<String?>
|
||||
when (columnPredicate.operator) {
|
||||
EQUAL -> criteriaBuilder.equal(column, literal)
|
||||
EQUAL_IGNORE_CASE -> criteriaBuilder.equal(criteriaBuilder.upper(column), literal.toUpperCase())
|
||||
NOT_EQUAL -> criteriaBuilder.notEqual(column, literal)
|
||||
NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(criteriaBuilder.upper(column), literal.toUpperCase())
|
||||
}
|
||||
} else {
|
||||
when (columnPredicate.operator) {
|
||||
EQUAL, EQUAL_IGNORE_CASE -> criteriaBuilder.equal(column, literal)
|
||||
NOT_EQUAL, NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(column, literal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun binaryComparisonToPredicate(column: Path<out Any?>, columnPredicate: BinaryComparison<*>): Predicate {
|
||||
val literal: Comparable<Any?>? = uncheckedCast(columnPredicate.rightLiteral)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<Comparable<Any?>?>
|
||||
return when (columnPredicate.operator) {
|
||||
GREATER_THAN -> criteriaBuilder.greaterThan(column, literal)
|
||||
GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal)
|
||||
LESS_THAN -> criteriaBuilder.lessThan(column, literal)
|
||||
LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal)
|
||||
}
|
||||
}
|
||||
|
||||
private fun likeComparisonToPredicate(column: Path<out Any?>, columnPredicate: Likeness): Predicate {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<String?>
|
||||
return when (columnPredicate.operator) {
|
||||
LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral)
|
||||
LIKE_IGNORE_CASE -> criteriaBuilder.like(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase())
|
||||
NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral)
|
||||
NOT_LIKE_IGNORE_CASE -> criteriaBuilder.notLike(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase())
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectionComparisonToPredicate(column: Path<out Any?>, columnPredicate: CollectionExpression<*>): Predicate {
|
||||
val literal = columnPredicate.rightLiteral
|
||||
return if (literal.any { it is String }) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<String?>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
literal as Collection<String>
|
||||
when (columnPredicate.operator) {
|
||||
IN -> column.`in`(literal)
|
||||
IN_IGNORE_CASE -> criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() })
|
||||
NOT_IN -> criteriaBuilder.not(column.`in`(literal))
|
||||
NOT_IN_IGNORE_CASE -> criteriaBuilder.not(criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() }))
|
||||
}
|
||||
} else {
|
||||
when (columnPredicate.operator) {
|
||||
IN, IN_IGNORE_CASE -> column.`in`(literal)
|
||||
NOT_IN, NOT_IN_IGNORE_CASE -> criteriaBuilder.not(column.`in`(literal))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun betweenComparisonToPredicate(column: Path<out Any?>, columnPredicate: Between<*>): Predicate {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
column as Path<Comparable<Any?>?>
|
||||
val fromLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightFromLiteral)
|
||||
val toLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightToLiteral)
|
||||
return criteriaBuilder.between(column, fromLiteral, toLiteral)
|
||||
}
|
||||
|
||||
private fun nullComparisonToPredicate(column: Path<out Any?>, columnPredicate: NullExpression<*>): Predicate {
|
||||
return when (columnPredicate.operator) {
|
||||
IS_NULL -> criteriaBuilder.isNull(column)
|
||||
NOT_NULL -> criteriaBuilder.isNotNull(column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: CriteriaBuilder,
|
||||
@ -474,7 +520,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
|
||||
// state status
|
||||
stateTypes = criteria.status
|
||||
if (criteria.status != Vault.StateStatus.ALL) {
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EqualityComparisonOperator.EQUAL)
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EQUAL)
|
||||
if (commonPredicates.containsKey(predicateID)) {
|
||||
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
|
||||
if (existingStatus != criteria.status) {
|
||||
@ -487,23 +533,23 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
|
||||
}
|
||||
|
||||
// state relevance.
|
||||
if (criteria.isRelevant != Vault.RelevancyStatus.ALL) {
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::isRelevant.name, EqualityComparisonOperator.EQUAL)
|
||||
if (criteria.relevancyStatus != Vault.RelevancyStatus.ALL) {
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::relevancyStatus.name, EQUAL)
|
||||
if (commonPredicates.containsKey(predicateID)) {
|
||||
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
|
||||
if (existingStatus != criteria.isRelevant) {
|
||||
log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::isRelevant.name}] value $existingStatus with ${criteria.status}")
|
||||
commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::isRelevant.name), criteria.isRelevant))
|
||||
if (existingStatus != criteria.relevancyStatus) {
|
||||
log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::relevancyStatus.name}] value $existingStatus with ${criteria.status}")
|
||||
commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::relevancyStatus.name), criteria.relevancyStatus))
|
||||
}
|
||||
} else {
|
||||
commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::isRelevant.name), criteria.isRelevant)
|
||||
commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::relevancyStatus.name), criteria.relevancyStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// contract state types
|
||||
val contractStateTypes = deriveContractStateTypes(criteria.contractStateTypes)
|
||||
if (contractStateTypes.isNotEmpty()) {
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, CollectionOperator.IN)
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, IN)
|
||||
if (commonPredicates.containsKey(predicateID)) {
|
||||
val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).values.map { (it as LiteralExpression).literal }.toSet()
|
||||
if (existingTypes != contractStateTypes) {
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
@ -113,10 +114,12 @@ class NodeVaultService(
|
||||
// For EVERY state to be committed to the vault, this checks whether it is spendable by the recording
|
||||
// node. The behaviour is as follows:
|
||||
//
|
||||
// 1) All vault updates marked as RELEVANT will, of, course all have isRelevant = true.
|
||||
// 2) For ALL_VISIBLE updates, those which are not relevant according to the relevancy rules will have isRelevant = false.
|
||||
// 1) All vault updates marked as RELEVANT will, of course, all have relevancy_status = 1 in the
|
||||
// "vault_states" table.
|
||||
// 2) For ALL_VISIBLE updates, those which are not relevant according to the relevancy rules will have
|
||||
// relevancy_status = 0 in the "vault_states" table.
|
||||
//
|
||||
// This is useful when it comes to querying for fungible states, when we do not want non-relevant states
|
||||
// This is useful when it comes to querying for fungible states, when we do not want irrelevant states
|
||||
// included in the result.
|
||||
//
|
||||
// The same functionality could be obtained by passing in a list of participants to the vault query,
|
||||
@ -134,7 +137,7 @@ class NodeVaultService(
|
||||
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
recordedTime = clock.instant(),
|
||||
isRelevant = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT
|
||||
relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT
|
||||
)
|
||||
stateToAdd.stateRef = PersistentStateRef(stateAndRef.key)
|
||||
session.save(stateToAdd)
|
||||
@ -402,7 +405,7 @@ class NodeVaultService(
|
||||
val enrichedCriteria = QueryCriteria.VaultQueryCriteria(
|
||||
contractStateTypes = setOf(contractStateType),
|
||||
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||
isRelevant = Vault.RelevancyStatus.RELEVANT
|
||||
relevancyStatus = Vault.RelevancyStatus.RELEVANT
|
||||
)
|
||||
val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter)
|
||||
|
||||
@ -433,7 +436,7 @@ class NodeVaultService(
|
||||
is OwnableState -> (state.participants.map { it.owningKey } + state.owner.owningKey).toSet()
|
||||
else -> state.participants.map { it.owningKey }
|
||||
}
|
||||
return keysToCheck.any { it in myKeys }
|
||||
return keysToCheck.any { it.containsAny(myKeys) }
|
||||
}
|
||||
|
||||
@Throws(VaultQueryException::class)
|
||||
@ -480,7 +483,8 @@ class NodeVaultService(
|
||||
// Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases.
|
||||
// So the floor may be necessary anyway.
|
||||
query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize)
|
||||
query.maxResults = paging.pageSize + 1 // detection too many results
|
||||
val pageSize = paging.pageSize + 1
|
||||
query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow
|
||||
|
||||
// execution
|
||||
val results = query.resultList
|
||||
@ -510,7 +514,7 @@ class NodeVaultService(
|
||||
vaultState.notary,
|
||||
vaultState.lockId,
|
||||
vaultState.lockUpdateTime,
|
||||
vaultState.isRelevant))
|
||||
vaultState.relevancyStatus))
|
||||
} else {
|
||||
// TODO: improve typing of returned other results
|
||||
log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" }
|
||||
|
@ -61,8 +61,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
var lockId: String? = null,
|
||||
|
||||
/** Used to determine whether a state abides by the relevancy rules of the recording node */
|
||||
@Column(name = "is_relevant", nullable = false)
|
||||
var isRelevant: Vault.RelevancyStatus,
|
||||
@Column(name = "relevancy_status", nullable = false)
|
||||
var relevancyStatus: Vault.RelevancyStatus,
|
||||
|
||||
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
|
||||
@Column(name = "lock_timestamp", nullable = true)
|
||||
|
@ -309,21 +309,20 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
|
||||
// Open for tests to override
|
||||
open class AppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
cacheFactory: NamedCacheFactory,
|
||||
name: String,
|
||||
toPersistentEntityKey: (K) -> EK,
|
||||
fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
toPersistentEntity: (key: K, value: V) -> E,
|
||||
persistentEntityClass: Class<E>,
|
||||
cacheBound: Long = 1024
|
||||
persistentEntityClass: Class<E>
|
||||
) : AppendOnlyPersistentMapBase<K, V, E, EK>(
|
||||
toPersistentEntityKey,
|
||||
fromPersistentEntity,
|
||||
toPersistentEntity,
|
||||
persistentEntityClass) {
|
||||
//TODO determine cacheBound based on entity class later or with node config allowing tuning, or using some heuristic based on heap size
|
||||
override val cache = NonInvalidatingCache(
|
||||
cacheFactory = cacheFactory,
|
||||
name = name,
|
||||
bound = cacheBound,
|
||||
loadFunction = { key: K ->
|
||||
// This gets called if a value is read and the cache has no Transactional for this key yet.
|
||||
val value: V? = loadValue(key)
|
||||
@ -355,12 +354,12 @@ open class AppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
|
||||
// Same as above, but with weighted values (e.g. memory footprint sensitive).
|
||||
class WeightBasedAppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
cacheFactory: NamedCacheFactory,
|
||||
name: String,
|
||||
toPersistentEntityKey: (K) -> EK,
|
||||
fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
toPersistentEntity: (key: K, value: V) -> E,
|
||||
persistentEntityClass: Class<E>,
|
||||
maxWeight: Long,
|
||||
weighingFunc: (K, Transactional<V>) -> Int
|
||||
) : AppendOnlyPersistentMapBase<K, V, E, EK>(
|
||||
toPersistentEntityKey,
|
||||
@ -368,8 +367,8 @@ class WeightBasedAppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
toPersistentEntity,
|
||||
persistentEntityClass) {
|
||||
override val cache = NonInvalidatingWeightBasedCache(
|
||||
name,
|
||||
maxWeight = maxWeight,
|
||||
cacheFactory = cacheFactory,
|
||||
name = name,
|
||||
weigher = Weigher { key, value -> weighingFunc(key, value) },
|
||||
loadFunction = { key: K ->
|
||||
val value: V? = loadValue(key)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.node.utilities
|
||||
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@ -10,19 +9,12 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
* via an executor. It will use an underlying thread factory to create the actual thread
|
||||
* and then override the thread name with the prefix and an ever increasing number
|
||||
*/
|
||||
class NamedThreadFactory(private val name: String, private val underlyingFactory: ThreadFactory) : ThreadFactory {
|
||||
val threadNumber = AtomicInteger(1)
|
||||
override fun newThread(runnable: Runnable?): Thread {
|
||||
val thread = underlyingFactory.newThread(runnable)
|
||||
class NamedThreadFactory(private val name: String,
|
||||
private val delegate: ThreadFactory = Executors.defaultThreadFactory()) : ThreadFactory {
|
||||
private val threadNumber = AtomicInteger(1)
|
||||
override fun newThread(runnable: Runnable): Thread {
|
||||
val thread = delegate.newThread(runnable)
|
||||
thread.name = name + "-" + threadNumber.getAndIncrement()
|
||||
return thread
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single thread executor with a NamedThreadFactory based on the default thread factory
|
||||
* defined in java.util.concurrent.Executors
|
||||
*/
|
||||
fun newNamedSingleThreadExecutor(name: String): ExecutorService {
|
||||
return Executors.newSingleThreadExecutor(NamedThreadFactory(name, Executors.defaultThreadFactory()))
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
package net.corda.node.utilities
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.github.benmanes.caffeine.cache.Cache
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import net.corda.core.internal.buildNamed
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
|
||||
/**
|
||||
* Allow passing metrics and config to caching implementations.
|
||||
*/
|
||||
interface NamedCacheFactory : SerializeAsToken {
|
||||
/**
|
||||
* Build a new cache factory of the same type that incorporates metrics.
|
||||
*/
|
||||
fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory
|
||||
|
||||
/**
|
||||
* Build a new cache factory of the same type that incorporates the associated configuration.
|
||||
*/
|
||||
fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory
|
||||
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V>
|
||||
}
|
||||
|
||||
class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() {
|
||||
constructor() : this(null, null)
|
||||
|
||||
override fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration)
|
||||
override fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration)
|
||||
|
||||
override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> {
|
||||
checkNotNull(metricRegistry)
|
||||
checkNotNull(nodeConfiguration)
|
||||
return caffeine.maximumSize(1024).buildNamed<K, V>(name)
|
||||
}
|
||||
|
||||
override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> {
|
||||
checkNotNull(metricRegistry)
|
||||
checkNotNull(nodeConfiguration)
|
||||
val configuredCaffeine = when (name) {
|
||||
"DBTransactionStorage_transactions" -> caffeine.maximumWeight(nodeConfiguration!!.transactionCacheSizeBytes)
|
||||
"NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(nodeConfiguration!!.attachmentContentCacheSizeBytes)
|
||||
"NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(nodeConfiguration!!.attachmentCacheBound)
|
||||
else -> caffeine.maximumSize(1024)
|
||||
}
|
||||
return configuredCaffeine.buildNamed<K, V>(name, loader)
|
||||
}
|
||||
}
|
@ -4,19 +4,18 @@ import com.github.benmanes.caffeine.cache.CacheLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import com.github.benmanes.caffeine.cache.Weigher
|
||||
import net.corda.core.internal.buildNamed
|
||||
|
||||
class NonInvalidatingCache<K, V> private constructor(
|
||||
val cache: LoadingCache<K, V>
|
||||
) : LoadingCache<K, V> by cache {
|
||||
|
||||
constructor(name: String, bound: Long, loadFunction: (K) -> V) :
|
||||
this(buildCache(name, bound, loadFunction))
|
||||
constructor(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V) :
|
||||
this(buildCache(cacheFactory, name, loadFunction))
|
||||
|
||||
private companion object {
|
||||
private fun <K, V> buildCache(name: String, bound: Long, loadFunction: (K) -> V): LoadingCache<K, V> {
|
||||
val builder = Caffeine.newBuilder().maximumSize(bound)
|
||||
return builder.buildNamed(name, NonInvalidatingCacheLoader(loadFunction))
|
||||
private fun <K, V> buildCache(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V): LoadingCache<K, V> {
|
||||
val builder = Caffeine.newBuilder()
|
||||
return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction))
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,13 +32,13 @@ class NonInvalidatingCache<K, V> private constructor(
|
||||
class NonInvalidatingWeightBasedCache<K, V> private constructor(
|
||||
val cache: LoadingCache<K, V>
|
||||
) : LoadingCache<K, V> by cache {
|
||||
constructor (name: String, maxWeight: Long, weigher: Weigher<K, V>, loadFunction: (K) -> V) :
|
||||
this(buildCache(name, maxWeight, weigher, loadFunction))
|
||||
constructor (cacheFactory: NamedCacheFactory, name: String, weigher: Weigher<K, V>, loadFunction: (K) -> V) :
|
||||
this(buildCache(cacheFactory, name, weigher, loadFunction))
|
||||
|
||||
private companion object {
|
||||
private fun <K, V> buildCache(name: String, maxWeight: Long, weigher: Weigher<K, V>, loadFunction: (K) -> V): LoadingCache<K, V> {
|
||||
val builder = Caffeine.newBuilder().maximumWeight(maxWeight).weigher(weigher)
|
||||
return builder.buildNamed(name, NonInvalidatingCache.NonInvalidatingCacheLoader(loadFunction))
|
||||
private fun <K, V> buildCache(cacheFactory: NamedCacheFactory, name: String, weigher: Weigher<K, V>, loadFunction: (K) -> V): LoadingCache<K, V> {
|
||||
val builder = Caffeine.newBuilder().weigher(weigher)
|
||||
return cacheFactory.buildNamed(builder, name, NonInvalidatingCache.NonInvalidatingCacheLoader(loadFunction))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package net.corda.node.utilities.logging
|
||||
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.core.LoggerContext
|
||||
import org.apache.logging.log4j.core.async.AsyncLoggerContext
|
||||
import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector
|
||||
import org.apache.logging.log4j.core.util.Constants
|
||||
import org.apache.logging.log4j.util.PropertiesUtil
|
||||
import java.net.URI
|
||||
|
||||
class AsyncLoggerContextSelectorNoThreadLocal : ClassLoaderContextSelector() {
|
||||
companion object {
|
||||
/**
|
||||
* Returns `true` if the user specified this selector as the Log4jContextSelector, to make all loggers
|
||||
* asynchronous.
|
||||
*
|
||||
* @return `true` if all loggers are asynchronous, `false` otherwise.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isSelected(): Boolean {
|
||||
return AsyncLoggerContextSelectorNoThreadLocal::class.java.name == PropertiesUtil.getProperties().getStringProperty(Constants.LOG4J_CONTEXT_SELECTOR)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// if we use async log4j logging, we need to shut down the logging to flush the loggers on exit
|
||||
addShutdownHook { LogManager.shutdown() }
|
||||
}
|
||||
|
||||
override fun createContext(name: String?, configLocation: URI?): LoggerContext {
|
||||
return AsyncLoggerContext(name, null, configLocation).also { it.setUseThreadLocals(false) }
|
||||
}
|
||||
|
||||
override fun toContextMapKey(loader: ClassLoader?): String {
|
||||
// LOG4J2-666 ensure unique name across separate instances created by webapp classloaders
|
||||
return "AsyncContextNoThreadLocal@" + Integer.toHexString(System.identityHashCode(loader))
|
||||
}
|
||||
|
||||
override fun defaultContextName(): String {
|
||||
return "DefaultAsyncContextNoThreadLocal@" + Thread.currentThread().name
|
||||
}
|
||||
}
|
@ -52,7 +52,9 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo:
|
||||
}
|
||||
|
||||
override fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||
return String(registrationURL.post(OpaqueBytes(request.encoded), "Client-Version" to "${versionInfo.platformVersion}"))
|
||||
return String(registrationURL.post(OpaqueBytes(request.encoded),
|
||||
"Platform-Version" to "${versionInfo.platformVersion}",
|
||||
"Client-Version" to versionInfo.releaseVersion))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,8 @@ import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.NodeRegistrationOption
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.config.CertificateStoreSupplier
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
@ -33,7 +34,8 @@ import javax.security.auth.x500.X500Principal
|
||||
* needed.
|
||||
*/
|
||||
// TODO: Use content signer instead of keypairs.
|
||||
open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
private val signingCertificateStore: CertificateStoreSupplier,
|
||||
private val myLegalName: CordaX500Name,
|
||||
private val emailAddress: String,
|
||||
private val certService: NetworkRegistrationService,
|
||||
@ -44,14 +46,12 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) {
|
||||
|
||||
companion object {
|
||||
const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key"
|
||||
const val SELF_SIGNED_PRIVATE_KEY = "SelfSignedPrivateKey"
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt"
|
||||
// TODO: Use different password for private key.
|
||||
private val privateKeyPassword = config.keyStorePassword
|
||||
private val rootTrustStore: X509KeyStore
|
||||
private val requestIdStore = certificatesDirectory / "certificate-request-id.txt"
|
||||
protected val rootTrustStore: X509KeyStore
|
||||
protected val rootCert: X509Certificate
|
||||
|
||||
init {
|
||||
@ -75,12 +75,14 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
* @throws CertificateRequestException if the certificate retrieved by doorman is invalid.
|
||||
*/
|
||||
fun buildKeystore() {
|
||||
config.certificatesDirectory.createDirectories()
|
||||
val nodeKeyStore = config.loadNodeKeyStore(createNew = true)
|
||||
certificatesDirectory.createDirectories()
|
||||
val nodeKeyStore = signingCertificateStore.get(createNew = true)
|
||||
if (keyAlias in nodeKeyStore) {
|
||||
println("Certificate already exists, Corda node will now terminate...")
|
||||
return
|
||||
}
|
||||
// TODO: Use different password for private key.
|
||||
val privateKeyPassword = nodeKeyStore.password
|
||||
val tlsCrlIssuerCert = validateAndGetTlsCrlIssuerCert()
|
||||
if (tlsCrlIssuerCert == null && isTlsCrlIssuerCertRequired()) {
|
||||
System.err.println("""tlsCrlIssuerCert config does not match the root certificate issuer and nor is there any other certificate in the trust store with a matching issuer.
|
||||
@ -89,7 +91,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
throw IllegalArgumentException("TLS CRL issuer certificate not found in the trust store.")
|
||||
}
|
||||
|
||||
val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY)
|
||||
val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword)
|
||||
|
||||
val requestId = try {
|
||||
submitOrResumeCertificateSigningRequest(keyPair)
|
||||
@ -110,7 +112,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
throw certificateRequestException
|
||||
}
|
||||
validateCertificates(keyPair.public, certificates)
|
||||
storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias)
|
||||
storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias, privateKeyPassword)
|
||||
onSuccess(keyPair, certificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name())
|
||||
// All done, clean up temp files.
|
||||
requestIdStore.deleteIfExists()
|
||||
@ -148,25 +150,32 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
println("Certificate signing request approved, storing private key with the certificate chain.")
|
||||
}
|
||||
|
||||
private fun storePrivateKeyWithCertificates(nodeKeystore: X509KeyStore, keyPair: KeyPair, certificates: List<X509Certificate>, keyAlias: String) {
|
||||
private fun storePrivateKeyWithCertificates(nodeKeystore: CertificateStore, keyPair: KeyPair, certificates: List<X509Certificate>, keyAlias: String, keyPassword: String) {
|
||||
// Save private key and certificate chain to the key store.
|
||||
nodeKeystore.setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = config.keyStorePassword)
|
||||
nodeKeystore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY)
|
||||
nodeKeystore.save()
|
||||
println("Private key '$keyAlias' and certificate stored in ${config.nodeKeystore}.")
|
||||
with(nodeKeystore.value) {
|
||||
setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = keyPassword)
|
||||
// The key was temporarily stored as SELF_SIGNED_PRIVATE_KEY, but now that it's signed by the Doorman we
|
||||
// can delete this old record.
|
||||
internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY)
|
||||
save()
|
||||
}
|
||||
println("Private key '$keyAlias' and certificate stored in node signing keystore.")
|
||||
}
|
||||
|
||||
private fun X509KeyStore.loadOrCreateKeyPair(alias: String): KeyPair {
|
||||
private fun CertificateStore.loadOrCreateKeyPair(alias: String, privateKeyPassword: String = password): KeyPair {
|
||||
// Create or load self signed keypair from the key store.
|
||||
// We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval.
|
||||
if (alias !in this) {
|
||||
// NODE_CA should be TLS compatible due to the cert hierarchy structure.
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair)
|
||||
// Save to the key store.
|
||||
setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword)
|
||||
save()
|
||||
with(value) {
|
||||
setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword)
|
||||
save()
|
||||
}
|
||||
}
|
||||
return getCertificateAndKeyPair(alias, privateKeyPassword).keyPair
|
||||
return query { getCertificateAndKeyPair(alias, privateKeyPassword) }.keyPair
|
||||
}
|
||||
|
||||
/**
|
||||
@ -243,7 +252,9 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration,
|
||||
class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause)
|
||||
|
||||
class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) :
|
||||
NetworkRegistrationHelper(config,
|
||||
NetworkRegistrationHelper(
|
||||
config.certificatesDirectory,
|
||||
config.signingCertificateStore,
|
||||
config.myLegalName,
|
||||
config.emailAddress,
|
||||
certService,
|
||||
@ -263,7 +274,7 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
}
|
||||
|
||||
private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>, tlsCertCrlIssuer: X500Name?) {
|
||||
config.loadSslKeyStore(createNew = true).update {
|
||||
config.p2pSslOptions.keyStore.get(createNew = true).update {
|
||||
println("Generating SSL certificate for node messaging service.")
|
||||
val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val sslCert = X509Utilities.createCertificate(
|
||||
@ -277,17 +288,26 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
logger.info("Generated TLS certificate: $sslCert")
|
||||
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates)
|
||||
}
|
||||
println("SSL private key and certificate stored in ${config.sslKeystore}.")
|
||||
println("SSL private key and certificate stored in ${config.p2pSslOptions.keyStore.path}.")
|
||||
}
|
||||
|
||||
private fun createTruststore(rootCertificate: X509Certificate) {
|
||||
// Save root certificates to trust store.
|
||||
config.loadTrustStore(createNew = true).update {
|
||||
config.p2pSslOptions.trustStore.get(createNew = true).update {
|
||||
if (this.aliases().hasNext()) {
|
||||
logger.warn("The node's trust store already exists. The following certificates will be overridden: ${this.aliases().asSequence()}")
|
||||
}
|
||||
println("Generating trust store for corda node.")
|
||||
// Assumes certificate chain always starts with client certificate and end with root certificate.
|
||||
setCertificate(CORDA_ROOT_CA, rootCertificate)
|
||||
// Copy remaining certificates from the network-trust-store
|
||||
rootTrustStore.aliases().asSequence().filter { it != CORDA_ROOT_CA }.forEach {
|
||||
val certificate = rootTrustStore.getCertificate(it)
|
||||
logger.info("Copying trusted certificate to the node's trust store: Alias: $it, Certificate: $certificate")
|
||||
setCertificate(it, certificate)
|
||||
}
|
||||
}
|
||||
println("Node trust store stored in ${config.trustStoreFile}.")
|
||||
println("Node trust store stored in ${config.p2pSslOptions.trustStore.path}.")
|
||||
}
|
||||
|
||||
override fun validateAndGetTlsCrlIssuerCert(): X509Certificate? {
|
||||
@ -296,8 +316,9 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
if (principalMatchesCertificatePrincipal(tlsCertCrlIssuer, rootCert)) {
|
||||
return rootCert
|
||||
}
|
||||
return if (config.trustStoreFile.exists()) {
|
||||
findMatchingCertificate(tlsCertCrlIssuer, config.loadTrustStore())
|
||||
val trustStore = config.p2pSslOptions.trustStore.getOptional()
|
||||
return if (trustStore != null) {
|
||||
findMatchingCertificate(tlsCertCrlIssuer, trustStore.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
2
node/src/main/resources/log4j2.component.properties
Normal file
2
node/src/main/resources/log4j2.component.properties
Normal file
@ -0,0 +1,2 @@
|
||||
Log4jContextSelector=net.corda.node.utilities.logging.AsyncLoggerContextSelectorNoThreadLocal
|
||||
AsyncLogger.RingBufferSize=262144
|
@ -3,6 +3,7 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet author="R3.Corda" id="add_tx_mapping_column">
|
||||
<preConditions onFail="MARK_RAN"><not><columnExists tableName="node_transactions" columnName="state_machine_run_id"/></not></preConditions>
|
||||
<addColumn tableName="node_transactions">
|
||||
<column name="state_machine_run_id" type="NVARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
|
@ -1,15 +1,19 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet author="R3.Corda" id="add_is_relevant_column">
|
||||
<addColumn tableName="vault_states">
|
||||
<column name="is_relevant" type="INT"/>
|
||||
</addColumn>
|
||||
<update tableName="vault_states">
|
||||
<column name="is_relevant" valueNumeric="0"/>
|
||||
</update>
|
||||
<addNotNullConstraint tableName="vault_states" columnName="is_relevant" columnDataType="INT" />
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet author="R3.Corda" id="add_relevancy_status_column">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="vault_states" columnName="relevancy_status"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="vault_states">
|
||||
<column name="relevancy_status" type="INT"/>
|
||||
</addColumn>
|
||||
<update tableName="vault_states">
|
||||
<column name="relevancy_status" valueNumeric="0"/>
|
||||
</update>
|
||||
<addNotNullConstraint tableName="vault_states" columnName="relevancy_status" columnDataType="INT"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
|
141
node/src/main/resources/net.corda.node.internal.NodeStartup.yml
Normal file
141
node/src/main/resources/net.corda.node.internal.NodeStartup.yml
Normal file
@ -0,0 +1,141 @@
|
||||
- commandName: "<main class>"
|
||||
positionalParams: []
|
||||
params:
|
||||
- parameterName: "--base-directory"
|
||||
parameterType: "java.nio.file.Path"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "--bootstrap-raft-cluster"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--clear-network-map-cache"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--config-file"
|
||||
parameterType: "java.nio.file.Path"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "--dev-mode"
|
||||
parameterType: "java.lang.Boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--initial-registration"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--install-shell-extensions"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--just-generate-node-info"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--just-generate-rpc-ssl-settings"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--log-to-console"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--logging-level"
|
||||
parameterType: "org.slf4j.event.Level"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "ERROR"
|
||||
- "WARN"
|
||||
- "INFO"
|
||||
- "DEBUG"
|
||||
- "TRACE"
|
||||
- parameterName: "--network-root-truststore"
|
||||
parameterType: "java.nio.file.Path"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "--network-root-truststore-password"
|
||||
parameterType: "java.lang.String"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--no-local-shell"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--on-unknown-config-keys"
|
||||
parameterType: "net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "FAIL"
|
||||
- "WARN"
|
||||
- "IGNORE"
|
||||
- parameterName: "--sshd"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--sshd-port"
|
||||
parameterType: "int"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--verbose"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-b"
|
||||
parameterType: "java.nio.file.Path"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "-c"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-d"
|
||||
parameterType: "java.lang.Boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-f"
|
||||
parameterType: "java.nio.file.Path"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "-n"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-p"
|
||||
parameterType: "java.lang.String"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-t"
|
||||
parameterType: "java.nio.file.Path"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "-v"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
@ -3,6 +3,7 @@ keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
||||
crlCheckSoftFail = true
|
||||
lazyBridgeStart = true
|
||||
additionalP2PAddresses = []
|
||||
dataSourceProperties = {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0;LOCK_TIMEOUT=10000"
|
||||
|
@ -1,189 +0,0 @@
|
||||
package net.corda.node
|
||||
|
||||
import joptsimple.OptionException
|
||||
import net.corda.core.internal.delete
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.slf4j.event.Level
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class NodeArgsParserTest {
|
||||
private val parser = NodeArgsParser()
|
||||
|
||||
companion object {
|
||||
private lateinit var workingDirectory: Path
|
||||
private lateinit var buildDirectory: Path
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun initDirectories() {
|
||||
workingDirectory = Paths.get(".").normalize().toAbsolutePath()
|
||||
buildDirectory = workingDirectory.resolve("build")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no command line arguments`() {
|
||||
assertThat(parser.parse()).isEqualTo(CmdLineOptions(
|
||||
baseDirectory = workingDirectory,
|
||||
configFile = workingDirectory / "node.conf",
|
||||
logToConsole = false,
|
||||
loggingLevel = Level.INFO,
|
||||
nodeRegistrationOption = null,
|
||||
isVersion = false,
|
||||
noLocalShell = false,
|
||||
sshdServer = false,
|
||||
justGenerateNodeInfo = false,
|
||||
justGenerateRpcSslCerts = false,
|
||||
bootstrapRaftCluster = false,
|
||||
unknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL,
|
||||
devMode = false,
|
||||
clearNetworkMapCache = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base-directory with relative path`() {
|
||||
val expectedBaseDir = Paths.get("tmp").normalize().toAbsolutePath()
|
||||
val cmdLineOptions = parser.parse("--base-directory", "tmp")
|
||||
assertThat(cmdLineOptions.baseDirectory).isEqualTo(expectedBaseDir)
|
||||
assertThat(cmdLineOptions.configFile).isEqualTo(expectedBaseDir / "node.conf")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base-directory with absolute path`() {
|
||||
val baseDirectory = Paths.get("tmp").normalize().toAbsolutePath()
|
||||
val cmdLineOptions = parser.parse("--base-directory", baseDirectory.toString())
|
||||
assertThat(cmdLineOptions.baseDirectory).isEqualTo(baseDirectory)
|
||||
assertThat(cmdLineOptions.configFile).isEqualTo(baseDirectory / "node.conf")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `config-file with relative path`() {
|
||||
val cmdLineOptions = parser.parse("--config-file", "different.conf")
|
||||
assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
|
||||
assertThat(cmdLineOptions.configFile).isEqualTo(workingDirectory / "different.conf")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `config-file with absolute path`() {
|
||||
val configFile = Paths.get("tmp", "a.conf").normalize().toAbsolutePath()
|
||||
val cmdLineOptions = parser.parse("--config-file", configFile.toString())
|
||||
assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
|
||||
assertThat(cmdLineOptions.configFile).isEqualTo(configFile)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base-directory without argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
parser.parse("--base-directory")
|
||||
}.withMessageContaining("base-directory")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `config-file without argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
parser.parse("--config-file")
|
||||
}.withMessageContaining("config-file")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log-to-console`() {
|
||||
val cmdLineOptions = parser.parse("--log-to-console")
|
||||
assertThat(cmdLineOptions.logToConsole).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logging-level`() {
|
||||
for (level in Level.values()) {
|
||||
val cmdLineOptions = parser.parse("--logging-level", level.name)
|
||||
assertThat(cmdLineOptions.loggingLevel).isEqualTo(level)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logging-level without argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
parser.parse("--logging-level")
|
||||
}.withMessageContaining("logging-level")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logging-level with invalid argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
parser.parse("--logging-level", "not-a-level")
|
||||
}.withMessageContaining("logging-level")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial-registration`() {
|
||||
// Create this temporary file in the "build" directory so that "clean" can delete it.
|
||||
val truststorePath = buildDirectory / "truststore" / "file.jks"
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
|
||||
}.withMessageContaining("Network root trust store path").withMessageContaining("doesn't exist")
|
||||
|
||||
X509KeyStore.fromFile(truststorePath, "dummy_password", createNew = true)
|
||||
try {
|
||||
val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
|
||||
assertNotNull(cmdLineOptions.nodeRegistrationOption)
|
||||
assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.nodeRegistrationOption?.networkRootTrustStorePath)
|
||||
assertEquals("password-test", cmdLineOptions.nodeRegistrationOption?.networkRootTrustStorePassword)
|
||||
} finally {
|
||||
truststorePath.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun version() {
|
||||
val cmdLineOptions = parser.parse("--version")
|
||||
assertThat(cmdLineOptions.isVersion).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generate node infos`() {
|
||||
val cmdLineOptions = parser.parse("--just-generate-node-info")
|
||||
assertThat(cmdLineOptions.justGenerateNodeInfo).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear network map cache`() {
|
||||
val cmdLineOptions = parser.parse("--clear-network-map-cache")
|
||||
assertThat(cmdLineOptions.clearNetworkMapCache).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bootstrap raft cluster`() {
|
||||
val cmdLineOptions = parser.parse("--bootstrap-raft-cluster")
|
||||
assertThat(cmdLineOptions.bootstrapRaftCluster).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on-unknown-config-keys options`() {
|
||||
UnknownConfigKeysPolicy.values().forEach { onUnknownConfigKeyPolicy ->
|
||||
val cmdLineOptions = parser.parse("--on-unknown-config-keys", onUnknownConfigKeyPolicy.name)
|
||||
assertThat(cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(onUnknownConfigKeyPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid argument`() {
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
parser.parse("foo")
|
||||
}.withMessageContaining("Unrecognized argument(s): foo")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid arguments`() {
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
parser.parse("foo", "bar")
|
||||
}.withMessageContaining("Unrecognized argument(s): foo, bar")
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.testing.CliBackwardsCompatibleTest
|
||||
|
||||
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java)
|
@ -0,0 +1,52 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.slf4j.event.Level
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class NodeStartupTest {
|
||||
private val startup = NodeStartup()
|
||||
|
||||
companion object {
|
||||
private lateinit var workingDirectory: Path
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun initDirectories() {
|
||||
workingDirectory = Paths.get(".").normalize().toAbsolutePath()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no command line arguments`() {
|
||||
CommandLine.populateCommand(startup)
|
||||
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
|
||||
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf")
|
||||
assertThat(startup.verbose).isEqualTo(false)
|
||||
assertThat(startup.loggingLevel).isEqualTo(Level.INFO)
|
||||
assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null)
|
||||
assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.justGenerateRpcSslCerts).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.bootstrapRaftCluster).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL)
|
||||
assertThat(startup.cmdLineOptions.devMode).isEqualTo(null)
|
||||
assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `--base-directory`() {
|
||||
CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString())
|
||||
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir")
|
||||
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks")
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user