mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
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:
parent
a813e9a088
commit
3b1e020082
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
|
202
core/src/main/kotlin/protocols/NotaryChangeProtocol.kt
Normal file
202
core/src/main/kotlin/protocols/NotaryChangeProtocol.kt
Normal 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"
|
||||
}
|
||||
}
|
Binary file not shown.
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
88
node/src/test/kotlin/node/services/NotaryChangeTests.kt
Normal file
88
node/src/test/kotlin/node/services/NotaryChangeTests.kt
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user