Remove support for timestamp commands

This commit is contained in:
Ross Nicoll 2016-07-13 11:05:31 +01:00
parent f0aa5a30d4
commit 17ae349f4d
34 changed files with 98 additions and 205 deletions

View File

@ -3,6 +3,9 @@ package com.r3corda.contracts;
import com.google.common.collect.*; import com.google.common.collect.*;
import com.r3corda.contracts.asset.*; import com.r3corda.contracts.asset.*;
import com.r3corda.core.contracts.*; 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.TransactionForContract.*;
import com.r3corda.core.contracts.clauses.*; import com.r3corda.core.contracts.clauses.*;
import com.r3corda.core.crypto.*; import com.r3corda.core.crypto.*;
@ -219,15 +222,14 @@ public class JavaCommercialPaper implements Contract {
if (!cmd.getSigners().contains(input.getOwner())) if (!cmd.getSigners().contains(input.getOwner()))
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
Party notary = cmd.getValue().notary; Timestamp timestamp = tx.getTimestamp();
TimestampCommand timestampCommand = tx.getTimestampBy(notary); Instant time = null == timestamp
Instant time = null == timestampCommand
? null ? null
: timestampCommand.getBefore(); : timestamp.getBefore();
Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner()); Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner());
requireThat(require -> { requireThat(require -> {
require.by("must be timestamped", timestampCommand != null); require.by("must be timestamped", timestamp != null);
require.by("received amount equals the face value: " require.by("received amount equals the face value: "
+ received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue())); + received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue()));
require.by("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate())); require.by("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate()));
@ -257,7 +259,7 @@ public class JavaCommercialPaper implements Contract {
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
State output = single(outputs); State output = single(outputs);
Party notary = cmd.getValue().notary; Party notary = cmd.getValue().notary;
TimestampCommand timestampCommand = tx.getTimestampBy(notary); Timestamp timestampCommand = tx.getTimestamp();
Instant time = null == timestampCommand Instant time = null == timestampCommand
? null ? null
: timestampCommand.getBefore(); : timestampCommand.getBefore();

View File

@ -109,8 +109,7 @@ class CommercialPaper : Contract {
token: Issued<Terms>): Set<CommandData> { token: Issued<Terms>): Set<CommandData> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, token) val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
val command = commands.requireSingleCommand<Commands.Issue>() val command = commands.requireSingleCommand<Commands.Issue>()
// If it's an issue, we can't take notary from inputs, so it must be specified in the command val timestamp = tx.timestamp
val timestamp: TimestampCommand? = tx.getTimestampBy(command.value.notary)
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" } 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) // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
// before requiring a single command // before requiring a single command
val command = commands.requireSingleCommand<Commands.Redeem>() val command = commands.requireSingleCommand<Commands.Redeem>()
// If it's an issue, we can't take notary from inputs, so it must be specified in the command val timestamp = tx.timestamp
val timestamp: TimestampCommand? = tx.getTimestampBy(command.value.notary)
val input = inputs.single() val input = inputs.single()
val received = tx.outputs.sumCashBy(input.owner) val received = tx.outputs.sumCashBy(input.owner)

View File

@ -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 // 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. // it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaperLegacy.Commands>() val command = tx.commands.requireSingleCommand<CommercialPaperLegacy.Commands>()
// If it's an issue, we can't take notary from inputs, so it must be specified in the command val timestamp: Timestamp? = tx.timestamp
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
}
for ((inputs, outputs, key) in groups) { for ((inputs, outputs, key) in groups) {
when (command.value) { when (command.value) {

View File

@ -447,8 +447,8 @@ class InterestRateSwap() : Contract {
fixingCalendar, index, indexSource, indexTenor) fixingCalendar, index, indexSource, indexTenor)
} }
private fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>> fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>() + tx.commands.select<TimestampCommand>() = tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx)) 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<Class<out CommandData>>() override val requiredCommands = emptySet<Class<out CommandData>>()
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> { override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
// TODO: This needs to either be the notary used for the inputs, or otherwise require(tx.timestamp?.midpoint != null) { "must be timestamped" }
// derived as the correct notary // We return an empty set because we don't process any commands
@Suppress("DEPRECATION") return emptySet()
val command = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
?: throw IllegalArgumentException("must be timestamped")
return setOf(command)
} }
} }

