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

@ -8,18 +8,22 @@
package com.r3corda.contracts.isolated
import com.r3corda.core.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes.
val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract()
class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdoor {
class State(val magicNumber: Int = 0, override val notary: Party) : ContractState {
data class State(val magicNumber: Int = 0, override val notary: Party) : ContractState {
override val contract = ANOTHER_DUMMY_PROGRAM_ID
override val participants: List<PublicKey>
get() = emptyList()
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
interface Commands : CommandData {

View File

@ -3,8 +3,8 @@ package com.r3corda.contracts;
import com.r3corda.contracts.cash.Cash;
import com.r3corda.contracts.cash.CashKt;
import com.r3corda.contracts.cash.InsufficientBalanceException;
import com.r3corda.core.contracts.TransactionForVerification.InOutGroup;
import com.r3corda.core.contracts.*;
import com.r3corda.core.contracts.TransactionForVerification.InOutGroup;
import com.r3corda.core.crypto.NullPublicKey;
import com.r3corda.core.crypto.Party;
import com.r3corda.core.crypto.SecureHash;
@ -14,6 +14,7 @@ import org.jetbrains.annotations.Nullable;
import java.security.PublicKey;
import java.time.Instant;
import java.util.Currency;
import java.util.ArrayList;
import java.util.List;
import static com.r3corda.core.contracts.ContractsDSLKt.requireSingleCommand;
@ -123,6 +124,20 @@ public class JavaCommercialPaper implements Contract {
public State withoutOwner() {
return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate, notary);
}
@NotNull
@Override
public ContractState withNewNotary(@NotNull Party newNotary) {
return new State(this.issuance, this.owner, this.faceValue, this.maturityDate, newNotary);
}
@NotNull
@Override
public List<PublicKey> getParticipants() {
List<PublicKey> keys = new ArrayList<>();
keys.add(this.owner);
return keys;
}
}
public static class Commands implements CommandData {

View File

@ -3,7 +3,6 @@ package com.r3corda.contracts
import com.r3corda.contracts.cash.Cash
import com.r3corda.contracts.cash.InsufficientBalanceException
import com.r3corda.contracts.cash.sumCashBy
import com.r3corda.core.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party
@ -51,6 +50,8 @@ class CommercialPaper : Contract {
override val notary: Party
) : OwnableState, ICommercialPaperState {
override val contract = CP_PROGRAM_ID
override val participants: List<PublicKey>
get() = listOf(owner)
fun withoutOwner() = copy(owner = NullPublicKey)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
@ -62,6 +63,8 @@ class CommercialPaper : Contract {
override fun withIssuance(newIssuance: PartyAndReference): ICommercialPaperState = copy(issuance = newIssuance)
override fun withFaceValue(newFaceValue: Amount<Issued<Currency>>): ICommercialPaperState = copy(faceValue = newFaceValue)
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
interface Commands : CommandData {

View File

@ -595,6 +595,9 @@ class InterestRateSwap() : Contract {
override val thread = SecureHash.sha256(common.tradeID)
override val ref = common.tradeID
override val participants: List<PublicKey>
get() = parties.map { it.owningKey }
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys)
}
@ -618,6 +621,8 @@ class InterestRateSwap() : Contract {
}
}
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary)
override fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) {

View File

@ -60,10 +60,13 @@ class Cash : FungibleAsset<Currency>() {
override val contract = CASH_PROGRAM_ID
override val issuanceDef: Issued<Currency>
get() = amount.token
override val participants: List<PublicKey>
get() = listOf(owner)
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
// Just for grouping

View File

@ -2,15 +2,30 @@ package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes.
val DUMMY_PROGRAM_ID = DummyContract()
class DummyContract : Contract {
class State(val magicNumber: Int = 0,
override val notary: Party) : ContractState {
data class State(val magicNumber: Int = 0,
override val notary: Party) : ContractState {
override val contract = DUMMY_PROGRAM_ID
override val participants: List<PublicKey>
get() = emptyList()
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
data class MultiOwnerState(val magicNumber: Int = 0,
val owners: List<PublicKey>,
override val notary: Party) : ContractState {
override val contract = DUMMY_PROGRAM_ID
override val participants: List<PublicKey>
get() = owners
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
interface Commands : CommandData {

View File

@ -1,9 +1,5 @@
package com.r3corda.core.contracts
import com.r3corda.core.contracts.TransactionBuilder
import com.r3corda.core.contracts.TransactionForVerification
import com.r3corda.core.contracts.Fix
import com.r3corda.core.contracts.FixOf
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
@ -33,6 +29,15 @@ interface ContractState {
/** Identity of the notary that ensures this state is not used as an input to a transaction more than once */
val notary: Party
/** List of public keys for each party that can consume this state in a valid transaction. */
val participants: List<PublicKey>
/**
* Copies the underlying data structure, replacing the notary field with the new value.
* To replace the notary, we need an approval (signature) from _all_ participants.
*/
fun withNewNotary(newNotary: Party): ContractState
}
/**
@ -161,10 +166,6 @@ abstract class TypeOnlyCommandData : CommandData {
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
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 }
@ -196,6 +197,12 @@ data class TimestampCommand(val after: Instant?, val before: Instant?) : Command
val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2)
}
/**
* Indicates that the transaction is only used for changing the Notary for a state. If present in a transaction,
* the contract code is not run, and special platform-level validation logic is used instead
*/
class ChangeNotary : TypeOnlyCommandData()
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the

View File

@ -1,6 +1,5 @@
package com.r3corda.core.contracts
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import java.util.*
@ -64,25 +63,20 @@ data class TransactionForVerification(val inStates: List<ContractState>,
/**
* Verifies that the transaction is valid:
* - Checks that the input states and the timestamp point to the same Notary
* - Runs the contracts for this transaction. If any contract fails to verify, the whole transaction
* is considered to be invalid
* - Runs the contracts for this transaction. If any contract fails to verify, the whole transaction is
* considered to be invalid. In case of a special type of transaction, e.g. for changing notary for a state,
* runs custom platform level validation logic instead.
*
* TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated.
*
* @throws TransactionVerificationException if a contract throws an exception (the original is in the cause field)
* or the transaction has references to more than one Notary
* @throws TransactionVerificationException if validation logic fails or if a contract throws an exception
* (the original is in the cause field)
*/
@Throws(TransactionVerificationException::class)
fun verify() {
verifySingleNotary()
val contracts = (inStates.map { it.contract } + outStates.map { it.contract }).toSet()
for (contract in contracts) {
try {
contract.verify(this)
} catch(e: Throwable) {
throw TransactionVerificationException.ContractRejection(this, contract, e)
}
}
if (isChangeNotaryTx()) verifyNotaryChange() else runContractVerify()
}
private fun verifySingleNotary() {
@ -93,6 +87,36 @@ data class TransactionForVerification(val inStates: List<ContractState>,
if (!timestampCmd.signers.contains(notary.owningKey)) throw TransactionVerificationException.MoreThanOneNotary(this)
}
private fun isChangeNotaryTx() = commands.any { it.value is ChangeNotary }
/**
* A notary change transaction is valid if:
* - It contains only a single command - [ChangeNotary]
* - Outputs are identical to inputs apart from the notary field (each input/output state pair must have the same index)
*/
private fun verifyNotaryChange() {
try {
check(commands.size == 1)
inStates.zip(outStates).forEach {
// TODO: Check that input and output state(s) differ only by notary pointer
check(it.first.notary != it.second.notary)
}
} catch (e: IllegalStateException) {
throw TransactionVerificationException.InvalidNotaryChange(this)
}
}
private fun runContractVerify() {
val contracts = (inStates.map { it.contract } + outStates.map { it.contract }).toSet()
for (contract in contracts) {
try {
contract.verify(this)
} catch(e: Throwable) {
throw TransactionVerificationException.ContractRejection(this, contract, e)
}
}
}
/**
* Utilities for contract writers to incorporate into their logic.
*/
@ -108,6 +132,7 @@ data class TransactionForVerification(val inStates: List<ContractState>,
/** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
/** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */
fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName)
@ -169,4 +194,5 @@ class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTra
sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) {
class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
}

View File

@ -1,7 +1,6 @@
package com.r3corda.core.contracts
import com.esotericsoftware.kryo.Kryo
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
@ -131,9 +130,15 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
return copy(sigs = sigs + sig)
}
fun withAdditionalSignatures(sigList: Collection<DigitalSignature.WithKey>): SignedTransaction {
return copy(sigs = sigs + sigList)
}
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
/**
* Returns the set of missing signatures - a signature must be present for every command pub key
* and the Notary (if it is specified)

View File

@ -0,0 +1,202 @@
package protocols
import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.protocols.AbstractRequestMessage
import com.r3corda.protocols.NotaryProtocol
import com.r3corda.protocols.ResolveTransactionsProtocol
import java.security.PublicKey
/**
* A protocol to be used for changing a state's Notary. This is required since all input states to a transaction
* must point to the same notary.
*
* The [Instigator] assembles the transaction for notary replacement and sends out change proposals to all participants
* ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction.
* Finally, [Instigator] sends the transaction containing all signatures back to each participant so they can record it and
* use the new updated state for future transactions.
*/
object NotaryChangeProtocol {
val TOPIC_INITIATE = "platform.notary.change.initiate"
val TOPIC_CHANGE = "platform.notary.change.execute"
data class Proposal(val stateRef: StateRef,
val newNotary: Party,
val sessionIdForSend: Long,
val sessionIdForReceive: Long)
class Handshake(val payload: Proposal,
replyTo: SingleMessageRecipient,
replySessionId: Long) : AbstractRequestMessage(replyTo, replySessionId)
class Instigator<T : ContractState>(val originalState: StateAndRef<T>,
val newNotary: Party,
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<StateAndRef<T>>() {
companion object {
object SIGNING : ProgressTracker.Step("Requesting signatures from other parties")
object NOTARY : ProgressTracker.Step("Requesting current Notary signature")
fun tracker() = ProgressTracker(SIGNING, NOTARY)
}
@Suspendable
override fun call(): StateAndRef<T> {
val (stx, participants) = assembleTx()
progressTracker.currentStep = SIGNING
val signatures = mutableListOf<DigitalSignature.WithKey>()
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
val me = listOf(myKey)
if (participants == me) {
signatures.add(getNotarySignature(stx.tx))
} else {
val participantSessions = collectSignatures(participants - me, signatures, stx)
signatures.add(getNotarySignature(stx.tx))
participantSessions.forEach { send(TOPIC_CHANGE, it.first.address, it.second, signatures) }
}
val finalTx = stx + signatures
serviceHub.recordTransactions(listOf(finalTx))
return finalTx.tx.outRef(0)
}
private fun assembleTx(): Pair<SignedTransaction, List<PublicKey>> {
val state = originalState.state
val newState = state.withNewNotary(newNotary)
val participants = state.participants
val cmd = Command(ChangeNotary(), participants)
val tx = TransactionBuilder().withItems(originalState.ref, newState, cmd)
tx.signWith(serviceHub.storageService.myLegalIdentityKey)
val stx = tx.toSignedTransaction(false)
return Pair(stx, participants)
}
@Suspendable
private fun collectSignatures(participants: List<PublicKey>, signatures: MutableCollection<DigitalSignature.WithKey>,
stx: SignedTransaction): MutableList<Pair<NodeInfo, Long>> {
val participantSessions = mutableListOf<Pair<NodeInfo, Long>>()
participants.forEach {
val participantNode = serviceHub.networkMapCache.getNodeByPublicKey(it) ?:
throw IllegalStateException("Participant $it to state $originalState not found on the network")
val sessionIdForSend = random63BitValue()
val participantSignature = getParticipantSignature(participantNode, stx, sessionIdForSend)
signatures.add(participantSignature)
participantSessions.add(participantNode to sessionIdForSend)
}
return participantSessions
}
@Suspendable
private fun getParticipantSignature(node: NodeInfo, stx: SignedTransaction, sessionIdForSend: Long): DigitalSignature.WithKey {
val sessionIdForReceive = random63BitValue()
val proposal = Proposal(originalState.ref, newNotary, sessionIdForSend, sessionIdForReceive)
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 response = sendAndReceive<DigitalSignature.WithKey>(TOPIC_CHANGE, node.address, sessionIdForSend, sessionIdForReceive, stx)
val participantSignature = response.validate {
check(it.by == node.identity.owningKey) { "Not signed by the required participant" }
it.verifyWithECDSA(stx.txBits)
it
}
return participantSignature
}
@Suspendable
private fun getNotarySignature(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
progressTracker.currentStep = NOTARY
return subProtocol(NotaryProtocol(wtx))
}
}
class Acceptor(val otherSide: SingleMessageRecipient,
val sessionIdForSend: Long,
val sessionIdForReceive: Long,
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
companion object {
object VERIFYING : ProgressTracker.Step("Verifying Notary change proposal")
object SIGNING : ProgressTracker.Step("Signing Notary change transaction")
fun tracker() = ProgressTracker(VERIFYING, SIGNING)
}
@Suspendable
override fun call() {
progressTracker.currentStep = VERIFYING
val proposedTx = receive<SignedTransaction>(TOPIC_CHANGE, sessionIdForReceive).validate { validateTx(it) }
progressTracker.currentStep = SIGNING
val mySignature = sign(proposedTx)
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, mySignature)
val allSignatures = swapSignatures.validate {
it.forEach { it.verifyWithECDSA(proposedTx.txBits) }
it
}
val finalTx = proposedTx + allSignatures
serviceHub.recordTransactions(listOf(finalTx))
}
@Suspendable
private fun validateTx(stx: SignedTransaction): SignedTransaction {
checkDependenciesValid(stx)
checkContractValid(stx)
checkCommand(stx.tx)
return stx
}
private fun checkCommand(tx: WireTransaction) {
val command = tx.commands.single { it.value is ChangeNotary }
val myKey = serviceHub.storageService.myLegalIdentityKey.public
val myIdentity = serviceHub.storageService.myLegalIdentity
val state = tx.inputs.first()
require(command.signers.contains(myKey)) { "Party $myIdentity is not a participant for the state: $state" }
}
@Suspendable
private fun checkDependenciesValid(stx: SignedTransaction) {
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
}
private fun checkContractValid(stx: SignedTransaction) {
val ltx = stx.tx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
serviceHub.verifyTransaction(ltx)
}
private fun sign(stx: SignedTransaction): DigitalSignature.WithKey {
val myKeyPair = serviceHub.storageService.myLegalIdentityKey
return myKeyPair.signWithECDSA(stx.txBits)
}
}
/** 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"
}
}

View File

@ -29,8 +29,13 @@ class TransactionGroupTests {
override val owner: PublicKey,
override val notary: Party) : OwnableState {
override val contract: Contract = TEST_PROGRAM_ID
override val participants: List<PublicKey>
get() = listOf(owner)
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
}
interface Commands : CommandData {
class Move() : TypeOnlyCommandData(), Commands
data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands

View File

@ -13,6 +13,7 @@ import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.net.URLClassLoader
import java.security.PublicKey
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.test.assertEquals
@ -32,9 +33,13 @@ class AttachmentClassLoaderTests {
}
class AttachmentDummyContract : Contract {
class State(val magicNumber: Int = 0,
override val notary: Party) : ContractState {
data class State(val magicNumber: Int = 0,
override val notary: Party) : ContractState {
override val contract = ATTACHMENT_TEST_PROGRAM_ID
override val participants: List<PublicKey>
get() = listOf()
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
interface Commands : CommandData {

View File

@ -30,7 +30,11 @@ class TransactionSerializationTests {
override val owner: PublicKey,
override val notary: Party) : OwnableState {
override val contract: Contract = TEST_PROGRAM_ID
override val participants: List<PublicKey>
get() = listOf(owner)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
override fun withNewNotary(newNotary: Party) = copy(notary = newNotary)
}
interface Commands : CommandData {
class Move() : TypeOnlyCommandData(), Commands
@ -39,7 +43,7 @@ class TransactionSerializationTests {
}
}
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
// It refers to a fake TX/state that we don't bother creating here.
val depositRef = MINI_CORP.ref(1)
val outputState = TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY)

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
}