Extended the data model so that every state has to define a set of 'participants' - parties that are able to consume that state in a valid transaction.

Added protocol for changing the notary for a state, which requires signatures from all participants
This commit is contained in:
Andrius Dagys
2016-05-20 17:44:49 +01:00
parent a813e9a088
commit 3b1e020082
17 changed files with 476 additions and 31 deletions

View File

@ -16,6 +16,7 @@ import com.r3corda.core.seconds
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.node.api.APIServer
import com.r3corda.node.services.NotaryChangeService
import com.r3corda.node.services.api.AcceptsFileUpload
import com.r3corda.node.services.api.CheckpointStorage
import com.r3corda.node.services.api.MonitoringService
@ -101,6 +102,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
lateinit var smm: StateMachineManager
lateinit var wallet: WalletService
lateinit var keyManagement: E2ETestKeyManagementService
lateinit var notaryChangeService: NotaryChangeService
var inNodeNetworkMapService: NetworkMapService? = null
var inNodeNotaryService: NotaryService? = null
lateinit var identity: IdentityService
@ -128,6 +130,9 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
net = makeMessagingService()
wallet = NodeWalletService(services)
makeInterestRatesOracleService()
api = APIServerImpl(this)
notaryChangeService = NotaryChangeService(net, smm)
identity = makeIdentityService()
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with

View File

@ -0,0 +1,53 @@
package com.r3corda.node.services
import com.r3corda.contracts.cash.Cash
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.node.services.api.AbstractNodeService
import com.r3corda.node.services.statemachine.StateMachineManager
import protocols.NotaryChangeProtocol
/**
* A service that monitors the network for requests for changing the notary of a state,
* and immediately runs the [NotaryChangeProtocol] if the auto-accept criteria are met.
*/
class NotaryChangeService(net: MessagingService, val smm: StateMachineManager) : AbstractNodeService(net) {
init {
addMessageHandler(NotaryChangeProtocol.TOPIC_INITIATE,
{ req: NotaryChangeProtocol.Handshake -> handleChangeNotaryRequest(req) }
)
}
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 state = smm.serviceHub.loadState(proposal.stateRef)
if (state is Cash.State) return false // TODO: delete this example check
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 " }
return true
}
}

View File

@ -0,0 +1,88 @@
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.TransactionBuilder
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.services.transactions.NotaryService
import com.r3corda.node.testutils.issueState
import org.junit.Before
import org.junit.Test
import protocols.NotaryChangeProtocol
import protocols.NotaryChangeProtocol.Instigator
import kotlin.test.assertEquals
class NotaryChangeTests {
lateinit var net: MockNetwork
lateinit var oldNotaryNode: MockNetwork.MockNode
lateinit var newNotaryNode: MockNetwork.MockNode
lateinit var clientNodeA: MockNetwork.MockNode
lateinit var clientNodeB: MockNetwork.MockNode
@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)
net.runNetwork() // Clear network map registration messages
}
@Test
fun `should change notary for a state with single participant`() {
val state = issueState(clientNodeA, DUMMY_NOTARY)
val ref = clientNodeA.services.loadState(state)
val newNotary = newNotaryNode.info.identity
val protocol = Instigator(StateAndRef(ref, state), newNotary)
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
net.runNetwork()
val newState = future.get()
assertEquals(newState.state.notary, newNotary)
}
@Test
fun `should change notary for a state with multiple participants`() {
val state = DummyContract.MultiOwnerState(0,
listOf(clientNodeA.info.identity.owningKey, clientNodeB.info.identity.owningKey),
DUMMY_NOTARY)
val tx = TransactionBuilder().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 protocol = Instigator(stateAndRef, newNotary)
val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol)
net.runNetwork()
val newState = future.get()
assertEquals(newState.state.notary, newNotary)
val loadedStateA = clientNodeA.services.loadState(newState.ref)
val loadedStateB = clientNodeB.services.loadState(newState.ref)
assertEquals(loadedStateA, loadedStateB)
}
// 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
// - Multiple states in a single "notary change" transaction
// - Transaction contains additional states and commands with business logic
}