Refactored NotaryChangeProtocol and tests: moved the proposal verification step into the protocol.

Added another proposal verification step in the NotaryChangeProtocol.
Added the cause exception message to the 'notary change refused' error.
This commit is contained in:
Andrius Dagys
2016-06-14 19:06:44 +01:00
parent 0a5b7ace35
commit 30ca340b6e
12 changed files with 182 additions and 107 deletions

View File

@ -164,7 +164,7 @@ class Cash : FungibleAsset<Currency>() {
throw InsufficientBalanceException(amount - gatheredAmount) throw InsufficientBalanceException(amount - gatheredAmount)
val change = if (takeChangeFrom != null && gatheredAmount > amount) { val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.state.issuanceDef) Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else { } else {
null null
} }

View File

@ -27,7 +27,17 @@ interface ContractState {
/** Contract by which the state belongs */ /** Contract by which the state belongs */
val contract: Contract val contract: Contract
/** List of public keys for each party that can consume this state in a valid transaction. */ /**
* A _participant_ is any party that is able to consume this state in a valid transaction.
*
* The list of participants is required for certain types of transactions. For example, when changing the notary
* for this state ([TransactionType.NotaryChange]), every participants has to be involved and approve the transaction
* so that they receive the updated state, and don't end up in a situation where they can no longer use a state
* they possess, since someone consumed that state during the notary change process.
*
* The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants
* list should just contain the owner.
*/
val participants: List<PublicKey> val participants: List<PublicKey>
} }
@ -172,6 +182,7 @@ data class Command(val value: CommandData, val signers: List<PublicKey>) {
init { init {
require(signers.isNotEmpty()) require(signers.isNotEmpty())
} }
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key)) constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it } private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }

View File

