From f0aa5a30d407418a04d0c53ff1bed43744a51c10 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 13 Jul 2016 10:26:17 +0100 Subject: [PATCH 1/5] Deprecate existing timestamp infrastructure --- .../com/r3corda/contracts/asset/Obligation.kt | 5 +++- .../r3corda/core/contracts/ContractsDSL.kt | 1 + .../com/r3corda/core/contracts/Structures.kt | 23 +++++++++++++++--- .../core/contracts/TransactionBuilder.kt | 24 +++++++++++++++++-- .../core/contracts/TransactionTools.kt | 2 +- .../core/contracts/TransactionVerification.kt | 8 +++---- .../r3corda/core/contracts/Transactions.kt | 6 +++-- 7 files changed, 55 insertions(+), 14 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt index 2f63ee0921..1995f02dd8 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -403,7 +403,10 @@ class Obligation

: Contract { if (input is State

) { val actualOutput = outputs[stateIdx] val deadline = input.dueBefore - val timestamp: TimestampCommand? = tx.timestamp + val timestamp: TimestampCommand? = if (tx.inputNotary == null) + null + else + tx.getTimestampBy(tx.inputNotary!!) val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle) requireThat { 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 c2a82f1294..cc9cd41aa9 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -85,6 +85,7 @@ fun Collection>.requireSingle mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject else null }.single() /** Returns a timestamp that was signed by the given authority, or returns null if missing. */ +@Deprecated("Get timestamp from the transaction") fun List>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? { val timestampCmds = filter { it.signers.contains(timestampingAuthority.owningKey) && it.value is TimestampCommand } return timestampCmds.singleOrNull()?.value as? TimestampCommand 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 9815225118..230546f114 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -337,13 +337,30 @@ data class AuthenticatedObject( val value: T ) +/** + * If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be + * between (after, before). + */ +data class Timestamp(val after: Instant?, val before: Instant?) { + init { + if (after == null && before == null) + throw IllegalArgumentException("At least one of before/after must be specified") + if (after != null && before != null) + check(after <= before) + } + + constructor(time: Instant, tolerance: Duration) : this(time - tolerance, time + tolerance) + + val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2) +} + /** * If present in a transaction, contains a time that was verified by the timestamping authority/authorities whose * public keys are identified in the containing [Command] object. The true time must be between (after, before). + * + * @deprecated timestamps are now a field on a transaction, and this exists just for legacy reasons. */ -// TODO: Timestamps are now always provided by the consensus service for the transaction, rather than potentially -// having multiple timestamps on a transaction. As such, it likely makes more sense for time to be a field on the -// transaction, rather than a command +@Deprecated("timestamps are now a field on a transaction, and this exists just for legacy reasons.") data class TimestampCommand(val after: Instant?, val before: Instant?) : CommandData { init { if (after == null && before == null) 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 59a76b1c9f..8e28d55657 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -27,8 +27,10 @@ open class TransactionBuilder( protected val attachments: MutableList = arrayListOf(), protected val outputs: MutableList> = arrayListOf(), protected val commands: MutableList = arrayListOf(), - protected val signers: MutableSet = mutableSetOf()) { + protected val signers: MutableSet = mutableSetOf(), + protected var timestamp: Timestamp? = null) { + @Deprecated("use timestamp instead") val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() /** @@ -57,12 +59,30 @@ open class TransactionBuilder( * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single * node. */ + @Deprecated("use setTime(Instant, Duration) instead") fun setTime(time: Instant, authority: Party, timeTolerance: Duration) { check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } commands.removeAll { it.value is TimestampCommand } addCommand(TimestampCommand(time, timeTolerance), authority.owningKey) } + /** + * Places a [TimestampCommand] in this transaction, removing any existing command if there is one. + * The command requires a signature from the Notary service, which acts as a Timestamp Authority. + * The signature can be obtained using [NotaryProtocol]. + * + * The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance]. + * If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance + * should be chosen such that your code can finish building the transaction and sending it to the TSA within that + * window of time, taking into account factors such as network latency. Transactions being built by a group of + * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single + * node. + */ + fun setTime(time: Instant, timeTolerance: Duration) { + check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } + timestamp = Timestamp(time, timeTolerance) + } + /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { @@ -115,7 +135,7 @@ open class TransactionBuilder( } fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands), signers.toList(), type) + ArrayList(outputs), ArrayList(commands), signers.toList(), type, timestamp) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { if (checkSufficientSignatures) { 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 936182580a..47b3cdb9b2 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -23,7 +23,7 @@ fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) } - return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, type) + return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, type, timestamp) } /** 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 8c7800f83b..06648fbd12 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -17,7 +17,8 @@ data class TransactionForContract(val inputs: List, val attachments: List, val commands: List>, val origHash: SecureHash, - val inputNotary: Party? = null) { + val inputNotary: Party? = null, + val timestamp: Timestamp? = null) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash @@ -83,11 +84,8 @@ data class TransactionForContract(val inputs: List, */ data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) - /** Get the timestamp command for this transaction, using the notary from the input states. */ - val timestamp: TimestampCommand? - get() = if (inputNotary == null) null else commands.getTimestampBy(inputNotary) - /** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */ + @Deprecated("use timestamp property instead") fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) /** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */ 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 0d4d887c1b..13f5d64495 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -49,7 +49,8 @@ data class WireTransaction(val inputs: List, val outputs: List>, val commands: List, val signers: List, - val type: TransactionType) : NamedByHash { + val type: TransactionType, + val timestamp: Timestamp? = null) : 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 @@ -165,7 +166,8 @@ data class LedgerTransaction( override val id: SecureHash, /** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */ val signers: List, - val type: TransactionType + val type: TransactionType, + val timestamp: Timestamp? = null ) : NamedByHash { @Suppress("UNCHECKED_CAST") fun outRef(index: Int) = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) From 17ae349f4d5de143e7912d81a4d6b8ae199e890e Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 13 Jul 2016 11:05:31 +0100 Subject: [PATCH 2/5] Remove support for timestamp commands --- .../contracts/JavaCommercialPaper.java | 14 ++++--- .../com/r3corda/contracts/CommercialPaper.kt | 6 +-- .../contracts/CommercialPaperLegacy.kt | 8 +--- .../main/kotlin/com/r3corda/contracts/IRS.kt | 13 +++--- .../com/r3corda/contracts/asset/Obligation.kt | 7 +--- .../protocols/TwoPartyTradeProtocol.kt | 2 +- .../r3corda/contracts/CommercialPaperTests.kt | 6 +-- .../kotlin/com/r3corda/contracts/IRSTests.kt | 4 +- .../r3corda/core/contracts/ContractsDSL.kt | 24 ----------- .../com/r3corda/core/contracts/Structures.kt | 20 --------- .../core/contracts/TransactionBuilder.kt | 29 ++++--------- .../core/contracts/TransactionTools.kt | 2 +- .../core/contracts/TransactionTypes.kt | 4 +- .../core/contracts/TransactionVerification.kt | 9 ---- .../r3corda/core/contracts/Transactions.kt | 8 ++-- .../core/node/services/TimestampChecker.kt | 4 +- .../com/r3corda/core/serialization/Kryo.kt | 4 +- .../com/r3corda/core/testing/TestDSL.kt | 4 ++ .../core/testing/TransactionDSLInterpreter.kt | 18 ++++---- .../com/r3corda/protocols/NotaryProtocol.kt | 41 +++++++------------ .../r3corda/protocols/TwoPartyDealProtocol.kt | 10 +++-- .../TransactionSerializationTests.kt | 4 +- .../r3corda/contracts/AccountReceivable.kt | 2 +- .../contracts/BillOfLadingAgreement.kt | 4 +- .../kotlin/com/r3corda/contracts/Invoice.kt | 2 +- .../com/r3corda/contracts/LCApplication.kt | 3 +- .../main/kotlin/com/r3corda/contracts/LOC.kt | 4 +- .../contracts/AccountReceivableTests.kt | 2 +- .../node/internal/testing/TradeSimulation.kt | 2 +- .../messaging/TwoPartyTradeProtocolTests.kt | 2 +- .../node/services/NotaryChangeTests.kt | 2 +- .../node/services/NotaryServiceTests.kt | 27 ++---------- .../node/services/TimestampCheckerTests.kt | 10 ++--- .../kotlin/com/r3corda/demos/TraderDemo.kt | 2 +- 34 files changed, 98 insertions(+), 205 deletions(-) diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 6413e88917..f1841630ed 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -3,6 +3,9 @@ package com.r3corda.contracts; import com.google.common.collect.*; import com.r3corda.contracts.asset.*; import com.r3corda.core.contracts.*; +import static com.r3corda.core.contracts.ContractsDSL.requireThat; + +import com.r3corda.core.contracts.Timestamp; import com.r3corda.core.contracts.TransactionForContract.*; import com.r3corda.core.contracts.clauses.*; import com.r3corda.core.crypto.*; @@ -219,15 +222,14 @@ public class JavaCommercialPaper implements Contract { if (!cmd.getSigners().contains(input.getOwner())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); - Party notary = cmd.getValue().notary; - TimestampCommand timestampCommand = tx.getTimestampBy(notary); - Instant time = null == timestampCommand + Timestamp timestamp = tx.getTimestamp(); + Instant time = null == timestamp ? null - : timestampCommand.getBefore(); + : timestamp.getBefore(); Amount> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner()); requireThat(require -> { - require.by("must be timestamped", timestampCommand != null); + require.by("must be timestamped", timestamp != null); require.by("received amount equals the face value: " + received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue())); require.by("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate())); @@ -257,7 +259,7 @@ public class JavaCommercialPaper implements Contract { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); State output = single(outputs); Party notary = cmd.getValue().notary; - TimestampCommand timestampCommand = tx.getTimestampBy(notary); + Timestamp timestampCommand = tx.getTimestamp(); Instant time = null == timestampCommand ? null : timestampCommand.getBefore(); diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 56b5c3847e..e2567e9318 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -109,8 +109,7 @@ class CommercialPaper : Contract { token: Issued): Set { val consumedCommands = super.verify(tx, inputs, outputs, commands, token) val command = commands.requireSingleCommand() - // If it's an issue, we can't take notary from inputs, so it must be specified in the command - val timestamp: TimestampCommand? = tx.getTimestampBy(command.value.notary) + val timestamp = tx.timestamp val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" } @@ -151,8 +150,7 @@ class CommercialPaper : Contract { // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date) // before requiring a single command val command = commands.requireSingleCommand() - // If it's an issue, we can't take notary from inputs, so it must be specified in the command - val timestamp: TimestampCommand? = tx.getTimestampBy(command.value.notary) + val timestamp = tx.timestamp val input = inputs.single() val received = tx.outputs.sumCashBy(input.owner) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt index c87795b862..22263ebae3 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt @@ -60,13 +60,7 @@ class CommercialPaperLegacy : Contract { // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming // it for cash on or after the maturity date. val command = tx.commands.requireSingleCommand() - // If it's an issue, we can't take notary from inputs, so it must be specified in the command - val cmdVal = command.value - val timestamp: TimestampCommand? = when (cmdVal) { - is Commands.Issue -> tx.getTimestampBy(cmdVal.notary) - is Commands.Redeem -> tx.getTimestampBy(cmdVal.notary) - else -> null - } + val timestamp: Timestamp? = tx.timestamp for ((inputs, outputs, key) in groups) { when (command.value) { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 6573879522..d6f36ada78 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -447,8 +447,8 @@ class InterestRateSwap() : Contract { fixingCalendar, index, indexSource, indexTenor) } - private fun extractCommands(tx: TransactionForContract): Collection> - = tx.commands.select() + tx.commands.select() + fun extractCommands(tx: TransactionForContract): Collection> + = tx.commands.select() override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx)) @@ -519,12 +519,9 @@ class InterestRateSwap() : Contract { override val requiredCommands = emptySet>() override fun verify(tx: TransactionForContract, commands: Collection>): Set { - // TODO: This needs to either be the notary used for the inputs, or otherwise - // derived as the correct notary - @Suppress("DEPRECATION") - val command = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") - ?: throw IllegalArgumentException("must be timestamped") - return setOf(command) + require(tx.timestamp?.midpoint != null) { "must be timestamped" } + // We return an empty set because we don't process any commands + return emptySet() } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt index 1995f02dd8..53ab2e2cb5 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -403,10 +403,7 @@ class Obligation

: Contract { if (input is State

) { val actualOutput = outputs[stateIdx] val deadline = input.dueBefore - val timestamp: TimestampCommand? = if (tx.inputNotary == null) - null - else - tx.getTimestampBy(tx.inputNotary!!) + val timestamp = tx.timestamp val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle) requireThat { @@ -544,7 +541,7 @@ class Obligation

: Contract { } tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.distinct()) } - tx.setTime(issuanceDef.dueBefore, notary, issuanceDef.timeTolerance) + tx.setTime(issuanceDef.dueBefore, issuanceDef.timeTolerance) } /** diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 5d58e3c772..a785a53c11 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -273,7 +273,7 @@ object TwoPartyTradeProtocol { // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. val currentTime = serviceHub.clock.instant() - ptx.setTime(currentTime, notary, 30.seconds) + ptx.setTime(currentTime, 30.seconds) return Pair(ptx, cashSigningPubKeys) } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 3812555d4b..4d8e65f6ca 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -184,7 +184,7 @@ class CommercialPaperTestsGeneric { } fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { - val ltx = LedgerTransaction(emptyList(), listOf(*outputs), emptyList(), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.General()) + val ltx = LedgerTransaction(emptyList(), listOf(*outputs), emptyList(), emptyList(), SecureHash.randomSHA256(), emptyList(), null, TransactionType.General()) return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } @@ -205,7 +205,7 @@ class CommercialPaperTestsGeneric { val issuance = bigCorpServices.storageService.myLegalIdentity.ref(1) val issueTX: SignedTransaction = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) + setTime(TEST_TX_TIME, 30.seconds) signWith(bigCorpServices.key) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -223,7 +223,7 @@ class CommercialPaperTestsGeneric { fun makeRedeemTX(time: Instant): SignedTransaction { val ptx = TransactionType.General.Builder() - ptx.setTime(time, DUMMY_NOTARY, 30.seconds) + ptx.setTime(time, 30.seconds) CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType()) ptx.signWith(aliceServices.key) ptx.signWith(bigCorpServices.key) diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index 2911676ba3..987ed0f752 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -217,7 +217,7 @@ class IRSTests { calculation = dummyIRS.calculation, common = dummyIRS.common, notary = DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) + setTime(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) @@ -303,7 +303,7 @@ class IRSTests { val fixing = Fix(nextFix, "0.052".percent.value) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) with(tx) { - setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) + setTime(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) 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 cc9cd41aa9..272d50f6f7 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -84,30 +84,6 @@ inline fun Collection fun Collection>.requireSingleCommand(klass: Class) = mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject else null }.single() -/** Returns a timestamp that was signed by the given authority, or returns null if missing. */ -@Deprecated("Get timestamp from the transaction") -fun List>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? { - val timestampCmds = filter { it.signers.contains(timestampingAuthority.owningKey) && it.value is TimestampCommand } - return timestampCmds.singleOrNull()?.value as? TimestampCommand -} - -/** - * Returns a timestamp that was signed by any of the the named authorities, or returns null if missing. - * Note that matching here is done by (verified, legal) name, not by public key. Any signature by any - * party with a name that matches (case insensitively) any of the given names will yield a match. - */ -@Deprecated(message = "Timestamping authority should always be notary for the transaction") -fun List>.getTimestampByName(vararg names: String): TimestampCommand? { - val timestampCmd = filter { it.value is TimestampCommand }.singleOrNull() ?: return null - val tsaNames = timestampCmd.signingParties.map { it.name.toLowerCase() } - val acceptableNames = names.map(String::toLowerCase) - val acceptableNameFound = tsaNames.intersect(acceptableNames).isNotEmpty() - if (acceptableNameFound) - return timestampCmd.value as TimestampCommand - else - return null -} - /** * Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key. * 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 230546f114..a245f9071d 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -354,26 +354,6 @@ data class Timestamp(val after: Instant?, val before: Instant?) { val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2) } -/** - * If present in a transaction, contains a time that was verified by the timestamping authority/authorities whose - * public keys are identified in the containing [Command] object. The true time must be between (after, before). - * - * @deprecated timestamps are now a field on a transaction, and this exists just for legacy reasons. - */ -@Deprecated("timestamps are now a field on a transaction, and this exists just for legacy reasons.") -data class TimestampCommand(val after: Instant?, val before: Instant?) : CommandData { - init { - if (after == null && before == null) - throw IllegalArgumentException("At least one of before/after must be specified") - if (after != null && before != null) - check(after <= before) - } - - constructor(time: Instant, tolerance: Duration) : this(time - tolerance, time + tolerance) - - val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2) -} - /** * 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 8e28d55657..18b3faa5ba 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -31,7 +31,7 @@ open class TransactionBuilder( protected var timestamp: Timestamp? = null) { @Deprecated("use timestamp instead") - val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() + val time: Timestamp? get() = timestamp /** * Creates a copy of the builder. @@ -44,7 +44,8 @@ open class TransactionBuilder( attachments = ArrayList(attachments), outputs = ArrayList(outputs), commands = ArrayList(commands), - signers = LinkedHashSet(signers) + signers = LinkedHashSet(signers), + timestamp = timestamp ) /** @@ -59,28 +60,12 @@ open class TransactionBuilder( * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single * node. */ - @Deprecated("use setTime(Instant, Duration) instead") - fun setTime(time: Instant, authority: Party, timeTolerance: Duration) { - check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } - commands.removeAll { it.value is TimestampCommand } - addCommand(TimestampCommand(time, timeTolerance), authority.owningKey) - } + fun setTime(time: Instant, timeTolerance: Duration) + = setTime(Timestamp(time, timeTolerance)) - /** - * Places a [TimestampCommand] in this transaction, removing any existing command if there is one. - * The command requires a signature from the Notary service, which acts as a Timestamp Authority. - * The signature can be obtained using [NotaryProtocol]. - * - * The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance]. - * If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance - * should be chosen such that your code can finish building the transaction and sending it to the TSA within that - * window of time, taking into account factors such as network latency. Transactions being built by a group of - * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single - * node. - */ - fun setTime(time: Instant, timeTolerance: Duration) { + fun setTime(newTimestamp: Timestamp) { check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } - timestamp = Timestamp(time, timeTolerance) + this.timestamp = newTimestamp } /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ 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 47b3cdb9b2..0d86f9adfd 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -23,7 +23,7 @@ fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) } - return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, type, timestamp) + return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, timestamp, 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 2e0f4174fb..dff0286c38 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -24,9 +24,7 @@ sealed class TransactionType { /** Check that the list of signers includes all the necessary keys */ fun verifySigners(tx: LedgerTransaction): Set { - val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand } - val timestampKey = timestamp?.signers.orEmpty() - val notaryKey = (tx.inputs.map { it.state.notary.owningKey } + timestampKey).toSet() + val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet() if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx) val requiredKeys = getRequiredSigners(tx) + notaryKey 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 06648fbd12..7ff6376adf 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -83,15 +83,6 @@ data class TransactionForContract(val inputs: List, * 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. */ - @Deprecated("use timestamp property instead") - fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) - - /** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */ - @Suppress("DEPRECATION") - @Deprecated(message = "Timestamping authority should always be notary for the transaction") - fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName) } class TransactionResolutionException(val hash: SecureHash) : Exception() { 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 13f5d64495..81a0f7ed9e 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -50,7 +50,7 @@ data class WireTransaction(val inputs: List, val commands: List, val signers: List, val type: TransactionType, - val timestamp: Timestamp? = null) : NamedByHash { + val timestamp: Timestamp?) : 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 @@ -166,8 +166,8 @@ data class LedgerTransaction( override val id: SecureHash, /** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */ val signers: List, - val type: TransactionType, - val timestamp: Timestamp? = null + val timestamp: Timestamp?, + val type: TransactionType ) : NamedByHash { @Suppress("UNCHECKED_CAST") fun outRef(index: Int) = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) @@ -177,7 +177,7 @@ data class LedgerTransaction( /** Strips the transaction down to a form that is usable by the contract verify functions */ fun toTransactionForContract(): TransactionForContract { return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id, - inputs.map { it.state.notary }.singleOrNull()) + inputs.map { it.state.notary }.singleOrNull(), timestamp) } /** diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/TimestampChecker.kt b/core/src/main/kotlin/com/r3corda/core/node/services/TimestampChecker.kt index 2c757d7dba..eaca2afaee 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/TimestampChecker.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/TimestampChecker.kt @@ -1,6 +1,6 @@ package com.r3corda.core.node.services -import com.r3corda.core.contracts.TimestampCommand +import com.r3corda.core.contracts.Timestamp import com.r3corda.core.seconds import com.r3corda.core.until import java.time.Clock @@ -11,7 +11,7 @@ import java.time.Duration */ class TimestampChecker(val clock: Clock = Clock.systemUTC(), val tolerance: Duration = 30.seconds) { - fun isValid(timestampCommand: TimestampCommand): Boolean { + fun isValid(timestampCommand: Timestamp): Boolean { val before = timestampCommand.before val after = timestampCommand.after 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 40de651303..30fbfb8ae3 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.commands) kryo.writeClassAndObject(output, obj.signers) kryo.writeClassAndObject(output, obj.type) + kryo.writeClassAndObject(output, obj.timestamp) } @Suppress("UNCHECKED_CAST") @@ -262,8 +263,9 @@ object WireTransactionSerializer : Serializer() { val commands = kryo.readClassAndObject(input) as List val signers = kryo.readClassAndObject(input) as List val transactionType = kryo.readClassAndObject(input) as TransactionType + val timestamp = kryo.readClassAndObject(input) as Timestamp? - return WireTransaction(inputs, attachmentHashes, outputs, commands, signers, transactionType) + return WireTransaction(inputs, attachmentHashes, outputs, commands, signers, transactionType, timestamp) } } } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index a8276513a8..436b92f113 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -134,6 +134,10 @@ data class TestTransactionDSLInterpreter private constructor( return EnforceVerifyOrFail.Token } + override fun timestamp(data: Timestamp) { + transactionBuilder.setTime(data) + } + override fun tweak( dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = dsl(TransactionDSL(copy())) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index 6d39d9c4fb..00c7a39acc 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -47,6 +47,12 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { fun _command(signers: List, commandData: CommandData) /** + * Adds a timestamp to the transaction. + * @param data The [TimestampCommand]. + */ + fun timestamp(data: Timestamp) + + /** * Creates a local scoped copy of the transaction. * @param dsl The transaction DSL to be interpreted using the copy. */ @@ -102,16 +108,8 @@ class TransactionDSL(val interpreter: T) : Tr * Adds a timestamp command to the transaction. * @param time The [Instant] of the [TimestampCommand]. * @param tolerance The tolerance of the [TimestampCommand]. - * @param notary The notary to sign the command. */ @JvmOverloads - fun timestamp(time: Instant, tolerance: Duration = 30.seconds, notary: PublicKey = DUMMY_NOTARY.owningKey) = - timestamp(TimestampCommand(time, tolerance), notary) - /** - * Adds a timestamp command to the transaction. - * @param data The [TimestampCommand]. - * @param notary The notary to sign the command. - */ - @JvmOverloads - fun timestamp(data: TimestampCommand, notary: PublicKey = DUMMY_NOTARY.owningKey) = command(notary, data) + fun timestamp(time: Instant, tolerance: Duration = 30.seconds) = + timestamp(Timestamp(time, tolerance)) } diff --git a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt index 4973767b29..c2118e49b6 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt @@ -2,7 +2,8 @@ package com.r3corda.protocols import co.paralleluniverse.fibers.Suspendable import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.TimestampCommand +import com.r3corda.core.contracts.StateRef +import com.r3corda.core.contracts.Timestamp import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.Party @@ -84,22 +85,16 @@ object NotaryProtocol { } private fun findNotaryParty(): Party { - var maybeNotaryKey: PublicKey? = null val wtx = stx.tx + val firstStateRef = wtx.inputs.firstOrNull() + var maybeNotaryParty: Party? = if (firstStateRef == null) + null + else + serviceHub.loadState(firstStateRef).notary - val timestampCommand = wtx.commands.singleOrNull { it.value is TimestampCommand } - if (timestampCommand != null) maybeNotaryKey = timestampCommand.signers.first() - - for (stateRef in wtx.inputs) { - val inputNotaryKey = serviceHub.loadState(stateRef).notary.owningKey - if (maybeNotaryKey != null) - check(maybeNotaryKey == inputNotaryKey) { "Input states and timestamp must have the same Notary" } - else maybeNotaryKey = inputNotaryKey - } - - val notaryKey = maybeNotaryKey ?: throw IllegalStateException("Transaction does not specify a Notary") - val notaryParty = serviceHub.networkMapCache.getNodeByPublicKey(notaryKey)?.identity - return notaryParty ?: throw IllegalStateException("No Notary node can be found with the specified public key") + val notaryParty = maybeNotaryParty ?: throw IllegalStateException("Transaction does not specify a Notary") + check(wtx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { "Input states must have the same Notary" } + return notaryParty } } @@ -138,18 +133,12 @@ object NotaryProtocol { send(otherSide, sendSessionID, result) } - private fun validateTimestamp(tx: WireTransaction) { - val timestampCmd = try { - tx.commands.noneOrSingle { it.value is TimestampCommand } ?: return - } catch (e: IllegalArgumentException) { - throw NotaryException(NotaryError.MoreThanOneTimestamp()) - } - val myIdentity = serviceHub.storageService.myLegalIdentity - if (!timestampCmd.signers.contains(myIdentity.owningKey)) - throw NotaryException(NotaryError.NotForMe()) - if (!timestampChecker.isValid(timestampCmd.value as TimestampCommand)) + private fun validateTimestamp(tx: WireTransaction) = + if (tx.timestamp != null + && !timestampChecker.isValid(tx.timestamp)) throw NotaryException(NotaryError.TimestampInvalid()) - } + else + Unit /** * No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index 9fee22c3f8..d5f4fe9347 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -101,9 +101,11 @@ object TwoPartyDealProtocol { untrustedPartialTX.validate { stx -> progressTracker.nextStep() + // TODO: Verify the notary on the transaction is set correctly + // Check that the tx proposed by the buyer is valid. val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false) - if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey)) + if (missingSigs != setOf(myKeyPair.public)) throw SignatureException("The set of missing signatures is not as expected: $missingSigs") val wtx: WireTransaction = stx.tx @@ -323,7 +325,7 @@ object TwoPartyDealProtocol { // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. - ptx.setTime(serviceHub.clock.instant(), notary, 30.seconds) + ptx.setTime(serviceHub.clock.instant(), 30.seconds) return Pair(ptx, arrayListOf(handshake.payload.parties.single { it.name == serviceHub.storageService.myLegalIdentity.name }.owningKey)) } @@ -375,7 +377,7 @@ object TwoPartyDealProtocol { val newDeal = deal - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(txState.notary) val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapCache.ratesOracleNodes[0].identity, fixOf, BigDecimal.ZERO, BigDecimal.ONE) { @Suspendable override fun beforeSigning(fix: Fix) { @@ -383,7 +385,7 @@ object TwoPartyDealProtocol { // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. - ptx.setTime(serviceHub.clock.instant(), txState.notary, 30.seconds) + ptx.setTime(serviceHub.clock.instant(), 30.seconds) } } subProtocol(addFixing) 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 10f24d97c1..1531740701 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -99,10 +99,10 @@ class TransactionSerializationTests { @Test fun timestamp() { - tx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) + tx.setTime(TEST_TX_TIME, 30.seconds) tx.signWith(DUMMY_KEY_1) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() - assertEquals(TEST_TX_TIME, (stx.tx.commands[1].value as TimestampCommand).midpoint) + assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint) } } diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt b/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt index e2f0f665df..4163e2f570 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt @@ -108,7 +108,7 @@ class AccountReceivable : Contract { override fun verify(tx: TransactionForContract) { val command = tx.commands.requireSingleCommand() - val time = tx.commands.getTimestampByName("Notary Service", "Seller")?.midpoint ?: + val time = tx.timestamp?.midpoint ?: throw IllegalArgumentException("must be timestamped") when (command.value) { diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/BillOfLadingAgreement.kt b/experimental/src/main/kotlin/com/r3corda/contracts/BillOfLadingAgreement.kt index 59db2d87f5..cb76d9e88f 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/BillOfLadingAgreement.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/BillOfLadingAgreement.kt @@ -74,7 +74,7 @@ class BillOfLadingAgreement : Contract { override fun verify(tx: TransactionForContract) { val command = tx.commands.requireSingleCommand() - val time = tx.commands.getTimestampByName("Notary Service")?.midpoint + val time = tx.timestamp?.midpoint if (time == null) throw IllegalArgumentException("must be timestamped") val txOutputStates: List = tx.outputs.filterIsInstance() @@ -114,7 +114,7 @@ class BillOfLadingAgreement : Contract { fun generateIssue(owner: PublicKey, beneficiary: Party, props: BillOfLadingProperties, notary: Party): TransactionBuilder { val state = State(owner, beneficiary, props) val builder = TransactionType.General.Builder(notary = notary) - builder.setTime(Instant.now(), notary, 1.days) + builder.setTime(Instant.now(), 1.days) return builder.withItems(state, Command(Commands.IssueBL(), props.carrierOwner.owningKey)) } diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt b/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt index afc8611bb0..c43e8392b5 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt @@ -113,7 +113,7 @@ class Invoice : Contract { override fun verify(tx: TransactionForContract) { val command = tx.commands.requireSingleCommand() - val time = tx.commands.getTimestampByName("Notary Service", "Seller")?.midpoint ?: + val time = tx.timestamp?.midpoint ?: throw IllegalArgumentException("must be timestamped") when (command.value) { diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt b/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt index 2255d488e6..6d7f4ac61f 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt @@ -33,7 +33,8 @@ class LCApplication : Contract { // Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be // hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about // who or what is a trusted authority. - tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") + // FIXME: This isn't used anywhere + tx.timestamp when (command.value) { is Commands.ApplyForLC -> { diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/LOC.kt b/experimental/src/main/kotlin/com/r3corda/contracts/LOC.kt index 7fc58657f0..73b0751d5a 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/LOC.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/LOC.kt @@ -71,7 +71,7 @@ class LOC : Contract { override fun verify(tx: TransactionForContract) { val command = tx.commands.requireSingleCommand() - val time = tx.commands.getTimestampByName("Notary Service")?.midpoint + val time = tx.timestamp?.midpoint if (time == null) throw IllegalArgumentException("must be timestamped") when (command.value) { @@ -146,7 +146,7 @@ class LOC : Contract { fun generateIssue(beneficiaryPaid: Boolean, issued: Boolean, terminated: Boolean, props: LOCProperties, notary: Party): TransactionBuilder { val state = State(beneficiaryPaid, issued, terminated, props) val builder = TransactionType.General.Builder(notary = notary) - builder.setTime(Instant.now(), notary, 1.days) + builder.setTime(Instant.now(), 1.days) return builder.withItems(state, Command(Commands.Issuance(), props.issuingbank.owningKey)) } diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt b/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt index f9d5de3202..e5a7788318 100644 --- a/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt +++ b/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt @@ -85,7 +85,7 @@ class AccountReceivableTests { } val gtx = invoice.generateInvoice(DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) + setTime(TEST_TX_TIME, 30.seconds) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) } diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt index ad844dcc13..fe751f55de 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt @@ -34,7 +34,7 @@ class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwo val issuance = run { val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER, Instant.now() + 10.days, notary.info.identity) - tx.setTime(Instant.now(), notary.info.identity, 30.seconds) + tx.setTime(Instant.now(), 30.seconds) tx.signWith(notary.storage.myLegalIdentityKey) tx.signWith(seller.storage.myLegalIdentityKey) tx.toSignedTransaction(true) 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 d991fb60f3..bfb8899647 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -475,7 +475,7 @@ class TwoPartyTradeProtocolTests { } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue(notary) } if (!withError) - timestamp(time = TEST_TX_TIME, notary = notary.owningKey) + timestamp(time = TEST_TX_TIME) if (attachmentID != null) attachment(attachmentID) if (withError) { diff --git a/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt index 04fecd4814..ed1bc61e11 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt @@ -120,7 +120,7 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode): 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) + tx.setTime(Instant.now(), 30.seconds) tx.signWith(node.storage.myLegalIdentityKey) val stx = tx.toSignedTransaction(false) node.services.recordTransactions(listOf(stx)) 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 d04b293406..5423750e14 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt @@ -1,6 +1,6 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.TimestampCommand +import com.r3corda.core.contracts.Timestamp import com.r3corda.core.contracts.TransactionType import com.r3corda.core.seconds import com.r3corda.core.testing.DUMMY_NOTARY @@ -40,7 +40,7 @@ class NotaryServiceTests { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder().withItems(inputState) - tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds) + tx.setTime(Instant.now(), 30.seconds) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) } @@ -73,7 +73,7 @@ class NotaryServiceTests { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder().withItems(inputState) - tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds) + tx.setTime(Instant.now().plusSeconds(3600), 30.seconds) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) } @@ -88,27 +88,6 @@ class NotaryServiceTests { } - @Test fun `should report error for transaction with more than one timestamp`() { - val stx = run { - val inputState = issueState(clientNode) - 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) - tx.signWith(clientNode.keyPair!!) - tx.toSignedTransaction(false) - } - - val protocol = NotaryProtocol.Client(stx) - val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) - net.runNetwork() - - val ex = assertFailsWith(ExecutionException::class) { future.get() } - val error = (ex.cause as NotaryException).error - assertTrue(error is NotaryError.MoreThanOneTimestamp) - } - - @Test fun `should report conflict for a duplicate transaction`() { val stx = run { val inputState = issueState(clientNode) diff --git a/node/src/test/kotlin/com/r3corda/node/services/TimestampCheckerTests.kt b/node/src/test/kotlin/com/r3corda/node/services/TimestampCheckerTests.kt index 170e7e9ea6..df9be52cd4 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/TimestampCheckerTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/TimestampCheckerTests.kt @@ -1,6 +1,6 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.TimestampCommand +import com.r3corda.core.contracts.Timestamp import com.r3corda.core.node.services.TimestampChecker import com.r3corda.core.seconds import org.junit.Test @@ -17,8 +17,8 @@ class TimestampCheckerTests { @Test fun `should return true for valid timestamp`() { val now = clock.instant() - val timestampPast = TimestampCommand(now - 60.seconds, now - 29.seconds) - val timestampFuture = TimestampCommand(now + 29.seconds, now + 60.seconds) + val timestampPast = Timestamp(now - 60.seconds, now - 29.seconds) + val timestampFuture = Timestamp(now + 29.seconds, now + 60.seconds) assertTrue { timestampChecker.isValid(timestampPast) } assertTrue { timestampChecker.isValid(timestampFuture) } } @@ -26,8 +26,8 @@ class TimestampCheckerTests { @Test fun `should return false for invalid timestamp`() { val now = clock.instant() - val timestampPast = TimestampCommand(now - 60.seconds, now - 31.seconds) - val timestampFuture = TimestampCommand(now + 31.seconds, now + 60.seconds) + val timestampPast = Timestamp(now - 60.seconds, now - 31.seconds) + val timestampFuture = Timestamp(now + 31.seconds, now + 60.seconds) assertFalse { timestampChecker.isValid(timestampPast) } assertFalse { timestampChecker.isValid(timestampFuture) } } diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index c61cee426a..3b8ef56ee2 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -350,7 +350,7 @@ private class TraderDemoProtocolSeller(val otherSide: Party, tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) // Requesting timestamping, all CP must be timestamped. - tx.setTime(Instant.now(), notaryNode.identity, 30.seconds) + tx.setTime(Instant.now(), 30.seconds) // Sign it as ourselves. tx.signWith(keyPair) From a2dff5488f1831292b072b16a5a5bc203ed4f287 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Mon, 8 Aug 2016 17:43:25 +0100 Subject: [PATCH 3/5] Add debugging information in case of missing signatures --- .../com/r3corda/core/contracts/Transactions.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 81a0f7ed9e..b709dca122 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -12,6 +12,7 @@ import com.r3corda.core.serialization.serialize import com.r3corda.core.utilities.Emoji import java.security.PublicKey import java.security.SignatureException +import java.util.* /** * Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object @@ -115,8 +116,20 @@ data class SignedTransaction(val txBits: SerializedBytes, // Now examine the contents and ensure the sigs we have line up with the advertised list of signers. val missing = getMissingSignatures() - if (missing.isNotEmpty() && throwIfSignaturesAreMissing) - throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}") + if (missing.isNotEmpty() && throwIfSignaturesAreMissing) { + // Take a best guess at where the signatures are required from, for debugging + // TODO: We need a much better way of structuring this data + val missingElements = ArrayList() + this.tx.commands.forEach { command -> + if (command.signers.any { signer -> missing.contains(signer) }) + missingElements.add(command.toString()) + } + this.tx.notary?.owningKey.apply { + if (missing.contains(this)) + missingElements.add("notary") + } + throw SignatureException("Missing signatures for ${missingElements} on transaction ${id.prefixChars()} for ${missing.map { it.toStringShort() }}") + } return missing } From a3d37a4d00e0bc58b535c468ce63779d700db415 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 4 Aug 2016 19:00:17 +0100 Subject: [PATCH 4/5] Require notary to be explicitely stated on each transaction --- .../contracts/JavaCommercialPaper.java | 2 +- .../clause/AbstractConserveAmount.kt | 11 +++- .../r3corda/contracts/testing/WalletFiller.kt | 9 ++- .../protocols/TwoPartyTradeProtocol.kt | 2 +- .../r3corda/contracts/CommercialPaperTests.kt | 6 +- .../kotlin/com/r3corda/contracts/IRSTests.kt | 2 +- .../com/r3corda/contracts/asset/CashTests.kt | 14 ++--- .../contracts/asset/ObligationTests.kt | 8 +-- .../core/contracts/TransactionBuilder.kt | 16 +++-- .../core/contracts/TransactionTools.kt | 2 +- .../core/contracts/TransactionTypes.kt | 19 +++++- .../core/contracts/TransactionVerification.kt | 3 + .../r3corda/core/contracts/Transactions.kt | 41 ++++++++----- .../com/r3corda/core/serialization/Kryo.kt | 4 +- .../com/r3corda/core/testing/CoreTestUtils.kt | 2 +- .../core/testing/LedgerDSLInterpreter.kt | 4 +- .../com/r3corda/core/testing/TestDSL.kt | 22 ++++--- .../core/testing/TransactionDSLInterpreter.kt | 2 +- .../r3corda/protocols/NotaryChangeProtocol.kt | 2 +- .../com/r3corda/protocols/NotaryProtocol.kt | 29 ++------- .../r3corda/protocols/TwoPartyDealProtocol.kt | 4 +- .../contracts/TransactionGraphSearchTests.kt | 9 ++- .../core/contracts/TransactionTypeTests.kt | 61 +++++++++++++++++++ .../TransactionSerializationTests.kt | 4 +- .../r3corda/contracts/AccountReceivable.kt | 2 +- .../com/r3corda/contracts/LCApplication.kt | 2 +- .../contracts/BillOfLadingAgreementTests.kt | 6 +- .../services/monitor/WalletMonitorService.kt | 4 +- .../messaging/TwoPartyTradeProtocolTests.kt | 17 +++++- .../node/services/NodeInterestRatesTest.kt | 4 +- .../node/services/NodeSchedulerServiceTest.kt | 4 +- .../node/services/NotaryChangeTests.kt | 2 +- .../node/services/NotaryServiceTests.kt | 8 +-- .../services/ValidatingNotaryServiceTests.kt | 4 +- .../node/services/WalletWithCashTest.kt | 9 ++- .../persistence/DataVendingServiceTests.kt | 4 +- .../kotlin/com/r3corda/demos/RateFixDemo.kt | 6 +- .../kotlin/com/r3corda/demos/TraderDemo.kt | 4 +- 38 files changed, 233 insertions(+), 121 deletions(-) create mode 100644 core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index f1841630ed..79289a9e10 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -340,7 +340,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 TransactionType.General.Builder().withItems(output, new Command(new Commands.Issue(notary), issuance.getParty().getOwningKey())); + return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(notary), issuance.getParty().getOwningKey())); } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index 976e3f8b94..eec0fa8035 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -66,11 +66,11 @@ abstract class AbstractConserveAmount, T: Any> : GroupClause val currency = amountIssued.token.product val amount = Amount(amountIssued.quantity, currency) var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } - val notary = acceptableCoins.firstOrNull()?.state?.notary + tx.notary = acceptableCoins.firstOrNull()?.state?.notary // TODO: We should be prepared to produce multiple transactions exiting inputs from // different notaries, or at least group states by notary and take the set with the // highest total value - acceptableCoins = acceptableCoins.filter { it.state.notary == notary } + acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary } val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency)) val takeChangeFrom = gathered.lastOrNull() @@ -128,13 +128,18 @@ abstract class AbstractConserveAmount, T: Any> : GroupClause // Finally, we add the states to the provided partial transaction. val currency = amount.token - val acceptableCoins = run { + var acceptableCoins = run { val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency } if (onlyFromParties != null) ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } else ofCurrency } + tx.notary = acceptableCoins.firstOrNull()?.state?.notary + // TODO: We should be prepared to produce multiple transactions spending inputs from + // different notaries, or at least group states by notary and take the set with the + // highest total value + acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary } val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) val takeChangeFrom = gathered.firstOrNull() diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt index e59f8bf086..ae6565e585 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt @@ -13,6 +13,8 @@ import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.Wallet import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.testing.DUMMY_NOTARY +import com.r3corda.core.testing.DUMMY_NOTARY_KEY +import java.security.KeyPair import java.security.PublicKey import java.util.* @@ -24,10 +26,11 @@ import java.util.* * * The service hub needs to provide at least a key management service and a storage service. * + * @param outputNotary the notary to use for output states. The transaction is NOT signed by this notary. * @return a wallet object that represents the generated states (it will NOT be the full wallet from the service hub!). */ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, - notary: Party = DUMMY_NOTARY, + outputNotary: Party = DUMMY_NOTARY, atLeastThisManyStates: Int = 3, atMostThisManyStates: Int = 10, rng: Random = Random(), @@ -40,8 +43,8 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, // We will allocate one state to one transaction, for simplicities sake. val cash = Cash() val transactions: List = amounts.map { pennies -> - val issuance = TransactionType.General.Builder() - cash.generateIssue(issuance, Amount(pennies, Issued(DUMMY_CASH_ISSUER.copy(reference = ref), howMuch.token)), myKey, notary) + val issuance = TransactionType.General.Builder(null) + cash.generateIssue(issuance, Amount(pennies, Issued(DUMMY_CASH_ISSUER.copy(reference = ref), howMuch.token)), myKey, outputNotary) issuance.signWith(DUMMY_CASH_ISSUER_KEY) return@map issuance.toSignedTransaction(true) diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index a785a53c11..53add46898 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -254,7 +254,7 @@ object TwoPartyTradeProtocol { } private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(notary) // 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() diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 4d8e65f6ca..a0280fc802 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -184,7 +184,7 @@ class CommercialPaperTestsGeneric { } fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { - val ltx = LedgerTransaction(emptyList(), listOf(*outputs), emptyList(), emptyList(), SecureHash.randomSHA256(), emptyList(), null, TransactionType.General()) + val ltx = LedgerTransaction(emptyList(), listOf(*outputs), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General()) return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } @@ -212,7 +212,7 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to BigCorp to own some of their debt. val moveTX: SignedTransaction = run { - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesWallet.statesOfType()) CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) ptx.signWith(bigCorpServices.key) @@ -222,7 +222,7 @@ class CommercialPaperTestsGeneric { } fun makeRedeemTX(time: Instant): SignedTransaction { - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.setTime(time, 30.seconds) CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType()) ptx.signWith(aliceServices.key) diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index 987ed0f752..753f922259 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -299,7 +299,7 @@ class IRSTests { while (true) { val nextFix: FixOf = currentIRS().nextFixingOf() ?: break val fixTX: SignedTransaction = run { - val tx = TransactionType.General.Builder() + val tx = TransactionType.General.Builder(DUMMY_NOTARY) val fixing = Fix(nextFix, "0.052".percent.value) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) with(tx) { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index dee6add98a..196241cc53 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -92,7 +92,7 @@ class CashTests { } // Test generation works. - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) 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 @@ -104,7 +104,7 @@ class CashTests { // Test issuance from the issuance definition val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) - val templatePtx = TransactionType.General.Builder() + val templatePtx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(templatePtx.inputStates().isEmpty()) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) @@ -171,14 +171,14 @@ class CashTests { @Test(expected = IllegalStateException::class) fun `reject issuance with inputs`() { // Issue some cash - var ptx = TransactionType.General.Builder() + var ptx = TransactionType.General.Builder(DUMMY_NOTARY) 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 = TransactionType.General.Builder() + ptx = TransactionType.General.Builder(DUMMY_NOTARY) 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) } @@ -384,13 +384,13 @@ class CashTests { * Generate an exit transaction, removing some amount of cash from the ledger. */ fun makeExit(amount: Amount, corp: Party, depositRef: Byte = 1): WireTransaction { - val tx = TransactionType.General.Builder() + val tx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), OUR_PUBKEY_1, WALLET) return tx.toWireTransaction() } fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { - val tx = TransactionType.General.Builder() + val tx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateSpend(tx, amount, dest, WALLET) return tx.toWireTransaction() } @@ -454,7 +454,7 @@ class CashTests { @Test fun generateSimpleSpendWithParties() { - val tx = TransactionType.General.Builder() + val tx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP)) assertEquals(WALLET[2].ref, tx.inputStates()[0]) } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index a865fbd736..3acc64a2d3 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -256,7 +256,7 @@ class ObligationTests { fun `generate payment net transaction with remainder`() { val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY) - val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + val tx = TransactionType.General.Builder(null).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(BOB_KEY) @@ -274,7 +274,7 @@ class ObligationTests { val dueBefore = TEST_TX_TIME - Duration.ofDays(7) // Generate a transaction issuing the obligation - var tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + var tx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity, beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) @@ -306,13 +306,13 @@ class ObligationTests { /** Test generating a transaction to settle an obligation. */ @Test fun `generate settlement transaction`() { - val cashTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + val cashTx = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction().tx // Generate a transaction issuing the obligation - val obligationTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + val obligationTx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) 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 18b3faa5ba..bf69b87694 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -16,13 +16,13 @@ import java.util.* * The builder can be customised for specific transaction types, e.g. where additional processing is needed * before adding a state/command. * - * @param notary The default notary that will be used for outputs that don't have a notary specified. When this is set, - * an output state can be added by just passing in a [ContractState] – a [TransactionState] with the - * default notary will be generated automatically. + * @param notary Notary used for the transaction. If null, this indicates the transaction DOES NOT have a notary. + * When this is set to a non-null value, an output state can be added by just passing in a [ContractState] – a + * [TransactionState] with this notary specified will be generated automatically. */ open class TransactionBuilder( protected val type: TransactionType = TransactionType.General(), - protected val notary: Party? = null, + var notary: Party? = null, protected val inputs: MutableList = arrayListOf(), protected val attachments: MutableList = arrayListOf(), protected val outputs: MutableList> = arrayListOf(), @@ -33,6 +33,10 @@ open class TransactionBuilder( @Deprecated("use timestamp instead") val time: Timestamp? get() = timestamp + init { + notary?.let { signers.add(it.owningKey) } + } + /** * Creates a copy of the builder. */ @@ -64,6 +68,7 @@ open class TransactionBuilder( = setTime(Timestamp(time, timeTolerance)) fun setTime(newTimestamp: Timestamp) { + check(notary != null) { "Only notarised transactions can have a timestamp" } check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } this.timestamp = newTimestamp } @@ -120,7 +125,7 @@ open class TransactionBuilder( } fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands), signers.toList(), type, timestamp) + ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timestamp) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { if (checkSufficientSignatures) { @@ -136,6 +141,7 @@ open class TransactionBuilder( fun addInputState(stateRef: StateRef, notary: Party) { check(currentSigs.isEmpty()) + require(notary == this.notary) { "Input state requires notary \"${notary}\" which does not match the transaction notary \"${this.notary}\"." } signers.add(notary.owningKey) inputs.add(stateRef) } 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 0d86f9adfd..c72e204625 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -23,7 +23,7 @@ fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) } - return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, timestamp, type) + return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, signers, timestamp, 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 dff0286c38..078009be88 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -17,6 +17,7 @@ sealed class TransactionType { * Note: Presence of _signatures_ is not checked, only the public keys to be signed for. */ fun verify(tx: LedgerTransaction) { + require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." } val missing = verifySigners(tx) if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList()) verifyTransaction(tx) @@ -45,14 +46,26 @@ 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) {} + class Builder(notary: Party?) : 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. */ override fun verifyTransaction(tx: LedgerTransaction) { - // TODO: Check that notary is unchanged + // Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there + // are any inputs, all outputs must have the same notary. + // TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more + // flexible on output notaries. + if (tx.notary != null + && tx.inputs.isNotEmpty()) { + tx.outputs.forEach { + if (it.notary != tx.notary) { + throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx, it.notary) + } + } + } + val ctx = tx.toTransactionForContract() // TODO: This will all be replaced in future once the sandbox and contract constraints work is done. @@ -78,7 +91,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(notary: Party? = null) : TransactionBuilder(NotaryChange(), notary) { + class Builder(notary: Party) : 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/contracts/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index 7ff6376adf..44ea14f1c7 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -97,4 +97,7 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: override fun toString() = "Signers missing: ${missing.map { it.toStringShort() }}" } class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null) + class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) { + override fun toString(): String = "Found unexpected notary change in transaction. Tx notary: ${tx.notary}, found: ${outputNotary}" + } } 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 b709dca122..da836c8476 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -2,8 +2,9 @@ package com.r3corda.core.contracts import com.esotericsoftware.kryo.Kryo import com.r3corda.core.crypto.DigitalSignature +import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.crypto.toStringShort +import com.r3corda.core.crypto.toStringsShort import com.r3corda.core.indexOfOrThrow import com.r3corda.core.serialization.SerializedBytes import com.r3corda.core.serialization.THREAD_LOCAL_KRYO @@ -49,6 +50,7 @@ data class WireTransaction(val inputs: List, val attachments: List, val outputs: List>, val commands: List, + val notary: Party?, val signers: List, val type: TransactionType, val timestamp: Timestamp?) : NamedByHash { @@ -117,23 +119,31 @@ data class SignedTransaction(val txBits: SerializedBytes, // Now examine the contents and ensure the sigs we have line up with the advertised list of signers. val missing = getMissingSignatures() if (missing.isNotEmpty() && throwIfSignaturesAreMissing) { - // Take a best guess at where the signatures are required from, for debugging - // TODO: We need a much better way of structuring this data - val missingElements = ArrayList() - this.tx.commands.forEach { command -> - if (command.signers.any { signer -> missing.contains(signer) }) - missingElements.add(command.toString()) - } - this.tx.notary?.owningKey.apply { - if (missing.contains(this)) - missingElements.add("notary") - } - throw SignatureException("Missing signatures for ${missingElements} on transaction ${id.prefixChars()} for ${missing.map { it.toStringShort() }}") + val missingElements = getMissingKeyDescriptions(missing) + throw SignatureException("Missing signatures for ${missingElements} on transaction ${id.prefixChars()} for ${missing.toStringsShort()}") } return missing } + /** + * Get a human readable description of where signatures are required from, and are missing, to assist in debugging + * the underlying cause. + */ + private fun getMissingKeyDescriptions(missing: Set): ArrayList { + // TODO: We need a much better way of structuring this data + val missingElements = ArrayList() + this.tx.commands.forEach { command -> + if (command.signers.any { signer -> missing.contains(signer) }) + missingElements.add(command.toString()) + } + this.tx.notary?.owningKey.apply { + if (missing.contains(this)) + missingElements.add("notary") + } + return missingElements + } + /** Returns the same transaction but with an additional (unchecked) signature */ fun withAdditionalSignature(sig: DigitalSignature.WithKey): SignedTransaction { // TODO: need to make sure the Notary signs last @@ -153,6 +163,7 @@ data class SignedTransaction(val txBits: SerializedBytes, * Returns the set of missing signatures - a signature must be present for each signer public key. */ private fun getMissingSignatures(): Set { + val notaryKey = tx.notary?.owningKey val requiredKeys = tx.signers.toSet() val sigKeys = sigs.map { it.by }.toSet() @@ -175,8 +186,10 @@ data class LedgerTransaction( val commands: List>, /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */ val attachments: List, - /** The hash of the original serialised WireTransaction */ + /** The hash of the original serialised WireTransaction. */ override val id: SecureHash, + /** The notary for this party, may be null for transactions with no notary. */ + val notary: Party?, /** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */ val signers: List, val timestamp: Timestamp?, 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 30fbfb8ae3..90bc4495ad 100644 --- a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt @@ -230,6 +230,7 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.attachments) kryo.writeClassAndObject(output, obj.outputs) kryo.writeClassAndObject(output, obj.commands) + kryo.writeClassAndObject(output, obj.notary) kryo.writeClassAndObject(output, obj.signers) kryo.writeClassAndObject(output, obj.type) kryo.writeClassAndObject(output, obj.timestamp) @@ -261,11 +262,12 @@ object WireTransactionSerializer : Serializer() { kryo.useClassLoader(classLoader) { val outputs = kryo.readClassAndObject(input) as List> val commands = kryo.readClassAndObject(input) as List + val notary = kryo.readClassAndObject(input) as Party? val signers = kryo.readClassAndObject(input) as List val transactionType = kryo.readClassAndObject(input) as TransactionType val timestamp = kryo.readClassAndObject(input) as Timestamp? - return WireTransaction(inputs, attachmentHashes, outputs, commands, signers, transactionType, timestamp) + return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, signers, transactionType, timestamp) } } } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/CoreTestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/CoreTestUtils.kt index d6663cf111..51a4d5afaa 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/CoreTestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/CoreTestUtils.kt @@ -111,6 +111,6 @@ fun freeLocalHostAndPort(): HostAndPort { */ @JvmOverloads fun transaction( transactionLabel: String? = null, - transactionBuilder: TransactionBuilder = TransactionBuilder(), + transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index e4f8ee4cc5..b07bb8ea72 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -126,14 +126,14 @@ class LedgerDSL.() -> EnforceVerifyOrFail) = _transaction(label, transactionBuilder, dsl) /** * @see LedgerDSLInterpreter._unverifiedTransaction */ @JvmOverloads - fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), + fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), dsl: TransactionDSL.() -> Unit) = _unverifiedTransaction(label, transactionBuilder, dsl) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index 436b92f113..6d3255c1d9 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -130,7 +130,12 @@ data class TestTransactionDSLInterpreter private constructor( } override fun verifies(): EnforceVerifyOrFail { - toWireTransaction().toLedgerTransaction(services).verify() + // Verify on a copy of the transaction builder, so if it's then further modified it doesn't error due to + // the existing signature + transactionBuilder.copy().apply { + signWith(DUMMY_NOTARY_KEY) + toWireTransaction().toLedgerTransaction(services).verify() + } return EnforceVerifyOrFail.Token } @@ -305,15 +310,18 @@ data class TestLedgerDSLInterpreter private constructor ( * @return List of [SignedTransaction]s. */ fun signAll(transactionsToSign: List, extraKeys: List) = transactionsToSign.map { wtx -> - val allPubKeys = wtx.signers.toMutableSet() + check(wtx.signers.isNotEmpty()) val bits = wtx.serialize() require(bits == wtx.serialized) val signatures = ArrayList() - for (key in ALL_TEST_KEYS + extraKeys) { - if (key.public in allPubKeys) { - signatures += key.signWithECDSA(bits) - allPubKeys -= key.public - } + val keyLookup = HashMap() + + (ALL_TEST_KEYS + extraKeys).forEach { + keyLookup[it.public] = it + } + wtx.signers.forEach { + val key = keyLookup[it] ?: throw IllegalArgumentException("Missing required key for ${it.toStringShort()}") + signatures += key.signWithECDSA(bits) } SignedTransaction(bits, signatures) } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index 00c7a39acc..d9265f4949 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -52,7 +52,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { */ fun timestamp(data: Timestamp) - /** + /** * Creates a local scoped copy of the transaction. * @param dsl The transaction DSL to be interpreted using the copy. */ diff --git a/core/src/main/kotlin/com/r3corda/protocols/NotaryChangeProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/NotaryChangeProtocol.kt index 31937b9391..9321d12df9 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/NotaryChangeProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/NotaryChangeProtocol.kt @@ -37,7 +37,7 @@ object NotaryChangeProtocol: AbstractStateReplacementProtocol() { val state = originalState.state val newState = state.withNotary(modification) val participants = state.data.participants - val tx = TransactionType.NotaryChange.Builder().withItems(originalState, newState) + val tx = TransactionType.NotaryChange.Builder(originalState.state.notary).withItems(originalState, newState) tx.signWith(serviceHub.storageService.myLegalIdentityKey) val stx = tx.toSignedTransaction(false) diff --git a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt index c2118e49b6..d7b8813a71 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt @@ -52,7 +52,9 @@ object NotaryProtocol { @Suspendable override fun call(): DigitalSignature.LegallyIdentifiable { progressTracker.currentStep = REQUESTING - notaryParty = findNotaryParty() + val wtx = stx.tx + notaryParty = wtx.notary ?: throw IllegalStateException("Transaction does not specify a Notary") + check(wtx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { "Input states must have the same Notary" } val sendSessionID = random63BitValue() val receiveSessionID = random63BitValue() @@ -83,19 +85,6 @@ object NotaryProtocol { check(sig.signer == notaryParty) { "Notary result not signed by the correct service" } sig.verifyWithECDSA(data) } - - private fun findNotaryParty(): Party { - val wtx = stx.tx - val firstStateRef = wtx.inputs.firstOrNull() - var maybeNotaryParty: Party? = if (firstStateRef == null) - null - else - serviceHub.loadState(firstStateRef).notary - - val notaryParty = maybeNotaryParty ?: throw IllegalStateException("Transaction does not specify a Notary") - check(wtx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { "Input states must have the same Notary" } - return notaryParty - } } /** @@ -133,12 +122,11 @@ object NotaryProtocol { send(otherSide, sendSessionID, result) } - private fun validateTimestamp(tx: WireTransaction) = + private fun validateTimestamp(tx: WireTransaction) { if (tx.timestamp != null - && !timestampChecker.isValid(tx.timestamp)) + && !timestampChecker.isValid(tx.timestamp)) throw NotaryException(NotaryError.TimestampInvalid()) - else - Unit + } /** * No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully @@ -212,11 +200,6 @@ sealed class NotaryError { override fun toString() = "One or more input states for transaction ${tx.id} have been used in another transaction" } - class MoreThanOneTimestamp : NotaryError() - - /** Thrown if the timestamp command in the transaction doesn't list this Notary as a signer */ - class NotForMe : NotaryError() - /** Thrown if the time specified in the timestamp command is outside the allowed tolerance */ class TimestampInvalid : NotaryError() diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index d5f4fe9347..a1b09a49cd 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -101,11 +101,9 @@ object TwoPartyDealProtocol { untrustedPartialTX.validate { stx -> progressTracker.nextStep() - // TODO: Verify the notary on the transaction is set correctly - // Check that the tx proposed by the buyer is valid. val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false) - if (missingSigs != setOf(myKeyPair.public)) + if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey)) throw SignatureException("The set of missing signatures is not as expected: $missingSigs") val wtx: WireTransaction = stx.tx 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 e181996ce0..a48ffe14fa 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt @@ -3,6 +3,7 @@ package com.r3corda.core.contracts import com.r3corda.core.crypto.newSecureRandom import com.r3corda.core.node.services.testing.MockTransactionStorage import com.r3corda.core.testing.DUMMY_NOTARY +import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.core.testing.MEGA_CORP_KEY import org.junit.Test import java.security.KeyPair @@ -26,14 +27,16 @@ class TransactionGraphSearchTests { * @param signer signer for the two transactions and their commands. */ fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage { - val originTx = TransactionType.General.Builder().apply { - addOutputState(DummyState(random31BitValue()), DUMMY_NOTARY) + val originTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + addOutputState(DummyState(random31BitValue())) addCommand(command, signer.public) signWith(signer) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction(false) - val inputTx = TransactionType.General.Builder().apply { + val inputTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { addInputState(originTx.tx.outRef(0)) signWith(signer) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction(false) return GraphTransactionStorage(originTx, inputTx) } diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt new file mode 100644 index 0000000000..8203b4eece --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt @@ -0,0 +1,61 @@ +package com.r3corda.core.contracts + +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.testing.* +import org.junit.Test +import kotlin.test.assertFailsWith + +class TransactionTypeTests { + @Test + fun `transactions with no inputs can have any notary`() { + val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) + val inputs = emptyList>() + val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB)) + val commands = emptyList>() + val attachments = emptyList() + val id = SecureHash.randomSHA256() + val signers = listOf(DUMMY_NOTARY_KEY.public) + val timestamp: Timestamp? = null + val transaction: LedgerTransaction = LedgerTransaction( + inputs, + outputs, + commands, + attachments, + id, + null, + signers, + timestamp, + TransactionType.General() + ) + + transaction.type.verify(transaction) + } + + @Test + fun `general transactions cannot change notary`() { + val notary: Party = DUMMY_NOTARY + val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), notary) + val outState = inState.copy(notary = ALICE) + val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0))) + val outputs = listOf(outState) + val commands = emptyList>() + val attachments = emptyList() + val id = SecureHash.randomSHA256() + val signers = listOf(DUMMY_NOTARY_KEY.public) + val timestamp: Timestamp? = null + val transaction: LedgerTransaction = LedgerTransaction( + inputs, + outputs, + commands, + attachments, + id, + notary, + signers, + timestamp, + TransactionType.General() + ) + + assertFailsWith { transaction.type.verify(transaction) } + } +} \ No newline at end of file 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 1531740701..b5654e95a8 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -49,7 +49,7 @@ class TransactionSerializationTests { @Before fun setup() { - tx = TransactionType.General.Builder().withItems( + tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems( inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(DUMMY_KEY_1.public)) ) } @@ -88,7 +88,7 @@ class TransactionSerializationTests { // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { - val tx2 = TransactionType.General.Builder().withItems(inputState, outputState, changeState, + val tx2 = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) tx2.signWith(DUMMY_NOTARY_KEY) tx2.signWith(DUMMY_KEY_2) diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt b/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt index 4163e2f570..da4c940651 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/AccountReceivable.kt @@ -84,7 +84,7 @@ class AccountReceivable : Contract { throw IllegalArgumentException("Cannot build AR with an already assigned invoice") } val ar = createARFromInvoice(invoice.state.data, discountRate, notary) - val tx = TransactionType.General.Builder() + val tx = TransactionType.General.Builder(notary) tx.addInputState(invoice) tx.addOutputState(invoice.state.data.copy(assigned = true)) tx.addCommand(Invoice.Commands.Assign(), invoice.state.data.owner.owningKey) diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt b/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt index 6d7f4ac61f..d825709b86 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt @@ -135,7 +135,7 @@ class LCApplication : Contract { fun generateApply(props: LCApplicationProperties, notary: Party, purchaseOrder: Attachment): TransactionBuilder { val state = State(props.issuer.owningKey, Status.PENDING_ISSUER_REVIEW, props) - val txBuilder = TransactionType.General.Builder().withItems(state, Command(Commands.ApplyForLC(), props.applicant.owningKey)) + val txBuilder = TransactionType.General.Builder(notary).withItems(state, Command(Commands.ApplyForLC(), props.applicant.owningKey)) txBuilder.addAttachment(purchaseOrder.id) return txBuilder } diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt b/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt index cf634be1d4..940dc4a996 100644 --- a/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt +++ b/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt @@ -93,7 +93,7 @@ class BillOfLadingAgreementTests { @Test(expected = IllegalStateException::class) fun transferAndEndorseGenerationMethod_MissingBeneficiarySignature() { - val ptx:TransactionBuilder = TransactionType.General.Builder() + val ptx:TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY) val sr = StateAndRef( TransactionState(Bill, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) @@ -105,7 +105,7 @@ class BillOfLadingAgreementTests { @Test(expected = IllegalStateException::class) fun transferAndEndorseGenerationMethod_MissingOwnerSignature() { - val ptx:TransactionBuilder = TransactionType.General.Builder() + val ptx:TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY) val sr = StateAndRef( TransactionState(Bill, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) @@ -129,7 +129,7 @@ class BillOfLadingAgreementTests { @Test(expected = IllegalStateException::class) fun transferPossessionGenerationMethod_Unsigned() { - val ptx:TransactionBuilder = TransactionType.General.Builder() + val ptx:TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY) val sr = StateAndRef( TransactionState(Bill, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) diff --git a/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt b/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt index 5b901af5fe..8894af7ab0 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt @@ -164,7 +164,7 @@ class WalletMonitorService(net: MessagingService, val smm: StateMachineManager, // TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service private fun initatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult { - val builder: TransactionBuilder = TransactionType.General.Builder() + val builder: TransactionBuilder = TransactionType.General.Builder(null) // TODO: Have some way of restricting this to states the caller controls try { Cash().generateSpend(builder, Amount(req.pennies, req.tokenDef.product), req.owner, @@ -185,7 +185,7 @@ class WalletMonitorService(net: MessagingService, val smm: StateMachineManager, // TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult { - val builder: TransactionBuilder = TransactionType.General.Builder() + val builder: TransactionBuilder = TransactionType.General.Builder(null) val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef) Cash().generateExit(builder, Amount(req.pennies, Issued(issuer, req.currency)), issuer.party.owningKey, services.walletService.currentWallet.statesOfType()) 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 bfb8899647..91b87be68f 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -39,6 +39,7 @@ import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull import kotlin.test.assertTrue /** @@ -407,7 +408,14 @@ class TwoPartyTradeProtocolTests { bobResult.get() } assertTrue(e.cause is TransactionVerificationException) - assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring)) + assertNotNull(e.cause!!.cause) + assertNotNull(e.cause!!.cause!!.message) + val underlyingMessage = e.cause!!.cause!!.message!! + if (underlyingMessage.contains(expectedMessageSubstring)) { + assertTrue(underlyingMessage.contains(expectedMessageSubstring)) + } else { + assertEquals(expectedMessageSubstring, underlyingMessage) + } } private fun insertFakeTransactions( @@ -425,8 +433,8 @@ class TwoPartyTradeProtocolTests { private fun LedgerDSL.fillUpForBuyer( withError: Boolean, - owner: PublicKey = BOB_PUBKEY, - issuer: PartyAndReference = DUMMY_CASH_ISSUER): Pair> { + owner: PublicKey = BOB_PUBKEY): Pair> { + val issuer = DUMMY_CASH_ISSUER // Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she // wants to sell to Bob. val eb1 = transaction { @@ -435,6 +443,9 @@ class TwoPartyTradeProtocolTests { output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } if (!withError) command(DUMMY_CASH_ISSUER_KEY.public) { Cash.Commands.Issue() } + else + // Put a broken command on so at least a signature is created + command(DUMMY_CASH_ISSUER_KEY.public) { Cash.Commands.Move() } timestamp(TEST_TX_TIME) if (withError) { this.fails() 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 ea29a4bf51..3dfeae6b26 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt @@ -106,7 +106,7 @@ class NodeInterestRatesTest { val (n1, n2) = net.createTwoNodes() n2.findService().oracle.knownFixes = TEST_DATA - val tx = TransactionType.General.Builder() + val tx = TransactionType.General.Builder(null) val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val protocol = RatesFixProtocol(tx, n2.info.identity, fixOf, "0.675".bd, "0.1".bd) LogHelper.setLevel("rates") @@ -122,5 +122,5 @@ class NodeInterestRatesTest { assertEquals("0.678".bd, fix.value) } - private fun makeTX() = TransactionType.General.Builder().withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY) + private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY) } diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt index 93312e4f40..2201217c6b 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt @@ -232,8 +232,8 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { apply { val freshKey = services.keyManagementService.freshKey() val state = TestState(factory.create(TestProtocolLogic::class.java, increment), instant) - val usefulTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - addOutputState(state) + val usefulTX = TransactionType.General.Builder(null).apply { + addOutputState(state, DUMMY_NOTARY) addCommand(Command(), freshKey.public) signWith(freshKey) }.toSignedTransaction() diff --git a/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt index ed1bc61e11..5e81eec3a3 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt @@ -107,7 +107,7 @@ fun issueState(node: AbstractNode): StateAndRef<*> { 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) + val tx = TransactionType.NotaryChange.Builder(DUMMY_NOTARY).withItems(state) tx.signWith(nodeA.storage.myLegalIdentityKey) tx.signWith(nodeB.storage.myLegalIdentityKey) tx.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 5423750e14..eaa1a830a6 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt @@ -39,7 +39,7 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction with a valid timestamp`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionType.General.Builder().withItems(inputState) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState) tx.setTime(Instant.now(), 30.seconds) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) @@ -56,7 +56,7 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction without a timestamp`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionType.General.Builder().withItems(inputState) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) } @@ -72,7 +72,7 @@ class NotaryServiceTests { @Test fun `should report error for transaction with an invalid timestamp`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionType.General.Builder().withItems(inputState) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState) tx.setTime(Instant.now().plusSeconds(3600), 30.seconds) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) @@ -91,7 +91,7 @@ class NotaryServiceTests { @Test fun `should report conflict for a duplicate transaction`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionType.General.Builder().withItems(inputState) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) } 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 44f08ae94f..f430be55e1 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt @@ -39,7 +39,7 @@ class ValidatingNotaryServiceTests { @Test fun `should report error for invalid transaction dependency`() { val stx = run { val inputState = issueInvalidState(clientNode) - val tx = TransactionType.General.Builder().withItems(inputState) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) } @@ -59,7 +59,7 @@ class ValidatingNotaryServiceTests { val inputState = issueState(clientNode) val command = Command(DummyContract.Commands.Move(), expectedMissingKey) - val tx = TransactionType.General.Builder().withItems(inputState, command) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState, command) tx.signWith(clientNode.keyPair!!) tx.toSignedTransaction(false) } diff --git a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt index 7d7f4f7b44..d27a75ecf4 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt @@ -71,21 +71,21 @@ class WalletWithCashTest { fun basics() { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() - val usefulTX = TransactionType.General.Builder().apply { + val usefulTX = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() val myOutput = usefulTX.toLedgerTransaction(services).outRef(0) // A tx that spends our money. - val spendTX = TransactionType.General.Builder().apply { + val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput)) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() // A tx that doesn't send us anything. - val irrelevantTX = TransactionType.General.Builder().apply { + val irrelevantTX = TransactionType.General.Builder(DUMMY_NOTARY).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) @@ -112,6 +112,7 @@ class WalletWithCashTest { val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) signWith(freshKey) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() wallet.notify(dummyIssue.tx) @@ -121,6 +122,7 @@ class WalletWithCashTest { val dummyIssue2 = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) signWith(freshKey) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() assertThatThrownBy { @@ -139,6 +141,7 @@ class WalletWithCashTest { val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) signWith(freshKey) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() wallet.notify(dummyIssue.tx) diff --git a/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt index 12d783c24c..44db3c9097 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt @@ -34,7 +34,7 @@ class DataVendingServiceTests { network.runNetwork() // Generate an issuance transaction - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(null) Cash().generateIssue(ptx, Amount(100, Issued(deposit, USD)), beneficiary, DUMMY_NOTARY) // Complete the cash transaction, and then manually relay it @@ -66,7 +66,7 @@ class DataVendingServiceTests { network.runNetwork() // Generate an issuance transaction - val ptx = TransactionType.General.Builder() + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateIssue(ptx, Amount(100, Issued(deposit, USD)), beneficiary, DUMMY_NOTARY) // The transaction tries issuing MEGA_CORP cash, but we aren't the issuer, so it's invalid diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index b4dca71554..139661a05b 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -85,11 +85,11 @@ fun main(args: Array) { val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, apiAddr, config, networkMapAddress, advertisedServices, DemoClock()).setup().start() } node.networkMapRegistrationFuture.get() - val notary = node.services.networkMapCache.notaryNodes[0] + val notaryNode = node.services.networkMapCache.notaryNodes[0] // Make a garbage transaction that includes a rate fix. - 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 tx = TransactionType.General.Builder(notaryNode.identity) + tx.addOutputState(TransactionState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public), notaryNode.identity)) val protocol = RatesFixProtocol(tx, oracleNode.identity, 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 3b8ef56ee2..8b1d62a044 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -208,7 +208,7 @@ private fun runBuyer(node: Node, amount: Amount) { // // TODO: At some point this demo should be extended to have a central bank node. node.services.fillWithSomeTestCash(3000.DOLLARS, - notary = node.info.identity, // In this demo, the buyer and notary are the same. + outputNotary = node.info.identity, // In this demo, the buyer and notary are the same. ownedBy = node.services.keyManagementService.freshKey().public) // Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band @@ -368,7 +368,7 @@ private class TraderDemoProtocolSeller(val otherSide: Party, // 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 = TransactionType.General.Builder() + val builder = TransactionType.General.Builder(notaryNode.identity) CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) builder.signWith(keyPair) val notarySignature = subProtocol(NotaryProtocol.Client(builder.toSignedTransaction(false))) From 8cfc88ed3a7abc745bac6069a4e4a8b3afa5f0e5 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 12 Aug 2016 13:51:16 +0100 Subject: [PATCH 5/5] Remove unused timestamp from LCApplication --- .../src/main/kotlin/com/r3corda/contracts/LCApplication.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt b/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt index d825709b86..079f86c66c 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/LCApplication.kt @@ -30,12 +30,6 @@ class LCApplication : Contract { val inputs = tx.inputs.filterIsInstance() val outputs = tx.outputs.filterIsInstance() - // Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be - // hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about - // who or what is a trusted authority. - // FIXME: This isn't used anywhere - tx.timestamp - when (command.value) { is Commands.ApplyForLC -> { verifyApply(inputs, outputs, command as AuthenticatedObject, tx)