mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +00:00
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:
parent
0a5b7ace35
commit
30ca340b6e
@ -164,7 +164,7 @@ class Cash : FungibleAsset<Currency>() {
|
||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||
|
||||
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 {
|
||||
null
|
||||
}
|
||||
|
@ -27,7 +27,17 @@ interface ContractState {
|
||||
/** Contract by which the state belongs */
|
||||
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>
|
||||
}
|
||||
|
||||
@ -172,6 +182,7 @@ data class Command(val value: CommandData, val signers: List<PublicKey>) {
|
||||
init {
|
||||
require(signers.isNotEmpty())
|
||||
}
|
||||
|
||||
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
|
||||
|
||||
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
|
||||
|
@ -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
|
||||
* intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set.
|
||||
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
|
||||
* multiple parties.
|
||||
* intended to be passed around contracts that may edit it by adding new states/commands. Then once the states
|
||||
* and commands are right, this class can be used as a holding bucket to gather signatures from 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(),
|
||||
protected val notary: Party? = null) {
|
||||
|
@ -4,7 +4,7 @@ import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.noneOrSingle
|
||||
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 {
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass
|
||||
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 */
|
||||
class General : TransactionType() {
|
||||
/** Just uses the default [TransactionBuilder] with no special logic */
|
||||
class Builder(notary: Party? = null) : TransactionBuilder(General(), notary) {}
|
||||
|
||||
/**
|
||||
|
@ -30,10 +30,9 @@ object NotaryChangeProtocol {
|
||||
|
||||
data class Proposal(val stateRef: StateRef,
|
||||
val newNotary: Party,
|
||||
val sessionIdForSend: Long,
|
||||
val sessionIdForReceive: Long)
|
||||
val stx: SignedTransaction)
|
||||
|
||||
class Handshake(val payload: Proposal,
|
||||
class Handshake(val sessionIdForSend: Long,
|
||||
replyTo: SingleMessageRecipient,
|
||||
replySessionId: Long) : AbstractRequestMessage(replyTo, replySessionId)
|
||||
|
||||
@ -102,18 +101,21 @@ object NotaryChangeProtocol {
|
||||
@Suspendable
|
||||
private fun getParticipantSignature(node: NodeInfo, stx: SignedTransaction, sessionIdForSend: Long): DigitalSignature.WithKey {
|
||||
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 protocolInitiated = sendAndReceive<Boolean>(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake).validate { it }
|
||||
if (!protocolInitiated) throw Refused(node.identity, originalState)
|
||||
val handshake = Handshake(sessionIdForSend, serviceHub.networkService.myAddress, sessionIdForReceive)
|
||||
sendAndReceive<Unit>(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake)
|
||||
|
||||
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 {
|
||||
check(it.by == node.identity.owningKey) { "Not signed by the required participant" }
|
||||
it.verifyWithECDSA(stx.txBits)
|
||||
it
|
||||
if (it.sig == null) throw NotaryChangeException(it.error!!)
|
||||
else {
|
||||
check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" }
|
||||
it.sig.verifyWithECDSA(stx.txBits)
|
||||
it.sig
|
||||
}
|
||||
}
|
||||
|
||||
return participantSignature
|
||||
}
|
||||
|
||||
@ -132,38 +134,88 @@ object NotaryChangeProtocol {
|
||||
companion object {
|
||||
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
|
||||
override fun call() {
|
||||
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)
|
||||
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, mySignature)
|
||||
approve(proposal.stx)
|
||||
}
|
||||
|
||||
@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 ->
|
||||
signatures.forEach { it.verifyWithECDSA(proposedTx.txBits) }
|
||||
signatures.forEach { it.verifyWithECDSA(stx.txBits) }
|
||||
signatures
|
||||
}
|
||||
|
||||
val finalTx = proposedTx + allSignatures
|
||||
val finalTx = stx + allSignatures
|
||||
finalTx.verify()
|
||||
serviceHub.recordTransactions(listOf(finalTx))
|
||||
}
|
||||
|
||||
@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)
|
||||
checkDependenciesValid(stx)
|
||||
checkValid(stx)
|
||||
return stx
|
||||
}
|
||||
|
||||
private fun checkMySignatureRequired(tx: WireTransaction) {
|
||||
@ -189,8 +241,20 @@ object NotaryChangeProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when a participant refuses to change the notary of the state */
|
||||
class Refused(val identity: Party, val originalState: StateAndRef<*>) : Exception() {
|
||||
override fun toString() = "A participant $identity refused to change the notary of state $originalState"
|
||||
// TODO: similar classes occur in other places (NotaryProtocol), need to consolidate
|
||||
data class Result private constructor(val sig: DigitalSignature.WithKey?, val error: NotaryChangeRefused?) {
|
||||
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()}"
|
||||
}
|
Binary file not shown.
@ -26,13 +26,13 @@ class TransactionGraphSearchTests {
|
||||
* @param signer signer for the two transactions and their commands.
|
||||
*/
|
||||
fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage {
|
||||
val originTx = TransactionBuilder().apply {
|
||||
addOutputState(DummyContract.State(random31BitValue(), DUMMY_NOTARY))
|
||||
val originTx = TransactionType.General.Builder().apply {
|
||||
addOutputState(DummyContract.State(random31BitValue()), DUMMY_NOTARY)
|
||||
addCommand(command, signer.public)
|
||||
signWith(signer)
|
||||
}.toSignedTransaction(false)
|
||||
val inputTx = TransactionBuilder().apply {
|
||||
addInputState(originTx.tx.outRef<DummyContract.State>(0).ref)
|
||||
val inputTx = TransactionType.General.Builder().apply {
|
||||
addInputState(originTx.tx.outRef<DummyContract.State>(0))
|
||||
signWith(signer)
|
||||
}.toSignedTransaction(false)
|
||||
return GraphTransactionStorage(originTx, inputTx)
|
||||
|
@ -5,6 +5,7 @@ import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.testing.DUMMY_NOTARY
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
||||
@ -12,14 +13,15 @@ class WalletUpdateTests {
|
||||
|
||||
object DummyContract : Contract {
|
||||
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
}
|
||||
|
||||
private class DummyState : ContractState {
|
||||
override val notary = DUMMY_NOTARY
|
||||
override val participants: List<PublicKey>
|
||||
get() = emptyList()
|
||||
override val contract = WalletUpdateTests.DummyContract
|
||||
}
|
||||
|
||||
@ -29,11 +31,11 @@ class WalletUpdateTests {
|
||||
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3)
|
||||
private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4)
|
||||
|
||||
private val stateAndRef0 = StateAndRef<DummyState>(DummyState(), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef<DummyState>(DummyState(), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef<DummyState>(DummyState(), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef<DummyState>(DummyState(), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef<DummyState>(DummyState(), stateRef4)
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef4)
|
||||
|
||||
@Test
|
||||
fun `nothing plus nothing is nothing`() {
|
||||
|
@ -3,6 +3,8 @@ package com.r3corda.node.internal.testing
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.DummyContract
|
||||
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.seconds
|
||||
import com.r3corda.core.testing.DUMMY_NOTARY
|
||||
@ -11,8 +13,8 @@ import com.r3corda.node.internal.AbstractNode
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary)
|
||||
fun issueState(node: AbstractNode): StateAndRef<*> {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY)
|
||||
tx.signWith(node.storage.myLegalIdentityKey)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
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))
|
||||
}
|
||||
|
||||
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<*> {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary)
|
||||
tx.setTime(Instant.now(), notary, 30.seconds)
|
||||
|
@ -3,7 +3,7 @@ package com.r3corda.node.internal.testing
|
||||
import com.r3corda.contracts.cash.Cash
|
||||
import com.r3corda.core.contracts.Amount
|
||||
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.node.ServiceHub
|
||||
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.
|
||||
val depositRef = myIdentity.ref(ref)
|
||||
|
||||
val issuance = TransactionBuilder()
|
||||
val issuance = TransactionType.General.Builder()
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary)
|
||||
issuance.signWith(myKey)
|
||||
|
@ -17,37 +17,11 @@ class NotaryChangeService(net: MessagingService, val smm: StateMachineManager) :
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake): Boolean {
|
||||
val proposal = req.payload
|
||||
val autoAccept = checkProposal(proposal)
|
||||
|
||||
if (autoAccept) {
|
||||
val protocol = NotaryChangeProtocol.Acceptor(
|
||||
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
|
||||
private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake) {
|
||||
val protocol = NotaryChangeProtocol.Acceptor(
|
||||
req.replyTo as SingleMessageRecipient,
|
||||
req.sessionID!!,
|
||||
req.sessionIdForSend)
|
||||
smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,24 @@
|
||||
package node.services
|
||||
|
||||
import com.r3corda.contracts.DummyContract
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
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.generateKeyPair
|
||||
import com.r3corda.core.testing.DUMMY_NOTARY
|
||||
import com.r3corda.core.testing.DUMMY_NOTARY_KEY
|
||||
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.services.transactions.NotaryService
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.NotaryChangeException
|
||||
import protocols.NotaryChangeProtocol
|
||||
import protocols.NotaryChangeProtocol.Instigator
|
||||
import protocols.NotaryChangeRefused
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NotaryChangeTests {
|
||||
lateinit var net: MockNetwork
|
||||
@ -26,22 +30,22 @@ class NotaryChangeTests {
|
||||
@Before
|
||||
fun setup() {
|
||||
net = MockNetwork()
|
||||
oldNotaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
clientNodeA = net.createPartyNode(networkMapAddr = oldNotaryNode.info)
|
||||
clientNodeB = net.createPartyNode(networkMapAddr = oldNotaryNode.info)
|
||||
newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info, advertisedServices = NotaryService.Type)
|
||||
oldNotaryNode = net.createNode(
|
||||
legalName = DUMMY_NOTARY.name,
|
||||
keyPair = DUMMY_NOTARY_KEY,
|
||||
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
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change notary for a state with single participant`() {
|
||||
val ref = issueState(clientNodeA, DUMMY_NOTARY).ref
|
||||
val state = clientNodeA.services.loadState(ref)
|
||||
|
||||
val state = issueState(clientNodeA)
|
||||
val newNotary = newNotaryNode.info.identity
|
||||
|
||||
val protocol = Instigator(StateAndRef(state, ref), newNotary)
|
||||
val protocol = Instigator(state, newNotary)
|
||||
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
|
||||
|
||||
net.runNetwork()
|
||||
@ -52,21 +56,9 @@ class NotaryChangeTests {
|
||||
|
||||
@Test
|
||||
fun `should change notary for a state with multiple participants`() {
|
||||
val state = TransactionState(DummyContract.MultiOwnerState(0,
|
||||
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 state = issueMultiPartyState(clientNodeA, clientNodeB)
|
||||
val newNotary = newNotaryNode.info.identity
|
||||
|
||||
val protocol = Instigator(stateAndRef, newNotary)
|
||||
val protocol = Instigator(state, newNotary)
|
||||
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
|
||||
|
||||
net.runNetwork()
|
||||
@ -78,8 +70,21 @@ class NotaryChangeTests {
|
||||
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:
|
||||
// - A participant refuses to change Notary
|
||||
// - A participant is offline/can't be found on the network
|
||||
// - The requesting party is not a participant
|
||||
// - The requesting party wants to change additional state fields
|
||||
|
Loading…
Reference in New Issue
Block a user