@ -10,9 +10,11 @@ import java.util.*
/** /**
* A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is * A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is
* intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set. * intended to be passed around contracts that may edit it by adding new states/commands. Then once the states
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from * and commands are right, this class can be used as a holding bucket to gather signatures from multiple parties.
* multiple parties. *
* The builder can be customised for specific transaction types, e.g. where additional processing is needed
* before adding a state/command.
*/ */
abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(), abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(),
protected val notary: Party? = null) { protected val notary: Party? = null) {

View File

@ -4,7 +4,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.noneOrSingle import com.r3corda.core.noneOrSingle
import java.security.PublicKey import java.security.PublicKey
/** Defines transaction validation rules for a specific transaction type */ /** Defines transaction build & validation logic for a specific transaction type */
sealed class TransactionType { sealed class TransactionType {
override fun equals(other: Any?) = other?.javaClass == javaClass override fun equals(other: Any?) = other?.javaClass == javaClass
override fun hashCode() = javaClass.name.hashCode() override fun hashCode() = javaClass.name.hashCode()
@ -48,6 +48,7 @@ sealed class TransactionType {
/** A general transaction type where transaction validity is determined by custom contract code */ /** A general transaction type where transaction validity is determined by custom contract code */
class General : TransactionType() { class General : TransactionType() {
/** Just uses the default [TransactionBuilder] with no special logic */
class Builder(notary: Party? = null) : TransactionBuilder(General(), notary) {} class Builder(notary: Party? = null) : TransactionBuilder(General(), notary) {}
/** /**

View File

@ -30,10 +30,9 @@ object NotaryChangeProtocol {
data class Proposal(val stateRef: StateRef, data class Proposal(val stateRef: StateRef,
val newNotary: Party, val newNotary: Party,
val sessionIdForSend: Long, val stx: SignedTransaction)
val sessionIdForReceive: Long)
class Handshake(val payload: Proposal, class Handshake(val sessionIdForSend: Long,
replyTo: SingleMessageRecipient, replyTo: SingleMessageRecipient,
replySessionId: Long) : AbstractRequestMessage(replyTo, replySessionId) replySessionId: Long) : AbstractRequestMessage(replyTo, replySessionId)
@ -102,18 +101,21 @@ object NotaryChangeProtocol {
@Suspendable @Suspendable
private fun getParticipantSignature(node: NodeInfo, stx: SignedTransaction, sessionIdForSend: Long): DigitalSignature.WithKey { private fun getParticipantSignature(node: NodeInfo, stx: SignedTransaction, sessionIdForSend: Long): DigitalSignature.WithKey {
val sessionIdForReceive = random63BitValue() val sessionIdForReceive = random63BitValue()
val proposal = Proposal(originalState.ref, newNotary, sessionIdForSend, sessionIdForReceive) val proposal = Proposal(originalState.ref, newNotary, stx)
val handshake = Handshake(proposal, serviceHub.networkService.myAddress, sessionIdForReceive) val handshake = Handshake(sessionIdForSend, serviceHub.networkService.myAddress, sessionIdForReceive)
val protocolInitiated = sendAndReceive<Boolean>(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake).validate { it } sendAndReceive<Unit>(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake)
if (!protocolInitiated) throw Refused(node.identity, originalState)
val response = sendAndReceive<DigitalSignature.WithKey>(TOPIC_CHANGE, node.address, sessionIdForSend, sessionIdForReceive, stx) val response = sendAndReceive<Result>(TOPIC_CHANGE, node.address, sessionIdForSend, sessionIdForReceive, proposal)
val participantSignature = response.validate { val participantSignature = response.validate {
check(it.by == node.identity.owningKey) { "Not signed by the required participant" } if (it.sig == null) throw NotaryChangeException(it.error!!)
it.verifyWithECDSA(stx.txBits) else {
it check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" }
it.sig.verifyWithECDSA(stx.txBits)
it.sig
}
} }
return participantSignature return participantSignature
} }
@ -132,38 +134,88 @@ object NotaryChangeProtocol {
companion object { companion object {
object VERIFYING : ProgressTracker.Step("Verifying Notary change proposal") object VERIFYING : ProgressTracker.Step("Verifying Notary change proposal")
object SIGNING : ProgressTracker.Step("Signing Notary change transaction") object APPROVING : ProgressTracker.Step("Notary change approved")
fun tracker() = ProgressTracker(VERIFYING, SIGNING) object REJECTING : ProgressTracker.Step("Notary change rejected")
fun tracker() = ProgressTracker(VERIFYING, APPROVING, REJECTING)
} }
@Suspendable @Suspendable
override fun call() { override fun call() {
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
val proposal = receive<Proposal>(TOPIC_CHANGE, sessionIdForReceive).validate { it }
val proposedTx = receive<SignedTransaction>(TOPIC_CHANGE, sessionIdForReceive).validate { validateTx(it) } try {
verifyProposal(proposal)
verifyTx(proposal.stx)
} catch(e: Exception) {
// TODO: catch only specific exceptions. However, there are numerous validation exceptions
// that might occur (tx validation/resolution, invalid proposal). Need to rethink how
// we manage exceptions and maybe introduce some platform exception hierarchy
val myIdentity = serviceHub.storageService.myLegalIdentity
val state = proposal.stateRef
val reason = NotaryChangeRefused(myIdentity, state, e.message)
progressTracker.currentStep = SIGNING reject(reason)
return
}
val mySignature = sign(proposedTx) approve(proposal.stx)
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, mySignature) }
@Suspendable
private fun approve(stx: SignedTransaction) {
progressTracker.currentStep = APPROVING
val mySignature = sign(stx)
val response = Result.noError(mySignature)
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, response)
val allSignatures = swapSignatures.validate { signatures -> val allSignatures = swapSignatures.validate { signatures ->
signatures.forEach { it.verifyWithECDSA(proposedTx.txBits) } signatures.forEach { it.verifyWithECDSA(stx.txBits) }
signatures signatures
} }
val finalTx = proposedTx + allSignatures val finalTx = stx + allSignatures
finalTx.verify() finalTx.verify()
serviceHub.recordTransactions(listOf(finalTx)) serviceHub.recordTransactions(listOf(finalTx))
} }
@Suspendable @Suspendable
private fun validateTx(stx: SignedTransaction): SignedTransaction { private fun reject(e: NotaryChangeRefused) {
progressTracker.currentStep = REJECTING
val response = Result.withError(e)
send(TOPIC_CHANGE, otherSide, sessionIdForSend, response)
}
/**
* Check the notary change proposal.
*
* For example, if the proposed new notary has the same behaviour (e.g. both are non-validating)
* and is also in a geographically convenient location we can just automatically approve the change.
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
*/
@Suspendable
private fun verifyProposal(proposal: NotaryChangeProtocol.Proposal) {
val newNotary = proposal.newNotary
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
val state = proposal.stateRef
val proposedTx = proposal.stx.tx
require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" }
// An example requirement
val blacklist = listOf("Evil Notary")
require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" }
}
@Suspendable
private fun verifyTx(stx: SignedTransaction) {
checkMySignatureRequired(stx.tx) checkMySignatureRequired(stx.tx)
checkDependenciesValid(stx) checkDependenciesValid(stx)
checkValid(stx) checkValid(stx)
return stx
} }
private fun checkMySignatureRequired(tx: WireTransaction) { private fun checkMySignatureRequired(tx: WireTransaction) {
@ -189,8 +241,20 @@ object NotaryChangeProtocol {
} }
} }
/** Thrown when a participant refuses to change the notary of the state */ // TODO: similar classes occur in other places (NotaryProtocol), need to consolidate
class Refused(val identity: Party, val originalState: StateAndRef<*>) : Exception() { data class Result private constructor(val sig: DigitalSignature.WithKey?, val error: NotaryChangeRefused?) {
override fun toString() = "A participant $identity refused to change the notary of state $originalState" companion object {
fun withError(error: NotaryChangeRefused) = Result(null, error)
fun noError(sig: DigitalSignature.WithKey) = Result(sig, null)
}
} }
}
/** Thrown when a participant refuses to change the notary of the state */
class NotaryChangeRefused(val identity: Party, val state: StateRef, val cause: String?) {
override fun toString() = "A participant $identity refused to change the notary of state $state"
}
class NotaryChangeException(val error: NotaryChangeRefused) : Exception() {
override fun toString() = "${super.toString()}: Notary change failed - ${error.toString()}"
} }

View File

@ -26,13 +26,13 @@ class TransactionGraphSearchTests {
* @param signer signer for the two transactions and their commands. * @param signer signer for the two transactions and their commands.
*/ */
fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage { fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage {
val originTx = TransactionBuilder().apply { val originTx = TransactionType.General.Builder().apply {
addOutputState(DummyContract.State(random31BitValue(), DUMMY_NOTARY)) addOutputState(DummyContract.State(random31BitValue()), DUMMY_NOTARY)
addCommand(command, signer.public) addCommand(command, signer.public)
signWith(signer) signWith(signer)
}.toSignedTransaction(false) }.toSignedTransaction(false)
val inputTx = TransactionBuilder().apply { val inputTx = TransactionType.General.Builder().apply {
addInputState(originTx.tx.outRef<DummyContract.State>(0).ref) addInputState(originTx.tx.outRef<DummyContract.State>(0))
signWith(signer) signWith(signer)
}.toSignedTransaction(false) }.toSignedTransaction(false)
return GraphTransactionStorage(originTx, inputTx) return GraphTransactionStorage(originTx, inputTx)

View File

@ -5,6 +5,7 @@ import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
import org.junit.Test import org.junit.Test
import java.security.PublicKey
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -12,14 +13,15 @@ class WalletUpdateTests {
object DummyContract : Contract { object DummyContract : Contract {
override fun verify(tx: TransactionForVerification) { override fun verify(tx: TransactionForContract) {
} }
override val legalContractReference: SecureHash = SecureHash.sha256("") override val legalContractReference: SecureHash = SecureHash.sha256("")
} }
private class DummyState : ContractState { private class DummyState : ContractState {
override val notary = DUMMY_NOTARY override val participants: List<PublicKey>
get() = emptyList()
override val contract = WalletUpdateTests.DummyContract override val contract = WalletUpdateTests.DummyContract
} }
@ -29,11 +31,11 @@ class WalletUpdateTests {
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3) private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3)
private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4) private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4)
private val stateAndRef0 = StateAndRef<DummyState>(DummyState(), stateRef0) private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef0)
private val stateAndRef1 = StateAndRef<DummyState>(DummyState(), stateRef1) private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef1)
private val stateAndRef2 = StateAndRef<DummyState>(DummyState(), stateRef2) private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef2)
private val stateAndRef3 = StateAndRef<DummyState>(DummyState(), stateRef3) private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef3)
private val stateAndRef4 = StateAndRef<DummyState>(DummyState(), stateRef4) private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef4)
@Test @Test
fun `nothing plus nothing is nothing`() { fun `nothing plus nothing is nothing`() {

View File

@ -3,6 +3,8 @@ package com.r3corda.node.internal.testing
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.DummyContract import com.r3corda.core.contracts.DummyContract
import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.TransactionState
import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
@ -11,8 +13,8 @@ import com.r3corda.node.internal.AbstractNode
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { fun issueState(node: AbstractNode): StateAndRef<*> {
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY)
tx.signWith(node.storage.myLegalIdentityKey) tx.signWith(node.storage.myLegalIdentityKey)
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction() val stx = tx.toSignedTransaction()
@ -20,6 +22,20 @@ fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*>
return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
} }
fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode): StateAndRef<DummyContract.MultiOwnerState> {
val state = TransactionState(DummyContract.MultiOwnerState(0,
listOf(nodeA.info.identity.owningKey, nodeB.info.identity.owningKey)), DUMMY_NOTARY)
val tx = TransactionType.NotaryChange.Builder().withItems(state)
tx.signWith(nodeA.storage.myLegalIdentityKey)
tx.signWith(nodeB.storage.myLegalIdentityKey)
tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction()
nodeA.services.recordTransactions(listOf(stx))
nodeB.services.recordTransactions(listOf(stx))
val stateAndRef = StateAndRef(state, StateRef(stx.id, 0))
return stateAndRef
}
fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> {
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary)
tx.setTime(Instant.now(), notary, 30.seconds) tx.setTime(Instant.now(), notary, 30.seconds)