View File

@ -403,10 +403,7 @@ class Obligation<P> : Contract {
if (input is State<P>) { if (input is State<P>) {
val actualOutput = outputs[stateIdx] val actualOutput = outputs[stateIdx]
val deadline = input.dueBefore val deadline = input.dueBefore
val timestamp: TimestampCommand? = if (tx.inputNotary == null) val timestamp = tx.timestamp
null
else
tx.getTimestampBy(tx.inputNotary!!)
val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle) val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle)
requireThat { requireThat {
@ -544,7 +541,7 @@ class Obligation<P> : Contract {
} }
tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.distinct()) tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.distinct())
} }
tx.setTime(issuanceDef.dueBefore, notary, issuanceDef.timeTolerance) tx.setTime(issuanceDef.dueBefore, issuanceDef.timeTolerance)
} }
/** /**

View File

@ -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 // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one. // to have one.
val currentTime = serviceHub.clock.instant() val currentTime = serviceHub.clock.instant()
ptx.setTime(currentTime, notary, 30.seconds) ptx.setTime(currentTime, 30.seconds)
return Pair(ptx, cashSigningPubKeys) return Pair(ptx, cashSigningPubKeys)
} }
} }

View File

@ -184,7 +184,7 @@ class CommercialPaperTestsGeneric {
} }
fun cashOutputsToWallet(vararg outputs: TransactionState<Cash.State>): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> { fun cashOutputsToWallet(vararg outputs: TransactionState<Cash.State>): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
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)) }) 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 issuance = bigCorpServices.storageService.myLegalIdentity.ref(1)
val issueTX: SignedTransaction = val issueTX: SignedTransaction =
CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { 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(bigCorpServices.key)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
}.toSignedTransaction() }.toSignedTransaction()
@ -223,7 +223,7 @@ class CommercialPaperTestsGeneric {
fun makeRedeemTX(time: Instant): SignedTransaction { fun makeRedeemTX(time: Instant): SignedTransaction {
val ptx = TransactionType.General.Builder() 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<Cash.State>()) CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType<Cash.State>())
ptx.signWith(aliceServices.key) ptx.signWith(aliceServices.key)
ptx.signWith(bigCorpServices.key) ptx.signWith(bigCorpServices.key)

View File

@ -217,7 +217,7 @@ class IRSTests {
calculation = dummyIRS.calculation, calculation = dummyIRS.calculation,
common = dummyIRS.common, common = dummyIRS.common,
notary = DUMMY_NOTARY).apply { notary = DUMMY_NOTARY).apply {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) setTime(TEST_TX_TIME, 30.seconds)
signWith(MEGA_CORP_KEY) signWith(MEGA_CORP_KEY)
signWith(MINI_CORP_KEY) signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
@ -303,7 +303,7 @@ class IRSTests {
val fixing = Fix(nextFix, "0.052".percent.value) val fixing = Fix(nextFix, "0.052".percent.value)
InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing)
with(tx) { with(tx) {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) setTime(TEST_TX_TIME, 30.seconds)
signWith(MEGA_CORP_KEY) signWith(MEGA_CORP_KEY)
signWith(MINI_CORP_KEY) signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)

View File

@ -84,30 +84,6 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) = fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) =
mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> else null }.single() mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> 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<AuthenticatedObject<CommandData>>.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<AuthenticatedObject<CommandData>>.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. * Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
* *

View File

@ -354,26 +354,6 @@ data class Timestamp(val after: Instant?, val before: Instant?) {
val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2) 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 * 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 * every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the

View File

@ -31,7 +31,7 @@ open class TransactionBuilder(
protected var timestamp: Timestamp? = null) { protected var timestamp: Timestamp? = null) {
@Deprecated("use timestamp instead") @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. * Creates a copy of the builder.
@ -44,7 +44,8 @@ open class TransactionBuilder(
attachments = ArrayList(attachments), attachments = ArrayList(attachments),
outputs = ArrayList(outputs), outputs = ArrayList(outputs),
commands = ArrayList(commands), 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 * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
* node. * node.
*/ */
@Deprecated("use setTime(Instant, Duration) instead") fun setTime(time: Instant, timeTolerance: Duration)
fun setTime(time: Instant, authority: Party, timeTolerance: Duration) { = setTime(Timestamp(time, timeTolerance))
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
commands.removeAll { it.value is TimestampCommand }
addCommand(TimestampCommand(time, timeTolerance), authority.owningKey)
}
/** fun setTime(newTimestamp: Timestamp) {
* 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" } 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 */ /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */

