diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 856fac0635..240f2ff6e7 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -131,6 +131,12 @@ public class JavaCommercialPaper implements Contract { } public static class Redeem extends Commands { + private final Party notary; + + public Redeem(Party setNotary) { + this.notary = setNotary; + } + @Override public boolean equals(Object obj) { return obj instanceof Redeem; @@ -138,6 +144,12 @@ public class JavaCommercialPaper implements Contract { } public static class Issue extends Commands { + private final Party notary; + + public Issue(Party setNotary) { + this.notary = setNotary; + } + @Override public boolean equals(Object obj) { return obj instanceof Issue; @@ -163,6 +175,7 @@ public class JavaCommercialPaper implements Contract { // For now do not allow multiple pieces of CP to trade in a single transaction. if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { + Commands.Issue issueCommand = (Commands.Issue) cmd.getValue(); State output = single(outputs); if (!inputs.isEmpty()) { throw new IllegalStateException("Failed Requirement: output values sum to more than the inputs"); @@ -171,7 +184,7 @@ public class JavaCommercialPaper implements Contract { throw new IllegalStateException("Failed Requirement: output values sum to more than the inputs"); } - TimestampCommand timestampCommand = tx.getTimestampByName("Notary Service"); + TimestampCommand timestampCommand = tx.getTimestampBy(issueCommand.notary); if (timestampCommand == null) throw new IllegalArgumentException("Failed Requirement: must be timestamped"); @@ -201,7 +214,7 @@ public class JavaCommercialPaper implements Contract { !output.getMaturityDate().equals(input.getMaturityDate())) throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner"); } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { - TimestampCommand timestampCommand = tx.getTimestampByName("Notary Service"); + TimestampCommand timestampCommand = tx.getTimestampBy(((Commands.Redeem) cmd.getValue()).notary); if (timestampCommand == null) throw new IllegalArgumentException("Failed Requirement: must be timestamped"); Instant time = timestampCommand.getBefore(); @@ -232,13 +245,13 @@ 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(), issuance.getParty().getOwningKey())); + return new TransactionType.General.Builder().withItems(output, new Command(new Commands.Issue(notary), issuance.getParty().getOwningKey())); } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { new Cash().generateSpend(tx, paper.getState().getData().getFaceValue(), paper.getState().getData().getOwner(), wallet); tx.addInputState(paper); - tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner())); + tx.addCommand(new Command(new Commands.Redeem(paper.getState().getNotary()), paper.getState().getData().getOwner())); } public void generateMove(TransactionBuilder tx, StateAndRef paper, PublicKey newOwner) { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 0299d65075..aea2800def 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -65,11 +65,11 @@ class CommercialPaper : Contract { } interface Commands : CommandData { - class Move : TypeOnlyCommandData(), Commands - class Redeem : TypeOnlyCommandData(), Commands + class Move: TypeOnlyCommandData(), Commands + data class Redeem(val notary: Party) : Commands // We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. // However, nothing in the platform enforces that uniqueness: it's up to the issuer. - class Issue : TypeOnlyCommandData(), Commands + data class Issue(val notary: Party) : Commands } override fun verify(tx: TransactionForContract) { @@ -79,11 +79,13 @@ class CommercialPaper : 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() - - // 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. - val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") + // If it's an issue, we can't take notary from inputs, so it must be specified in the command + val timestamp: TimestampCommand? = if (command.value is Commands.Issue) + tx.getTimestampBy((command.value as Commands.Issue).notary) + else if (command.value is Commands.Redeem) + tx.getTimestampBy((command.value as Commands.Redeem).notary) + else + null for ((inputs, outputs, key) in groups) { when (command.value) { @@ -139,7 +141,7 @@ class CommercialPaper : Contract { fun generateIssue(faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { val issuance = faceValue.token.issuer val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary) - return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(notary), issuance.party.owningKey)) } /** @@ -164,7 +166,7 @@ class CommercialPaper : Contract { val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } Cash().generateSpend(tx, amount, paper.state.data.owner, wallet) tx.addInputState(paper) - tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner) + tx.addCommand(CommercialPaper.Commands.Redeem(paper.state.notary), paper.state.data.owner) } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 33c2c0bb2d..b36d935e92 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -496,6 +496,8 @@ class InterestRateSwap() : Contract { val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID } val command = tx.commands.requireSingleCommand() + // TODO: This needs to either be the notary used for the inputs, or otherwise + // derived as the correct notary val time = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")?.midpoint if (time == null) throw IllegalArgumentException("must be timestamped") 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 6021c10e1f..f88a23d087 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -390,9 +390,7 @@ class Obligation

: Contract { for ((stateIdx, input) in inputs.withIndex()) { val actualOutput = outputs[stateIdx] val deadline = input.dueBefore - // TODO: Determining correct timestamp authority needs rework now that timestamping service is part of - // notary. - val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") + val timestamp: TimestampCommand? = tx.timestamp val expectedOutput: State

= input.copy(lifecycle = expectedOutputLifecycle) requireThat { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 7c436e6db2..093fda24bc 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -3,6 +3,7 @@ package com.r3corda.contracts import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.testing.* import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.days import com.r3corda.core.node.services.testing.MockStorageService @@ -18,8 +19,8 @@ import kotlin.test.assertTrue interface ICommercialPaperTestTemplate { fun getPaper(): ICommercialPaperState - fun getIssueCommand(): CommandData - fun getRedeemCommand(): CommandData + fun getIssueCommand(notary: Party): CommandData + fun getRedeemCommand(notary: Party): CommandData fun getMoveCommand(): CommandData } @@ -31,8 +32,8 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate { TEST_TX_TIME + 7.days ) - override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue() - override fun getRedeemCommand(): CommandData = JavaCommercialPaper.Commands.Redeem() + override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue(notary) + override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem(notary) override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move() } @@ -44,8 +45,8 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate { maturityDate = TEST_TX_TIME + 7.days ) - override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue() - override fun getRedeemCommand(): CommandData = CommercialPaper.Commands.Redeem() + override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue(notary) + override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem(notary) override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move() } @@ -74,7 +75,7 @@ class CommercialPaperTestsGeneric { // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { output("paper") { thisTest.getPaper() } - command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } timestamp(TEST_TX_TIME) this.verifies() } @@ -103,7 +104,7 @@ class CommercialPaperTestsGeneric { } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - command(ALICE_PUBKEY) { thisTest.getRedeemCommand() } + command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) } tweak { outputs(700.DOLLARS `issued by` issuer) @@ -133,7 +134,7 @@ class CommercialPaperTestsGeneric { fun `key mismatch at issue`() { transaction { output { thisTest.getPaper() } - command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() } + command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand(DUMMY_NOTARY) } timestamp(TEST_TX_TIME) this `fails with` "output states are issued by a command signer" } @@ -143,7 +144,7 @@ class CommercialPaperTestsGeneric { fun `face value is not zero`() { transaction { output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } - command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } timestamp(TEST_TX_TIME) this `fails with` "output values sum to more than the inputs" } @@ -153,7 +154,7 @@ class CommercialPaperTestsGeneric { fun `maturity date not in the past`() { transaction { output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) } - command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } timestamp(TEST_TX_TIME) this `fails with` "maturity date is not in the past" } @@ -164,7 +165,7 @@ class CommercialPaperTestsGeneric { transaction { input(thisTest.getPaper()) output { thisTest.getPaper() } - command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } timestamp(TEST_TX_TIME) this `fails with` "output values sum to more than the inputs" } 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 529ad5f8df..45aa0ef037 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -96,6 +96,7 @@ fun List>.getTimestampBy(timestampingAuthority: * 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() } 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 2048474434..09a4cb6f10 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -316,6 +316,9 @@ data class AuthenticatedObject( * 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) */ +// 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 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/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index bd83d1a629..b732d2b6d1 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -73,7 +73,8 @@ data class TransactionForVerification(val inputs: List, val outputs: List, val attachments: List, val commands: List>, - val origHash: SecureHash) { + val origHash: SecureHash, + val inputNotary: Party? = null) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash @@ -158,10 +160,15 @@ 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. */ fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) /** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */ + @Deprecated(message = "Timestamping authority should always be notary for the transaction") fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName) } @@ -174,4 +181,4 @@ sealed class TransactionVerificationException(val tx: TransactionForVerification class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null) class SignersMissing(tx: TransactionForVerification, missing: List) : TransactionVerificationException(tx, null) class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null) -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 62863f46dc..e1d28def5e 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -481,7 +481,7 @@ class TwoPartyTradeProtocolTests { output("alice's paper") { CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days) } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue(notary) } if (!withError) timestamp(time = TEST_TX_TIME, notary = notary.owningKey) if (attachmentID != null)