mirror of
https://github.com/corda/corda.git
synced 2024-12-19 13:08:04 +00:00
Fix validating notary flow to handle notary change transactions properly. (#1687)
Add a notary change test for checking longer chains involving both regular and notary change transactions.
This commit is contained in:
parent
dc0a8fecc4
commit
bbc8bdf8a5
@ -1,13 +1,14 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.CordaService
|
import net.corda.core.node.services.CordaService
|
||||||
import net.corda.core.node.services.TimeWindowChecker
|
import net.corda.core.node.services.TimeWindowChecker
|
||||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.TransactionWithSignatures
|
||||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
@ -35,10 +36,17 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
|||||||
override fun receiveAndVerifyTx(): TransactionParts {
|
override fun receiveAndVerifyTx(): TransactionParts {
|
||||||
try {
|
try {
|
||||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||||
checkNotary(stx.notary)
|
val notary = stx.notary
|
||||||
checkSignatures(stx)
|
checkNotary(notary)
|
||||||
val wtx = stx.tx
|
var timeWindow: TimeWindow? = null
|
||||||
return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow, wtx.notary)
|
val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) {
|
||||||
|
stx.resolveNotaryChangeTransaction(serviceHub)
|
||||||
|
} else {
|
||||||
|
timeWindow = stx.tx.timeWindow
|
||||||
|
stx
|
||||||
|
}
|
||||||
|
checkSignatures(transactionWithSignatures)
|
||||||
|
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw when (e) {
|
throw when (e) {
|
||||||
is TransactionVerificationException,
|
is TransactionVerificationException,
|
||||||
@ -48,9 +56,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSignatures(stx: SignedTransaction) {
|
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||||
try {
|
try {
|
||||||
stx.verifySignaturesExcept(service.notaryIdentityKey)
|
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||||
} catch (e: SignatureException) {
|
} catch (e: SignatureException) {
|
||||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
throw NotaryException(NotaryError.TransactionInvalid(e))
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package net.corda.node.services.transactions
|
package net.corda.node.services.transactions
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.TransactionWithSignatures
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,9 +16,8 @@ import java.security.SignatureException
|
|||||||
*/
|
*/
|
||||||
class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) {
|
class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) {
|
||||||
/**
|
/**
|
||||||
* The received transaction is checked for contract-validity, which requires fully resolving it into a
|
* Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that
|
||||||
* [TransactionForVerification], for which the caller also has to to reveal the whole transaction
|
* the transaction in question has all required signatures apart from the notary's.
|
||||||
* dependency chain.
|
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun receiveAndVerifyTx(): TransactionParts {
|
override fun receiveAndVerifyTx(): TransactionParts {
|
||||||
@ -25,9 +25,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
|||||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||||
val notary = stx.notary
|
val notary = stx.notary
|
||||||
checkNotary(notary)
|
checkNotary(notary)
|
||||||
checkSignatures(stx)
|
var timeWindow: TimeWindow? = null
|
||||||
val wtx = stx.tx
|
val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) {
|
||||||
return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow, notary!!)
|
stx.resolveNotaryChangeTransaction(serviceHub)
|
||||||
|
} else {
|
||||||
|
timeWindow = stx.tx.timeWindow
|
||||||
|
stx
|
||||||
|
}
|
||||||
|
checkSignatures(transactionWithSignatures)
|
||||||
|
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw when (e) {
|
throw when (e) {
|
||||||
is TransactionVerificationException,
|
is TransactionVerificationException,
|
||||||
@ -37,10 +43,10 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSignatures(stx: SignedTransaction) {
|
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||||
try {
|
try {
|
||||||
stx.verifySignaturesExcept(service.notaryIdentityKey)
|
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||||
} catch(e: SignatureException) {
|
} catch (e: SignatureException) {
|
||||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
throw NotaryException(NotaryError.TransactionInvalid(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.node.services
|
|||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.flows.NotaryChangeFlow
|
import net.corda.core.flows.NotaryChangeFlow
|
||||||
|
import net.corda.core.flows.NotaryFlow
|
||||||
import net.corda.core.flows.StateReplacementException
|
import net.corda.core.flows.StateReplacementException
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -12,7 +13,7 @@ import net.corda.core.utilities.getOrThrow
|
|||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||||
import net.corda.nodeapi.internal.ServiceInfo
|
import net.corda.nodeapi.internal.ServiceInfo
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
import net.corda.testing.DUMMY_NOTARY
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
@ -34,8 +35,8 @@ class NotaryChangeTests {
|
|||||||
lateinit var newNotaryNode: StartedNode<MockNetwork.MockNode>
|
lateinit var newNotaryNode: StartedNode<MockNetwork.MockNode>
|
||||||
lateinit var clientNodeA: StartedNode<MockNetwork.MockNode>
|
lateinit var clientNodeA: StartedNode<MockNetwork.MockNode>
|
||||||
lateinit var clientNodeB: StartedNode<MockNetwork.MockNode>
|
lateinit var clientNodeB: StartedNode<MockNetwork.MockNode>
|
||||||
lateinit var notaryNewId: Party
|
lateinit var newNotaryParty: Party
|
||||||
lateinit var notaryOldId: Party
|
lateinit var oldNotaryParty: Party
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
@ -43,15 +44,15 @@ class NotaryChangeTests {
|
|||||||
mockNet = MockNetwork()
|
mockNet = MockNetwork()
|
||||||
oldNotaryNode = mockNet.createNode(
|
oldNotaryNode = mockNet.createNode(
|
||||||
legalName = DUMMY_NOTARY.name,
|
legalName = DUMMY_NOTARY.name,
|
||||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type)))
|
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
|
||||||
clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress)
|
clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress)
|
||||||
clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress)
|
clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress)
|
||||||
newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(SimpleNotaryService.type))
|
newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(ValidatingNotaryService.type))
|
||||||
mockNet.registerIdentities()
|
mockNet.registerIdentities()
|
||||||
mockNet.runNetwork() // Clear network map registration messages
|
mockNet.runNetwork() // Clear network map registration messages
|
||||||
oldNotaryNode.internals.ensureRegistered()
|
oldNotaryNode.internals.ensureRegistered()
|
||||||
notaryNewId = newNotaryNode.info.legalIdentities[1]
|
newNotaryParty = newNotaryNode.info.legalIdentities[1]
|
||||||
notaryOldId = oldNotaryNode.info.legalIdentities[1]
|
oldNotaryParty = oldNotaryNode.info.legalIdentities[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -62,21 +63,16 @@ class NotaryChangeTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should change notary for a state with single participant`() {
|
fun `should change notary for a state with single participant`() {
|
||||||
val state = issueState(clientNodeA, oldNotaryNode, notaryOldId)
|
val state = issueState(clientNodeA, oldNotaryParty)
|
||||||
val newNotary = notaryNewId
|
assertEquals(state.state.notary, oldNotaryParty)
|
||||||
val flow = NotaryChangeFlow(state, newNotary)
|
val newState = changeNotary(state, clientNodeA, newNotaryParty)
|
||||||
val future = clientNodeA.services.startFlow(flow)
|
assertEquals(newState.state.notary, newNotaryParty)
|
||||||
|
|
||||||
mockNet.runNetwork()
|
|
||||||
|
|
||||||
val newState = future.resultFuture.getOrThrow()
|
|
||||||
assertEquals(newState.state.notary, newNotary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should change notary for a state with multiple participants`() {
|
fun `should change notary for a state with multiple participants`() {
|
||||||
val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, notaryOldId)
|
val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, oldNotaryParty)
|
||||||
val newNotary = notaryNewId
|
val newNotary = newNotaryParty
|
||||||
val flow = NotaryChangeFlow(state, newNotary)
|
val flow = NotaryChangeFlow(state, newNotary)
|
||||||
val future = clientNodeA.services.startFlow(flow)
|
val future = clientNodeA.services.startFlow(flow)
|
||||||
|
|
||||||
@ -91,7 +87,7 @@ class NotaryChangeTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should throw when a participant refuses to change Notary`() {
|
fun `should throw when a participant refuses to change Notary`() {
|
||||||
val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, notaryOldId)
|
val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, oldNotaryParty)
|
||||||
val newEvilNotary = getTestPartyAndCertificate(CordaX500Name(organisation = "Evil R3", locality = "London", country = "GB"), generateKeyPair().public)
|
val newEvilNotary = getTestPartyAndCertificate(CordaX500Name(organisation = "Evil R3", locality = "London", country = "GB"), generateKeyPair().public)
|
||||||
val flow = NotaryChangeFlow(state, newEvilNotary.party)
|
val flow = NotaryChangeFlow(state, newEvilNotary.party)
|
||||||
val future = clientNodeA.services.startFlow(flow)
|
val future = clientNodeA.services.startFlow(flow)
|
||||||
@ -105,10 +101,10 @@ class NotaryChangeTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should not break encumbrance links`() {
|
fun `should not break encumbrance links`() {
|
||||||
val issueTx = issueEncumberedState(clientNodeA, notaryOldId)
|
val issueTx = issueEncumberedState(clientNodeA, oldNotaryParty)
|
||||||
|
|
||||||
val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0))
|
val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0))
|
||||||
val newNotary = notaryNewId
|
val newNotary = newNotaryParty
|
||||||
val flow = NotaryChangeFlow(state, newNotary)
|
val flow = NotaryChangeFlow(state, newNotary)
|
||||||
val future = clientNodeA.services.startFlow(flow)
|
val future = clientNodeA.services.startFlow(flow)
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
@ -137,6 +133,47 @@ class NotaryChangeTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notary change and regular transactions are properly handled during resolution in longer chains`() {
|
||||||
|
val issued = issueState(clientNodeA, oldNotaryParty)
|
||||||
|
val moved = moveState(issued, clientNodeA, clientNodeB)
|
||||||
|
|
||||||
|
// We don't to tx resolution when moving state to another node, so need to add the issue transaction manually
|
||||||
|
// to node B. The resolution process is tested later during notarisation.
|
||||||
|
clientNodeB.services.recordTransactions(clientNodeA.services.validatedTransactions.getTransaction(issued.ref.txhash)!!)
|
||||||
|
|
||||||
|
val changedNotary = changeNotary(moved, clientNodeB, newNotaryParty)
|
||||||
|
val movedBack = moveState(changedNotary, clientNodeB, clientNodeA)
|
||||||
|
val changedNotaryBack = changeNotary(movedBack, clientNodeA, oldNotaryParty)
|
||||||
|
|
||||||
|
assertEquals(issued.state, changedNotaryBack.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeNotary(movedState: StateAndRef<DummyContract.SingleOwnerState>, node: StartedNode<*>, newNotary: Party): StateAndRef<DummyContract.SingleOwnerState> {
|
||||||
|
val flow = NotaryChangeFlow(movedState, newNotary)
|
||||||
|
val future = node.services.startFlow(flow)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
return future.resultFuture.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveState(state: StateAndRef<DummyContract.SingleOwnerState>, fromNode: StartedNode<*>, toNode: StartedNode<*>): StateAndRef<DummyContract.SingleOwnerState> {
|
||||||
|
val tx = DummyContract.move(state, toNode.info.chooseIdentity())
|
||||||
|
val stx = fromNode.services.signInitialTransaction(tx)
|
||||||
|
|
||||||
|
val notaryFlow = NotaryFlow.Client(stx)
|
||||||
|
val future = fromNode.services.startFlow(notaryFlow)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
val notarySignature = future.resultFuture.getOrThrow()
|
||||||
|
val finalTransaction = stx + notarySignature
|
||||||
|
|
||||||
|
fromNode.services.recordTransactions(finalTransaction)
|
||||||
|
toNode.services.recordTransactions(finalTransaction)
|
||||||
|
|
||||||
|
return finalTransaction.tx.outRef(0)
|
||||||
|
}
|
||||||
|
|
||||||
private fun issueEncumberedState(node: StartedNode<*>, notaryIdentity: Party): WireTransaction {
|
private fun issueEncumberedState(node: StartedNode<*>, notaryIdentity: Party): WireTransaction {
|
||||||
val owner = node.info.chooseIdentity().ref(0)
|
val owner = node.info.chooseIdentity().ref(0)
|
||||||
val stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party)
|
val stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party)
|
||||||
@ -163,30 +200,31 @@ class NotaryChangeTests {
|
|||||||
// - The transaction type is not a notary change transaction at all.
|
// - The transaction type is not a notary change transaction at all.
|
||||||
}
|
}
|
||||||
|
|
||||||
fun issueState(node: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef<*> {
|
fun issueState(node: StartedNode<*>, notaryIdentity: Party): StateAndRef<DummyContract.SingleOwnerState> {
|
||||||
val tx = DummyContract.generateInitial(Random().nextInt(), notaryIdentity, node.info.chooseIdentity().ref(0))
|
val tx = DummyContract.generateInitial(Random().nextInt(), notaryIdentity, node.info.chooseIdentity().ref(0))
|
||||||
val signedByNode = node.services.signInitialTransaction(tx)
|
val stx = node.services.signInitialTransaction(tx)
|
||||||
val stx = notaryNode.services.addSignature(signedByNode, notaryIdentity.owningKey)
|
|
||||||
node.services.recordTransactions(stx)
|
node.services.recordTransactions(stx)
|
||||||
return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
return stx.tx.outRef(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun issueMultiPartyState(nodeA: StartedNode<*>, nodeB: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef<DummyContract.MultiOwnerState> {
|
fun issueMultiPartyState(nodeA: StartedNode<*>, nodeB: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef<DummyContract.MultiOwnerState> {
|
||||||
val state = TransactionState(DummyContract.MultiOwnerState(0,
|
val participants = listOf(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity())
|
||||||
listOf(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity())), DUMMY_PROGRAM_ID, notaryIdentity)
|
val state = TransactionState(
|
||||||
val tx = TransactionBuilder(notary = notaryIdentity).withItems(state, dummyCommand())
|
DummyContract.MultiOwnerState(0, participants),
|
||||||
|
DUMMY_PROGRAM_ID, notaryIdentity)
|
||||||
|
val tx = TransactionBuilder(notary = notaryIdentity).withItems(state, dummyCommand(participants.first().owningKey))
|
||||||
val signedByA = nodeA.services.signInitialTransaction(tx)
|
val signedByA = nodeA.services.signInitialTransaction(tx)
|
||||||
val signedByAB = nodeB.services.addSignature(signedByA)
|
val signedByAB = nodeB.services.addSignature(signedByA)
|
||||||
val stx = notaryNode.services.addSignature(signedByAB, notaryIdentity.owningKey)
|
val stx = notaryNode.services.addSignature(signedByAB, notaryIdentity.owningKey)
|
||||||
nodeA.services.recordTransactions(stx)
|
nodeA.services.recordTransactions(stx)
|
||||||
nodeB.services.recordTransactions(stx)
|
nodeB.services.recordTransactions(stx)
|
||||||
return StateAndRef(state, StateRef(stx.id, 0))
|
return stx.tx.outRef(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef<*> {
|
fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef<DummyContract.SingleOwnerState> {
|
||||||
val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0))
|
val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0))
|
||||||
tx.setTimeWindow(Instant.now(), 30.seconds)
|
tx.setTimeWindow(Instant.now(), 30.seconds)
|
||||||
val stx = node.services.signInitialTransaction(tx)
|
val stx = node.services.signInitialTransaction(tx)
|
||||||
node.services.recordTransactions(stx)
|
node.services.recordTransactions(stx)
|
||||||
return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
return stx.tx.outRef(0)
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user