View File

@ -3,7 +3,7 @@ package com.r3corda.node.internal.testing
import com.r3corda.contracts.cash.Cash import com.r3corda.contracts.cash.Cash
import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Issued import com.r3corda.core.contracts.Issued
import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
@ -34,7 +34,7 @@ object WalletFiller {
// this field as there's no other database or source of truth we need to sync with. // this field as there's no other database or source of truth we need to sync with.
val depositRef = myIdentity.ref(ref) val depositRef = myIdentity.ref(ref)
val issuance = TransactionBuilder() val issuance = TransactionType.General.Builder()
val freshKey = services.keyManagementService.freshKey() val freshKey = services.keyManagementService.freshKey()
cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary) cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary)
issuance.signWith(myKey) issuance.signWith(myKey)

View File

@ -17,37 +17,11 @@ class NotaryChangeService(net: MessagingService, val smm: StateMachineManager) :
) )
} }
private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake): Boolean { private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake) {
val proposal = req.payload val protocol = NotaryChangeProtocol.Acceptor(
val autoAccept = checkProposal(proposal) req.replyTo as SingleMessageRecipient,
req.sessionID!!,
if (autoAccept) { req.sessionIdForSend)
val protocol = NotaryChangeProtocol.Acceptor( smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
req.replyTo as SingleMessageRecipient,
proposal.sessionIdForReceive,
proposal.sessionIdForSend)
smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
}
return autoAccept
}
/**
* Check the notary change for a state proposal and decide whether to allow the change and initiate the protocol
* or deny the change.
*
* For example, if the proposed new notary has the same behaviour (e.g. both are non-validating)
* and is also in a geographically convenient location we can just automatically approve the change.
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
*/
private fun checkProposal(proposal: NotaryChangeProtocol.Proposal): Boolean {
val newNotary = proposal.newNotary
val isNotary = smm.serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
// An example requirement
val blacklist = listOf("Evil Notary")
require(!blacklist.contains(newNotary.name))
return true
} }
} }

