From 3b1e0200825b2b18c2d005a30a17bdb616d90581 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 20 May 2016 17:44:49 +0100 Subject: [PATCH 1/5] 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 --- .../r3corda/contracts/AnotherDummyContract.kt | 8 +- .../contracts/JavaCommercialPaper.java | 17 +- .../com/r3corda/contracts/CommercialPaper.kt | 5 +- .../main/kotlin/com/r3corda/contracts/IRS.kt | 5 + .../kotlin/com/r3corda/contracts/cash/Cash.kt | 3 + .../r3corda/core/contracts/DummyContract.kt | 19 +- .../com/r3corda/core/contracts/Structures.kt | 23 +- .../core/contracts/TransactionVerification.kt | 52 +++-- .../r3corda/core/contracts/Transactions.kt | 7 +- .../kotlin/protocols/NotaryChangeProtocol.kt | 202 ++++++++++++++++++ .../com/r3corda/core/node/isolated.jar | Bin 7553 -> 8545 bytes .../core/contracts/TransactionGroupTests.kt | 5 + .../core/node/AttachmentClassLoaderTests.kt | 9 +- .../TransactionSerializationTests.kt | 6 +- .../com/r3corda/node/internal/AbstractNode.kt | 5 + .../node/services/NotaryChangeService.kt | 53 +++++ .../kotlin/node/services/NotaryChangeTests.kt | 88 ++++++++ 17 files changed, 476 insertions(+), 31 deletions(-) create mode 100644 core/src/main/kotlin/protocols/NotaryChangeProtocol.kt create mode 100644 node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt create mode 100644 node/src/test/kotlin/node/services/NotaryChangeTests.kt diff --git a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt index c8b60ed9aa..79aee3b9fe 100644 --- a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt +++ b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt @@ -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 + get() = emptyList() + + override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 1d5609c91c..5c323b85fd 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -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 getParticipants() { + List keys = new ArrayList<>(); + keys.add(this.owner); + return keys; + } } public static class Commands implements CommandData { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 8c670c8cd2..48040dbdc7 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -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 + 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>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) + + override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 49bb534a87..774e7f8f45 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -595,6 +595,9 @@ class InterestRateSwap() : Contract { override val thread = SecureHash.sha256(common.tradeID) override val ref = common.tradeID + override val participants: List + get() = parties.map { it.owningKey } + override fun isRelevant(ourKeys: Set): 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) { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 522fbbc522..4e19b69609 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -60,10 +60,13 @@ class Cash : FungibleAsset() { override val contract = CASH_PROGRAM_ID override val issuanceDef: Issued get() = amount.token + override val participants: List + 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 diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt index d2c1cf75e2..5ebf2413ed 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt @@ -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 + get() = emptyList() + + override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) + } + + data class MultiOwnerState(val magicNumber: Int = 0, + val owners: List, + override val notary: Party) : ContractState { + override val contract = DUMMY_PROGRAM_ID + override val participants: List + get() = owners + + override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 798e9616c4..4a9dc75b9a 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -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 + + /** + * 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) { - 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 diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index 551ca6734a..91140de88d 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -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, /** * 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, 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, /** 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) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt index 9d4260650d..8a4b3eea79 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -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, return copy(sigs = sigs + sig) } + fun withAdditionalSignatures(sigList: Collection): 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) = 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) diff --git a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt new file mode 100644 index 0000000000..f120266561 --- /dev/null +++ b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt @@ -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(val originalState: StateAndRef, + val newNotary: Party, + override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic>() { + 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 { + val (stx, participants) = assembleTx() + + progressTracker.currentStep = SIGNING + + val signatures = mutableListOf() + + 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> { + 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, signatures: MutableCollection, + stx: SignedTransaction): MutableList> { + val participantSessions = mutableListOf>() + + 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(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake).validate { it } + if (!protocolInitiated) throw Refused(node.identity, originalState) + + val response = sendAndReceive(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() { + + 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(TOPIC_CHANGE, sessionIdForReceive).validate { validateTx(it) } + + progressTracker.currentStep = SIGNING + + val mySignature = sign(proposedTx) + val swapSignatures = sendAndReceive>(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" + } +} \ No newline at end of file diff --git a/core/src/main/resources/com/r3corda/core/node/isolated.jar b/core/src/main/resources/com/r3corda/core/node/isolated.jar index 974d8d182fae079e016c163a10c468eace9efa37..b2c8673b8c8c3515ac051987bc6d7cb747cbb229 100644 GIT binary patch delta 4337 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 delta 3337 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 TATt7L6_XDkOa=`Y00000D{WOR diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt index c2e60ed365..d08b997b8e 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt @@ -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 + 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 diff --git a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt index b984c62436..dcf392a2f3 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt @@ -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 + get() = listOf() + + override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index 1ab212e742..5917e8d87d 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -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 + 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) diff --git a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt index 6a8eb1fd67..52af6ad13b 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt @@ -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 diff --git a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt new file mode 100644 index 0000000000..6f084636e3 --- /dev/null +++ b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt @@ -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 + } +} diff --git a/node/src/test/kotlin/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/node/services/NotaryChangeTests.kt new file mode 100644 index 0000000000..aca155f8d1 --- /dev/null +++ b/node/src/test/kotlin/node/services/NotaryChangeTests.kt @@ -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 +} \ No newline at end of file From 70495a021ee5faeed0a20b452391abc060b86615 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 7 Jun 2016 10:42:50 +0100 Subject: [PATCH 2/5] Introduce TransactionState, which wraps ContractState and holds the notary pointer. Remove notary from ContractState. Introduce TransactionType, which specifies custom validation logic for a transaction. --- .../r3corda/contracts/AnotherDummyContract.kt | 8 +- .../contracts/JavaCommercialPaper.java | 57 ++++----- .../com/r3corda/contracts/CommercialPaper.kt | 25 ++-- .../main/kotlin/com/r3corda/contracts/IRS.kt | 27 +++-- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 37 +++--- .../r3corda/contracts/cash/FungibleAsset.kt | 5 +- .../r3corda/contracts/testing/TestUtils.kt | 12 +- .../protocols/TwoPartyTradeProtocol.kt | 14 +-- .../r3corda/contracts/CommercialPaperTests.kt | 37 +++--- .../kotlin/com/r3corda/contracts/IRSTests.kt | 47 ++++---- .../com/r3corda/contracts/cash/CashTests.kt | 63 +++++----- .../r3corda/core/contracts/ContractsDSL.kt | 2 +- .../r3corda/core/contracts/DummyContract.kt | 21 ++-- .../com/r3corda/core/contracts/Structures.kt | 35 ++++-- .../core/contracts/TransactionBuilder.kt | 28 +++-- .../core/contracts/TransactionTools.kt | 6 +- .../core/contracts/TransactionTypes.kt | 73 ++++++++++++ .../core/contracts/TransactionVerification.kt | 111 +++++++----------- .../r3corda/core/contracts/Transactions.kt | 18 +-- .../com/r3corda/core/node/ServiceHub.kt | 2 +- .../r3corda/core/node/services/Services.kt | 6 +- .../com/r3corda/core/serialization/Kryo.kt | 7 +- .../com/r3corda/core/testing/TestUtils.kt | 49 +++++--- .../r3corda/protocols/TwoPartyDealProtocol.kt | 10 +- .../kotlin/protocols/NotaryChangeProtocol.kt | 45 ++++--- .../com/r3corda/core/node/isolated.jar | Bin 8545 -> 8329 bytes .../core/contracts/TransactionGroupTests.kt | 19 ++- .../core/node/AttachmentClassLoaderTests.kt | 15 +-- .../TransactionSerializationTests.kt | 22 ++-- .../kotlin/com/r3corda/node/api/APIServer.kt | 7 +- .../r3corda/node/internal/APIServerImpl.kt | 4 +- .../com/r3corda/node/internal/AbstractNode.kt | 5 +- .../node/internal/testing/IRSSimulation.kt | 2 +- .../node/internal/testing/TestUtils.kt | 9 +- .../node/services/NotaryChangeService.kt | 8 +- .../node/services/wallet/NodeWalletService.kt | 6 +- .../node/services/wallet/WalletImpl.kt | 2 +- .../r3corda/node/internal/testing/trade.json | 3 +- .../messaging/TwoPartyTradeProtocolTests.kt | 2 +- .../node/services/NodeInterestRatesTest.kt | 4 +- .../node/services/NodeWalletServiceTest.kt | 8 +- .../node/services/UniquenessProviderTests.kt | 13 +- .../node/visualiser/GroupToGraphConversion.kt | 7 +- .../kotlin/node/services/NotaryChangeTests.kt | 19 ++- .../kotlin/com/r3corda/demos/RateFixDemo.kt | 7 +- .../kotlin/com/r3corda/demos/TraderDemo.kt | 2 +- .../r3corda/demos/api/InterestRateSwapAPI.kt | 4 +- .../protocols/UpdateBusinessDayProtocol.kt | 18 +-- 48 files changed, 482 insertions(+), 449 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt diff --git a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt index 79aee3b9fe..35b88403b4 100644 --- a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt +++ b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt @@ -18,19 +18,17 @@ import java.security.PublicKey val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract() class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdoor { - data class State(val magicNumber: Int = 0, override val notary: Party) : ContractState { + data class State(val magicNumber: Int = 0) : ContractState { override val contract = ANOTHER_DUMMY_PROGRAM_ID override val participants: List get() = emptyList() - - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { class Create : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Always accepts. } @@ -38,7 +36,7 @@ class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdo override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") override fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = State(magicNumber, notary) + val state = TransactionState(State(magicNumber), notary) return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) } diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 5c323b85fd..43b31cef30 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -1,10 +1,11 @@ package com.r3corda.contracts; +import com.google.common.collect.ImmutableList; import com.r3corda.contracts.cash.Cash; import com.r3corda.contracts.cash.CashKt; import com.r3corda.contracts.cash.InsufficientBalanceException; import com.r3corda.core.contracts.*; -import com.r3corda.core.contracts.TransactionForVerification.InOutGroup; +import com.r3corda.core.contracts.TransactionForContract.InOutGroup; import com.r3corda.core.crypto.NullPublicKey; import com.r3corda.core.crypto.Party; import com.r3corda.core.crypto.SecureHash; @@ -14,7 +15,6 @@ 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; @@ -34,38 +34,36 @@ public class JavaCommercialPaper implements Contract { private PublicKey owner; private Amount> faceValue; private Instant maturityDate; - private Party notary; public State() { } // For serialization public State(PartyAndReference issuance, PublicKey owner, Amount> faceValue, - Instant maturityDate, Party notary) { + Instant maturityDate) { this.issuance = issuance; this.owner = owner; this.faceValue = faceValue; this.maturityDate = maturityDate; - this.notary = notary; } public State copy() { - return new State(this.issuance, this.owner, this.faceValue, this.maturityDate, this.notary); + return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); } public ICommercialPaperState withOwner(PublicKey newOwner) { - return new State(this.issuance, newOwner, this.faceValue, this.maturityDate, this.notary); + return new State(this.issuance, newOwner, this.faceValue, this.maturityDate); } public ICommercialPaperState withIssuance(PartyAndReference newIssuance) { - return new State(newIssuance, this.owner, this.faceValue, this.maturityDate, this.notary); + return new State(newIssuance, this.owner, this.faceValue, this.maturityDate); } public ICommercialPaperState withFaceValue(Amount> newFaceValue) { - return new State(this.issuance, this.owner, newFaceValue, this.maturityDate, this.notary); + return new State(this.issuance, this.owner, newFaceValue, this.maturityDate); } public ICommercialPaperState withMaturityDate(Instant newMaturityDate) { - return new State(this.issuance, this.owner, this.faceValue, newMaturityDate, this.notary); + return new State(this.issuance, this.owner, this.faceValue, newMaturityDate); } public PartyAndReference getIssuance() { @@ -84,12 +82,6 @@ public class JavaCommercialPaper implements Contract { return maturityDate; } - @NotNull - @Override - public Party getNotary() { - return notary; - } - @NotNull @Override public Contract getContract() { @@ -107,7 +99,6 @@ public class JavaCommercialPaper implements Contract { if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - if (notary != null ? !notary.equals(state.notary) : state.notary != null) return false; return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); } @@ -117,26 +108,17 @@ public class JavaCommercialPaper implements Contract { result = 31 * result + (owner != null ? owner.hashCode() : 0); result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); - result = 31 * result + (notary != null ? notary.hashCode() : 0); return result; } 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); + return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate); } @NotNull @Override public List getParticipants() { - List keys = new ArrayList<>(); - keys.add(this.owner); - return keys; + return ImmutableList.of(this.owner); } } @@ -164,7 +146,7 @@ public class JavaCommercialPaper implements Contract { } @Override - public void verify(@NotNull TransactionForVerification tx) { + public void verify(@NotNull TransactionForContract tx) { // There are three possible things that can be done with CP. // Issuance, trading (aka moving in this prototype) and redeeming. // Each command has it's own set of restrictions which the verify function ... verifies. @@ -248,19 +230,20 @@ public class JavaCommercialPaper implements Contract { } public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { - State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate, notary); - return new TransactionBuilder().withItems(state, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); + State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); + TransactionState output = new TransactionState<>(state, notary); + return new TransactionBuilder().withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { - new Cash().generateSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet); - tx.addInputState(paper.getRef()); - tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getOwner())); + new Cash().generateSpend(tx, paper.getState().getData().getFaceValue(), paper.getState().getData().getOwner(), wallet); + tx.addInputState(paper); + tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner())); } public void generateMove(TransactionBuilder tx, StateAndRef paper, PublicKey newOwner) { - tx.addInputState(paper.getRef()); - tx.addOutputState(new State(paper.getState().getIssuance(), newOwner, paper.getState().getFaceValue(), paper.getState().getMaturityDate(), paper.getState().getNotary())); - tx.addCommand(new Command(new Commands.Move(), paper.getState().getOwner())); + tx.addInputState(paper); + tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), paper.getState().getNotary())); + tx.addCommand(new Command(new Commands.Move(), paper.getState().getData().getOwner())); } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 48040dbdc7..dd0c665bb7 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -11,7 +11,7 @@ import com.r3corda.core.crypto.toStringShort import com.r3corda.core.utilities.Emoji import java.security.PublicKey import java.time.Instant -import java.util.Currency +import java.util.* /** * This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate @@ -46,8 +46,7 @@ class CommercialPaper : Contract { val issuance: PartyAndReference, override val owner: PublicKey, val faceValue: Amount>, - val maturityDate: Instant, - override val notary: Party + val maturityDate: Instant ) : OwnableState, ICommercialPaperState { override val contract = CP_PROGRAM_ID override val participants: List @@ -63,8 +62,6 @@ class CommercialPaper : Contract { override fun withIssuance(newIssuance: PartyAndReference): ICommercialPaperState = copy(issuance = newIssuance) override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) - - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { @@ -75,7 +72,7 @@ class CommercialPaper : Contract { class Issue : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. val groups = tx.groupStates() { it: State -> it.withoutOwner() } @@ -141,7 +138,7 @@ class CommercialPaper : Contract { */ fun generateIssue(faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { val issuance = faceValue.token.issuer - val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate, notary) + val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary) return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } @@ -149,9 +146,9 @@ class CommercialPaper : Contract { * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. */ fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { - tx.addInputState(paper.ref) - tx.addOutputState(paper.state.copy(owner = newOwner)) - tx.addCommand(Commands.Move(), paper.state.owner) + tx.addInputState(paper) + tx.addOutputState(TransactionState(paper.state.data.copy(owner = newOwner), paper.state.notary)) + tx.addCommand(Commands.Move(), paper.state.data.owner) } /** @@ -164,10 +161,10 @@ class CommercialPaper : Contract { @Throws(InsufficientBalanceException::class) fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - val amount = paper.state.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } - Cash().generateSpend(tx, amount, paper.state.owner, wallet) - tx.addInputState(paper.ref) - tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner) + val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } + Cash().generateSpend(tx, amount, paper.state.data.owner, wallet) + tx.addInputState(paper) + tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner) } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 774e7f8f45..a812ec1e6f 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -487,7 +487,7 @@ class InterestRateSwap() : Contract { /** * verify() with some examples of what needs to be checked. */ - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Group by Trade ID for in / out states val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID } @@ -587,8 +587,7 @@ class InterestRateSwap() : Contract { val fixedLeg: FixedLeg, val floatingLeg: FloatingLeg, val calculation: Calculation, - val common: Common, - override val notary: Party + val common: Common ) : FixableDealState { override val contract = IRS_PROGRAM_ID @@ -621,12 +620,11 @@ class InterestRateSwap() : Contract { } } - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) + // TODO: pass a notary + override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) - override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) - - override fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) { - InterestRateSwap().generateFix(ptx, StateAndRef(this, oldStateRef), Pair(fix.of.forDay, Rate(RatioUnit(fix.value)))) + override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { + InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), Pair(fix.of.forDay, Rate(RatioUnit(fix.value)))) } override fun nextFixingOf(): FixOf? { @@ -720,8 +718,8 @@ class InterestRateSwap() : Contract { val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule) // Put all the above into a new State object. - val state = State(fixedLeg, floatingLeg, newCalculation, common, notary) - return TransactionBuilder().withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) + val state = TransactionState(State(fixedLeg, floatingLeg, newCalculation, common), notary) + return TransactionBuilder().withItems(state, Command(Commands.Agree(), listOf(state.data.floatingLeg.floatingRatePayer.owningKey, state.data.fixedLeg.fixedRatePayer.owningKey))) } private fun calcFixingDate(date: LocalDate, fixingPeriod: DateOffset, calendar: BusinessCalendar): LocalDate { @@ -734,8 +732,11 @@ class InterestRateSwap() : Contract { // TODO: Replace with rates oracle fun generateFix(tx: TransactionBuilder, irs: StateAndRef, fixing: Pair) { - tx.addInputState(irs.ref) - tx.addOutputState(irs.state.copy(calculation = irs.state.calculation.applyFixing(fixing.first, FixedRate(fixing.second)))) - tx.addCommand(Commands.Fix(), listOf(irs.state.floatingLeg.floatingRatePayer.owningKey, irs.state.fixedLeg.fixedRatePayer.owningKey)) + tx.addInputState(irs) + tx.addOutputState( + TransactionState( + irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.first, FixedRate(fixing.second))), + irs.state.notary)) + tx.addCommand(Commands.Fix(), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey)) } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 4e19b69609..be46a304b1 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -49,12 +49,10 @@ class Cash : FungibleAsset() { override val amount: Amount>, /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: PublicKey, - - override val notary: Party + override val owner: PublicKey ) : FungibleAsset.State { - constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey, notary: Party) - : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner, notary) + constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) + : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) override val deposit: PartyAndReference get() = amount.token.issuer override val contract = CASH_PROGRAM_ID @@ -66,7 +64,6 @@ class Cash : FungibleAsset() { 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 @@ -97,9 +94,9 @@ class Cash : FungibleAsset() { */ fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) - check(tx.outputStates().sumCashOrNull() == null) + check(tx.outputStates().map { it.data }.sumCashOrNull() == null) val at = amount.token.issuer - tx.addOutputState(Cash.State(amount, owner, notary)) + tx.addOutputState(TransactionState(Cash.State(amount, owner), notary)) tx.addCommand(Cash.Commands.Issue(), at.party.owningKey) } @@ -146,9 +143,9 @@ class Cash : FungibleAsset() { val currency = amount.token val acceptableCoins = run { - val ofCurrency = cashStates.filter { it.state.amount.token.product == currency } + val ofCurrency = cashStates.filter { it.state.data.amount.token.product == currency } if (onlyFromParties != null) - ofCurrency.filter { it.state.deposit.party in onlyFromParties } + ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } else ofCurrency } @@ -159,7 +156,7 @@ class Cash : FungibleAsset() { for (c in acceptableCoins) { if (gatheredAmount >= amount) break gathered.add(c) - gatheredAmount += Amount(c.state.amount.quantity, currency) + gatheredAmount += Amount(c.state.data.amount.quantity, currency) takeChangeFrom = c } @@ -167,30 +164,30 @@ class Cash : FungibleAsset() { throw InsufficientBalanceException(amount - gatheredAmount) val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.issuanceDef) + Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.state.issuanceDef) } else { null } - val keysUsed = gathered.map { it.state.owner }.toSet() + val keysUsed = gathered.map { it.state.data.owner }.toSet() - val states = gathered.groupBy { it.state.deposit }.map { + val states = gathered.groupBy { it.state.data.deposit }.map { val (deposit, coins) = it - val totalAmount = coins.map { it.state.amount }.sumOrThrow() - State(totalAmount, to, coins.first().state.notary) + val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() + TransactionState(State(totalAmount, to), coins.first().state.notary) } val outputs = if (change != null) { // Just copy a key across as the change key. In real life of course, this works but leaks private data. // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow // value flows through the transaction graph. - val changeKey = gathered.first().state.owner + val changeKey = gathered.first().state.data.owner // Add a change output and adjust the last output downwards. states.subList(0, states.lastIndex) + - states.last().let { it.copy(amount = it.amount - change) } + - State(change, changeKey, gathered.last().state.notary) + states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } + + TransactionState(State(change, changeKey), gathered.last().state.notary) } else states - for (state in gathered) tx.addInputState(state.ref) + for (state in gathered) tx.addInputState(state) for (state in outputs) tx.addOutputState(state) // What if we already have a move command with the right keys? Filter it out here or in platform code? val keysList = keysUsed.toList() diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt index 417d16392e..a865e6bcd0 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -37,7 +37,6 @@ abstract class FungibleAsset : Contract { override val amount: Amount> /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey - override val notary: Party } // Just for grouping @@ -58,7 +57,7 @@ abstract class FungibleAsset : Contract { } /** This is the function EVERYONE runs */ - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible // and must be kept separated for bookkeeping purposes. val groups = tx.groupStates() { it: FungibleAsset.State -> it.issuanceDef } @@ -97,7 +96,7 @@ abstract class FungibleAsset : Contract { private fun verifyIssueCommand(inputs: List>, outputs: List>, - tx: TransactionForVerification, + tx: TransactionForContract, issueCommand: AuthenticatedObject, token: Issued, issuer: Party) { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index eb575cf7b1..e1d01a5079 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -9,10 +9,11 @@ import com.r3corda.core.contracts.DUMMY_PROGRAM_ID import com.r3corda.core.contracts.DummyContract import com.r3corda.core.contracts.PartyAndReference import com.r3corda.core.contracts.Issued +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.generateKeyPair -import com.r3corda.core.testing.DUMMY_NOTARY import java.security.PublicKey import java.util.* @@ -26,7 +27,7 @@ val TEST_PROGRAM_MAP: Map> = mapOf( IRS_PROGRAM_ID to InterestRateSwap::class.java ) -fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().nextInt(), notary) +fun generateState() = DummyContract.State(Random().nextInt()) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -50,8 +51,10 @@ fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().n infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner) infix fun Cash.State.`issued by`(party: Party) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party)))) infix fun Cash.State.`issued by`(deposit: PartyAndReference) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit))) +infix fun Cash.State.`with notary`(notary: Party) = TransactionState(this, notary) infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) +infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = @@ -62,7 +65,8 @@ val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public). /** Allows you to write 100.DOLLARS.CASH */ val Amount.CASH: Cash.State get() = Cash.State( Amount>(this.quantity, Issued(DUMMY_CASH_ISSUER, this.token)), - NullPublicKey, DUMMY_NOTARY) + NullPublicKey) -val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey, DUMMY_NOTARY) +val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey) +infix fun ContractState.`with notary`(notary: Party) = TransactionState(this, notary) diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 21bafe4fb0..b91de13496 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -17,7 +17,7 @@ import com.r3corda.core.utilities.trace import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException -import java.util.Currency +import java.util.* /** * This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer @@ -129,7 +129,7 @@ object TwoPartyTradeProtocol { // This verifies that the transaction is contract-valid, even though it is missing signatures. serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)) - if (wtx.outputs.sumCashBy(myKeyPair.public) != price) + if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public) != price) throw IllegalArgumentException("Transaction is not sending us the right amount of cash") // There are all sorts of funny games a malicious secondary might play here, we should fix them: @@ -221,7 +221,7 @@ object TwoPartyTradeProtocol { progressTracker.currentStep = VERIFYING maybeTradeRequest.validate { // What is the seller trying to sell us? - val asset = it.assetForSale.state + val asset = it.assetForSale.state.data val assetTypeName = asset.javaClass.name logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" } @@ -272,15 +272,15 @@ object TwoPartyTradeProtocol { val cashStates = wallet.statesOfType() val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) // Add inputs/outputs/a command for the movement of the asset. - ptx.addInputState(tradeRequest.assetForSale.ref) + ptx.addInputState(tradeRequest.assetForSale) // Just pick some new public key for now. This won't be linked with our identity in any way, which is what // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to // reveal who the owner actually is. The key management service is expected to derive a unique key from some // initial seed in order to provide privacy protection. val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public) - ptx.addOutputState(state) - ptx.addCommand(command, tradeRequest.assetForSale.state.owner) + val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public) + ptx.addOutputState(TransactionState(state, tradeRequest.assetForSale.state.notary)) + ptx.addCommand(command, tradeRequest.assetForSale.state.data.owner) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 80d7a8814b..6feacc0e6a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -1,10 +1,7 @@ package com.r3corda.contracts -import com.r3corda.contracts.testing.CASH -import com.r3corda.contracts.testing.`issued by` -import com.r3corda.contracts.testing.`owned by` import com.r3corda.contracts.cash.Cash -import com.r3corda.contracts.testing.STATE +import com.r3corda.contracts.testing.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.days @@ -32,8 +29,7 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate { MEGA_CORP.ref(123), MEGA_CORP_PUBKEY, 1000.DOLLARS `issued by` MEGA_CORP.ref(123), - TEST_TX_TIME + 7.days, - DUMMY_NOTARY + TEST_TX_TIME + 7.days ) override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue() @@ -46,8 +42,7 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate { issuance = MEGA_CORP.ref(123), owner = MEGA_CORP_PUBKEY, faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), - maturityDate = TEST_TX_TIME + 7.days, - notary = DUMMY_NOTARY + maturityDate = TEST_TX_TIME + 7.days ) override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue() @@ -121,7 +116,7 @@ class CommercialPaperTestsGeneric { fun `issue cannot replace an existing state`() { transactionGroup { roots { - transaction(thisTest.getPaper() label "paper") + transaction(thisTest.getPaper() `with notary` DUMMY_NOTARY label "paper") } transaction { input("paper") @@ -144,9 +139,9 @@ class CommercialPaperTestsGeneric { trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed") } - fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { - val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256()) - return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) + fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { + val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), TransactionType.Business()) + return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } @Test @@ -163,9 +158,9 @@ class CommercialPaperTestsGeneric { } val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( - 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY, - 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY, - 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY ) // Alice pays $9000 to MiniCorp to own some of their debt. @@ -181,8 +176,8 @@ class CommercialPaperTestsGeneric { // Won't be validated. val (corpWalletTX, corpWallet) = cashOutputsToWallet( - 9000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY, - 4000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY + 9000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY, + 4000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY ) fun makeRedeemTX(time: Instant): LedgerTransaction { @@ -213,8 +208,8 @@ class CommercialPaperTestsGeneric { val someProfits = 1200.DOLLARS `issued by` issuer return transactionGroupFor() { roots { - transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY label "alice's $900") - transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY label "some profits") + transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "alice's $900") + transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY `with notary` DUMMY_NOTARY label "some profits") } // Some CP is issued onto the ledger by MegaCorp. @@ -230,7 +225,7 @@ class CommercialPaperTestsGeneric { input("paper") input("alice's $900") output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output `owned by` ALICE_PUBKEY } + output("alice's paper") { "paper".output.data `owned by` ALICE_PUBKEY } arg(ALICE_PUBKEY) { Cash.Commands.Move() } arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } } @@ -244,7 +239,7 @@ class CommercialPaperTestsGeneric { output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } if (!destroyPaperAtRedemption) - output { "paper".output } + output { "paper".output.data } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index d1d8a64c50..abf8f02199 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -97,7 +97,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") ) - InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY) + InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common) } 2 -> { // 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides) @@ -187,7 +187,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") ) - return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY) + return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common) } else -> TODO("IRS number $irsSelect not defined") @@ -204,8 +204,7 @@ class IRSTests { exampleIRS.fixedLeg, exampleIRS.floatingLeg, exampleIRS.calculation, - exampleIRS.common, - DUMMY_NOTARY + exampleIRS.common ) val outState = inState.copy() @@ -255,7 +254,7 @@ class IRSTests { * Utility so I don't have to keep typing this */ fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { - return generateIRSTxn(irsSelector).outputs.filterIsInstance().single() + return generateIRSTxn(irsSelector).outputs.map { it.data }.filterIsInstance().single() } /** @@ -287,7 +286,7 @@ class IRSTests { newCalculation = newCalculation.applyFixing(it.key, FixedRate(PercentageRatioUnit(it.value))) } - val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_NOTARY) + val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common) println(newIRS.exportIRSToCSV()) } @@ -306,7 +305,7 @@ class IRSTests { @Test fun generateIRSandFixSome() { var previousTXN = generateIRSTxn(1) - var currentIRS = previousTXN.outputs.filterIsInstance().single() + var currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance().single() println(currentIRS.prettyPrint()) while (true) { val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break @@ -323,7 +322,7 @@ class IRSTests { } tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) } - currentIRS = previousTXN.outputs.filterIsInstance().single() + currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance().single() println(currentIRS.prettyPrint()) previousTXN = fixTX } @@ -387,11 +386,11 @@ class IRSTests { transaction("Fix") { input("irs post agreement") output("irs post first fixing") { - "irs post agreement".output.copy( - "irs post agreement".output.fixedLeg, - "irs post agreement".output.floatingLeg, - "irs post agreement".output.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), - "irs post agreement".output.common + "irs post agreement".output.data.copy( + "irs post agreement".output.data.fixedLeg, + "irs post agreement".output.data.floatingLeg, + "irs post agreement".output.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), + "irs post agreement".output.data.common ) } arg(ORACLE_PUBKEY) { @@ -678,7 +677,6 @@ class IRSTests { irs.floatingLeg, irs.calculation, irs.common.copy(tradeID = "t1") - ) } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } @@ -692,7 +690,6 @@ class IRSTests { irs.floatingLeg, irs.calculation, irs.common.copy(tradeID = "t2") - ) } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } @@ -703,19 +700,19 @@ class IRSTests { input("irs post agreement1") input("irs post agreement2") output("irs post first fixing1") { - "irs post agreement1".output.copy( - "irs post agreement1".output.fixedLeg, - "irs post agreement1".output.floatingLeg, - "irs post agreement1".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - "irs post agreement1".output.common.copy(tradeID = "t1") + "irs post agreement1".output.data.copy( + "irs post agreement1".output.data.fixedLeg, + "irs post agreement1".output.data.floatingLeg, + "irs post agreement1".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + "irs post agreement1".output.data.common.copy(tradeID = "t1") ) } output("irs post first fixing2") { - "irs post agreement2".output.copy( - "irs post agreement2".output.fixedLeg, - "irs post agreement2".output.floatingLeg, - "irs post agreement2".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - "irs post agreement2".output.common.copy(tradeID = "t2") + "irs post agreement2".output.data.copy( + "irs post agreement2".output.data.fixedLeg, + "irs post agreement2".output.data.floatingLeg, + "irs post agreement2".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + "irs post agreement2".output.data.common.copy(tradeID = "t2") ) } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt index 2b7ee70510..81406f338a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt @@ -4,6 +4,7 @@ import com.r3corda.core.contracts.DummyContract import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` import com.r3corda.contracts.testing.`with deposit` +import com.r3corda.contracts.testing.`with notary` import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash @@ -12,19 +13,14 @@ import com.r3corda.core.testing.* import org.junit.Test import java.security.PublicKey import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue +import kotlin.test.* class CashTests { val defaultRef = OpaqueBytes(ByteArray(1, {1})) val defaultIssuer = MEGA_CORP.ref(defaultRef) val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = DUMMY_PUBKEY_1, - notary = DUMMY_NOTARY + owner = DUMMY_PUBKEY_1 ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) @@ -71,7 +67,7 @@ class CashTests { fun issueMoney() { // Check we can't "move" money into existence. transaction { - input { DummyContract.State(notary = DUMMY_NOTARY) } + input { DummyContract.State() } output { outState } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } @@ -89,8 +85,7 @@ class CashTests { output { Cash.State( amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), - owner = DUMMY_PUBKEY_1, - notary = DUMMY_NOTARY + owner = DUMMY_PUBKEY_1 ) } tweak { @@ -105,7 +100,7 @@ class CashTests { val ptx = TransactionBuilder() Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(ptx.inputStates().isEmpty()) - val s = ptx.outputStates()[0] as Cash.State + val s = ptx.outputStates()[0].data as Cash.State assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) assertEquals(MINI_CORP, s.deposit.party) assertEquals(DUMMY_PUBKEY_1, s.owner) @@ -189,7 +184,7 @@ class CashTests { // Include the previously issued cash in a new issuance command ptx = TransactionBuilder() - ptx.addInputState(tx.tx.outRef(0).ref) + ptx.addInputState(tx.tx.outRef(0)) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) } @@ -359,7 +354,7 @@ class CashTests { fun multiCurrency() { // Check we can do an atomic currency trade tx. transaction { - val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), DUMMY_PUBKEY_2, DUMMY_NOTARY) + val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), DUMMY_PUBKEY_2) input { inState `owned by` DUMMY_PUBKEY_1 } input { pounds } output { inState `owned by` DUMMY_PUBKEY_2 } @@ -379,7 +374,7 @@ class CashTests { fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( - Cash.State(amount `issued by` corp.ref(depositRef), OUR_PUBKEY_1, DUMMY_NOTARY), + Cash.State(amount `issued by` corp.ref(depositRef), OUR_PUBKEY_1) `with notary` DUMMY_NOTARY, StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) @@ -400,7 +395,7 @@ class CashTests { fun generateSimpleDirectSpend() { val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -415,8 +410,8 @@ class CashTests { fun generateSimpleSpendWithChange() { val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) - assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(WALLET[0].state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -425,7 +420,7 @@ class CashTests { val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[1].ref, wtx.inputs[1]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -436,8 +431,8 @@ class CashTests { assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[1].ref, wtx.inputs[1]) assertEquals(WALLET[2].ref, wtx.inputs[2]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) - assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(WALLET[2].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -458,9 +453,9 @@ class CashTests { */ @Test fun aggregation() { - val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP_PUBKEY, DUMMY_NOTARY) - val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP_PUBKEY, DUMMY_NOTARY) - val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY, DUMMY_NOTARY) + val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP_PUBKEY) + val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP_PUBKEY) + val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY) // Obviously it must be possible to aggregate states with themselves assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef) @@ -474,7 +469,7 @@ class CashTests { // States cannot be aggregated if the currency differs assertNotEquals(oneThousandDollarsFromMini.issuanceDef, - Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY, DUMMY_NOTARY).issuanceDef) + Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).issuanceDef) // States cannot be aggregated if the reference differs assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef) @@ -484,9 +479,9 @@ class CashTests { @Test fun `summing by owner`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP_PUBKEY)) } @@ -494,8 +489,8 @@ class CashTests { @Test(expected = UnsupportedOperationException::class) fun `summing by owner throws`() { val states = listOf( - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) states.sumCashBy(MINI_CORP_PUBKEY) } @@ -516,9 +511,9 @@ class CashTests { @Test fun `summing a single currency`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) // Test that summing everything produces the total number of dollars var expected = 7000.DOLLARS `issued by` defaultIssuer @@ -529,8 +524,8 @@ class CashTests { @Test(expected = IllegalArgumentException::class) fun `summing multiple currencies`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) // Test that summing everything fails because we're mixing units states.sumCash() diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt index 75555e18b0..d7018b9c3e 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -92,7 +92,7 @@ fun List>.getTimestampByName(vararg names: Stri */ @Throws(IllegalArgumentException::class) // TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here? -inline fun verifyMoveCommands(inputs: List, tx: TransactionForVerification) { +inline fun verifyMoveCommands(inputs: List, tx: TransactionForContract) { // Now check the digital signatures on the move command. Every input has an owning public key, and we must // see a signature from each of those keys. The actual signatures have been verified against the transaction // data by the platform before execution. diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt index 5ebf2413ed..0205ea5695 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt @@ -9,30 +9,33 @@ import java.security.PublicKey val DUMMY_PROGRAM_ID = DummyContract() class DummyContract : Contract { - data class State(val magicNumber: Int = 0, - override val notary: Party) : ContractState { + data class State(val magicNumber: Int = 0) : ContractState { override val contract = DUMMY_PROGRAM_ID override val participants: List get() = emptyList() + } - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) + data class SingleOwnerState(val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState { + override val contract = DUMMY_PROGRAM_ID + override val participants: List + get() = listOf(owner) + + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } data class MultiOwnerState(val magicNumber: Int = 0, - val owners: List, - override val notary: Party) : ContractState { + val owners: List) : ContractState { override val contract = DUMMY_PROGRAM_ID override val participants: List get() = owners - - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { class Create : TypeOnlyCommandData(), Commands + class Move : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Always accepts. } @@ -40,7 +43,7 @@ class DummyContract : Contract { override val legalContractReference: SecureHash = SecureHash.sha256("") fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = State(magicNumber, notary) + val state = TransactionState(SingleOwnerState(magicNumber, owner.party.owningKey), notary) return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 4a9dc75b9a..32bba6809d 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -27,17 +27,20 @@ interface ContractState { /** Contract by which the state belongs */ val contract: Contract - /** 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 +} +/** A wrapper for [ContractState] containing additional platform-level state information. This is the state */ +data class TransactionState( + val data: T, + /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ + val notary: Party) { /** - * 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. + * Copies the underlying state, replacing the notary field with the new value. + * To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState] */ - fun withNewNotary(newNotary: Party): ContractState + fun withNewNotary(newNotary: Party) = TransactionState(this.data, newNotary) } /** @@ -105,7 +108,7 @@ interface DealState : LinearState { * TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a * Contract instance from a ContractState are imminent, at which point we can move this out of here */ - fun generateAgreement(): TransactionBuilder + fun generateAgreement(notary: Party): TransactionBuilder } /** @@ -125,7 +128,7 @@ interface FixableDealState : DealState { * TODO: This would also likely move to methods on the Contract once the changes to reference * the Contract from the ContractState are in */ - fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) + fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) } /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ @@ -140,11 +143,11 @@ data class StateRef(val txhash: SecureHash, val index: Int) { } /** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */ -data class StateAndRef(val state: T, val ref: StateRef) +data class StateAndRef(val state: TransactionState, val ref: StateRef) /** Filters a list of [StateAndRef] objects according to the type of the states */ inline fun List>.filterStatesOfType(): List> { - return mapNotNull { if (it.state is T) StateAndRef(it.state, it.ref) else null } + return mapNotNull { if (it.state.data is T) StateAndRef(TransactionState(it.state.data, it.state.notary), it.ref) else null } } /** @@ -166,6 +169,9 @@ 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) { + 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 } @@ -198,11 +204,14 @@ data class TimestampCommand(val after: Instant?, val before: Instant?) : Command } /** - * 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 + * Command that has to be signed by all participants of the states in the transaction + * in order to perform a notary change */ class ChangeNotary : TypeOnlyCommandData() +/** Command that indicates the requirement of a Notary signature for the input states */ +class NotaryCommand : 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 @@ -217,7 +226,7 @@ interface Contract { * existing contract code. */ @Throws(IllegalArgumentException::class) - fun verify(tx: TransactionForVerification) + fun verify(tx: TransactionForContract) /** * Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 820d9221f0..4c5222c9a7 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -1,8 +1,5 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.WireTransaction -import com.r3corda.core.contracts.* import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash @@ -22,8 +19,9 @@ import java.util.* */ class TransactionBuilder(private val inputs: MutableList = arrayListOf(), private val attachments: MutableList = arrayListOf(), - private val outputs: MutableList = arrayListOf(), - private val commands: MutableList = arrayListOf()) { + private val outputs: MutableList> = arrayListOf(), + private val commands: MutableList = arrayListOf(), + private val type: TransactionType = TransactionType.Business()) { val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() @@ -49,8 +47,8 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { when (t) { - is StateRef -> addInputState(t) - is ContractState -> addOutputState(t) + is StateAndRef<*> -> addInputState(t) + is TransactionState<*> -> addOutputState(t) is Command -> addCommand(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } @@ -96,7 +94,7 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf } fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands)) + ArrayList(outputs), ArrayList(commands), type) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { if (checkSufficientSignatures) { @@ -109,9 +107,15 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - fun addInputState(ref: StateRef) { + fun addInputState(stateAndRef: StateAndRef<*>) { check(currentSigs.isEmpty()) - inputs.add(ref) + + val notaryKey = stateAndRef.state.notary.owningKey + if (commands.none { it.signers.contains(notaryKey) }) { + commands.add(Command(NotaryCommand(), notaryKey)) + } + + inputs.add(stateAndRef.ref) } fun addAttachment(attachment: Attachment) { @@ -119,7 +123,7 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf attachments.add(attachment.id) } - fun addOutputState(state: ContractState) { + fun addOutputState(state: TransactionState<*>) { check(currentSigs.isEmpty()) outputs.add(state) } @@ -136,7 +140,7 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputs) - fun outputStates(): List = ArrayList(outputs) + fun outputStates(): List> = ArrayList(outputs) fun commands(): List = ArrayList(commands) fun attachments(): List = ArrayList(attachments) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt index d749e31e95..39325f4528 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -1,9 +1,5 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.AuthenticatedObject -import com.r3corda.core.contracts.LedgerTransaction -import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.node.services.AttachmentStorage import com.r3corda.core.node.services.IdentityService import java.io.FileNotFoundException @@ -22,7 +18,7 @@ fun WireTransaction.toLedgerTransaction(identityService: IdentityService, val attachments = attachments.map { attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } - return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id) + return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id, type) } /** diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt new file mode 100644 index 0000000000..5c3421e397 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -0,0 +1,73 @@ +package com.r3corda.core.contracts + +/** Defines transaction validation rules for a specific transaction type */ +sealed class TransactionType { + override fun equals(other: Any?) = other?.javaClass == javaClass + override fun hashCode() = javaClass.name.hashCode() + + /** + * Check that the transaction is valid based on: + * - General platform rules + * - Rules for the specific transaction type + * + * Note: Presence of _signatures_ is not checked, only the public keys to be signed for. + */ + fun verify(tx: TransactionForVerification) { + verifyNotary(tx) + typeSpecificVerify(tx) + } + + private fun verifyNotary(tx: TransactionForVerification) { + if (tx.inStates.isEmpty()) return + val notary = tx.inStates.first().notary + if (tx.inStates.any { it.notary != notary }) throw TransactionVerificationException.MoreThanOneNotary(tx) + if (tx.commands.none { it.signers.contains(notary.owningKey) }) throw TransactionVerificationException.NotaryMissing(tx) + } + + abstract fun typeSpecificVerify(tx: TransactionForVerification) + + /** A general type used for business transactions, where transaction validity is determined by custom contract code */ + class Business : TransactionType() { + /** + * Check the transaction is contract-valid by running the verify() for each input and output state contract. + * If any contract fails to verify, the whole transaction is considered to be invalid. + */ + override fun typeSpecificVerify(tx: TransactionForVerification) { + val ctx = tx.toTransactionForContract() + + val contracts = (ctx.inStates.map { it.contract } + ctx.outStates.map { it.contract }).toSet() + for (contract in contracts) { + try { + contract.verify(ctx) + } catch(e: Throwable) { + throw TransactionVerificationException.ContractRejection(tx, contract, e) + } + } + } + } + + /** + * A special transaction type for reassigning a notary for a state. Validation does not involve running + * any contract code, it just checks that the states are unmodified apart from the notary field. + */ + class NotaryChange : TransactionType() { + /** + * Check that the difference between inputs and outputs is only the notary field, + * and that all required signing public keys are present + */ + override fun typeSpecificVerify(tx: TransactionForVerification) { + try { + tx.inStates.zip(tx.outStates).forEach { + check(it.first.data == it.second.data) + check(it.first.notary != it.second.notary) + } + val command = tx.commands.requireSingleCommand() + val requiredSigners = tx.inStates.flatMap { it.data.participants } + check(command.signers.containsAll(requiredSigners)) + } catch (e: IllegalStateException) { + throw TransactionVerificationException.InvalidNotaryChange(tx) + } + } + } +} + diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index 91140de88d..d7c576a3c3 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -29,7 +29,7 @@ class TransactionGroup(val transactions: Set, val nonVerified val resolved = HashSet(transactions.size) for (tx in transactions) { - val inputs = ArrayList(tx.inputs.size) + val inputs = ArrayList>(tx.inputs.size) for (ref in tx.inputs) { val conflict = refToConsumingTXMap[ref] if (conflict != null) @@ -41,31 +41,27 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the output in that transaction by index. inputs.add(ltx.outputs[ref.index]) } - resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id)) + resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.type)) } for (tx in resolved) tx.verify() return resolved } - } /** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */ -data class TransactionForVerification(val inStates: List, - val outStates: List, +data class TransactionForVerification(val inStates: List>, + val outStates: List>, val attachments: List, val commands: List>, - val origHash: SecureHash) { + val origHash: SecureHash, + val type: TransactionType) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash /** - * 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. In case of a special type of transaction, e.g. for changing notary for a state, - * runs custom platform level validation logic instead. + * Verifies that the transaction is valid by running type-specific validation logic. * * TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated. * @@ -73,68 +69,22 @@ data class TransactionForVerification(val inStates: List, * (the original is in the cause field) */ @Throws(TransactionVerificationException::class) - fun verify() { - verifySingleNotary() + fun verify() = type.verify(this) - if (isChangeNotaryTx()) verifyNotaryChange() else runContractVerify() - } + fun toTransactionForContract() = TransactionForContract(inStates.map { it.data }, outStates.map { it.data }, attachments, commands, origHash) +} - private fun verifySingleNotary() { - if (inStates.isEmpty()) return - val notary = inStates.first().notary - if (inStates.any { it.notary != notary }) throw TransactionVerificationException.MoreThanOneNotary(this) - val timestampCmd = commands.singleOrNull { it.value is TimestampCommand } ?: return - 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. - */ - - /** - * A set of related inputs and outputs that are connected by some common attributes. An InOutGroup is calculated - * using [groupStates] and is useful for handling cases where a transaction may contain similar but unrelated - * state evolutions, for example, a transaction that moves cash in two different currencies. The numbers must add - * up on both sides of the transaction, but the values must be summed independently per currency. Grouping can - * be used to simplify this logic. - */ - data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) - - /** 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) +/** + * A transaction to be passed as input to a contract verification function. Defines helper methods to + * simplify verification logic in contracts. + */ +data class TransactionForContract(val inStates: List, + val outStates: List, + val attachments: List, + val commands: List>, + val origHash: SecureHash) { + override fun hashCode() = origHash.hashCode() + override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash /** * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they @@ -186,6 +136,24 @@ data class TransactionForVerification(val inStates: List, return result } + + /** Utilities for contract writers to incorporate into their logic. */ + + /** + * A set of related inputs and outputs that are connected by some common attributes. An InOutGroup is calculated + * using [groupStates] and is useful for handling cases where a transaction may contain similar but unrelated + * state evolutions, for example, a transaction that moves cash in two different currencies. The numbers must add + * up on both sides of the transaction, but the values must be summed independently per currency. Grouping can + * be used to simplify this logic. + */ + data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) + + /** 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) + } class TransactionResolutionException(val hash: SecureHash) : Exception() @@ -194,5 +162,6 @@ 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 NotaryMissing(tx: TransactionForVerification) : TransactionVerificationException(tx, null) class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt index 8a4b3eea79..d7eb239e78 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -43,8 +43,9 @@ import java.security.SignatureException /** Transaction ready for serialisation, without any signatures attached. */ data class WireTransaction(val inputs: List, val attachments: List, - val outputs: List, - val commands: List) : NamedByHash { + val outputs: List>, + val commands: List, + val type: TransactionType) : NamedByHash { // Cache the serialised form of the transaction and its hash to give us fast access to it. @Volatile @Transient private var cachedBits: SerializedBytes? = null @@ -63,11 +64,11 @@ data class WireTransaction(val inputs: List, @Suppress("UNCHECKED_CAST") fun outRef(index: Int): StateAndRef { require(index >= 0 && index < outputs.size) - return StateAndRef(outputs[index] as T, StateRef(id, index)) + return StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) } /** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */ - fun outRef(state: ContractState): StateAndRef = outRef(outputs.indexOfOrThrow(state)) + fun outRef(state: ContractState): StateAndRef = outRef(outputs.map { it.data }.indexOfOrThrow(state)) override fun toString(): String { val buf = StringBuilder() @@ -130,7 +131,7 @@ data class SignedTransaction(val txBits: SerializedBytes, return copy(sigs = sigs + sig) } - fun withAdditionalSignatures(sigList: Collection): SignedTransaction { + fun withAdditionalSignatures(sigList: Iterable): SignedTransaction { return copy(sigs = sigs + sigList) } @@ -165,12 +166,13 @@ data class LedgerTransaction( /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */ val attachments: List, /** The states that will be generated by the execution of this transaction. */ - val outputs: List, + val outputs: List>, /** Arbitrary data passed to the program of each input state. */ val commands: List>, /** The hash of the original serialised WireTransaction */ - override val id: SecureHash + override val id: SecureHash, + val type: TransactionType ) : NamedByHash { @Suppress("UNCHECKED_CAST") - fun outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(id, index)) + fun outRef(index: Int) = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt b/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt index a0e8c253bb..4f526b5b55 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt @@ -53,7 +53,7 @@ interface ServiceHub { * * @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction */ - fun loadState(stateRef: StateRef): ContractState { + fun loadState(stateRef: StateRef): TransactionState<*> { val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return definingTx.tx.outputs[stateRef.index] } diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index 5d2c106311..946208d72a 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -30,7 +30,7 @@ abstract class Wallet { abstract val states: List> @Suppress("UNCHECKED_CAST") - inline fun statesOfType() = states.filter { it.state is T } as List> + inline fun statesOfType() = states.filter { it.state.data is T } as List> /** * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for @@ -99,10 +99,10 @@ interface WalletService { /** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */ @Suppress("UNCHECKED_CAST") fun linearHeadsOfType_(stateType: Class): Map> { - return linearHeads.filterValues { stateType.isInstance(it.state) }.mapValues { StateAndRef(it.value.state as T, it.value.ref) } + return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState, it.value.ref) } } - fun statesForRefs(refs: List): Map { + fun statesForRefs(refs: List): Map?> { val refsToStates = currentWallet.states.associateBy { it.ref } return refs.associateBy({ it }, { refsToStates[it]?.state }) } diff --git a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt index f7f5a1cff7..588656484c 100644 --- a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt @@ -232,6 +232,7 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.attachments) kryo.writeClassAndObject(output, obj.outputs) kryo.writeClassAndObject(output, obj.commands) + kryo.writeClassAndObject(output, obj.type) } @Suppress("UNCHECKED_CAST") @@ -258,10 +259,11 @@ object WireTransactionSerializer : Serializer() { } else javaClass.classLoader kryo.useClassLoader(classLoader) { - val outputs = kryo.readClassAndObject(input) as List + val outputs = kryo.readClassAndObject(input) as List> val commands = kryo.readClassAndObject(input) as List + val transactionType = kryo.readClassAndObject(input) as TransactionType - return WireTransaction(inputs, attachmentHashes, outputs, commands) + return WireTransaction(inputs, attachmentHashes, outputs, commands, transactionType) } } } @@ -343,6 +345,7 @@ fun createKryo(k: Kryo = Kryo()): Kryo { // Work around a bug in Kryo handling nested generics register(Issued::class.java, ImmutableClassSerializer(Issued::class)) + register(TransactionState::class.java, ImmutableClassSerializer(TransactionState::class)) noReferencesWithin() } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index 604ebd6ad8..de92c7342a 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -4,12 +4,12 @@ package com.r3corda.core.testing import com.google.common.base.Throwables import com.google.common.net.HostAndPort -import com.r3corda.core.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.* -import com.r3corda.core.serialization.serialize import com.r3corda.core.node.services.testing.MockIdentityService import com.r3corda.core.node.services.testing.MockStorageService +import com.r3corda.core.seconds +import com.r3corda.core.serialization.serialize import java.net.ServerSocket import java.security.KeyPair import java.security.PublicKey @@ -96,20 +96,25 @@ fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) // // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block -class LabeledOutput(val label: String?, val state: ContractState) { +class LabeledOutput(val label: String?, val state: TransactionState<*>) { override fun toString() = state.toString() + (if (label != null) " ($label)" else "") override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state) override fun hashCode(): Int = state.hashCode() } -infix fun ContractState.label(label: String) = LabeledOutput(label, this) +infix fun TransactionState<*>.label(label: String) = LabeledOutput(label, this) abstract class AbstractTransactionForTest { protected val attachments = ArrayList() protected val outStates = ArrayList() protected val commands = ArrayList() + protected val type = TransactionType.Business() - open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) } + init { + arg(DUMMY_NOTARY.owningKey) { NotaryCommand() } + } + + open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) } protected fun commandsToAuthenticatedObjects(): List> { return commands.map { AuthenticatedObject(it.signers, it.signers.mapNotNull { MOCK_IDENTITY_SERVICE.partyFromKey(it) }, it.value) } @@ -150,12 +155,12 @@ sealed class LastLineShouldTestForAcceptOrFailure { // Corresponds to the args to Contract.verify open class TransactionForTest : AbstractTransactionForTest() { - private val inStates = arrayListOf() - fun input(s: () -> ContractState) = inStates.add(s()) + private val inStates = arrayListOf>() + fun input(s: () -> ContractState) = inStates.add(TransactionState(s(), DUMMY_NOTARY)) protected fun runCommandsAndVerify(time: Instant) { val cmds = commandsToAuthenticatedObjects() - val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256()) + val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), type) tx.verify() } @@ -240,16 +245,24 @@ class TransactionGroupDSL(private val stateType: Class) { private val inStates = ArrayList() fun input(label: String) { + val notaryKey = label.output.notary.owningKey + if (commands.none { it.signers.contains(notaryKey) }) commands.add(Command(NotaryCommand(), notaryKey)) inStates.add(label.outputRef) } - fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands) + fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, type) } - val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found") + val String.output: TransactionState + get() = + labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found") val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"") - fun lookup(label: String) = StateAndRef(label.output as C, label.outputRef) + fun lookup(label: String): StateAndRef { + val output = label.output + val newOutput = TransactionState(output.data as C, output.notary) + return StateAndRef(newOutput, label.outputRef) + } private inner class InternalWireTransactionDSL : WireTransactionDSL() { fun finaliseAndInsertLabels(): WireTransaction { @@ -257,8 +270,8 @@ class TransactionGroupDSL(private val stateType: Class) { for ((index, labelledState) in outStates.withIndex()) { if (labelledState.label != null) { labelToRefs[labelledState.label] = StateRef(wtx.id, index) - if (stateType.isInstance(labelledState.state)) { - labelToOutputs[labelledState.label] = labelledState.state as T + if (stateType.isInstance(labelledState.state.data)) { + labelToOutputs[labelledState.label] = labelledState.state as TransactionState } outputsToLabels[labelledState.state] = labelledState.label } @@ -269,20 +282,20 @@ class TransactionGroupDSL(private val stateType: Class) { private val rootTxns = ArrayList() private val labelToRefs = HashMap() - private val labelToOutputs = HashMap() - private val outputsToLabels = HashMap() + private val labelToOutputs = HashMap>() + private val outputsToLabels = HashMap, String>() - fun labelForState(state: T): String? = outputsToLabels[state] + fun labelForState(output: TransactionState<*>): String? = outputsToLabels[output] inner class Roots { fun transaction(vararg outputStates: LabeledOutput) { val outs = outputStates.map { it.state } - val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList()) + val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), TransactionType.Business()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! labelToRefs[label] = StateRef(wtx.id, index) outputsToLabels[state.state] = label - labelToOutputs[label] = state.state as T + labelToOutputs[label] = state.state as TransactionState } rootTxns.add(wtx) } diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index 81e0a2bcaa..10eeb69d4a 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -314,7 +314,7 @@ object TwoPartyDealProtocol { } override fun assembleSharedTX(handshake: Handshake): Pair> { - val ptx = handshake.payload.generateAgreement() + val ptx = handshake.payload.generateAgreement(notary) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. @@ -336,7 +336,7 @@ object TwoPartyDealProtocol { val dealToFix: StateAndRef, sessionID: Long, val replacementProgressTracker: ProgressTracker? = null) : Secondary(otherSide, notary, sessionID) { - private val ratesFixTracker = RatesFixProtocol.tracker(dealToFix.state.nextFixingOf()!!.name) + private val ratesFixTracker = RatesFixProtocol.tracker(dealToFix.state.data.nextFixingOf()!!.name) override val progressTracker: ProgressTracker = replacementProgressTracker ?: createTracker() @@ -358,11 +358,11 @@ object TwoPartyDealProtocol { @Suspendable override fun assembleSharedTX(handshake: Handshake): Pair> { - val fixOf = dealToFix.state.nextFixingOf()!! + val fixOf = dealToFix.state.data.nextFixingOf()!! // TODO Do we need/want to substitute in new public keys for the Parties? val myName = serviceHub.storageService.myLegalIdentity.name - val deal: T = dealToFix.state + val deal: T = dealToFix.state.data val myOldParty = deal.parties.single { it.name == myName } @Suppress("UNCHECKED_CAST") @@ -373,7 +373,7 @@ object TwoPartyDealProtocol { val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapCache.ratesOracleNodes[0], fixOf, BigDecimal.ZERO, BigDecimal.ONE) { @Suspendable override fun beforeSigning(fix: Fix) { - newDeal.generateFix(ptx, oldRef, fix) + newDeal.generateFix(ptx, dealToFix, fix) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. diff --git a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt index f120266561..142d4cea38 100644 --- a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt +++ b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt @@ -55,18 +55,13 @@ object NotaryChangeProtocol { progressTracker.currentStep = SIGNING - val signatures = mutableListOf() - val myKey = serviceHub.storageService.myLegalIdentity.owningKey val me = listOf(myKey) - if (participants == me) { - signatures.add(getNotarySignature(stx.tx)) + val signatures = if (participants == me) { + listOf(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) } + collectSignatures(participants - me, stx) } val finalTx = stx + signatures @@ -77,9 +72,9 @@ object NotaryChangeProtocol { private fun assembleTx(): Pair> { val state = originalState.state val newState = state.withNewNotary(newNotary) - val participants = state.participants + val participants = state.data.participants val cmd = Command(ChangeNotary(), participants) - val tx = TransactionBuilder().withItems(originalState.ref, newState, cmd) + val tx = TransactionBuilder(type = TransactionType.NotaryChange()).withItems(originalState, newState, cmd) tx.signWith(serviceHub.storageService.myLegalIdentityKey) val stx = tx.toSignedTransaction(false) @@ -87,21 +82,22 @@ object NotaryChangeProtocol { } @Suspendable - private fun collectSignatures(participants: List, signatures: MutableCollection, - stx: SignedTransaction): MutableList> { - val participantSessions = mutableListOf>() + private fun collectSignatures(participants: List, stx: SignedTransaction): List { + val sessions = mutableMapOf() - participants.forEach { + val participantSignatures = participants.map { 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) + sessions[participantNode] = sessionIdForSend - participantSessions.add(participantNode to sessionIdForSend) + getParticipantSignature(participantNode, stx, sessionIdForSend) } - return participantSessions + val allSignatures = participantSignatures + getNotarySignature(stx.tx) + sessions.forEach { send(TOPIC_CHANGE, it.key.address, it.value, allSignatures) } + + return allSignatures } @Suspendable @@ -125,7 +121,7 @@ object NotaryChangeProtocol { @Suspendable private fun getNotarySignature(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable { progressTracker.currentStep = NOTARY - return subProtocol(NotaryProtocol(wtx)) + return subProtocol(NotaryProtocol.Client(wtx)) } } @@ -153,19 +149,20 @@ object NotaryChangeProtocol { val mySignature = sign(proposedTx) val swapSignatures = sendAndReceive>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, mySignature) - val allSignatures = swapSignatures.validate { - it.forEach { it.verifyWithECDSA(proposedTx.txBits) } - it + val allSignatures = swapSignatures.validate { signatures -> + signatures.forEach { it.verifyWithECDSA(proposedTx.txBits) } + signatures } val finalTx = proposedTx + allSignatures + finalTx.verify() serviceHub.recordTransactions(listOf(finalTx)) } @Suspendable private fun validateTx(stx: SignedTransaction): SignedTransaction { checkDependenciesValid(stx) - checkContractValid(stx) + checkValid(stx) checkCommand(stx.tx) return stx } @@ -184,7 +181,7 @@ object NotaryChangeProtocol { subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide)) } - private fun checkContractValid(stx: SignedTransaction) { + private fun checkValid(stx: SignedTransaction) { val ltx = stx.tx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) serviceHub.verifyTransaction(ltx) } diff --git a/core/src/main/resources/com/r3corda/core/node/isolated.jar b/core/src/main/resources/com/r3corda/core/node/isolated.jar index b2c8673b8c8c3515ac051987bc6d7cb747cbb229..315a6e0989c20f20a270b7b9e002376eadae97c6 100644 GIT binary patch delta 3631 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, - override val owner: PublicKey, - override val notary: Party) : OwnableState { + override val owner: PublicKey) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID override val participants: List get() = listOf(owner) - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } @@ -44,12 +42,13 @@ class TransactionGroupTests { } infix fun TestCash.State.`owned by`(owner: PublicKey) = copy(owner = owner) + infix fun TestCash.State.`with notary`(notary: Party) = TransactionState(this, notary) @Test fun success() { transactionGroup { roots { - transaction(A_THOUSAND_POUNDS label "£1000") + transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000") } transaction { @@ -122,17 +121,17 @@ class TransactionGroupTests { // We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that // points nowhere. - val input = generateStateRef() + val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef()) tg.txns += TransactionBuilder().apply { addInputState(input) - addOutputState(A_THOUSAND_POUNDS) + addOutputState(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY) addCommand(TestCash.Commands.Move(), BOB_PUBKEY) }.toWireTransaction() val e = assertFailsWith(TransactionResolutionException::class) { tg.verify() } - assertEquals(e.hash, input.txhash) + assertEquals(e.hash, input.ref.txhash) } @Test @@ -140,7 +139,7 @@ class TransactionGroupTests { // Check that a transaction cannot refer to the same input more than once. transactionGroup { roots { - transaction(A_THOUSAND_POUNDS label "£1000") + transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000") } transaction { diff --git a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt index dcf392a2f3..4d390a9476 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt @@ -33,20 +33,17 @@ class AttachmentClassLoaderTests { } class AttachmentDummyContract : Contract { - data class State(val magicNumber: Int = 0, - override val notary: Party) : ContractState { + data class State(val magicNumber: Int = 0) : ContractState { override val contract = ATTACHMENT_TEST_PROGRAM_ID override val participants: List get() = listOf() - - override fun withNewNotary(newNotary: Party) = copy(notary = newNotary) } interface Commands : CommandData { class Create : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Always accepts. } @@ -54,7 +51,7 @@ class AttachmentClassLoaderTests { override val legalContractReference: SecureHash = SecureHash.sha256("") fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = State(magicNumber, notary) + val state = TransactionState(State(magicNumber), notary) return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) } } @@ -220,7 +217,7 @@ class AttachmentClassLoaderTests { val copiedWireTransaction = bytes.deserialize() assertEquals(1, copiedWireTransaction.outputs.size) - assertEquals(42, (copiedWireTransaction.outputs[0] as AttachmentDummyContract.State).magicNumber) + assertEquals(42, (copiedWireTransaction.outputs[0].data as AttachmentDummyContract.State).magicNumber) } @Test @@ -250,8 +247,8 @@ class AttachmentClassLoaderTests { val copiedWireTransaction = bytes.deserialize(kryo2) assertEquals(1, copiedWireTransaction.outputs.size) - val contract2 = copiedWireTransaction.outputs[0].contract as DummyContractBackdoor - assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0])) + val contract2 = copiedWireTransaction.outputs[0].data.contract as DummyContractBackdoor + assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0].data)) } @Test diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index 5917e8d87d..b605018e99 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -1,7 +1,6 @@ package com.r3corda.core.serialization import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.seconds @@ -21,20 +20,18 @@ class TransactionSerializationTests { class TestCash : Contract { override val legalContractReference = SecureHash.sha256("TestCash") - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { } data class State( val deposit: PartyAndReference, val amount: Amount, - override val owner: PublicKey, - override val notary: Party) : OwnableState { + override val owner: PublicKey) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID override val participants: List 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 @@ -46,21 +43,24 @@ class TransactionSerializationTests { // 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) - val changeState = TestCash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY) - val fakeStateRef = generateStateRef() + val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY), fakeStateRef) + val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY) + val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, TestUtils.keypair.public), DUMMY_NOTARY) + + lateinit var tx: TransactionBuilder @Before fun setup() { tx = TransactionBuilder().withItems( - fakeStateRef, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) + inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) ) } @Test fun signWireTX() { + tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(TestUtils.keypair) val signedTX = tx.toSignedTransaction() @@ -82,6 +82,7 @@ class TransactionSerializationTests { } tx.signWith(TestUtils.keypair) + tx.signWith(DUMMY_NOTARY_KEY) val signedTX = tx.toSignedTransaction() // Cannot construct with an empty sigs list. @@ -91,8 +92,9 @@ class TransactionSerializationTests { // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { - val tx2 = TransactionBuilder().withItems(fakeStateRef, outputState, changeState, + val tx2 = TransactionBuilder().withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), TestUtils.keypair2.public)) + tx2.signWith(DUMMY_NOTARY_KEY) tx2.signWith(TestUtils.keypair2) signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() diff --git a/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt b/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt index 0c2331244f..7e3979c16a 100644 --- a/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt +++ b/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt @@ -1,10 +1,7 @@ package com.r3corda.node.api +import com.r3corda.core.contracts.* import com.r3corda.node.api.StatesQuery -import com.r3corda.core.contracts.ContractState -import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.SecureHash import com.r3corda.core.serialization.SerializedBytes @@ -43,7 +40,7 @@ interface APIServer { */ fun queryStates(query: StatesQuery): List - fun fetchStates(states: List): Map + fun fetchStates(states: List): Map?> /** * Query for immutable transactions (results can be cached indefinitely by their id/hash). diff --git a/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt b/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt index bf9075f67e..4a8fd1f038 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt @@ -27,7 +27,7 @@ class APIServerImpl(val node: AbstractNode) : APIServer { return states.values.map { it.ref } } else if (query.criteria is StatesQuery.Criteria.Deal) { val states = node.services.walletService.linearHeadsOfType().filterValues { - it.state.ref == query.criteria.ref + it.state.data.ref == query.criteria.ref } return states.values.map { it.ref } } @@ -35,7 +35,7 @@ class APIServerImpl(val node: AbstractNode) : APIServer { return emptyList() } - override fun fetchStates(states: List): Map { + override fun fetchStates(states: List): Map?> { return node.services.walletService.statesForRefs(states) } diff --git a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt index 52af6ad13b..8a6e05c45f 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt @@ -102,7 +102,6 @@ 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 @@ -130,9 +129,6 @@ 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 @@ -144,6 +140,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, // This object doesn't need to be referenced from this class because it registers handlers on the network // service and so that keeps it from being collected. DataVendingService(net, storage) + NotaryChangeService(net, smm) buildAdvertisedServices() diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt index 225f1a5d16..dc5c80a1cd 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt @@ -85,7 +85,7 @@ class IRSSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork val theDealRef: StateAndRef = swaps.values.single() // Do we have any more days left in this deal's lifetime? If not, return. - val nextFixingDate = theDealRef.state.calculation.nextFixingDate() ?: return null + val nextFixingDate = theDealRef.state.data.calculation.nextFixingDate() ?: return null extraNodeLabels[node1] = "Fixing event on $nextFixingDate" extraNodeLabels[node2] = "Fixing event on $nextFixingDate" diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt index 05966b6540..ed6e3154f2 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt @@ -1,5 +1,6 @@ package com.r3corda.node.internal.testing +import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.DummyContract import com.r3corda.core.contracts.StateRef import com.r3corda.core.crypto.Party @@ -10,20 +11,20 @@ import com.r3corda.node.internal.AbstractNode import java.time.Instant import java.util.* -fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef { +fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) tx.signWith(node.storage.myLegalIdentityKey) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() node.services.recordTransactions(listOf(stx)) - return StateRef(stx.id, 0) + return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } -fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef { +fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) tx.setTime(Instant.now(), notary, 30.seconds) tx.signWith(node.storage.myLegalIdentityKey) val stx = tx.toSignedTransaction(false) node.services.recordTransactions(listOf(stx)) - return StateRef(stx.id, 0) + return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt index 6f084636e3..81a2165a55 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt @@ -1,6 +1,5 @@ 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 @@ -41,13 +40,14 @@ class NotaryChangeService(net: MessagingService, val smm: StateMachineManager) : * 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 " } + // An example requirement + val blacklist = listOf("Evil Notary") + require(!blacklist.contains(newNotary.name)) + return true } } diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt index b51d8a0f3f..c9a45975e0 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt @@ -51,7 +51,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer */ override val linearHeads: Map> get() = mutex.locked { wallet }.let { wallet -> - wallet.states.filterStatesOfType().associateBy { it.state.thread }.mapValues { it.value } + wallet.states.filterStatesOfType().associateBy { it.state.data.thread }.mapValues { it.value } } override fun notifyAll(txns: Iterable): Wallet { @@ -103,8 +103,8 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer private fun Wallet.update(tx: WireTransaction, ourKeys: Set): Pair { val ourNewStates = tx.outputs. - filter { isRelevant(it, ourKeys) }. - map { tx.outRef(it) } + filter { isRelevant(it.data, ourKeys) }. + map { tx.outRef(it.data) } // Now calculate the states that are being spent by this transaction. val consumed: Set = states.map { it.ref }.intersect(tx.inputs) diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt index fe6fdd03a5..745237ecfc 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt @@ -24,7 +24,7 @@ class WalletImpl(override val states: List>) : Wallet */ override val cashBalances: Map> get() = states. // Select the states we own which are cash, ignore the rest, take the amounts. - mapNotNull { (it.state as? Cash.State)?.amount }. + mapNotNull { (it.state.data as? Cash.State)?.amount }. // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } groupBy { it.token.product }. // Collapse to Map by summing all the amounts of the same currency together. diff --git a/node/src/main/resources/com/r3corda/node/internal/testing/trade.json b/node/src/main/resources/com/r3corda/node/internal/testing/trade.json index 0d5d125897..c2b7d51a3f 100644 --- a/node/src/main/resources/com/r3corda/node/internal/testing/trade.json +++ b/node/src/main/resources/com/r3corda/node/internal/testing/trade.json @@ -99,6 +99,5 @@ "dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360", "tradeID": "tradeXXX", "hashLegalDocs": "put hash here" - }, - "notary": "Notary Service" + } } diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 2d45aa3030..a82246bbaa 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -468,7 +468,7 @@ class TwoPartyTradeProtocolTests { attachmentID: SecureHash?): Pair> { val ap = transaction { output("alice's paper") { - CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days, notary) + CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days) } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } if (!withError) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt index 0ce88da61e..77561353e7 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt @@ -4,6 +4,7 @@ import com.r3corda.contracts.cash.Cash import com.r3corda.contracts.testing.CASH import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` +import com.r3corda.contracts.testing.`with notary` import com.r3corda.core.bd import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.Fix @@ -11,6 +12,7 @@ import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.testing.ALICE_PUBKEY +import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.MEGA_CORP import com.r3corda.core.testing.MEGA_CORP_KEY import com.r3corda.core.utilities.BriefLogFormatter @@ -117,5 +119,5 @@ class NodeInterestRatesTest { assertEquals("0.678".bd, fix.value) } - private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY)) + private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY)) } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt index 129d42ac5d..af37073722 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt @@ -51,14 +51,14 @@ class NodeWalletServiceTest { val w = wallet.currentWallet assertEquals(3, w.states.size) - val state = w.states[0].state as Cash.State + val state = w.states[0].state.data as Cash.State val myIdentity = services.storageService.myLegalIdentity val myPartyRef = myIdentity.ref(ref) assertEquals(29.01.DOLLARS `issued by` myPartyRef, state.amount) assertEquals(ALICE_PUBKEY, state.owner) - assertEquals(33.34.DOLLARS `issued by` myPartyRef, (w.states[2].state as Cash.State).amount) - assertEquals(35.61.DOLLARS `issued by` myPartyRef, (w.states[1].state as Cash.State).amount) + assertEquals(33.34.DOLLARS `issued by` myPartyRef, (w.states[2].state.data as Cash.State).amount) + assertEquals(35.61.DOLLARS `issued by` myPartyRef, (w.states[1].state.data as Cash.State).amount) } @Test @@ -77,12 +77,14 @@ class NodeWalletServiceTest { val spendTX = TransactionBuilder().apply { Cash().generateSpend(this, 80.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_PUBKEY, listOf(myOutput)) signWith(freshKey) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() // A tx that doesn't send us anything. val irrelevantTX = TransactionBuilder().apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() assertNull(wallet.cashBalances[USD]) diff --git a/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt b/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt index f7b76fbc32..f3c8197488 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt @@ -1,6 +1,8 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.StateRef +import com.r3corda.core.contracts.TransactionType +import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.node.services.UniquenessException import com.r3corda.core.testing.MEGA_CORP import com.r3corda.core.testing.generateStateRef @@ -15,7 +17,8 @@ class UniquenessProviderTests { @Test fun `should commit a transaction with unused inputs without exception`() { val provider = InMemoryUniquenessProvider() val inputState = generateStateRef() - val tx = TransactionBuilder().withItems(inputState).toWireTransaction() + val tx = buildTransaction(inputState) + provider.commit(tx, identity) } @@ -23,10 +26,10 @@ class UniquenessProviderTests { val provider = InMemoryUniquenessProvider() val inputState = generateStateRef() - val tx1 = TransactionBuilder().withItems(inputState).toWireTransaction() + val tx1 = buildTransaction(inputState) provider.commit(tx1, identity) - val tx2 = TransactionBuilder().withItems(inputState).toWireTransaction() + val tx2 = buildTransaction(inputState) val ex = assertFailsWith { provider.commit(tx2, identity) } val consumingTx = ex.error.stateHistory[inputState]!! @@ -34,4 +37,6 @@ class UniquenessProviderTests { assertEquals(consumingTx.inputIndex, tx1.inputs.indexOf(inputState)) assertEquals(consumingTx.requestingParty, identity) } + + private fun buildTransaction(inputState: StateRef) = WireTransaction(listOf(inputState), emptyList(), emptyList(), emptyList(), TransactionType.Business()) } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt index a36aa7731b..9cb29a4e18 100644 --- a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt +++ b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt @@ -2,6 +2,7 @@ package com.r3corda.node.visualiser import com.r3corda.core.contracts.CommandData import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.crypto.SecureHash import com.r3corda.core.testing.TransactionGroupDSL import org.graphstream.graph.Edge @@ -30,7 +31,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { val node = graph.addNode(tx.outRef(outIndex).ref.toString()) val state = tx.outputs[outIndex] node.label = stateToLabel(state) - node.styleClass = stateToCSSClass(state) + ",state" + node.styleClass = stateToCSSClass(state.data) + ",state" node.setAttribute("state", state) val edge = graph.addEdge("tx$txIndex-out$outIndex", txNode, node, true) edge.weight = 0.7 @@ -55,8 +56,8 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { return graph } - private fun stateToLabel(state: ContractState): String { - return dsl.labelForState(state) ?: stateToTypeName(state) + private fun stateToLabel(state: TransactionState<*>): String { + return dsl.labelForState(state) ?: stateToTypeName(state.data) } private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.') diff --git a/node/src/test/kotlin/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/node/services/NotaryChangeTests.kt index aca155f8d1..c76e176ba9 100644 --- a/node/src/test/kotlin/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/node/services/NotaryChangeTests.kt @@ -1,14 +1,12 @@ 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.contracts.* import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.node.internal.testing.MockNetwork +import com.r3corda.node.internal.testing.issueState import com.r3corda.node.services.transactions.NotaryService -import com.r3corda.node.testutils.issueState import org.junit.Before import org.junit.Test import protocols.NotaryChangeProtocol @@ -35,12 +33,12 @@ class NotaryChangeTests { @Test fun `should change notary for a state with single participant`() { - val state = issueState(clientNodeA, DUMMY_NOTARY) - val ref = clientNodeA.services.loadState(state) + val ref = issueState(clientNodeA, DUMMY_NOTARY).ref + val state = clientNodeA.services.loadState(ref) val newNotary = newNotaryNode.info.identity - val protocol = Instigator(StateAndRef(ref, state), newNotary) + val protocol = Instigator(StateAndRef(state, ref), newNotary) val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) net.runNetwork() @@ -51,11 +49,10 @@ class NotaryChangeTests { @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 state = TransactionState(DummyContract.MultiOwnerState(0, + listOf(clientNodeA.info.identity.owningKey, clientNodeB.info.identity.owningKey)), DUMMY_NOTARY) - val tx = TransactionBuilder().withItems(state) + val tx = TransactionBuilder(type = TransactionType.NotaryChange()).withItems(state) tx.signWith(clientNodeA.storage.myLegalIdentityKey) tx.signWith(clientNodeB.storage.myLegalIdentityKey) tx.signWith(DUMMY_NOTARY_KEY) diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index 41fb88f3f9..a7916282cd 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -1,10 +1,7 @@ package com.r3corda.demos import com.r3corda.contracts.cash.Cash -import com.r3corda.core.contracts.DOLLARS -import com.r3corda.core.contracts.FixOf -import com.r3corda.core.contracts.`issued by` -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.logElapsedTime import com.r3corda.core.node.NodeInfo @@ -87,7 +84,7 @@ fun main(args: Array) { // Make a garbage transaction that includes a rate fix. val tx = TransactionBuilder() - tx.addOutputState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public, notary.identity)) + tx.addOutputState(TransactionState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public), notary.identity)) val protocol = RatesFixProtocol(tx, oracleNode, fixOf, expectedRate, rateTolerance) node.smm.add("demo.ratefix", protocol).get() node.stop() diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index a7ccff583f..fb842a7e7d 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -356,7 +356,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, // Sign it as ourselves. tx.signWith(keyPair) - // Get the notary to sign it, thus committing the outputs. + // Get the notary to sign the timestamp val notarySig = subProtocol(NotaryProtocol.Client(tx.toWireTransaction())) tx.addSignatureUnchecked(notarySig) diff --git a/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt b/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt index a9b206234f..9197216da7 100644 --- a/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt +++ b/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt @@ -44,14 +44,14 @@ class InterestRateSwapAPI(val api: APIServer) { private fun getDealByRef(ref: String): InterestRateSwap.State? { val states = api.queryStates(StatesQuery.selectDeal(ref)) return if (states.isEmpty()) null else { - val deals = api.fetchStates(states).values.map { it as InterestRateSwap.State }.filterNotNull() + val deals = api.fetchStates(states).values.map { it?.data as InterestRateSwap.State }.filterNotNull() return if (deals.isEmpty()) null else deals[0] } } private fun getAllDeals(): Array { val states = api.queryStates(StatesQuery.selectAllDeals()) - val swaps = api.fetchStates(states).values.map { it as InterestRateSwap.State }.filterNotNull().toTypedArray() + val swaps = api.fetchStates(states).values.map { it?.data as InterestRateSwap.State }.filterNotNull().toTypedArray() return swaps } diff --git a/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt b/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt index e32bc610a3..452e315871 100644 --- a/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt +++ b/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import com.r3corda.contracts.InterestRateSwap import com.r3corda.core.contracts.DealState import com.r3corda.core.contracts.StateAndRef +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.services.linearHeadsOfType import com.r3corda.core.protocols.ProtocolLogic @@ -13,6 +14,7 @@ import com.r3corda.core.utilities.ProgressTracker import com.r3corda.demos.DemoClock import com.r3corda.node.internal.Node import com.r3corda.node.services.network.MockNetworkMapCache +import com.r3corda.node.utilities.ANSIProgressRenderer import com.r3corda.protocols.TwoPartyDealProtocol import java.time.LocalDate @@ -41,12 +43,12 @@ object UpdateBusinessDayProtocol { // Get deals progressTracker.currentStep = FETCHING val dealStateRefs = serviceHub.walletService.linearHeadsOfType() - val otherPartyToDeals = dealStateRefs.values.groupBy { otherParty(it.state) } + val otherPartyToDeals = dealStateRefs.values.groupBy { otherParty(it.state.data) } // TODO we need to process these in parallel to stop there being an ordering problem across more than two nodes val sortedParties = otherPartyToDeals.keys.sortedBy { it.identity.name } for (party in sortedParties) { - val sortedDeals = otherPartyToDeals[party]!!.sortedBy { it.state.ref } + val sortedDeals = otherPartyToDeals[party]!!.sortedBy { it.state.data.ref } for (deal in sortedDeals) { progressTracker.currentStep = ITERATING_DEALS processDeal(party, deal, date, sessionID) @@ -64,9 +66,9 @@ object UpdateBusinessDayProtocol { // TODO we should make this more object oriented when we can ask a state for it's contract @Suspendable fun processDeal(party: NodeInfo, deal: StateAndRef, date: LocalDate, sessionID: Long) { - val s = deal.state + val s = deal.state.data when (s) { - is InterestRateSwap.State -> processInterestRateSwap(party, StateAndRef(s, deal.ref), date, sessionID) + is InterestRateSwap.State -> processInterestRateSwap(party, StateAndRef(TransactionState(s, deal.state.notary), deal.ref), date, sessionID) } } @@ -74,7 +76,7 @@ object UpdateBusinessDayProtocol { @Suspendable fun processInterestRateSwap(party: NodeInfo, deal: StateAndRef, date: LocalDate, sessionID: Long) { var dealStateAndRef: StateAndRef? = deal - var nextFixingDate = deal.state.calculation.nextFixingDate() + var nextFixingDate = deal.state.data.calculation.nextFixingDate() while (nextFixingDate != null && !nextFixingDate.isAfter(date)) { progressTracker.currentStep = ITERATING_FIXINGS /* @@ -83,12 +85,12 @@ object UpdateBusinessDayProtocol { * One of the parties needs to take the lead in the coordination and this is a reliable deterministic way * to do it. */ - if (party.identity.name == deal.state.fixedLeg.fixedRatePayer.name) { + if (party.identity.name == deal.state.data.fixedLeg.fixedRatePayer.name) { dealStateAndRef = nextFixingFloatingLeg(dealStateAndRef!!, party, sessionID) } else { dealStateAndRef = nextFixingFixedLeg(dealStateAndRef!!, party, sessionID) } - nextFixingDate = dealStateAndRef?.state?.calculation?.nextFixingDate() + nextFixingDate = dealStateAndRef?.state?.data?.calculation?.nextFixingDate() } } @@ -98,7 +100,7 @@ object UpdateBusinessDayProtocol { progressTracker.currentStep = FIXING val myName = serviceHub.storageService.myLegalIdentity.name - val deal: InterestRateSwap.State = dealStateAndRef.state + val deal: InterestRateSwap.State = dealStateAndRef.state.data val myOldParty = deal.parties.single { it.name == myName } val keyPair = serviceHub.keyManagementService.toKeyPair(myOldParty.owningKey) val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.notaryNodes[0], dealStateAndRef, From 9958b5c60339b8bd2ebbbc3e7f4649485e50b7db Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Mon, 13 Jun 2016 18:56:39 +0100 Subject: [PATCH 3/5] Added 'signers' property to the transaction data models. Signers holds the list of all public keys that need to be signed for (command keys and additional ones such as notary). Removed Notary & ChangeNotary commands, keys to be signed for are added to the signers list during transaction build phase. --- .../main/kotlin/com/r3corda/contracts/IRS.kt | 1 - .../r3corda/contracts/CommercialPaperTests.kt | 2 +- .../com/r3corda/core/contracts/Structures.kt | 9 --- .../core/contracts/TransactionBuilder.kt | 36 ++++----- .../core/contracts/TransactionTools.kt | 2 +- .../core/contracts/TransactionTypes.kt | 70 ++++++++++++++---- .../core/contracts/TransactionVerification.kt | 6 +- .../r3corda/core/contracts/Transactions.kt | 9 ++- .../core/node/services/UniquenessProvider.kt | 5 +- .../com/r3corda/core/serialization/Kryo.kt | 5 +- .../com/r3corda/core/testing/TestUtils.kt | 32 +++++--- .../com/r3corda/protocols/NotaryProtocol.kt | 2 +- .../kotlin/protocols/NotaryChangeProtocol.kt | 15 ++-- .../com/r3corda/core/node/isolated.jar | Bin 8329 -> 8334 bytes .../InMemoryUniquenessProvider.kt | 12 ++- .../node/services/UniquenessProviderTests.kt | 21 ++---- 16 files changed, 127 insertions(+), 100 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index a812ec1e6f..922b143e2c 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -620,7 +620,6 @@ class InterestRateSwap() : Contract { } } - // TODO: pass a notary override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 6feacc0e6a..ac32ef7a06 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -140,7 +140,7 @@ class CommercialPaperTestsGeneric { } fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { - val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), TransactionType.Business()) + val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.Business()) return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 32bba6809d..f4ad39066f 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -203,15 +203,6 @@ data class TimestampCommand(val after: Instant?, val before: Instant?) : Command val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2) } -/** - * Command that has to be signed by all participants of the states in the transaction - * in order to perform a notary change - */ -class ChangeNotary : TypeOnlyCommandData() - -/** Command that indicates the requirement of a Notary signature for the input states */ -class NotaryCommand : 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 diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 4c5222c9a7..049c6d0469 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -1,9 +1,6 @@ package com.r3corda.core.contracts -import com.r3corda.core.crypto.DigitalSignature -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.crypto.signWithECDSA +import com.r3corda.core.crypto.* import com.r3corda.core.serialization.serialize import java.security.KeyPair import java.security.PublicKey @@ -17,11 +14,12 @@ import java.util.* * Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from * multiple parties. */ -class TransactionBuilder(private val inputs: MutableList = arrayListOf(), - private val attachments: MutableList = arrayListOf(), - private val outputs: MutableList> = arrayListOf(), - private val commands: MutableList = arrayListOf(), - private val type: TransactionType = TransactionType.Business()) { +open class TransactionBuilder(protected val inputs: MutableList = arrayListOf(), + protected val attachments: MutableList = arrayListOf(), + protected val outputs: MutableList> = arrayListOf(), + protected val commands: MutableList = arrayListOf(), + protected val signers: MutableSet = mutableSetOf(), + protected val type: TransactionType = TransactionType.Business()) { val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() @@ -57,7 +55,7 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf } /** The signatures that have been collected so far - might be incomplete! */ - private val currentSigs = arrayListOf() + protected val currentSigs = arrayListOf() fun signWith(key: KeyPair) { check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" } @@ -94,26 +92,23 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf } fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands), type) + ArrayList(outputs), ArrayList(commands), signers.toList(), type) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { if (checkSufficientSignatures) { val gotKeys = currentSigs.map { it.by }.toSet() - for (command in commands) { - if (!gotKeys.containsAll(command.signers)) - throw IllegalStateException("Missing signatures on the transaction for a ${command.value.javaClass.canonicalName} command") - } + val missing = signers - gotKeys + if (missing.isNotEmpty()) + throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.map { it.toStringShort() }}") } return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - fun addInputState(stateAndRef: StateAndRef<*>) { + open fun addInputState(stateAndRef: StateAndRef<*>) { check(currentSigs.isEmpty()) val notaryKey = stateAndRef.state.notary.owningKey - if (commands.none { it.signers.contains(notaryKey) }) { - commands.add(Command(NotaryCommand(), notaryKey)) - } + signers.add(notaryKey) inputs.add(stateAndRef.ref) } @@ -130,7 +125,8 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf fun addCommand(arg: Command) { check(currentSigs.isEmpty()) - // We should probably merge the lists of pubkeys for identical commands here. + // TODO: replace pubkeys in commands with 'pointers' to keys in signers + signers.addAll(arg.signers) commands.add(arg) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt index 39325f4528..82b140c8b8 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -18,7 +18,7 @@ fun WireTransaction.toLedgerTransaction(identityService: IdentityService, val attachments = attachments.map { attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } - return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id, type) + return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id, signers, type) } /** diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt index 5c3421e397..bc427f9a41 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -1,5 +1,8 @@ package com.r3corda.core.contracts +import com.r3corda.core.noneOrSingle +import java.security.PublicKey + /** Defines transaction validation rules for a specific transaction type */ sealed class TransactionType { override fun equals(other: Any?) = other?.javaClass == javaClass @@ -13,26 +16,43 @@ sealed class TransactionType { * Note: Presence of _signatures_ is not checked, only the public keys to be signed for. */ fun verify(tx: TransactionForVerification) { - verifyNotary(tx) - typeSpecificVerify(tx) + + val missing = verifySigners(tx) + if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList()) + + verifyTransaction(tx) } - private fun verifyNotary(tx: TransactionForVerification) { - if (tx.inStates.isEmpty()) return - val notary = tx.inStates.first().notary - if (tx.inStates.any { it.notary != notary }) throw TransactionVerificationException.MoreThanOneNotary(tx) - if (tx.commands.none { it.signers.contains(notary.owningKey) }) throw TransactionVerificationException.NotaryMissing(tx) + /** Check that the list of signers includes all the necessary keys */ + fun verifySigners(tx: TransactionForVerification): Set { + val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand } + val timestampKey = timestamp?.signers.orEmpty() + val notaryKey = (tx.inStates.map { it.notary.owningKey } + timestampKey).toSet() + if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx) + + val requiredKeys = getRequiredSigners(tx) + notaryKey + val missing = requiredKeys - tx.signers + + return missing } - abstract fun typeSpecificVerify(tx: TransactionForVerification) + /** + * Return the list of public keys that that require signatures for the transaction type. + * Note: the notary key is checked separately for all transactions and need not be included + */ + abstract fun getRequiredSigners(tx: TransactionForVerification): Set + + /** Implement type specific transaction validation logic */ + abstract fun verifyTransaction(tx: TransactionForVerification) /** A general type used for business transactions, where transaction validity is determined by custom contract code */ class Business : TransactionType() { /** * Check the transaction is contract-valid by running the verify() for each input and output state contract. - * If any contract fails to verify, the whole transaction is considered to be invalid. + * If any contract fails to verify, the whole transaction is considered to be invalid */ - override fun typeSpecificVerify(tx: TransactionForVerification) { + override fun verifyTransaction(tx: TransactionForVerification) { + // TODO: Check that notary is unchanged val ctx = tx.toTransactionForContract() val contracts = (ctx.inStates.map { it.contract } + ctx.outStates.map { it.contract }).toSet() @@ -44,6 +64,11 @@ sealed class TransactionType { } } } + + override fun getRequiredSigners(tx: TransactionForVerification): Set { + val commandKeys = tx.commands.flatMap { it.signers }.toSet() + return commandKeys + } } /** @@ -51,23 +76,36 @@ sealed class TransactionType { * any contract code, it just checks that the states are unmodified apart from the notary field. */ class NotaryChange : TransactionType() { + /** + * A transaction builder that automatically sets the transaction type to [NotaryChange] + * and adds the list of participants to the signers set for every input state. + */ + class Builder() : TransactionBuilder(type = NotaryChange()) { + override fun addInputState(stateAndRef: StateAndRef<*>) { + signers.addAll(stateAndRef.state.data.participants) + super.addInputState(stateAndRef) + } + } + /** * Check that the difference between inputs and outputs is only the notary field, * and that all required signing public keys are present */ - override fun typeSpecificVerify(tx: TransactionForVerification) { + override fun verifyTransaction(tx: TransactionForVerification) { try { tx.inStates.zip(tx.outStates).forEach { check(it.first.data == it.second.data) check(it.first.notary != it.second.notary) } - val command = tx.commands.requireSingleCommand() - val requiredSigners = tx.inStates.flatMap { it.data.participants } - check(command.signers.containsAll(requiredSigners)) + check(tx.commands.isEmpty()) } catch (e: IllegalStateException) { throw TransactionVerificationException.InvalidNotaryChange(tx) } } - } -} + override fun getRequiredSigners(tx: TransactionForVerification): Set { + val participantKeys = tx.inStates.flatMap { it.data.participants }.toSet() + return participantKeys + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index d7c576a3c3..459a5c3eda 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -2,6 +2,7 @@ package com.r3corda.core.contracts import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash +import java.security.PublicKey import java.util.* // TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts. @@ -41,7 +42,7 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the output in that transaction by index. inputs.add(ltx.outputs[ref.index]) } - resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.type)) + resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.signers, tx.type)) } for (tx in resolved) @@ -56,6 +57,7 @@ data class TransactionForVerification(val inStates: List, val commands: List>, val origHash: SecureHash, + val signers: List, val type: TransactionType) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash @@ -162,6 +164,6 @@ 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 NotaryMissing(tx: TransactionForVerification) : TransactionVerificationException(tx, null) + class SignersMissing(tx: TransactionForVerification, missing: List) : TransactionVerificationException(tx, null) class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt index d7eb239e78..7f5ea0f8f5 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -45,6 +45,7 @@ data class WireTransaction(val inputs: List, val attachments: List, val outputs: List>, val commands: List, + val signers: List, val type: TransactionType) : NamedByHash { // Cache the serialised form of the transaction and its hash to give us fast access to it. @@ -110,7 +111,7 @@ data class SignedTransaction(val txBits: SerializedBytes, /** * Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains - * the set of pubkeys in the commands. If any signatures are missing, either throws an exception (by default) or + * the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or * returns the list of keys that have missing signatures, depending on the parameter. * * @throws SignatureException if a signature is invalid, does not match or if any signature is missing. @@ -141,11 +142,10 @@ data class SignedTransaction(val txBits: SerializedBytes, operator fun plus(sigList: Collection) = 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) + * Returns the set of missing signatures - a signature must be present for each signer public key */ fun getMissingSignatures(): Set { - val requiredKeys = tx.commands.flatMap { it.signers }.toSet() + val requiredKeys = tx.signers.toSet() val sigKeys = sigs.map { it.by }.toSet() if (sigKeys.containsAll(requiredKeys)) return emptySet() @@ -171,6 +171,7 @@ data class LedgerTransaction( val commands: List>, /** The hash of the original serialised WireTransaction */ override val id: SecureHash, + val signers: List, val type: TransactionType ) : NamedByHash { @Suppress("UNCHECKED_CAST") diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt b/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt index 1ee6b76330..c54290e5ab 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt @@ -1,8 +1,7 @@ package com.r3corda.core.node.services -import com.r3corda.core.crypto.Party import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.WireTransaction +import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash /** @@ -11,7 +10,7 @@ import com.r3corda.core.crypto.SecureHash */ interface UniquenessProvider { /** Commits all input states of the given transaction */ - fun commit(tx: WireTransaction, callerIdentity: Party) + fun commit(states: List, txId: SecureHash, callerIdentity: Party) /** Specifies the consuming transaction for every conflicting state */ data class Conflict(val stateHistory: Map) diff --git a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt index 588656484c..1d582082f4 100644 --- a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt @@ -29,6 +29,7 @@ import java.io.ObjectOutputStream import java.lang.reflect.InvocationTargetException import java.nio.file.Files import java.nio.file.Path +import java.security.PublicKey import java.time.Instant import java.util.* import javax.annotation.concurrent.ThreadSafe @@ -232,6 +233,7 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.attachments) kryo.writeClassAndObject(output, obj.outputs) kryo.writeClassAndObject(output, obj.commands) + kryo.writeClassAndObject(output, obj.signers) kryo.writeClassAndObject(output, obj.type) } @@ -261,9 +263,10 @@ object WireTransactionSerializer : Serializer() { kryo.useClassLoader(classLoader) { val outputs = kryo.readClassAndObject(input) as List> val commands = kryo.readClassAndObject(input) as List + val signers = kryo.readClassAndObject(input) as List val transactionType = kryo.readClassAndObject(input) as TransactionType - return WireTransaction(inputs, attachmentHashes, outputs, commands, transactionType) + return WireTransaction(inputs, attachmentHashes, outputs, commands, signers, transactionType) } } } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index de92c7342a..b9faeb3fb9 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -108,12 +108,9 @@ abstract class AbstractTransactionForTest { protected val attachments = ArrayList() protected val outStates = ArrayList() protected val commands = ArrayList() + protected val signers = LinkedHashSet() protected val type = TransactionType.Business() - init { - arg(DUMMY_NOTARY.owningKey) { NotaryCommand() } - } - open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) } protected fun commandsToAuthenticatedObjects(): List> { @@ -126,7 +123,7 @@ abstract class AbstractTransactionForTest { fun arg(vararg key: PublicKey, c: () -> CommandData) { val keys = listOf(*key) - commands.add(Command(c(), keys)) + addCommand(Command(c(), keys)) } fun timestamp(time: Instant) { @@ -135,7 +132,12 @@ abstract class AbstractTransactionForTest { } fun timestamp(data: TimestampCommand) { - commands.add(Command(data, DUMMY_NOTARY.owningKey)) + addCommand(Command(data, DUMMY_NOTARY.owningKey)) + } + + fun addCommand(cmd: Command) { + signers.addAll(cmd.signers) + commands.add(cmd) } // Forbid patterns like: transaction { ... transaction { ... } } @@ -156,11 +158,14 @@ sealed class LastLineShouldTestForAcceptOrFailure { // Corresponds to the args to Contract.verify open class TransactionForTest : AbstractTransactionForTest() { private val inStates = arrayListOf>() - fun input(s: () -> ContractState) = inStates.add(TransactionState(s(), DUMMY_NOTARY)) + fun input(s: () -> ContractState) { + signers.add(DUMMY_NOTARY.owningKey) + inStates.add(TransactionState(s(), DUMMY_NOTARY)) + } protected fun runCommandsAndVerify(time: Instant) { val cmds = commandsToAuthenticatedObjects() - val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), type) + val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), signers.toList(), type) tx.verify() } @@ -215,6 +220,9 @@ open class TransactionForTest : AbstractTransactionForTest() { tx.inStates.addAll(inStates) tx.outStates.addAll(outStates) tx.commands.addAll(commands) + + tx.signers.addAll(tx.inStates.map { it.notary.owningKey }) + tx.signers.addAll(commands.flatMap { it.signers }) return tx.body() } @@ -246,11 +254,11 @@ class TransactionGroupDSL(private val stateType: Class) { fun input(label: String) { val notaryKey = label.output.notary.owningKey - if (commands.none { it.signers.contains(notaryKey) }) commands.add(Command(NotaryCommand(), notaryKey)) + signers.add(notaryKey) inStates.add(label.outputRef) } - fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, type) + fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, signers.toList(), type) } val String.output: TransactionState @@ -290,7 +298,7 @@ class TransactionGroupDSL(private val stateType: Class) { inner class Roots { fun transaction(vararg outputStates: LabeledOutput) { val outs = outputStates.map { it.state } - val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), TransactionType.Business()) + val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), emptyList(), TransactionType.Business()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! labelToRefs[label] = StateRef(wtx.id, index) @@ -370,7 +378,7 @@ class TransactionGroupDSL(private val stateType: Class) { fun signAll(txnsToSign: List = txns, vararg extraKeys: KeyPair): List { return txnsToSign.map { wtx -> - val allPubKeys = wtx.commands.flatMap { it.signers }.toMutableSet() + val allPubKeys = wtx.signers.toMutableSet() val bits = wtx.serialize() require(bits == wtx.serialized) val sigs = ArrayList() diff --git a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt index 49eed3cb0b..6345e4ecbb 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt @@ -163,7 +163,7 @@ object NotaryProtocol { private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) { try { - uniquenessProvider.commit(tx, reqIdentity) + uniquenessProvider.commit(tx.inputs, tx.id, reqIdentity) } catch (e: UniquenessException) { val conflictData = e.error.serialize() val signedConflict = SignedData(conflictData, sign(conflictData)) diff --git a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt index 142d4cea38..1965a93a8b 100644 --- a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt +++ b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt @@ -73,8 +73,7 @@ object NotaryChangeProtocol { val state = originalState.state val newState = state.withNewNotary(newNotary) val participants = state.data.participants - val cmd = Command(ChangeNotary(), participants) - val tx = TransactionBuilder(type = TransactionType.NotaryChange()).withItems(originalState, newState, cmd) + val tx = TransactionType.NotaryChange.Builder().withItems(originalState, newState) tx.signWith(serviceHub.storageService.myLegalIdentityKey) val stx = tx.toSignedTransaction(false) @@ -161,18 +160,16 @@ object NotaryChangeProtocol { @Suspendable private fun validateTx(stx: SignedTransaction): SignedTransaction { + checkMySignatureRequired(stx.tx) checkDependenciesValid(stx) checkValid(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" } + private fun checkMySignatureRequired(tx: WireTransaction) { + // TODO: use keys from the keyManagementService instead + val myKey = serviceHub.storageService.myLegalIdentity.owningKey + require(tx.signers.contains(myKey)) { "Party is not a participant for any of the input states of transaction ${tx.id}" } } @Suspendable diff --git a/core/src/main/resources/com/r3corda/core/node/isolated.jar b/core/src/main/resources/com/r3corda/core/node/isolated.jar index 315a6e0989c20f20a270b7b9e002376eadae97c6..4ec15af9b05c7a0cb7b93001cdebe1edea4e4c2a 100644 GIT binary patch delta 1817 zcmV+!2j=*RL5@MNRStjaR~yF_|BVD%A!`e7@W6Q0}BQCOqUm_$b zY1&~mMhmN5b!UW4UvZs2lD_+;e?UL=Lwou-E%el9@yrscWH=a!PDEj@=TQ8ItWT~C#TKUJ$#Rwu*H z1{gzXiLY?e;qIb&cVS6rZj|B8?Qp)>g1Nc9oniO|LDwq@bK5x++*&Sqo^QZlh^+|U zez?kTC6nLdkXfI*l2*3o-t_ziQuf>&>6Z8WMRQ501)tllG`UN*aV;s$JD$2zbDS(g zL_Llp$?*EJryPIVHJ4T@rtK=>yWBBz1V>wKOBy)FaLOtRYxy4cc}0*!PKGcHXEXVo z{>g6P6g12tX_Z6@ClWZ0G((#z+mhjO{tzJ{#V;MgEeu_bSmaKF@jyHjzHlwUFwg?d zU#%+7EC{RSi)k*)w7+LjsQfMvgPC*t0ZHN%x;qfTs|GvxPl=r;N6%X}HAF_ORl zMj1v90`K4&71zrZ znBs~XxXy6?AQfbQax6||9eiQv-=m7_X^_MhF`j?G4NNe+wx_6pDTX9r-+km#1H3H= z|5d^df+%%Mu+^%0ueRXW7JVe-oA^=!?_!!^>Lq#GG&#E~T-}4W61alf3>}YbRn93< zks5sd#fE**R*}RE?&$ouOHD)S0-~bWCq}*A7)B4)?HGI6 ziU5DzVO0?pIq7(Sj&Lp)$9;zGuyk}GO>(K;eQb%Uw!^@e89s9966~{r3$|}T&`PR4 z<@LFqI}&_Sw5^ZM91YJLZ5}t;sEpB8l`$G95yyuNCo`?@)3f*}fq8tD;TYxF%vR@6 z3FVUDSkQgbVi?-L*$K4iqH9t0JFhbXim^3Cv7J?NhHOZhz=_13wH=f)~M7krJzj%X21G@2~Nm76D z##=)u;6^sN(e8~WW04DtevxaDQ{5Bo&wm|@q@uU_Qt{J~3ms#zRD$RcqDi7hi5??* ze7fsaDwPU3Cy1s4&MUO)B+f~qUDI6;I#X@5YX9K*ui6M#Yje6S9!nX|e-rD`_KY#S zwVyP~wrmBv&%7jW9(YnAFUK2q2ep5ou)Mn(C8aOus_lr|LA)5Q|37z=f$!4*+_FfQ z6?OFoHCNdcn&Vb%X)icpd~@2S5oVGMXGrHgi#v1Nw>e47>IaR0dD|8Bk_|ZnJF%J* z_cqX@WUsY6!>i%8M8T{1mbhu_mt@YRA#T#)QVNpo2_ffRP>$Mngo-_a7~+5A&$J#+ ziGE5s93y&!XgkpkBuM5nddm`x5H*I=NB@df{z%IRenii%IvbrhsVjh1N#Zg5IE0F! zA3xE27UGgxI31#zx?=QMLw`o^ADWoSIy1ye!^I*C{!%yCI^J&oJKBoTp0+}fK3yUCcZ%!C zZs7C7YnWU^ZXNl;8t!c>_ck5WPt(bLJRlF!kxkQ!IDXO4}(wvy~F7u*)?4J7mhx|d@=o%b$oyApBorlL+Q8l zJ-Q_#y-l@xNma2eBE$G;ef^$+^E;zM!xugEu@i^})^rZ7Xhzb5fS;*3N=Vpih268#c$ z67Nggmk<&i0W&GlCGk|^6N&Hr4^T@72>2P#b*}~h051@ee*zq{Cl!4O3HTY$b*}~h z056lR9)SW96_ZgNHIrTy3X^XiF#&s%p&v&9$&;ZU6qEfQI0GCQ0F&Vt7n4sQGzPyK H00000PYr6c delta 1807 zcmV+q2k`igL5V@IRStjKR~yF}|BVD%A=+H(AoocvKa{bsci7YkcB9JJr= zH#5KaZoirD$DjWFUjU={g5g=iDd{df-2&GQ$M#%qcv3f|V{v~^6!bCM@ruHotdvTt z6Tz?s#*kRzD_pm@y{KPVSQ1JbW_Wr#oGZ3qZZ27*(Nrbak(u?nvR`5K1+sW9d}V*65fK#OAvFu13+#B zj}eC9Jq3SO!%2p(0*+KX)6%o1tTAK{-_310vszZA(TLEK;s&o+9yu;Ow_nU$j5B{c@xZAXCK=*{edVT2$K54C_>U95 zpGB$80MlF5uT~Z;)1W~@zJOO_cm-1olMgA>y36TZ;i?h57{eJ{VraW*dc}+YK( z-+w&s^=yb^8kbdlT%l7!D#j2Cn#8CQjA3MN#~Z`G5??fp%aziCa2Z&Jq30p#q72ZD zRTh5%tI5Va90A8OQCwr_45*`sG{L3!$}K~bl^+^@$?%pp8u) zV-Nd79xdyoJx78)N9x;+)R-~Sh#4ck5mCIsa5&ZY_B@8SVwlI<3 zr&D9%$D*KOF+x2{6s3l&-W^wiLwANO^3l_07+!7^yk_joO^p!%8Tl)?F_b~nIaE7F zr-jE0ihtBHl~%&kOj1V}TJYaMrAZEj3a1!;*tmTm7U~Rb{-QM*_T|Ryx=2K8U$uWp zzBaU}4R>zb9u1vj^b4H}9qAlzz4zBhC=tHcn}{9_oopM8Bw{4@k&KhvPx1iCgHs(B z6NyBiIYct)YaXMwcG4Us*)i2|y*<%FZ>@9pzHA{}WzEr+Xe6QC`&*<-`7_G!OfzMa zZg>>z-rmxDeb224MLAl#JLm`s=-q!&qm;Ul%cdnR`T1fv|8wpp4Zo#!w;@TFJv!B| zS8UHLQRB8^N^`*yW1GD-^(+(QI72e)7~GoWuE}Y}jCxSdmo;rsrEH+---%W4ueX8j zCx4CQX44iVWyh#*SwOsaowl;o#W z!x57ENVbw}LyYG9p1#r~LnO7KMzm5*702Hf6$T-ceUj5G;)TfzmQ)?dIK*G ztzlvfnRR4yYq+{;+jF!}AD+iGT&Doij&;xH(1V%;%GDuH&8mZD4<34TZnccz8=f zy6a|jlc{1`LWc0?>icUNPV7t$^Y;ss;c68M6-+nh6OX&MVof1^@sVld>Lx0}T}b xlkox|lW-mslWHG30d|w7A4dVilkOi(0v8yQ?;jYGP#7JPaUd54x*7lg0063vW*z_l diff --git a/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt b/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt index faa447b7b2..4b9192444b 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt @@ -2,8 +2,8 @@ package com.r3corda.node.services.transactions import com.r3corda.core.ThreadBox import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.UniquenessException import com.r3corda.core.node.services.UniquenessProvider import java.util.* @@ -15,12 +15,10 @@ class InMemoryUniquenessProvider() : UniquenessProvider { /** For each input state store the consuming transaction information */ private val committedStates = ThreadBox(HashMap()) - // TODO: the uniqueness provider shouldn't be able to see all tx outputs and commands - override fun commit(tx: WireTransaction, callerIdentity: Party) { - val inputStates = tx.inputs + override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { committedStates.locked { val conflictingStates = LinkedHashMap() - for (inputState in inputStates) { + for (inputState in states) { val consumingTx = get(inputState) if (consumingTx != null) conflictingStates[inputState] = consumingTx } @@ -28,8 +26,8 @@ class InMemoryUniquenessProvider() : UniquenessProvider { val conflict = UniquenessProvider.Conflict(conflictingStates) throw UniquenessException(conflict) } else { - inputStates.forEachIndexed { i, stateRef -> - put(stateRef, UniquenessProvider.ConsumingTx(tx.id, i, callerIdentity)) + states.forEachIndexed { i, stateRef -> + put(stateRef, UniquenessProvider.ConsumingTx(txId, i, callerIdentity)) } } diff --git a/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt b/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt index f3c8197488..4146bee109 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt @@ -1,8 +1,6 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.TransactionType -import com.r3corda.core.contracts.WireTransaction +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.UniquenessException import com.r3corda.core.testing.MEGA_CORP import com.r3corda.core.testing.generateStateRef @@ -13,30 +11,27 @@ import kotlin.test.assertFailsWith class UniquenessProviderTests { val identity = MEGA_CORP + val txID = SecureHash.randomSHA256() @Test fun `should commit a transaction with unused inputs without exception`() { val provider = InMemoryUniquenessProvider() val inputState = generateStateRef() - val tx = buildTransaction(inputState) - provider.commit(tx, identity) + provider.commit(listOf(inputState), txID, identity) } @Test fun `should report a conflict for a transaction with previously used inputs`() { val provider = InMemoryUniquenessProvider() val inputState = generateStateRef() - val tx1 = buildTransaction(inputState) - provider.commit(tx1, identity) + val inputs = listOf(inputState) + provider.commit(inputs, txID, identity) - val tx2 = buildTransaction(inputState) - val ex = assertFailsWith { provider.commit(tx2, identity) } + val ex = assertFailsWith { provider.commit(inputs, txID, identity) } val consumingTx = ex.error.stateHistory[inputState]!! - assertEquals(consumingTx.id, tx1.id) - assertEquals(consumingTx.inputIndex, tx1.inputs.indexOf(inputState)) + assertEquals(consumingTx.id, txID) + assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState)) assertEquals(consumingTx.requestingParty, identity) } - - private fun buildTransaction(inputState: StateRef) = WireTransaction(listOf(inputState), emptyList(), emptyList(), emptyList(), TransactionType.Business()) } \ No newline at end of file From 0a5b7ace35f23a82e3dff81b9e42c758bb1f7a5f Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 14 Jun 2016 11:47:13 +0100 Subject: [PATCH 4/5] Made TransactionBuilder abstract. Every transaction type now needs to implement and provide its own Builder. This is required since for specific types we need to run different logic when adding new items to the transaction. For example, when adding a new input state to a transaction of type NotaryChange we need to add all of the states participants to the signers list. --- .../r3corda/contracts/AnotherDummyContract.kt | 4 ++-- .../contracts/JavaCommercialPaper.java | 2 +- .../com/r3corda/contracts/CommercialPaper.kt | 2 +- .../main/kotlin/com/r3corda/contracts/IRS.kt | 10 ++++----- .../protocols/TwoPartyTradeProtocol.kt | 4 ++-- .../r3corda/contracts/CommercialPaperTests.kt | 6 +++--- .../kotlin/com/r3corda/contracts/IRSTests.kt | 2 +- .../com/r3corda/contracts/cash/CashTests.kt | 12 +++++------ .../r3corda/core/contracts/DummyContract.kt | 4 ++-- .../core/contracts/TransactionBuilder.kt | 21 +++++++++++++------ .../core/contracts/TransactionTypes.kt | 9 +++++--- .../com/r3corda/core/testing/TestUtils.kt | 4 ++-- .../r3corda/protocols/TwoPartyDealProtocol.kt | 3 +-- .../core/contracts/TransactionGroupTests.kt | 4 ++-- .../core/node/AttachmentClassLoaderTests.kt | 4 ++-- .../TransactionSerializationTests.kt | 6 +++--- .../node/services/NodeInterestRatesTest.kt | 6 +++--- .../node/services/NodeWalletServiceTest.kt | 8 +++---- .../node/services/NotaryServiceTests.kt | 12 +++++------ .../services/ValidatingNotaryServiceTests.kt | 4 ++-- .../kotlin/node/services/NotaryChangeTests.kt | 7 +++++-- .../kotlin/com/r3corda/demos/RateFixDemo.kt | 2 +- .../kotlin/com/r3corda/demos/TraderDemo.kt | 2 +- 23 files changed, 76 insertions(+), 62 deletions(-) diff --git a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt index 35b88403b4..d2195e3b25 100644 --- a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt +++ b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt @@ -36,8 +36,8 @@ class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdo override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") override fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = TransactionState(State(magicNumber), notary) - return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + val state = State(magicNumber) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) } override fun inspectState(state: ContractState): Int = (state as State).magicNumber diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 43b31cef30..690111d5a2 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -232,7 +232,7 @@ public class JavaCommercialPaper implements Contract { public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); TransactionState output = new TransactionState<>(state, notary); - return new TransactionBuilder().withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); + return new TransactionType.General.Builder().withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index dd0c665bb7..ca270e3469 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -139,7 +139,7 @@ class CommercialPaper : Contract { fun generateIssue(faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { val issuance = faceValue.token.issuer val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary) - return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } /** diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 922b143e2c..2169cb02a1 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -717,8 +717,8 @@ class InterestRateSwap() : Contract { val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule) // Put all the above into a new State object. - val state = TransactionState(State(fixedLeg, floatingLeg, newCalculation, common), notary) - return TransactionBuilder().withItems(state, Command(Commands.Agree(), listOf(state.data.floatingLeg.floatingRatePayer.owningKey, state.data.fixedLeg.fixedRatePayer.owningKey))) + val state = State(fixedLeg, floatingLeg, newCalculation, common) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) } private fun calcFixingDate(date: LocalDate, fixingPeriod: DateOffset, calendar: BusinessCalendar): LocalDate { @@ -733,9 +733,9 @@ class InterestRateSwap() : Contract { fun generateFix(tx: TransactionBuilder, irs: StateAndRef, fixing: Pair) { tx.addInputState(irs) tx.addOutputState( - TransactionState( - irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.first, FixedRate(fixing.second))), - irs.state.notary)) + irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.first, FixedRate(fixing.second))), + irs.state.notary + ) tx.addCommand(Commands.Fix(), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey)) } } diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index b91de13496..b681362336 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -266,7 +266,7 @@ object TwoPartyTradeProtocol { } private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() // Add input and output states for the movement of cash, by using the Cash contract to generate the states. val wallet = serviceHub.walletService.currentWallet val cashStates = wallet.statesOfType() @@ -279,7 +279,7 @@ object TwoPartyTradeProtocol { // initial seed in order to provide privacy protection. val freshKey = serviceHub.keyManagementService.freshKey() val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public) - ptx.addOutputState(TransactionState(state, tradeRequest.assetForSale.state.notary)) + ptx.addOutputState(state, tradeRequest.assetForSale.state.notary) ptx.addCommand(command, tradeRequest.assetForSale.state.data.owner) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index ac32ef7a06..69fe21d40c 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -140,7 +140,7 @@ class CommercialPaperTestsGeneric { } fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { - val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.Business()) + val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.General()) return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } @@ -165,7 +165,7 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to MiniCorp to own some of their debt. val moveTX: LedgerTransaction = run { - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY) ptx.signWith(MINI_CORP_KEY) @@ -181,7 +181,7 @@ class CommercialPaperTestsGeneric { ) fun makeRedeemTX(time: Instant): LedgerTransaction { - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() ptx.setTime(time, DUMMY_NOTARY, 30.seconds) CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet) ptx.signWith(ALICE_KEY) diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index abf8f02199..77cc263fa8 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -311,7 +311,7 @@ class IRSTests { val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n") var fixTX: LedgerTransaction = run { - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() val fixing = Pair(nextFixingDate, FixedRate("0.052".percent)) InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing) with(tx) { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt index 81406f338a..163a1378db 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt @@ -97,7 +97,7 @@ class CashTests { } // Test generation works. - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(ptx.inputStates().isEmpty()) val s = ptx.outputStates()[0].data as Cash.State @@ -109,7 +109,7 @@ class CashTests { // Test issuance from the issuance definition val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) - val templatePtx = TransactionBuilder() + val templatePtx = TransactionType.General.Builder() Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(templatePtx.inputStates().isEmpty()) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) @@ -176,14 +176,14 @@ class CashTests { @Test(expected = IllegalStateException::class) fun `reject issuance with inputs`() { // Issue some cash - var ptx = TransactionBuilder() + var ptx = TransactionType.General.Builder() Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) ptx.signWith(MINI_CORP_KEY) val tx = ptx.toSignedTransaction() // Include the previously issued cash in a new issuance command - ptx = TransactionBuilder() + ptx = TransactionType.General.Builder() ptx.addInputState(tx.tx.outRef(0)) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) } @@ -386,7 +386,7 @@ class CashTests { ) fun makeSpend(amount: Amount, dest: PublicKey, corp: Party, depositRef: OpaqueBytes = defaultRef): WireTransaction { - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() Cash().generateSpend(tx, amount, dest, WALLET) return tx.toWireTransaction() } @@ -401,7 +401,7 @@ class CashTests { @Test fun generateSimpleSpendWithParties() { - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP)) assertEquals(WALLET[2].ref, tx.inputStates()[0]) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt index 0205ea5695..41038e128b 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt @@ -43,7 +43,7 @@ class DummyContract : Contract { override val legalContractReference: SecureHash = SecureHash.sha256("") fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = TransactionState(SingleOwnerState(magicNumber, owner.party.owningKey), notary) - return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + val state = SingleOwnerState(magicNumber, owner.party.owningKey) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 049c6d0469..1629e643a8 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -14,12 +14,13 @@ import java.util.* * Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from * multiple parties. */ -open class TransactionBuilder(protected val inputs: MutableList = arrayListOf(), - protected val attachments: MutableList = arrayListOf(), - protected val outputs: MutableList> = arrayListOf(), - protected val commands: MutableList = arrayListOf(), - protected val signers: MutableSet = mutableSetOf(), - protected val type: TransactionType = TransactionType.Business()) { +abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(), + protected val notary: Party? = null) { + protected val inputs: MutableList = arrayListOf() + protected val attachments: MutableList = arrayListOf() + protected val outputs: MutableList> = arrayListOf() + protected val commands: MutableList = arrayListOf() + protected val signers: MutableSet = mutableSetOf() val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() @@ -47,6 +48,7 @@ open class TransactionBuilder(protected val inputs: MutableList = arra when (t) { is StateAndRef<*> -> addInputState(t) is TransactionState<*> -> addOutputState(t) + is ContractState -> addOutputState(t) is Command -> addCommand(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } @@ -123,6 +125,13 @@ open class TransactionBuilder(protected val inputs: MutableList = arra outputs.add(state) } + fun addOutputState(state: ContractState, notary: Party) = addOutputState(TransactionState(state, notary)) + + fun addOutputState(state: ContractState) { + checkNotNull(notary) { "Need to specify a Notary for the state, or set a default one on TransactionBuilder initialisation" } + addOutputState(state, notary!!) + } + fun addCommand(arg: Command) { check(currentSigs.isEmpty()) // TODO: replace pubkeys in commands with 'pointers' to keys in signers diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt index bc427f9a41..ef67030124 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -1,5 +1,6 @@ package com.r3corda.core.contracts +import com.r3corda.core.crypto.Party import com.r3corda.core.noneOrSingle import java.security.PublicKey @@ -45,8 +46,10 @@ sealed class TransactionType { /** Implement type specific transaction validation logic */ abstract fun verifyTransaction(tx: TransactionForVerification) - /** A general type used for business transactions, where transaction validity is determined by custom contract code */ - class Business : TransactionType() { + /** A general transaction type where transaction validity is determined by custom contract code */ + class General : TransactionType() { + class Builder(notary: Party? = null) : TransactionBuilder(General(), notary) {} + /** * Check the transaction is contract-valid by running the verify() for each input and output state contract. * If any contract fails to verify, the whole transaction is considered to be invalid @@ -80,7 +83,7 @@ sealed class TransactionType { * A transaction builder that automatically sets the transaction type to [NotaryChange] * and adds the list of participants to the signers set for every input state. */ - class Builder() : TransactionBuilder(type = NotaryChange()) { + class Builder(notary: Party? = null) : TransactionBuilder(NotaryChange(), notary) { override fun addInputState(stateAndRef: StateAndRef<*>) { signers.addAll(stateAndRef.state.data.participants) super.addInputState(stateAndRef) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index b9faeb3fb9..09ac70c4c5 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -109,7 +109,7 @@ abstract class AbstractTransactionForTest { protected val outStates = ArrayList() protected val commands = ArrayList() protected val signers = LinkedHashSet() - protected val type = TransactionType.Business() + protected val type = TransactionType.General() open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) } @@ -298,7 +298,7 @@ class TransactionGroupDSL(private val stateType: Class) { inner class Roots { fun transaction(vararg outputStates: LabeledOutput) { val outs = outputStates.map { it.state } - val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), emptyList(), TransactionType.Business()) + val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), emptyList(), TransactionType.General()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! labelToRefs[label] = StateRef(wtx.id, index) diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index 10eeb69d4a..34b83f33f3 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -367,9 +367,8 @@ object TwoPartyDealProtocol { @Suppress("UNCHECKED_CAST") val newDeal = deal - val oldRef = dealToFix.ref - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapCache.ratesOracleNodes[0], fixOf, BigDecimal.ZERO, BigDecimal.ONE) { @Suspendable override fun beforeSigning(fix: Fix) { diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt index 4143aaffcd..84f595dc52 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt @@ -7,7 +7,7 @@ import com.r3corda.core.testing.* import org.junit.Test import java.security.PublicKey import java.security.SecureRandom -import java.util.Currency +import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals @@ -122,7 +122,7 @@ class TransactionGroupTests { // We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that // points nowhere. val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef()) - tg.txns += TransactionBuilder().apply { + tg.txns += TransactionType.General.Builder().apply { addInputState(input) addOutputState(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY) addCommand(TestCash.Commands.Move(), BOB_PUBKEY) diff --git a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt index 4d390a9476..a375fcebfb 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt @@ -51,8 +51,8 @@ class AttachmentClassLoaderTests { override val legalContractReference: SecureHash = SecureHash.sha256("") fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = TransactionState(State(magicNumber), notary) - return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + val state = State(magicNumber) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) } } diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index b605018e99..23cf58b655 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -10,7 +10,7 @@ import org.junit.Test import java.security.PublicKey import java.security.SecureRandom import java.security.SignatureException -import java.util.Currency +import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -53,7 +53,7 @@ class TransactionSerializationTests { @Before fun setup() { - tx = TransactionBuilder().withItems( + tx = TransactionType.General.Builder().withItems( inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) ) } @@ -92,7 +92,7 @@ class TransactionSerializationTests { // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { - val tx2 = TransactionBuilder().withItems(inputState, outputState, changeState, + val tx2 = TransactionType.General.Builder().withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), TestUtils.keypair2.public)) tx2.signWith(DUMMY_NOTARY_KEY) tx2.signWith(TestUtils.keypair2) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt index 77561353e7..4062170da1 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt @@ -8,9 +8,9 @@ import com.r3corda.contracts.testing.`with notary` import com.r3corda.core.bd import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.Fix -import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.generateKeyPair +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.testing.ALICE_PUBKEY import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.MEGA_CORP @@ -104,7 +104,7 @@ class NodeInterestRatesTest { val (n1, n2) = net.createTwoNodes() n2.interestRatesService.oracle.knownFixes = TEST_DATA - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val protocol = RatesFixProtocol(tx, n2.info, fixOf, "0.675".bd, "0.1".bd) BriefLogFormatter.initVerbose("rates") @@ -119,5 +119,5 @@ class NodeInterestRatesTest { assertEquals("0.678".bd, fix.value) } - private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY)) + private fun makeTX() = TransactionType.General.Builder().withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY) } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt index af37073722..a64927640d 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt @@ -3,7 +3,7 @@ package com.r3corda.node.services import com.r3corda.contracts.cash.Cash import com.r3corda.core.contracts.`issued by` import com.r3corda.core.contracts.DOLLARS -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.contracts.USD import com.r3corda.core.contracts.verifyToLedgerTransaction import com.r3corda.core.node.ServiceHub @@ -67,21 +67,21 @@ class NodeWalletServiceTest { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() - val usefulTX = TransactionBuilder().apply { + val usefulTX = TransactionType.General.Builder().apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef(0) // A tx that spends our money. - val spendTX = TransactionBuilder().apply { + val spendTX = TransactionType.General.Builder().apply { Cash().generateSpend(this, 80.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_PUBKEY, listOf(myOutput)) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() // A tx that doesn't send us anything. - val irrelevantTX = TransactionBuilder().apply { + val irrelevantTX = TransactionType.General.Builder().apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt index 47b3f3aeae..559df095a7 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt @@ -1,7 +1,7 @@ package com.r3corda.node.services import com.r3corda.core.contracts.TimestampCommand -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.seconds import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY @@ -38,7 +38,7 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction with a valid timestamp`() { val inputState = issueState(clientNode) - val tx = TransactionBuilder().withItems(inputState) + val tx = TransactionType.General.Builder().withItems(inputState) tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds) val wtx = tx.toWireTransaction() @@ -52,7 +52,7 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction without a timestamp`() { val inputState = issueState(clientNode) - val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() + val wtx = TransactionType.General.Builder().withItems(inputState).toWireTransaction() val protocol = NotaryProtocol.Client(wtx) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) @@ -64,7 +64,7 @@ class NotaryServiceTests { @Test fun `should report error for transaction with an invalid timestamp`() { val inputState = issueState(clientNode) - val tx = TransactionBuilder().withItems(inputState) + val tx = TransactionType.General.Builder().withItems(inputState) tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds) val wtx = tx.toWireTransaction() @@ -79,7 +79,7 @@ class NotaryServiceTests { @Test fun `should report error for transaction with more than one timestamp`() { val inputState = issueState(clientNode) - val tx = TransactionBuilder().withItems(inputState) + val tx = TransactionType.General.Builder().withItems(inputState) val timestamp = TimestampCommand(Instant.now(), 30.seconds) tx.addCommand(timestamp, DUMMY_NOTARY.owningKey) tx.addCommand(timestamp, DUMMY_NOTARY.owningKey) @@ -96,7 +96,7 @@ class NotaryServiceTests { @Test fun `should report conflict for a duplicate transaction`() { val inputState = issueState(clientNode) - val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() + val wtx = TransactionType.General.Builder().withItems(inputState).toWireTransaction() val firstSpend = NotaryProtocol.Client(wtx) val secondSpend = NotaryProtocol.Client(wtx) diff --git a/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt index 9897bf2972..9906a0128a 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt @@ -1,6 +1,6 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.node.internal.testing.MockNetwork @@ -34,7 +34,7 @@ class ValidatingNotaryServiceTests { @Test fun `should report error for invalid transaction dependency`() { val inputState = issueInvalidState(clientNode) - val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() + val wtx = TransactionType.General.Builder().withItems(inputState).toWireTransaction() val protocol = NotaryProtocol.Client(wtx) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) diff --git a/node/src/test/kotlin/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/node/services/NotaryChangeTests.kt index c76e176ba9..f44a778ee5 100644 --- a/node/src/test/kotlin/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/node/services/NotaryChangeTests.kt @@ -1,7 +1,10 @@ package node.services import com.r3corda.contracts.DummyContract -import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.StateAndRef +import com.r3corda.core.contracts.StateRef +import com.r3corda.core.contracts.TransactionState +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.node.internal.testing.MockNetwork @@ -52,7 +55,7 @@ class NotaryChangeTests { val state = TransactionState(DummyContract.MultiOwnerState(0, listOf(clientNodeA.info.identity.owningKey, clientNodeB.info.identity.owningKey)), DUMMY_NOTARY) - val tx = TransactionBuilder(type = TransactionType.NotaryChange()).withItems(state) + val tx = TransactionType.NotaryChange.Builder().withItems(state) tx.signWith(clientNodeA.storage.myLegalIdentityKey) tx.signWith(clientNodeB.storage.myLegalIdentityKey) tx.signWith(DUMMY_NOTARY_KEY) diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index a7916282cd..e8d981e047 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -83,7 +83,7 @@ fun main(args: Array) { val notary = node.services.networkMapCache.notaryNodes[0] // Make a garbage transaction that includes a rate fix. - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() tx.addOutputState(TransactionState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public), notary.identity)) val protocol = RatesFixProtocol(tx, oracleNode, fixOf, expectedRate, rateTolerance) node.smm.add("demo.ratefix", protocol).get() diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index fb842a7e7d..f4e65d2e16 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -369,7 +369,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, // Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works. val move: SignedTransaction = run { - val builder = TransactionBuilder() + val builder = TransactionType.General.Builder() CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) builder.signWith(keyPair) builder.addSignatureUnchecked(subProtocol(NotaryProtocol.Client(builder.toWireTransaction()))) From 30ca340b6e54cfb17f77fabee1a939308a9d79eb Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 14 Jun 2016 19:06:44 +0100 Subject: [PATCH 5/5] 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. --- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 2 +- .../com/r3corda/core/contracts/Structures.kt | 13 +- .../core/contracts/TransactionBuilder.kt | 8 +- .../core/contracts/TransactionTypes.kt | 3 +- .../kotlin/protocols/NotaryChangeProtocol.kt | 112 ++++++++++++++---- .../com/r3corda/core/node/isolated.jar | Bin 8334 -> 8270 bytes .../contracts/TransactionGraphSearchTests.kt | 8 +- .../r3corda/core/node/WalletUpdateTests.kt | 16 +-- .../node/internal/testing/TestUtils.kt | 20 +++- .../node/internal/testing/WalletFiller.kt | 4 +- .../node/services/NotaryChangeService.kt | 38 +----- .../kotlin/node/services/NotaryChangeTests.kt | 65 +++++----- 12 files changed, 182 insertions(+), 107 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index be46a304b1..addf838088 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -164,7 +164,7 @@ class Cash : FungibleAsset() { throw InsufficientBalanceException(amount - gatheredAmount) val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.state.issuanceDef) + Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) } else { null } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index f4ad39066f..4be3e957b9 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -27,7 +27,17 @@ interface ContractState { /** Contract by which the state belongs */ val contract: Contract - /** List of public keys for each party that can consume this state in a valid transaction. */ + /** + * A _participant_ is any party that is able to consume this state in a valid transaction. + * + * The list of participants is required for certain types of transactions. For example, when changing the notary + * for this state ([TransactionType.NotaryChange]), every participants has to be involved and approve the transaction + * so that they receive the updated state, and don't end up in a situation where they can no longer use a state + * they possess, since someone consumed that state during the notary change process. + * + * The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants + * list should just contain the owner. + */ val participants: List } @@ -172,6 +182,7 @@ data class Command(val value: CommandData, val signers: List) { 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 } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 1629e643a8..b66803898f 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -10,9 +10,11 @@ import java.util.* /** * A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is - * intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set. - * Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from - * multiple parties. + * intended to be passed around contracts that may edit it by adding new states/commands. Then once the states + * and commands are right, this class can be used as a holding bucket to gather signatures from multiple parties. + * + * The builder can be customised for specific transaction types, e.g. where additional processing is needed + * before adding a state/command. */ abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(), protected val notary: Party? = null) { diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt index ef67030124..4216c8bf68 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -4,7 +4,7 @@ import com.r3corda.core.crypto.Party import com.r3corda.core.noneOrSingle import java.security.PublicKey -/** Defines transaction validation rules for a specific transaction type */ +/** Defines transaction build & validation logic for a specific transaction type */ sealed class TransactionType { override fun equals(other: Any?) = other?.javaClass == javaClass override fun hashCode() = javaClass.name.hashCode() @@ -48,6 +48,7 @@ sealed class TransactionType { /** A general transaction type where transaction validity is determined by custom contract code */ class General : TransactionType() { + /** Just uses the default [TransactionBuilder] with no special logic */ class Builder(notary: Party? = null) : TransactionBuilder(General(), notary) {} /** diff --git a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt index 1965a93a8b..9acebf7aa6 100644 --- a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt +++ b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt @@ -30,10 +30,9 @@ object NotaryChangeProtocol { data class Proposal(val stateRef: StateRef, val newNotary: Party, - val sessionIdForSend: Long, - val sessionIdForReceive: Long) + val stx: SignedTransaction) - class Handshake(val payload: Proposal, + class Handshake(val sessionIdForSend: Long, replyTo: SingleMessageRecipient, replySessionId: Long) : AbstractRequestMessage(replyTo, replySessionId) @@ -102,18 +101,21 @@ object NotaryChangeProtocol { @Suspendable private fun getParticipantSignature(node: NodeInfo, stx: SignedTransaction, sessionIdForSend: Long): DigitalSignature.WithKey { val sessionIdForReceive = random63BitValue() - val proposal = Proposal(originalState.ref, newNotary, sessionIdForSend, sessionIdForReceive) + val proposal = Proposal(originalState.ref, newNotary, stx) - val handshake = Handshake(proposal, serviceHub.networkService.myAddress, sessionIdForReceive) - val protocolInitiated = sendAndReceive(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake).validate { it } - if (!protocolInitiated) throw Refused(node.identity, originalState) + val handshake = Handshake(sessionIdForSend, serviceHub.networkService.myAddress, sessionIdForReceive) + sendAndReceive(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake) - val response = sendAndReceive(TOPIC_CHANGE, node.address, sessionIdForSend, sessionIdForReceive, stx) + val response = sendAndReceive(TOPIC_CHANGE, node.address, sessionIdForSend, sessionIdForReceive, proposal) val participantSignature = response.validate { - check(it.by == node.identity.owningKey) { "Not signed by the required participant" } - it.verifyWithECDSA(stx.txBits) - it + if (it.sig == null) throw NotaryChangeException(it.error!!) + else { + check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" } + it.sig.verifyWithECDSA(stx.txBits) + it.sig + } } + return participantSignature } @@ -132,38 +134,88 @@ object NotaryChangeProtocol { companion object { object VERIFYING : ProgressTracker.Step("Verifying Notary change proposal") - object SIGNING : ProgressTracker.Step("Signing Notary change transaction") + object APPROVING : ProgressTracker.Step("Notary change approved") - fun tracker() = ProgressTracker(VERIFYING, SIGNING) + object REJECTING : ProgressTracker.Step("Notary change rejected") + + fun tracker() = ProgressTracker(VERIFYING, APPROVING, REJECTING) } @Suspendable override fun call() { progressTracker.currentStep = VERIFYING + val proposal = receive(TOPIC_CHANGE, sessionIdForReceive).validate { it } - val proposedTx = receive(TOPIC_CHANGE, sessionIdForReceive).validate { validateTx(it) } + try { + verifyProposal(proposal) + verifyTx(proposal.stx) + } catch(e: Exception) { + // TODO: catch only specific exceptions. However, there are numerous validation exceptions + // that might occur (tx validation/resolution, invalid proposal). Need to rethink how + // we manage exceptions and maybe introduce some platform exception hierarchy + val myIdentity = serviceHub.storageService.myLegalIdentity + val state = proposal.stateRef + val reason = NotaryChangeRefused(myIdentity, state, e.message) - progressTracker.currentStep = SIGNING + reject(reason) + return + } - val mySignature = sign(proposedTx) - val swapSignatures = sendAndReceive>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, mySignature) + approve(proposal.stx) + } + + @Suspendable + private fun approve(stx: SignedTransaction) { + progressTracker.currentStep = APPROVING + + val mySignature = sign(stx) + val response = Result.noError(mySignature) + val swapSignatures = sendAndReceive>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, response) val allSignatures = swapSignatures.validate { signatures -> - signatures.forEach { it.verifyWithECDSA(proposedTx.txBits) } + signatures.forEach { it.verifyWithECDSA(stx.txBits) } signatures } - val finalTx = proposedTx + allSignatures + val finalTx = stx + allSignatures finalTx.verify() serviceHub.recordTransactions(listOf(finalTx)) } @Suspendable - private fun validateTx(stx: SignedTransaction): SignedTransaction { + private fun reject(e: NotaryChangeRefused) { + progressTracker.currentStep = REJECTING + val response = Result.withError(e) + send(TOPIC_CHANGE, otherSide, sessionIdForSend, response) + } + + /** + * Check the notary change proposal. + * + * For example, if the proposed new notary has the same behaviour (e.g. both are non-validating) + * and is also in a geographically convenient location we can just automatically approve the change. + * TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal + */ + @Suspendable + private fun verifyProposal(proposal: NotaryChangeProtocol.Proposal) { + val newNotary = proposal.newNotary + val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary } + require(isNotary) { "The proposed node $newNotary does not run a Notary service " } + + val state = proposal.stateRef + val proposedTx = proposal.stx.tx + require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" } + + // An example requirement + val blacklist = listOf("Evil Notary") + require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" } + } + + @Suspendable + private fun verifyTx(stx: SignedTransaction) { checkMySignatureRequired(stx.tx) checkDependenciesValid(stx) checkValid(stx) - return stx } private fun checkMySignatureRequired(tx: WireTransaction) { @@ -189,8 +241,20 @@ object NotaryChangeProtocol { } } - /** Thrown when a participant refuses to change the notary of the state */ - class Refused(val identity: Party, val originalState: StateAndRef<*>) : Exception() { - override fun toString() = "A participant $identity refused to change the notary of state $originalState" + // TODO: similar classes occur in other places (NotaryProtocol), need to consolidate + data class Result private constructor(val sig: DigitalSignature.WithKey?, val error: NotaryChangeRefused?) { + companion object { + fun withError(error: NotaryChangeRefused) = Result(null, error) + fun noError(sig: DigitalSignature.WithKey) = Result(sig, null) + } } +} + +/** Thrown when a participant refuses to change the notary of the state */ +class NotaryChangeRefused(val identity: Party, val state: StateRef, val cause: String?) { + override fun toString() = "A participant $identity refused to change the notary of state $state" +} + +class NotaryChangeException(val error: NotaryChangeRefused) : Exception() { + override fun toString() = "${super.toString()}: Notary change failed - ${error.toString()}" } \ No newline at end of file diff --git a/core/src/main/resources/com/r3corda/core/node/isolated.jar b/core/src/main/resources/com/r3corda/core/node/isolated.jar index 4ec15af9b05c7a0cb7b93001cdebe1edea4e4c2a..93f4c94cbd151e4e8b8c28cabdc56b5f4957420a 100644 GIT binary patch delta 1749 zcmV;`1}gcELC!$1RStjOR~yF_{zd|=kY!s|Fb0RjIFVx+0&C;gZNN3uS-~R)^F#aTP(R3=h%com1bkngt*DyV$Tgrba8=frbW47ZhO8085 zQdyk{KWh*IsU>5@(94Fspx>BZlD;-7@bY#zS8l=FoZc=l@Pwi5l%&3GopHllE;){? zK@*6rNY}c%DsVZQ-(!(k*RT~wmSaykZUbrb+->@nciaViNqY0HVcAMIY{q8zmP)_w zc-L#?@~A+>yB~i?QsBj9$17X5zO+)&E!&f>ZItyK;W*VYm4+h%C(K1@F5fg_7HEzssIT1wzj{s1YV;};J=i$GUdE*Ry8;+DKC zU1^(AAkzZQU9EbKUXW(Zl~aaVE(BUcqMyoKOlceoIrO6B6wcl=z_GR%P@1c z{<4D=@M@2WMBSGV@MAS2+`UtKpo$ORX5& zMnSOLKmvdLc$MaRwrr(h*d+}^0uu*`Hv~|b30DS@pBIQ`>kbSjZ~+%tCQoxXz)eg^d9wCXC{f)F_OR!!L+}yr>TY?2_%VqoWQB4B#-&v z-dtt8g}TRsPvN;fs(W}Y7ss?fcev~O%{5^t@9q1htoqDoCP|DBUEO?wt# z39x^PIuGoCxsuP^5YO^B7F&Pr2)pBISmMsB7LDOcmj&KzMfHCCEKH3N zfKHZCNnja_?^r&poEhmECBJuhm8h&ldC%fyDKa07z(QW;Tj_$pzc(IECL-OD&40Wh zqk-Ib*c7RF#7Xd|22X!pj$;mCQxe`F+bqI~O3=5puPtlQ-!6)r@zxmN#f9CU=EXNz?4!5sxGw5nG;2`m;R#{#P@+ENP|2fw* zJmfX7WlJx6JP~fyY|pCjHnU_Ge!@AP%n>$w z1TnL8DByohK3^cRNF zPB}S{?xTBy>6iF?giNt_9odn#N9ZpOu4AzM?`SJVd)f*`s`TO`Tqv&NwGCVzSi_Yy zjIZPE!Wwd$p1elXE}X|Su91H(;y2@*#3|$huO=~r>j4XsjP(XR@;&y6OX4hNXSaFn zb8$2pzMvV(P5e0I`Df8Iw>L9g}e&7Y2VC00000rGaAD delta 1814 zcmV+x2kH3EK#oDMRStjaR~yF_|BVD%A!`e7@W6Q0}BQCOqUm_$b zY1&~mMhmN5b!UW4UvZs2lD_+;e?UL=Lwou-E%el9@yrscWH=a!PDEj@=TQ8ItWT~C#TKUJ$#Rwu*H z1{gzXiLY?e;qIb&cVS6rZj|B8?Qp)>g1Nc9oniO|LDwq@bK5x++*&Sqo^QZlh^+|U zez?kTC6nLdkXfI*l2*3o-t_ziQuf>&>6Z8WMRQ501)tllG`UN*aV;s$JD$2zbDS(g zL_Llp$?*EJryPIVHJ4T@rtK=>yWBBz1V>wKOBy)FaLOtRYxy4cc}0*!PKGcHXEXVo z{>g6P6g12tX_Z6@ClWZ0G((#z+mhjO{tzJ{#V;MgEeu_bSmaKF@jyHjzHlwUFwg?d zU#%+7EC{RSi)k*)w7+LjsQfMvgPC*t0ZHN%x;qfTs|GvxPl=r;N6%X}HAF_ORl zMj1v90`K4&71zrZ znBs~XxXy6?AQfbQax6||9eiQv-=m7_X^_MhF`j?G4NNe+wx_6pDTX9r-+km#1H3H= z|5d^df+%%Mu+^%0ueRXW7JVe-oA^=!?_!!^>Lq#GG&#E~T-}4W61alf3>}YbRn93< zks5sd#fE**R*}RE?&$ouOHD)S0-~bWCq}*A7)B4)?HGI6 ziU5DzVO0?pIq7(Sj&Lp)$9;zGuyk}GO>(K;eQb%Uw!^@e89s9966~{r3$|}T&`PR4 z<@LFqI}&_Sw5^ZM91YJLZ5}t;sEpB8l`$G95yyuNCo`?@)3f*}fq8tD;TYxF%vR@6 z3FVUDSkQgbVi?-L*$K4iqH9t0JFhbXim^3Cv7J?NhHOZhz=_13wH=f)~M7krJzj%X21G@2~Nm76D z##=)u;6^sN(e8~WW04DtevxaDQ{5Bo&wm|@q@uU_Qt{J~3ms#zRD$RcqDi7hi5??* ze7fsaDwPU3Cy1s4&MUO)B+f~qUDI6;I#X@5YX9K*ui6M#Yje6S9!nX|e-rD`_KY#S zwVyP~wrmBv&%7jW9(YnAFUK2q2ep5ou)Mn(C8aOus_lr|LA)5Q|37z=f$!4*+_FfQ z6?OFoHCNdcn&Vb%X)icpd~@2S5oVGMXGrHgi#v1Nw>e47>IaR0dD|8Bk_|ZnJF%J* z_cqX@WUsY6!>i%8M8T{1mbhu_mt@YRA#T#)QVNpo2_ffRP>$Mngo-_a7~+5A&$J#+ ziGE5s93y&!XgkpkBuM5nddm`x5H*I=NB@df{z%IRenii%IvbrhsVjh1N#Zg5IE0F! zA3xE27UGgxI31#zx?=QMLw`o^ADWoSIy1ye!^I*C{!%yCI^J&oJKBoTp0+}fK3yUCcZ%!C zZs7C7YnWU^ZXNl;8t!c>_ck5WPt(bLJRlF!kxkQ!IDXO4}(wvy~F7u*)?4J7mhx|d@=o%b$oyApBorlL+Q8l zJ-Q_#y-l@xNma2eBE$G;ef^$+^E;zM!xugEu@i^})^rZ7Xhzb5fS;*3N=Vpih268#c$ z67Nggmk<&i0W&GlCGk|^6N&Hr4^T@72>2P#b*}~h051@;&=i>o3iuh%b*}~h051@e za~^;L5)}ZG@d6-|Fdh_>Djz!mdy{4#M*+!`t{+SS92k?Y9~hGm7#))_AQuL|8UO$Q E0Pk686951J diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt index 247e5e05c8..8fcf313900 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt @@ -26,13 +26,13 @@ class TransactionGraphSearchTests { * @param signer signer for the two transactions and their commands. */ fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage { - val originTx = TransactionBuilder().apply { - addOutputState(DummyContract.State(random31BitValue(), DUMMY_NOTARY)) + val originTx = TransactionType.General.Builder().apply { + addOutputState(DummyContract.State(random31BitValue()), DUMMY_NOTARY) addCommand(command, signer.public) signWith(signer) }.toSignedTransaction(false) - val inputTx = TransactionBuilder().apply { - addInputState(originTx.tx.outRef(0).ref) + val inputTx = TransactionType.General.Builder().apply { + addInputState(originTx.tx.outRef(0)) signWith(signer) }.toSignedTransaction(false) return GraphTransactionStorage(originTx, inputTx) diff --git a/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt b/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt index 437f8ecf73..8905950d2d 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt @@ -5,6 +5,7 @@ import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.Wallet import com.r3corda.core.testing.DUMMY_NOTARY import org.junit.Test +import java.security.PublicKey import kotlin.test.assertEquals @@ -12,14 +13,15 @@ class WalletUpdateTests { object DummyContract : Contract { - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { } override val legalContractReference: SecureHash = SecureHash.sha256("") } private class DummyState : ContractState { - override val notary = DUMMY_NOTARY + override val participants: List + get() = emptyList() override val contract = WalletUpdateTests.DummyContract } @@ -29,11 +31,11 @@ class WalletUpdateTests { private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3) private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4) - private val stateAndRef0 = StateAndRef(DummyState(), stateRef0) - private val stateAndRef1 = StateAndRef(DummyState(), stateRef1) - private val stateAndRef2 = StateAndRef(DummyState(), stateRef2) - private val stateAndRef3 = StateAndRef(DummyState(), stateRef3) - private val stateAndRef4 = StateAndRef(DummyState(), stateRef4) + private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef0) + private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef1) + private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef2) + private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef3) + private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef4) @Test fun `nothing plus nothing is nothing`() { diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt index ed6e3154f2..703a038f62 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt @@ -3,6 +3,8 @@ package com.r3corda.node.internal.testing import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.DummyContract import com.r3corda.core.contracts.StateRef +import com.r3corda.core.contracts.TransactionState +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.crypto.Party import com.r3corda.core.seconds import com.r3corda.core.testing.DUMMY_NOTARY @@ -11,8 +13,8 @@ import com.r3corda.node.internal.AbstractNode import java.time.Instant import java.util.* -fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { - val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) +fun issueState(node: AbstractNode): StateAndRef<*> { + val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY) tx.signWith(node.storage.myLegalIdentityKey) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() @@ -20,6 +22,20 @@ fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } +fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode): StateAndRef { + val state = TransactionState(DummyContract.MultiOwnerState(0, + listOf(nodeA.info.identity.owningKey, nodeB.info.identity.owningKey)), DUMMY_NOTARY) + val tx = TransactionType.NotaryChange.Builder().withItems(state) + tx.signWith(nodeA.storage.myLegalIdentityKey) + tx.signWith(nodeB.storage.myLegalIdentityKey) + tx.signWith(DUMMY_NOTARY_KEY) + val stx = tx.toSignedTransaction() + nodeA.services.recordTransactions(listOf(stx)) + nodeB.services.recordTransactions(listOf(stx)) + val stateAndRef = StateAndRef(state, StateRef(stx.id, 0)) + return stateAndRef +} + fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) tx.setTime(Instant.now(), notary, 30.seconds) diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt index 538360bdfe..615766135c 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt @@ -3,7 +3,7 @@ package com.r3corda.node.internal.testing import com.r3corda.contracts.cash.Cash import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Issued -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.crypto.Party import com.r3corda.core.node.ServiceHub import com.r3corda.core.serialization.OpaqueBytes @@ -34,7 +34,7 @@ object WalletFiller { // this field as there's no other database or source of truth we need to sync with. val depositRef = myIdentity.ref(ref) - val issuance = TransactionBuilder() + val issuance = TransactionType.General.Builder() val freshKey = services.keyManagementService.freshKey() cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary) issuance.signWith(myKey) diff --git a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt index 81a2165a55..7b13f01e44 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt @@ -17,37 +17,11 @@ class NotaryChangeService(net: MessagingService, val smm: StateMachineManager) : ) } - private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake): Boolean { - val proposal = req.payload - val autoAccept = checkProposal(proposal) - - if (autoAccept) { - val protocol = NotaryChangeProtocol.Acceptor( - req.replyTo as SingleMessageRecipient, - proposal.sessionIdForReceive, - proposal.sessionIdForSend) - smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) - } - return autoAccept - } - - /** - * Check the notary change for a state proposal and decide whether to allow the change and initiate the protocol - * or deny the change. - * - * For example, if the proposed new notary has the same behaviour (e.g. both are non-validating) - * and is also in a geographically convenient location we can just automatically approve the change. - * TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal - */ - private fun checkProposal(proposal: NotaryChangeProtocol.Proposal): Boolean { - val newNotary = proposal.newNotary - val isNotary = smm.serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary } - require(isNotary) { "The proposed node $newNotary does not run a Notary service " } - - // An example requirement - val blacklist = listOf("Evil Notary") - require(!blacklist.contains(newNotary.name)) - - return true + private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake) { + val protocol = NotaryChangeProtocol.Acceptor( + req.replyTo as SingleMessageRecipient, + req.sessionID!!, + req.sessionIdForSend) + smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) } } diff --git a/node/src/test/kotlin/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/node/services/NotaryChangeTests.kt index f44a778ee5..4059a1dcd9 100644 --- a/node/src/test/kotlin/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/node/services/NotaryChangeTests.kt @@ -1,20 +1,24 @@ package node.services -import com.r3corda.contracts.DummyContract -import com.r3corda.core.contracts.StateAndRef -import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.TransactionState -import com.r3corda.core.contracts.TransactionType +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.node.internal.testing.MockNetwork +import com.r3corda.node.internal.testing.issueMultiPartyState import com.r3corda.node.internal.testing.issueState -import com.r3corda.node.services.transactions.NotaryService +import com.r3corda.node.services.network.NetworkMapService +import com.r3corda.node.services.transactions.SimpleNotaryService import org.junit.Before import org.junit.Test +import protocols.NotaryChangeException import protocols.NotaryChangeProtocol import protocols.NotaryChangeProtocol.Instigator +import protocols.NotaryChangeRefused +import java.util.concurrent.ExecutionException import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class NotaryChangeTests { lateinit var net: MockNetwork @@ -26,22 +30,22 @@ class NotaryChangeTests { @Before fun setup() { net = MockNetwork() - oldNotaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) - clientNodeA = net.createPartyNode(networkMapAddr = oldNotaryNode.info) - clientNodeB = net.createPartyNode(networkMapAddr = oldNotaryNode.info) - newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info, advertisedServices = NotaryService.Type) + oldNotaryNode = net.createNode( + legalName = DUMMY_NOTARY.name, + keyPair = DUMMY_NOTARY_KEY, + advertisedServices = *arrayOf(NetworkMapService.Type, SimpleNotaryService.Type)) + clientNodeA = net.createNode(networkMapAddress = oldNotaryNode.info) + clientNodeB = net.createNode(networkMapAddress = oldNotaryNode.info) + newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info, advertisedServices = SimpleNotaryService.Type) net.runNetwork() // Clear network map registration messages } @Test fun `should change notary for a state with single participant`() { - val ref = issueState(clientNodeA, DUMMY_NOTARY).ref - val state = clientNodeA.services.loadState(ref) - + val state = issueState(clientNodeA) val newNotary = newNotaryNode.info.identity - - val protocol = Instigator(StateAndRef(state, ref), newNotary) + val protocol = Instigator(state, newNotary) val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) net.runNetwork() @@ -52,21 +56,9 @@ class NotaryChangeTests { @Test fun `should change notary for a state with multiple participants`() { - val state = TransactionState(DummyContract.MultiOwnerState(0, - listOf(clientNodeA.info.identity.owningKey, clientNodeB.info.identity.owningKey)), DUMMY_NOTARY) - - val tx = TransactionType.NotaryChange.Builder().withItems(state) - tx.signWith(clientNodeA.storage.myLegalIdentityKey) - tx.signWith(clientNodeB.storage.myLegalIdentityKey) - tx.signWith(DUMMY_NOTARY_KEY) - val stx = tx.toSignedTransaction() - clientNodeA.services.recordTransactions(listOf(stx)) - clientNodeB.services.recordTransactions(listOf(stx)) - val stateAndRef = StateAndRef(state, StateRef(stx.id, 0)) - + val state = issueMultiPartyState(clientNodeA, clientNodeB) val newNotary = newNotaryNode.info.identity - - val protocol = Instigator(stateAndRef, newNotary) + val protocol = Instigator(state, newNotary) val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) net.runNetwork() @@ -78,8 +70,21 @@ class NotaryChangeTests { assertEquals(loadedStateA, loadedStateB) } + @Test + fun `should throw when a participant refuses to change Notary`() { + val state = issueMultiPartyState(clientNodeA, clientNodeB) + val newEvilNotary = Party("Evil Notary", generateKeyPair().public) + val protocol = Instigator(state, newEvilNotary) + val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) + + net.runNetwork() + + val ex = assertFailsWith(ExecutionException::class) { future.get() } + val error = (ex.cause as NotaryChangeException).error + assertTrue(error is NotaryChangeRefused) + } + // TODO: Add more test cases once we have a general protocol/service exception handling mechanism: - // - A participant refuses to change Notary // - A participant is offline/can't be found on the network // - The requesting party is not a participant // - The requesting party wants to change additional state fields