mirror of
https://github.com/corda/corda.git
synced 2025-06-22 17:09:00 +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:
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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()}"
|
||||||
|
}
|
Binary file not shown.
@ -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)
|
||||||
|
@ -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`() {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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 autoAccept = checkProposal(proposal)
|
|
||||||
|
|
||||||
if (autoAccept) {
|
|
||||||
val protocol = NotaryChangeProtocol.Acceptor(
|
val protocol = NotaryChangeProtocol.Acceptor(
|
||||||
req.replyTo as SingleMessageRecipient,
|
req.replyTo as SingleMessageRecipient,
|
||||||
proposal.sessionIdForReceive,
|
req.sessionID!!,
|
||||||
proposal.sessionIdForSend)
|
req.sessionIdForSend)
|
||||||
smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user