View File

@ -23,7 +23,7 @@ fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString()) services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
} }
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) } 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)
} }
/** /**

View File

@ -24,9 +24,7 @@ sealed class TransactionType {
/** Check that the list of signers includes all the necessary keys */ /** Check that the list of signers includes all the necessary keys */
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> { fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand } val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet()
val timestampKey = timestamp?.signers.orEmpty()
val notaryKey = (tx.inputs.map { it.state.notary.owningKey } + timestampKey).toSet()
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx) if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
val requiredKeys = getRequiredSigners(tx) + notaryKey val requiredKeys = getRequiredSigners(tx) + notaryKey

View File

@ -83,15 +83,6 @@ data class TransactionForContract(val inputs: List<ContractState>,
* be used to simplify this logic. * be used to simplify this logic.
*/ */
data class InOutGroup<out T : ContractState, out K : Any>(val inputs: List<T>, val outputs: List<T>, val groupingKey: K) data class InOutGroup<out T : ContractState, out K : Any>(val inputs: List<T>, val outputs: List<T>, 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() { class TransactionResolutionException(val hash: SecureHash) : Exception() {

View File

@ -50,7 +50,7 @@ data class WireTransaction(val inputs: List<StateRef>,
val commands: List<Command>, val commands: List<Command>,
val signers: List<PublicKey>, val signers: List<PublicKey>,
val type: TransactionType, 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. // Cache the serialised form of the transaction and its hash to give us fast access to it.
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null @Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
@ -166,8 +166,8 @@ data class LedgerTransaction(
override val id: SecureHash, override val id: SecureHash,
/** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */ /** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */
val signers: List<PublicKey>, val signers: List<PublicKey>,
val type: TransactionType, val timestamp: Timestamp?,
val timestamp: Timestamp? = null val type: TransactionType
) : NamedByHash { ) : NamedByHash {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index)) fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, 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 */ /** Strips the transaction down to a form that is usable by the contract verify functions */
fun toTransactionForContract(): TransactionForContract { fun toTransactionForContract(): TransactionForContract {
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id, 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)
} }
/** /**

View File

@ -1,6 +1,6 @@
package com.r3corda.core.node.services 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.seconds
import com.r3corda.core.until import com.r3corda.core.until
import java.time.Clock import java.time.Clock
@ -11,7 +11,7 @@ import java.time.Duration
*/ */
class TimestampChecker(val clock: Clock = Clock.systemUTC(), class TimestampChecker(val clock: Clock = Clock.systemUTC(),
val tolerance: Duration = 30.seconds) { val tolerance: Duration = 30.seconds) {
fun isValid(timestampCommand: TimestampCommand): Boolean { fun isValid(timestampCommand: Timestamp): Boolean {
val before = timestampCommand.before val before = timestampCommand.before
val after = timestampCommand.after val after = timestampCommand.after

View File

@ -232,6 +232,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.writeClassAndObject(output, obj.commands) kryo.writeClassAndObject(output, obj.commands)
kryo.writeClassAndObject(output, obj.signers) kryo.writeClassAndObject(output, obj.signers)
kryo.writeClassAndObject(output, obj.type) kryo.writeClassAndObject(output, obj.type)
kryo.writeClassAndObject(output, obj.timestamp)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -262,8 +263,9 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
val commands = kryo.readClassAndObject(input) as List<Command> val commands = kryo.readClassAndObject(input) as List<Command>
val signers = kryo.readClassAndObject(input) as List<PublicKey> val signers = kryo.readClassAndObject(input) as List<PublicKey>
val transactionType = kryo.readClassAndObject(input) as TransactionType 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)
} }
} }
} }

View File

