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 {
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<Issued<Currency>> 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<State> paper, List<StateAndRef<Cash.State>> 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<State> paper, PublicKey newOwner) {

View File

@ -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<CommercialPaper.Commands>()
// 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<Issued<Currency>>, 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<Currency>(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)
}
}

View File

@ -496,6 +496,8 @@ class InterestRateSwap() : Contract {
val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
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
if (time == null) throw IllegalArgumentException("must be timestamped")

View File

@ -390,9 +390,7 @@ class Obligation<P> : 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<P> = input.copy(lifecycle = expectedOutputLifecycle)
requireThat {

View File

@ -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"
}

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
* 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() }

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
* 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)

View File

@ -73,7 +73,8 @@ data class TransactionForVerification(val inputs: List<TransactionState<Contract
@Throws(TransactionVerificationException::class)
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 attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
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<ContractState>,
*/
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. */
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<PublicKey>) : TransactionVerificationException(tx, null)
class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
}
}

View File

@ -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)