mirror of
https://github.com/corda/corda.git
synced 2025-06-18 15:18:16 +00:00
Fix demobench as part of network parameters work (#2148)
* Fix demobench - network parameters Demobench uses ServiceIdentityGenerator to pregenerate notary identity for network parameters.
This commit is contained in:
committed by
GitHub
parent
c2731c6797
commit
6958cbbc44
@ -1,4 +1,4 @@
|
|||||||
package net.corda.node.utilities
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
@ -31,7 +31,7 @@ object ServiceIdentityGenerator {
|
|||||||
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
||||||
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
||||||
|
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
val issuer = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
val issuer = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||||
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||||
|
|
@ -24,7 +24,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
|
|||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.transactions.minClusterSize
|
import net.corda.node.services.transactions.minClusterSize
|
||||||
import net.corda.node.services.transactions.minCorrectReplicas
|
import net.corda.node.services.transactions.minCorrectReplicas
|
||||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.NotaryInfo
|
import net.corda.nodeapi.internal.NotaryInfo
|
||||||
import net.corda.testing.chooseIdentity
|
import net.corda.testing.chooseIdentity
|
||||||
import net.corda.nodeapi.internal.NetworkParametersCopier
|
import net.corda.nodeapi.internal.NetworkParametersCopier
|
||||||
|
@ -89,11 +89,11 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
|
|||||||
val legalName = MEGA_CORP.name
|
val legalName = MEGA_CORP.name
|
||||||
certificatesDirectory.createDirectories()
|
certificatesDirectory.createDirectories()
|
||||||
if (!trustStoreFile.exists()) {
|
if (!trustStoreFile.exists()) {
|
||||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
val caKeyStore = loadKeyStore(
|
val caKeyStore = loadKeyStore(
|
||||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"),
|
javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"),
|
||||||
"cordacadevpass")
|
"cordacadevpass")
|
||||||
|
|
||||||
val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
|
val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
|
||||||
|
@ -50,10 +50,10 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust
|
|||||||
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||||
certificatesDirectory.createDirectories()
|
certificatesDirectory.createDirectories()
|
||||||
if (!trustStoreFile.exists()) {
|
if (!trustStoreFile.exists()) {
|
||||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
||||||
}
|
}
|
||||||
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
|
createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
|
||||||
|
|
||||||
// Move distributed service composite key (generated by ServiceIdentityGenerator.generateToDisk) to keystore if exists.
|
// Move distributed service composite key (generated by ServiceIdentityGenerator.generateToDisk) to keystore if exists.
|
||||||
|
@ -10,7 +10,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
|
|||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.transactions.BFTNonValidatingNotaryService
|
import net.corda.node.services.transactions.BFTNonValidatingNotaryService
|
||||||
import net.corda.node.services.transactions.minCorrectReplicas
|
import net.corda.node.services.transactions.minCorrectReplicas
|
||||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import net.corda.testing.ALICE
|
import net.corda.testing.ALICE
|
||||||
import net.corda.testing.BOB
|
import net.corda.testing.BOB
|
||||||
import net.corda.testing.internal.demorun.*
|
import net.corda.testing.internal.demorun.*
|
||||||
|
@ -10,7 +10,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
|||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.config.RaftConfig
|
import net.corda.node.services.config.RaftConfig
|
||||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import net.corda.testing.ALICE
|
import net.corda.testing.ALICE
|
||||||
import net.corda.testing.BOB
|
import net.corda.testing.BOB
|
||||||
import net.corda.testing.internal.demorun.*
|
import net.corda.testing.internal.demorun.*
|
||||||
|
@ -32,7 +32,7 @@ import net.corda.node.services.config.*
|
|||||||
import net.corda.node.services.transactions.BFTNonValidatingNotaryService
|
import net.corda.node.services.transactions.BFTNonValidatingNotaryService
|
||||||
import net.corda.node.services.transactions.RaftNonValidatingNotaryService
|
import net.corda.node.services.transactions.RaftNonValidatingNotaryService
|
||||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||||
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
||||||
import net.corda.nodeapi.internal.NodeInfoFilesCopier
|
import net.corda.nodeapi.internal.NodeInfoFilesCopier
|
||||||
|
@ -37,7 +37,7 @@ import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
|||||||
import net.corda.node.utilities.AffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor
|
||||||
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.NotaryInfo
|
import net.corda.nodeapi.internal.NotaryInfo
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
import net.corda.testing.DUMMY_NOTARY
|
||||||
import net.corda.nodeapi.internal.NetworkParametersCopier
|
import net.corda.nodeapi.internal.NetworkParametersCopier
|
||||||
|
@ -67,18 +67,18 @@ val DUMMY_REGULATOR: Party get() = Party(CordaX500Name(organisation = "Regulator
|
|||||||
|
|
||||||
val DEV_CA: CertificateAndKeyPair by lazy {
|
val DEV_CA: CertificateAndKeyPair by lazy {
|
||||||
// TODO: Should be identity scheme
|
// TODO: Should be identity scheme
|
||||||
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||||
}
|
}
|
||||||
|
|
||||||
val ROOT_CA: CertificateAndKeyPair by lazy {
|
val ROOT_CA: CertificateAndKeyPair by lazy {
|
||||||
// TODO: Should be identity scheme
|
// TODO: Should be identity scheme
|
||||||
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, "cordacadevkeypass")
|
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, "cordacadevkeypass")
|
||||||
}
|
}
|
||||||
val DEV_TRUST_ROOT: X509CertificateHolder by lazy {
|
val DEV_TRUST_ROOT: X509CertificateHolder by lazy {
|
||||||
// TODO: Should be identity scheme
|
// TODO: Should be identity scheme
|
||||||
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
caKeyStore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last().toX509CertHolder()
|
caKeyStore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last().toX509CertHolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
package net.corda.demobench
|
package net.corda.demobench
|
||||||
|
|
||||||
import javafx.scene.image.Image
|
import javafx.scene.image.Image
|
||||||
|
import net.corda.client.rpc.internal.KryoClientSerializationScheme
|
||||||
|
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||||
|
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||||
import net.corda.demobench.views.DemoBenchView
|
import net.corda.demobench.views.DemoBenchView
|
||||||
|
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
||||||
|
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||||
|
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.nio.charset.StandardCharsets.UTF_8
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
@ -47,6 +53,17 @@ class DemoBench : App(DemoBenchView::class) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
addStageIcon(Image("cordalogo.png"))
|
addStageIcon(Image("cordalogo.png"))
|
||||||
|
initialiseSerialization()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialiseSerialization() {
|
||||||
|
val context = KRYO_P2P_CONTEXT
|
||||||
|
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||||
|
SerializationFactoryImpl().apply {
|
||||||
|
registerScheme(KryoClientSerializationScheme())
|
||||||
|
registerScheme(AMQPClientSerializationScheme())
|
||||||
|
},
|
||||||
|
context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,19 +2,26 @@ package net.corda.demobench.model
|
|||||||
|
|
||||||
import javafx.beans.binding.IntegerExpression
|
import javafx.beans.binding.IntegerExpression
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.copyToDirectory
|
import net.corda.core.internal.copyToDirectory
|
||||||
import net.corda.core.internal.createDirectories
|
import net.corda.core.internal.createDirectories
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.internal.noneOrSingle
|
import net.corda.core.internal.noneOrSingle
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.core.utilities.days
|
||||||
import net.corda.demobench.plugin.CordappController
|
import net.corda.demobench.plugin.CordappController
|
||||||
import net.corda.demobench.pty.R3Pty
|
import net.corda.demobench.pty.R3Pty
|
||||||
|
import net.corda.nodeapi.internal.NetworkParameters
|
||||||
|
import net.corda.nodeapi.internal.NetworkParametersCopier
|
||||||
|
import net.corda.nodeapi.internal.NotaryInfo
|
||||||
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.management.ManagementFactory
|
import java.lang.management.ManagementFactory
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
@ -35,6 +42,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
private val command = jvm.commandFor(cordaPath).toTypedArray()
|
private val command = jvm.commandFor(cordaPath).toTypedArray()
|
||||||
|
|
||||||
private val nodes = LinkedHashMap<String, NodeConfigWrapper>()
|
private val nodes = LinkedHashMap<String, NodeConfigWrapper>()
|
||||||
|
private var notaryIdentity: Party? = null
|
||||||
|
private var networkParametersCopier: NetworkParametersCopier? = null
|
||||||
private val port = AtomicInteger(firstPort)
|
private val port = AtomicInteger(firstPort)
|
||||||
|
|
||||||
val activeNodes: List<NodeConfigWrapper>
|
val activeNodes: List<NodeConfigWrapper>
|
||||||
@ -58,6 +67,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
fun IntegerExpression.toLocalAddress() = NetworkHostAndPort("localhost", value)
|
fun IntegerExpression.toLocalAddress() = NetworkHostAndPort("localhost", value)
|
||||||
|
|
||||||
val location = nodeData.nearestCity.value
|
val location = nodeData.nearestCity.value
|
||||||
|
val notary = nodeData.extraServices.filterIsInstance<NotaryService>().noneOrSingle()
|
||||||
val nodeConfig = NodeConfig(
|
val nodeConfig = NodeConfig(
|
||||||
myLegalName = CordaX500Name(
|
myLegalName = CordaX500Name(
|
||||||
organisation = nodeData.legalName.value.trim(),
|
organisation = nodeData.legalName.value.trim(),
|
||||||
@ -67,7 +77,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
p2pAddress = nodeData.p2pPort.toLocalAddress(),
|
p2pAddress = nodeData.p2pPort.toLocalAddress(),
|
||||||
rpcAddress = nodeData.rpcPort.toLocalAddress(),
|
rpcAddress = nodeData.rpcPort.toLocalAddress(),
|
||||||
webAddress = nodeData.webPort.toLocalAddress(),
|
webAddress = nodeData.webPort.toLocalAddress(),
|
||||||
notary = nodeData.extraServices.filterIsInstance<NotaryService>().noneOrSingle(),
|
notary = notary,
|
||||||
h2port = nodeData.h2Port.value,
|
h2port = nodeData.h2Port.value,
|
||||||
issuableCurrencies = nodeData.extraServices.filterIsInstance<CurrencyIssuer>().map { it.currency.toString() }
|
issuableCurrencies = nodeData.extraServices.filterIsInstance<CurrencyIssuer>().map { it.currency.toString() }
|
||||||
)
|
)
|
||||||
@ -102,6 +112,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
|
|
||||||
fun runCorda(pty: R3Pty, config: NodeConfigWrapper): Boolean {
|
fun runCorda(pty: R3Pty, config: NodeConfigWrapper): Boolean {
|
||||||
try {
|
try {
|
||||||
|
// Notary can be removed and then added again, that's why we need to perform this check.
|
||||||
|
require((config.nodeConfig.notary != null).xor(notaryIdentity != null)) { "There must be exactly one notary in the network" }
|
||||||
config.nodeDir.createDirectories()
|
config.nodeDir.createDirectories()
|
||||||
|
|
||||||
// Install any built-in plugins into the working directory.
|
// Install any built-in plugins into the working directory.
|
||||||
@ -115,6 +127,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
val cordaEnv = System.getenv().toMutableMap().apply {
|
val cordaEnv = System.getenv().toMutableMap().apply {
|
||||||
jvm.setCapsuleCacheDir(this)
|
jvm.setCapsuleCacheDir(this)
|
||||||
}
|
}
|
||||||
|
(networkParametersCopier ?: makeNetworkParametersCopier(config)).install(config.nodeDir)
|
||||||
pty.run(command, cordaEnv, config.nodeDir.toString())
|
pty.run(command, cordaEnv, config.nodeDir.toString())
|
||||||
log.info("Launched node: ${config.nodeConfig.myLegalName}")
|
log.info("Launched node: ${config.nodeConfig.myLegalName}")
|
||||||
return true
|
return true
|
||||||
@ -124,6 +137,30 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun makeNetworkParametersCopier(config: NodeConfigWrapper): NetworkParametersCopier {
|
||||||
|
val identity = getNotaryIdentity(config)
|
||||||
|
val parametersCopier = NetworkParametersCopier(NetworkParameters(
|
||||||
|
minimumPlatformVersion = 1,
|
||||||
|
notaries = listOf(NotaryInfo(identity, config.nodeConfig.notary!!.validating)),
|
||||||
|
modifiedTime = Instant.now(),
|
||||||
|
eventHorizon = 10000.days,
|
||||||
|
maxMessageSize = 40000,
|
||||||
|
maxTransactionSize = 40000,
|
||||||
|
epoch = 1
|
||||||
|
))
|
||||||
|
notaryIdentity = identity
|
||||||
|
networkParametersCopier = parametersCopier
|
||||||
|
return parametersCopier
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate notary identity and save it into node's directory. This identity will be used in network parameters.
|
||||||
|
private fun getNotaryIdentity(config: NodeConfigWrapper): Party {
|
||||||
|
return ServiceIdentityGenerator.generateToDisk(
|
||||||
|
dirs = listOf(config.nodeDir),
|
||||||
|
serviceName = config.nodeConfig.myLegalName,
|
||||||
|
serviceId = "identity")
|
||||||
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
baseDir = baseDirFor(System.currentTimeMillis())
|
baseDir = baseDirFor(System.currentTimeMillis())
|
||||||
log.info("Changed base directory: $baseDir")
|
log.info("Changed base directory: $baseDir")
|
||||||
@ -131,6 +168,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
|||||||
// Wipe out any knowledge of previous nodes.
|
// Wipe out any knowledge of previous nodes.
|
||||||
nodes.clear()
|
nodes.clear()
|
||||||
nodeInfoFilesCopier.reset()
|
nodeInfoFilesCopier.reset()
|
||||||
|
notaryIdentity = null
|
||||||
|
networkParametersCopier = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user