@ -134,6 +134,10 @@ data class TestTransactionDSLInterpreter private constructor(
return EnforceVerifyOrFail.Token return EnforceVerifyOrFail.Token
} }
override fun timestamp(data: Timestamp) {
transactionBuilder.setTime(data)
}
override fun tweak( override fun tweak(
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail
) = dsl(TransactionDSL(copy())) ) = dsl(TransactionDSL(copy()))

View File

@ -46,6 +46,12 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
*/ */
fun _command(signers: List<PublicKey>, commandData: CommandData) fun _command(signers: List<PublicKey>, commandData: CommandData)
/**
* Adds a timestamp to the transaction.
* @param data The [TimestampCommand].
*/
fun timestamp(data: Timestamp)
/** /**
* Creates a local scoped copy of the transaction. * Creates a local scoped copy of the transaction.
* @param dsl The transaction DSL to be interpreted using the copy. * @param dsl The transaction DSL to be interpreted using the copy.
@ -102,16 +108,8 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(val interpreter: T) : Tr
* Adds a timestamp command to the transaction. * Adds a timestamp command to the transaction.
* @param time The [Instant] of the [TimestampCommand]. * @param time The [Instant] of the [TimestampCommand].
* @param tolerance The tolerance of the [TimestampCommand]. * @param tolerance The tolerance of the [TimestampCommand].
* @param notary The notary to sign the command.
*/ */
@JvmOverloads @JvmOverloads
fun timestamp(time: Instant, tolerance: Duration = 30.seconds, notary: PublicKey = DUMMY_NOTARY.owningKey) = fun timestamp(time: Instant, tolerance: Duration = 30.seconds) =
timestamp(TimestampCommand(time, tolerance), notary) timestamp(Timestamp(time, tolerance))
/**
* 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)
} }

View File

@ -2,7 +2,8 @@ package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.SignedTransaction 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.contracts.WireTransaction
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
@ -84,22 +85,16 @@ object NotaryProtocol {
} }
private fun findNotaryParty(): Party { private fun findNotaryParty(): Party {
var maybeNotaryKey: PublicKey? = null
val wtx = stx.tx 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 } val notaryParty = maybeNotaryParty ?: throw IllegalStateException("Transaction does not specify a Notary")
if (timestampCommand != null) maybeNotaryKey = timestampCommand.signers.first() check(wtx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { "Input states must have the same Notary" }
return notaryParty
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")
} }
} }
@ -138,18 +133,12 @@ object NotaryProtocol {
send(otherSide, sendSessionID, result) send(otherSide, sendSessionID, result)
} }
private fun validateTimestamp(tx: WireTransaction) { private fun validateTimestamp(tx: WireTransaction) =
val timestampCmd = try { if (tx.timestamp != null
tx.commands.noneOrSingle { it.value is TimestampCommand } ?: return && !timestampChecker.isValid(tx.timestamp))
} 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))
throw NotaryException(NotaryError.TimestampInvalid()) throw NotaryException(NotaryError.TimestampInvalid())
} else
Unit
/** /**
* No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully * No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully

View File

@ -101,9 +101,11 @@ object TwoPartyDealProtocol {
untrustedPartialTX.validate { stx -> untrustedPartialTX.validate { stx ->
progressTracker.nextStep() progressTracker.nextStep()
// TODO: Verify the notary on the transaction is set correctly
// Check that the tx proposed by the buyer is valid. // Check that the tx proposed by the buyer is valid.
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false) 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") throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
val wtx: WireTransaction = stx.tx 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 // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one. // 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)) 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 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) { val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapCache.ratesOracleNodes[0].identity, fixOf, BigDecimal.ZERO, BigDecimal.ONE) {
@Suspendable @Suspendable
override fun beforeSigning(fix: Fix) { 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 // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one. // to have one.
ptx.setTime(serviceHub.clock.instant(), txState.notary, 30.seconds) ptx.setTime(serviceHub.clock.instant(), 30.seconds)
} }
} }
subProtocol(addFixing) subProtocol(addFixing)

View File

@ -99,10 +99,10 @@ class TransactionSerializationTests {
@Test @Test
fun timestamp() { 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_KEY_1)
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction() val stx = tx.toSignedTransaction()
assertEquals(TEST_TX_TIME, (stx.tx.commands[1].value as TimestampCommand).midpoint) assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint)
} }
} }

View File

@ -108,7 +108,7 @@ class AccountReceivable : Contract {
override fun verify(tx: TransactionForContract) { override fun verify(tx: TransactionForContract) {
val command = tx.commands.requireSingleCommand<AccountReceivable.Commands>() val command = tx.commands.requireSingleCommand<AccountReceivable.Commands>()
val time = tx.commands.getTimestampByName("Notary Service", "Seller")?.midpoint ?: val time = tx.timestamp?.midpoint ?:
throw IllegalArgumentException("must be timestamped") throw IllegalArgumentException("must be timestamped")
when (command.value) { when (command.value) {

View File

@ -74,7 +74,7 @@ class BillOfLadingAgreement : Contract {
override fun verify(tx: TransactionForContract) { override fun verify(tx: TransactionForContract) {
val command = tx.commands.requireSingleCommand<BillOfLadingAgreement.Commands>() val command = tx.commands.requireSingleCommand<BillOfLadingAgreement.Commands>()
val time = tx.commands.getTimestampByName("Notary Service")?.midpoint val time = tx.timestamp?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped") if (time == null) throw IllegalArgumentException("must be timestamped")
val txOutputStates: List<BillOfLadingAgreement.State> = tx.outputs.filterIsInstance<BillOfLadingAgreement.State>() val txOutputStates: List<BillOfLadingAgreement.State> = tx.outputs.filterIsInstance<BillOfLadingAgreement.State>()
@ -114,7 +114,7 @@ class BillOfLadingAgreement : Contract {
fun generateIssue(owner: PublicKey, beneficiary: Party, props: BillOfLadingProperties, notary: Party): TransactionBuilder { fun generateIssue(owner: PublicKey, beneficiary: Party, props: BillOfLadingProperties, notary: Party): TransactionBuilder {
val state = State(owner, beneficiary, props) val state = State(owner, beneficiary, props)
val builder = TransactionType.General.Builder(notary = notary) 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)) return builder.withItems(state, Command(Commands.IssueBL(), props.carrierOwner.owningKey))
} }

View File

@ -113,7 +113,7 @@ class Invoice : Contract {
override fun verify(tx: TransactionForContract) { override fun verify(tx: TransactionForContract) {
val command = tx.commands.requireSingleCommand<Invoice.Commands>() val command = tx.commands.requireSingleCommand<Invoice.Commands>()
val time = tx.commands.getTimestampByName("Notary Service", "Seller")?.midpoint ?: val time = tx.timestamp?.midpoint ?:
throw IllegalArgumentException("must be timestamped") throw IllegalArgumentException("must be timestamped")
when (command.value) { when (command.value) {

View File

@ -33,7 +33,8 @@ class LCApplication : Contract {
// Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be // 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 // hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about
// who or what is a trusted authority. // 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) { when (command.value) {
is Commands.ApplyForLC -> { is Commands.ApplyForLC -> {

View File

@ -71,7 +71,7 @@ class LOC : Contract {
override fun verify(tx: TransactionForContract) { override fun verify(tx: TransactionForContract) {
val command = tx.commands.requireSingleCommand<LOC.Commands>() val command = tx.commands.requireSingleCommand<LOC.Commands>()
val time = tx.commands.getTimestampByName("Notary Service")?.midpoint val time = tx.timestamp?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped") if (time == null) throw IllegalArgumentException("must be timestamped")
when (command.value) { when (command.value) {
@ -146,7 +146,7 @@ class LOC : Contract {
fun generateIssue(beneficiaryPaid: Boolean, issued: Boolean, terminated: Boolean, props: LOCProperties, notary: Party): TransactionBuilder { fun generateIssue(beneficiaryPaid: Boolean, issued: Boolean, terminated: Boolean, props: LOCProperties, notary: Party): TransactionBuilder {
val state = State(beneficiaryPaid, issued, terminated, props) val state = State(beneficiaryPaid, issued, terminated, props)
val builder = TransactionType.General.Builder(notary = notary) 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)) return builder.withItems(state, Command(Commands.Issuance(), props.issuingbank.owningKey))
} }

View File

@ -85,7 +85,7 @@ class AccountReceivableTests {
} }
val gtx = invoice.generateInvoice(DUMMY_NOTARY).apply { 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(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
} }

View File

@ -34,7 +34,7 @@ class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwo
val issuance = run { val issuance = run {
val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER, 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) 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(notary.storage.myLegalIdentityKey)
tx.signWith(seller.storage.myLegalIdentityKey) tx.signWith(seller.storage.myLegalIdentityKey)
tx.toSignedTransaction(true) tx.toSignedTransaction(true)

View File

@ -475,7 +475,7 @@ class TwoPartyTradeProtocolTests {
} }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue(notary) } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue(notary) }
if (!withError) if (!withError)
timestamp(time = TEST_TX_TIME, notary = notary.owningKey) timestamp(time = TEST_TX_TIME)
if (attachmentID != null) if (attachmentID != null)
attachment(attachmentID) attachment(attachmentID)
if (withError) { if (withError) {

View File

@ -120,7 +120,7 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode): StateAndRef<
fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> {
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) 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) tx.signWith(node.storage.myLegalIdentityKey)
val stx = tx.toSignedTransaction(false) val stx = tx.toSignedTransaction(false)
node.services.recordTransactions(listOf(stx)) node.services.recordTransactions(listOf(stx))

View File

@ -1,6 +1,6 @@
package com.r3corda.node.services 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.contracts.TransactionType
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
@ -40,7 +40,7 @@ class NotaryServiceTests {
val stx = run { val stx = run {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)
val tx = TransactionType.General.Builder().withItems(inputState) 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.signWith(clientNode.keyPair!!)
tx.toSignedTransaction(false) tx.toSignedTransaction(false)
} }
@ -73,7 +73,7 @@ class NotaryServiceTests {
val stx = run { val stx = run {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)
val tx = TransactionType.General.Builder().withItems(inputState) 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.signWith(clientNode.keyPair!!)
tx.toSignedTransaction(false) 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`() { @Test fun `should report conflict for a duplicate transaction`() {
val stx = run { val stx = run {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)

View File

@ -1,6 +1,6 @@
package com.r3corda.node.services 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.node.services.TimestampChecker
import com.r3corda.core.seconds import com.r3corda.core.seconds
import org.junit.Test import org.junit.Test
@ -17,8 +17,8 @@ class TimestampCheckerTests {
@Test @Test
fun `should return true for valid timestamp`() { fun `should return true for valid timestamp`() {
val now = clock.instant() val now = clock.instant()
val timestampPast = TimestampCommand(now - 60.seconds, now - 29.seconds) val timestampPast = Timestamp(now - 60.seconds, now - 29.seconds)
val timestampFuture = TimestampCommand(now + 29.seconds, now + 60.seconds) val timestampFuture = Timestamp(now + 29.seconds, now + 60.seconds)
assertTrue { timestampChecker.isValid(timestampPast) } assertTrue { timestampChecker.isValid(timestampPast) }
assertTrue { timestampChecker.isValid(timestampFuture) } assertTrue { timestampChecker.isValid(timestampFuture) }
} }
@ -26,8 +26,8 @@ class TimestampCheckerTests {
@Test @Test
fun `should return false for invalid timestamp`() { fun `should return false for invalid timestamp`() {
val now = clock.instant() val now = clock.instant()
val timestampPast = TimestampCommand(now - 60.seconds, now - 31.seconds) val timestampPast = Timestamp(now - 60.seconds, now - 31.seconds)
val timestampFuture = TimestampCommand(now + 31.seconds, now + 60.seconds) val timestampFuture = Timestamp(now + 31.seconds, now + 60.seconds)
assertFalse { timestampChecker.isValid(timestampPast) } assertFalse { timestampChecker.isValid(timestampPast) }
assertFalse { timestampChecker.isValid(timestampFuture) } assertFalse { timestampChecker.isValid(timestampFuture) }
} }

View File

@ -350,7 +350,7 @@ private class TraderDemoProtocolSeller(val otherSide: Party,
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
// Requesting timestamping, all CP must be timestamped. // 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. // Sign it as ourselves.
tx.signWith(keyPair) tx.signWith(keyPair)