View File

@ -1,20 +1,24 @@
package node.services package node.services
import com.r3corda.contracts.DummyContract import com.r3corda.core.crypto.Party
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.TransactionState
import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.core.testing.DUMMY_NOTARY_KEY
import com.r3corda.node.internal.testing.MockNetwork import com.r3corda.node.internal.testing.MockNetwork
import com.r3corda.node.internal.testing.issueMultiPartyState
import com.r3corda.node.internal.testing.issueState import com.r3corda.node.internal.testing.issueState
import com.r3corda.node.services.transactions.NotaryService import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.SimpleNotaryService
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import protocols.NotaryChangeException
import protocols.NotaryChangeProtocol import protocols.NotaryChangeProtocol
import protocols.NotaryChangeProtocol.Instigator import protocols.NotaryChangeProtocol.Instigator
import protocols.NotaryChangeRefused
import java.util.concurrent.ExecutionException
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class NotaryChangeTests { class NotaryChangeTests {
lateinit var net: MockNetwork lateinit var net: MockNetwork
@ -26,22 +30,22 @@ class NotaryChangeTests {
@Before @Before
fun setup() { fun setup() {
net = MockNetwork() net = MockNetwork()
oldNotaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) oldNotaryNode = net.createNode(
clientNodeA = net.createPartyNode(networkMapAddr = oldNotaryNode.info) legalName = DUMMY_NOTARY.name,
clientNodeB = net.createPartyNode(networkMapAddr = oldNotaryNode.info) keyPair = DUMMY_NOTARY_KEY,
newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info, advertisedServices = NotaryService.Type) advertisedServices = *arrayOf(NetworkMapService.Type, SimpleNotaryService.Type))
clientNodeA = net.createNode(networkMapAddress = oldNotaryNode.info)
clientNodeB = net.createNode(networkMapAddress = oldNotaryNode.info)
newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info, advertisedServices = SimpleNotaryService.Type)
net.runNetwork() // Clear network map registration messages net.runNetwork() // Clear network map registration messages
} }
@Test @Test
fun `should change notary for a state with single participant`() { fun `should change notary for a state with single participant`() {
val ref = issueState(clientNodeA, DUMMY_NOTARY).ref val state = issueState(clientNodeA)
val state = clientNodeA.services.loadState(ref)
val newNotary = newNotaryNode.info.identity val newNotary = newNotaryNode.info.identity
val protocol = Instigator(state, newNotary)
val protocol = Instigator(StateAndRef(state, ref), newNotary)
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
net.runNetwork() net.runNetwork()
@ -52,21 +56,9 @@ class NotaryChangeTests {
@Test @Test
fun `should change notary for a state with multiple participants`() { fun `should change notary for a state with multiple participants`() {
val state = TransactionState(DummyContract.MultiOwnerState(0, val state = issueMultiPartyState(clientNodeA, clientNodeB)
listOf(clientNodeA.info.identity.owningKey, clientNodeB.info.identity.owningKey)), DUMMY_NOTARY)
val tx = TransactionType.NotaryChange.Builder().withItems(state)
tx.signWith(clientNodeA.storage.myLegalIdentityKey)
tx.signWith(clientNodeB.storage.myLegalIdentityKey)
tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction()
clientNodeA.services.recordTransactions(listOf(stx))
clientNodeB.services.recordTransactions(listOf(stx))
val stateAndRef = StateAndRef(state, StateRef(stx.id, 0))
val newNotary = newNotaryNode.info.identity val newNotary = newNotaryNode.info.identity
val protocol = Instigator(state, newNotary)
val protocol = Instigator(stateAndRef, newNotary)
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
net.runNetwork() net.runNetwork()
@ -78,8 +70,21 @@ class NotaryChangeTests {
assertEquals(loadedStateA, loadedStateB) assertEquals(loadedStateA, loadedStateB)
} }
@Test
fun `should throw when a participant refuses to change Notary`() {
val state = issueMultiPartyState(clientNodeA, clientNodeB)
val newEvilNotary = Party("Evil Notary", generateKeyPair().public)
val protocol = Instigator(state, newEvilNotary)
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
net.runNetwork()
val ex = assertFailsWith(ExecutionException::class) { future.get() }
val error = (ex.cause as NotaryChangeException).error
assertTrue(error is NotaryChangeRefused)
}
// TODO: Add more test cases once we have a general protocol/service exception handling mechanism: // TODO: Add more test cases once we have a general protocol/service exception handling mechanism:
// - A participant refuses to change Notary
// - A participant is offline/can't be found on the network // - A participant is offline/can't be found on the network
// - The requesting party is not a participant // - The requesting party is not a participant
// - The requesting party wants to change additional state fields // - The requesting party wants to change additional state fields