Merged in rnicoll-timestamping-notary-take2 (pull request #206)

Replace named timestamping authority with notary
This commit is contained in:
Ross Nicoll 2016-07-11 15:00:35 +01:00
commit 2cd5014941
9 changed files with 60 additions and 33 deletions

View File

@ -131,6 +131,12 @@ public class JavaCommercialPaper implements Contract {
} }
public static class Redeem extends Commands { public static class Redeem extends Commands {
private final Party notary;
public Redeem(Party setNotary) {
this.notary = setNotary;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof Redeem; return obj instanceof Redeem;
@ -138,6 +144,12 @@ public class JavaCommercialPaper implements Contract {
} }
public static class Issue extends Commands { public static class Issue extends Commands {
private final Party notary;
public Issue(Party setNotary) {
this.notary = setNotary;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof Issue; 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. // For now do not allow multiple pieces of CP to trade in a single transaction.
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) {
Commands.Issue issueCommand = (Commands.Issue) cmd.getValue();
State output = single(outputs); State output = single(outputs);
if (!inputs.isEmpty()) { if (!inputs.isEmpty()) {
throw new IllegalStateException("Failed Requirement: output values sum to more than the inputs"); 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"); 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) if (timestampCommand == null)
throw new IllegalArgumentException("Failed Requirement: must be timestamped"); throw new IllegalArgumentException("Failed Requirement: must be timestamped");
@ -201,7 +214,7 @@ public class JavaCommercialPaper implements Contract {
!output.getMaturityDate().equals(input.getMaturityDate())) !output.getMaturityDate().equals(input.getMaturityDate()))
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner"); 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) { } 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) if (timestampCommand == null)
throw new IllegalArgumentException("Failed Requirement: must be timestamped"); throw new IllegalArgumentException("Failed Requirement: must be timestamped");
Instant time = timestampCommand.getBefore(); Instant time = timestampCommand.getBefore();
@ -232,13 +245,13 @@ public class JavaCommercialPaper implements Contract {
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
TransactionState output = new TransactionState<>(state, notary); 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<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException { public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException {
new Cash().generateSpend(tx, paper.getState().getData().getFaceValue(), paper.getState().getData().getOwner(), wallet); new Cash().generateSpend(tx, paper.getState().getData().getFaceValue(), paper.getState().getData().getOwner(), wallet);
tx.addInputState(paper); 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<State> paper, PublicKey newOwner) { public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) {

View File

@ -65,11 +65,11 @@ class CommercialPaper : Contract {
} }
interface Commands : CommandData { interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands class Move: TypeOnlyCommandData(), Commands
class Redeem : 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. // 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. // 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) { 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 // 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<CommercialPaper.Commands>() val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
// If it's an issue, we can't take notary from inputs, so it must be specified in the command
// Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be val timestamp: TimestampCommand? = if (command.value is Commands.Issue)
// hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about tx.getTimestampBy((command.value as Commands.Issue).notary)
// who or what is a trusted authority. else if (command.value is Commands.Redeem)
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") tx.getTimestampBy((command.value as Commands.Redeem).notary)
else
null
for ((inputs, outputs, key) in groups) { for ((inputs, outputs, key) in groups) {
when (command.value) { when (command.value) {
@ -139,7 +141,7 @@ class CommercialPaper : Contract {
fun generateIssue(faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder { fun generateIssue(faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder {
val issuance = faceValue.token.issuer val issuance = faceValue.token.issuer
val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary) 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<Currency>(amount.quantity, amount.token.product) } val amount = paper.state.data.faceValue.let { amount -> Amount<Currency>(amount.quantity, amount.token.product) }
Cash().generateSpend(tx, amount, paper.state.data.owner, wallet) Cash().generateSpend(tx, amount, paper.state.data.owner, wallet)
tx.addInputState(paper) tx.addInputState(paper)
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner) tx.addCommand(CommercialPaper.Commands.Redeem(paper.state.notary), paper.state.data.owner)
} }
} }

View File

@ -496,6 +496,8 @@ class InterestRateSwap() : Contract {
val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID } val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>() val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>()
// 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 val time = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped") if (time == null) throw IllegalArgumentException("must be timestamped")

View File

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

View File

@ -3,6 +3,7 @@ package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.testing.* import com.r3corda.contracts.testing.*
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days import com.r3corda.core.days
import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.node.services.testing.MockStorageService
@ -18,8 +19,8 @@ import kotlin.test.assertTrue
interface ICommercialPaperTestTemplate { interface ICommercialPaperTestTemplate {
fun getPaper(): ICommercialPaperState fun getPaper(): ICommercialPaperState
fun getIssueCommand(): CommandData fun getIssueCommand(notary: Party): CommandData
fun getRedeemCommand(): CommandData fun getRedeemCommand(notary: Party): CommandData
fun getMoveCommand(): CommandData fun getMoveCommand(): CommandData
} }
@ -31,8 +32,8 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
TEST_TX_TIME + 7.days TEST_TX_TIME + 7.days
) )
override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue() override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue(notary)
override fun getRedeemCommand(): CommandData = JavaCommercialPaper.Commands.Redeem() override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem(notary)
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move() override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
} }
@ -44,8 +45,8 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
maturityDate = TEST_TX_TIME + 7.days maturityDate = TEST_TX_TIME + 7.days
) )
override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue() override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue(notary)
override fun getRedeemCommand(): CommandData = CommercialPaper.Commands.Redeem() override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem(notary)
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move() override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
} }
@ -74,7 +75,7 @@ class CommercialPaperTestsGeneric {
// Some CP is issued onto the ledger by MegaCorp. // Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") { transaction("Issuance") {
output("paper") { thisTest.getPaper() } output("paper") { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this.verifies() this.verifies()
} }
@ -103,7 +104,7 @@ class CommercialPaperTestsGeneric {
} }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(ALICE_PUBKEY) { thisTest.getRedeemCommand() } command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) }
tweak { tweak {
outputs(700.DOLLARS `issued by` issuer) outputs(700.DOLLARS `issued by` issuer)
@ -133,7 +134,7 @@ class CommercialPaperTestsGeneric {
fun `key mismatch at issue`() { fun `key mismatch at issue`() {
transaction { transaction {
output { thisTest.getPaper() } output { thisTest.getPaper() }
command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() } command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this `fails with` "output states are issued by a command signer" this `fails with` "output states are issued by a command signer"
} }
@ -143,7 +144,7 @@ class CommercialPaperTestsGeneric {
fun `face value is not zero`() { fun `face value is not zero`() {
transaction { transaction {
output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } 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) timestamp(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs" this `fails with` "output values sum to more than the inputs"
} }
@ -153,7 +154,7 @@ class CommercialPaperTestsGeneric {
fun `maturity date not in the past`() { fun `maturity date not in the past`() {
transaction { transaction {
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) } 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) timestamp(TEST_TX_TIME)
this `fails with` "maturity date is not in the past" this `fails with` "maturity date is not in the past"
} }
@ -164,7 +165,7 @@ class CommercialPaperTestsGeneric {
transaction { transaction {
input(thisTest.getPaper()) input(thisTest.getPaper())
output { thisTest.getPaper() } output { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs" this `fails with` "output values sum to more than the inputs"
} }

View File

@ -96,6 +96,7 @@ fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority:
* Note that matching here is done by (verified, legal) name, not by public key. Any signature by any * 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. * 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? { fun List<AuthenticatedObject<CommandData>>.getTimestampByName(vararg names: String): TimestampCommand? {
val timestampCmd = filter { it.value is TimestampCommand }.singleOrNull() ?: return null val timestampCmd = filter { it.value is TimestampCommand }.singleOrNull() ?: return null
val tsaNames = timestampCmd.signingParties.map { it.name.toLowerCase() } val tsaNames = timestampCmd.signingParties.map { it.name.toLowerCase() }

View File

@ -316,6 +316,9 @@ data class AuthenticatedObject<out T : Any>(
* If present in a transaction, contains a time that was verified by the timestamping authority/authorities whose * 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) * 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 { data class TimestampCommand(val after: Instant?, val before: Instant?) : CommandData {
init { init {
if (after == null && before == null) if (after == null && before == null)

View File

@ -73,7 +73,8 @@ data class TransactionForVerification(val inputs: List<TransactionState<Contract
@Throws(TransactionVerificationException::class) @Throws(TransactionVerificationException::class)
fun verify() = type.verify(this) fun verify() = type.verify(this)
fun toTransactionForContract() = TransactionForContract(inputs.map { it.data }, outputs.map { it.data }, attachments, commands, origHash) fun toTransactionForContract() = TransactionForContract(inputs.map { it.data }, outputs.map { it.data },
attachments, commands, origHash, inputs.map { it.notary }.singleOrNull())
} }
/** /**
@ -84,7 +85,8 @@ data class TransactionForContract(val inputs: List<ContractState>,
val outputs: List<ContractState>, val outputs: List<ContractState>,
val attachments: List<Attachment>, val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>, val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash) { val origHash: SecureHash,
val inputNotary: Party? = null) {
override fun hashCode() = origHash.hashCode() override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash
@ -158,10 +160,15 @@ data class TransactionForContract(val inputs: List<ContractState>,
*/ */
data class InOutGroup<T : ContractState, K : Any>(val inputs: List<T>, val outputs: List<T>, val groupingKey: K) data class InOutGroup<T : ContractState, K : Any>(val inputs: List<T>, val outputs: List<T>, 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. */ /** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
/** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */ /** 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) fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName)
} }

View File

@ -481,7 +481,7 @@ class TwoPartyTradeProtocolTests {
output("alice's paper") { output("alice's paper") {
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days) 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) if (!withError)
timestamp(time = TEST_TX_TIME, notary = notary.owningKey) timestamp(time = TEST_TX_TIME, notary = notary.owningKey)
if (attachmentID != null) if (attachmentID != null)