diff --git a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt index c8b60ed9aa..d2195e3b25 100644 --- a/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt +++ b/contracts/isolated/src/main/kotlin/com/r3corda/contracts/AnotherDummyContract.kt @@ -8,25 +8,27 @@ package com.r3corda.contracts.isolated -import com.r3corda.core.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash +import java.security.PublicKey // The dummy contract doesn't do anything useful. It exists for testing purposes. val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract() class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdoor { - class State(val magicNumber: Int = 0, override val notary: Party) : ContractState { + data class State(val magicNumber: Int = 0) : ContractState { override val contract = ANOTHER_DUMMY_PROGRAM_ID + override val participants: List + get() = emptyList() } interface Commands : CommandData { class Create : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Always accepts. } @@ -34,8 +36,8 @@ class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdo override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") override fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = State(magicNumber, notary) - return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + val state = State(magicNumber) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) } override fun inspectState(state: ContractState): Int = (state as State).magicNumber diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 1d5609c91c..690111d5a2 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -1,10 +1,11 @@ package com.r3corda.contracts; +import com.google.common.collect.ImmutableList; import com.r3corda.contracts.cash.Cash; import com.r3corda.contracts.cash.CashKt; import com.r3corda.contracts.cash.InsufficientBalanceException; -import com.r3corda.core.contracts.TransactionForVerification.InOutGroup; import com.r3corda.core.contracts.*; +import com.r3corda.core.contracts.TransactionForContract.InOutGroup; import com.r3corda.core.crypto.NullPublicKey; import com.r3corda.core.crypto.Party; import com.r3corda.core.crypto.SecureHash; @@ -33,38 +34,36 @@ public class JavaCommercialPaper implements Contract { private PublicKey owner; private Amount> faceValue; private Instant maturityDate; - private Party notary; public State() { } // For serialization public State(PartyAndReference issuance, PublicKey owner, Amount> faceValue, - Instant maturityDate, Party notary) { + Instant maturityDate) { this.issuance = issuance; this.owner = owner; this.faceValue = faceValue; this.maturityDate = maturityDate; - this.notary = notary; } public State copy() { - return new State(this.issuance, this.owner, this.faceValue, this.maturityDate, this.notary); + return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); } public ICommercialPaperState withOwner(PublicKey newOwner) { - return new State(this.issuance, newOwner, this.faceValue, this.maturityDate, this.notary); + return new State(this.issuance, newOwner, this.faceValue, this.maturityDate); } public ICommercialPaperState withIssuance(PartyAndReference newIssuance) { - return new State(newIssuance, this.owner, this.faceValue, this.maturityDate, this.notary); + return new State(newIssuance, this.owner, this.faceValue, this.maturityDate); } public ICommercialPaperState withFaceValue(Amount> newFaceValue) { - return new State(this.issuance, this.owner, newFaceValue, this.maturityDate, this.notary); + return new State(this.issuance, this.owner, newFaceValue, this.maturityDate); } public ICommercialPaperState withMaturityDate(Instant newMaturityDate) { - return new State(this.issuance, this.owner, this.faceValue, newMaturityDate, this.notary); + return new State(this.issuance, this.owner, this.faceValue, newMaturityDate); } public PartyAndReference getIssuance() { @@ -83,12 +82,6 @@ public class JavaCommercialPaper implements Contract { return maturityDate; } - @NotNull - @Override - public Party getNotary() { - return notary; - } - @NotNull @Override public Contract getContract() { @@ -106,7 +99,6 @@ public class JavaCommercialPaper implements Contract { if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - if (notary != null ? !notary.equals(state.notary) : state.notary != null) return false; return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); } @@ -116,12 +108,17 @@ public class JavaCommercialPaper implements Contract { result = 31 * result + (owner != null ? owner.hashCode() : 0); result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); - result = 31 * result + (notary != null ? notary.hashCode() : 0); return result; } public State withoutOwner() { - return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate, notary); + return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate); + } + + @NotNull + @Override + public List getParticipants() { + return ImmutableList.of(this.owner); } } @@ -149,7 +146,7 @@ public class JavaCommercialPaper implements Contract { } @Override - public void verify(@NotNull TransactionForVerification tx) { + public void verify(@NotNull TransactionForContract tx) { // There are three possible things that can be done with CP. // Issuance, trading (aka moving in this prototype) and redeeming. // Each command has it's own set of restrictions which the verify function ... verifies. @@ -233,19 +230,20 @@ 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, notary); - return new TransactionBuilder().withItems(state, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); + 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())); } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { - new Cash().generateSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet); - tx.addInputState(paper.getRef()); - tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getOwner())); + 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())); } public void generateMove(TransactionBuilder tx, StateAndRef paper, PublicKey newOwner) { - tx.addInputState(paper.getRef()); - tx.addOutputState(new State(paper.getState().getIssuance(), newOwner, paper.getState().getFaceValue(), paper.getState().getMaturityDate(), paper.getState().getNotary())); - tx.addCommand(new Command(new Commands.Move(), paper.getState().getOwner())); + tx.addInputState(paper); + tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), paper.getState().getNotary())); + tx.addCommand(new Command(new Commands.Move(), paper.getState().getData().getOwner())); } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 8c670c8cd2..ca270e3469 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -3,7 +3,6 @@ package com.r3corda.contracts import com.r3corda.contracts.cash.Cash import com.r3corda.contracts.cash.InsufficientBalanceException import com.r3corda.contracts.cash.sumCashBy -import com.r3corda.core.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party @@ -12,7 +11,7 @@ import com.r3corda.core.crypto.toStringShort import com.r3corda.core.utilities.Emoji import java.security.PublicKey import java.time.Instant -import java.util.Currency +import java.util.* /** * This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate @@ -47,10 +46,11 @@ class CommercialPaper : Contract { val issuance: PartyAndReference, override val owner: PublicKey, val faceValue: Amount>, - val maturityDate: Instant, - override val notary: Party + val maturityDate: Instant ) : OwnableState, ICommercialPaperState { override val contract = CP_PROGRAM_ID + override val participants: List + get() = listOf(owner) fun withoutOwner() = copy(owner = NullPublicKey) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) @@ -72,7 +72,7 @@ class CommercialPaper : Contract { class Issue : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. val groups = tx.groupStates() { it: State -> it.withoutOwner() } @@ -138,17 +138,17 @@ class CommercialPaper : Contract { */ fun generateIssue(faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { val issuance = faceValue.token.issuer - val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate, notary) - return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) + 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)) } /** * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. */ fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { - tx.addInputState(paper.ref) - tx.addOutputState(paper.state.copy(owner = newOwner)) - tx.addCommand(Commands.Move(), paper.state.owner) + tx.addInputState(paper) + tx.addOutputState(TransactionState(paper.state.data.copy(owner = newOwner), paper.state.notary)) + tx.addCommand(Commands.Move(), paper.state.data.owner) } /** @@ -161,10 +161,10 @@ class CommercialPaper : Contract { @Throws(InsufficientBalanceException::class) fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - val amount = paper.state.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } - Cash().generateSpend(tx, amount, paper.state.owner, wallet) - tx.addInputState(paper.ref) - tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner) + 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) } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 49bb534a87..2169cb02a1 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -487,7 +487,7 @@ class InterestRateSwap() : Contract { /** * verify() with some examples of what needs to be checked. */ - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Group by Trade ID for in / out states val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID } @@ -587,14 +587,16 @@ class InterestRateSwap() : Contract { val fixedLeg: FixedLeg, val floatingLeg: FloatingLeg, val calculation: Calculation, - val common: Common, - override val notary: Party + val common: Common ) : FixableDealState { override val contract = IRS_PROGRAM_ID override val thread = SecureHash.sha256(common.tradeID) override val ref = common.tradeID + override val participants: List + get() = parties.map { it.owningKey } + override fun isRelevant(ourKeys: Set): Boolean { return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys) } @@ -618,10 +620,10 @@ class InterestRateSwap() : Contract { } } - override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) + override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) - override fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) { - InterestRateSwap().generateFix(ptx, StateAndRef(this, oldStateRef), Pair(fix.of.forDay, Rate(RatioUnit(fix.value)))) + override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { + InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), Pair(fix.of.forDay, Rate(RatioUnit(fix.value)))) } override fun nextFixingOf(): FixOf? { @@ -715,8 +717,8 @@ class InterestRateSwap() : Contract { val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule) // Put all the above into a new State object. - val state = State(fixedLeg, floatingLeg, newCalculation, common, notary) - return TransactionBuilder().withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) + val state = State(fixedLeg, floatingLeg, newCalculation, common) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) } private fun calcFixingDate(date: LocalDate, fixingPeriod: DateOffset, calendar: BusinessCalendar): LocalDate { @@ -729,8 +731,11 @@ class InterestRateSwap() : Contract { // TODO: Replace with rates oracle fun generateFix(tx: TransactionBuilder, irs: StateAndRef, fixing: Pair) { - tx.addInputState(irs.ref) - tx.addOutputState(irs.state.copy(calculation = irs.state.calculation.applyFixing(fixing.first, FixedRate(fixing.second)))) - tx.addCommand(Commands.Fix(), listOf(irs.state.floatingLeg.floatingRatePayer.owningKey, irs.state.fixedLeg.fixedRatePayer.owningKey)) + tx.addInputState(irs) + tx.addOutputState( + irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.first, FixedRate(fixing.second))), + irs.state.notary + ) + tx.addCommand(Commands.Fix(), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey)) } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 522fbbc522..addf838088 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -49,17 +49,17 @@ class Cash : FungibleAsset() { override val amount: Amount>, /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: PublicKey, - - override val notary: Party + override val owner: PublicKey ) : FungibleAsset.State { - constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey, notary: Party) - : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner, notary) + constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) + : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) override val deposit: PartyAndReference get() = amount.token.issuer override val contract = CASH_PROGRAM_ID override val issuanceDef: Issued get() = amount.token + override val participants: List + get() = listOf(owner) override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" @@ -94,9 +94,9 @@ class Cash : FungibleAsset() { */ fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) - check(tx.outputStates().sumCashOrNull() == null) + check(tx.outputStates().map { it.data }.sumCashOrNull() == null) val at = amount.token.issuer - tx.addOutputState(Cash.State(amount, owner, notary)) + tx.addOutputState(TransactionState(Cash.State(amount, owner), notary)) tx.addCommand(Cash.Commands.Issue(), at.party.owningKey) } @@ -143,9 +143,9 @@ class Cash : FungibleAsset() { val currency = amount.token val acceptableCoins = run { - val ofCurrency = cashStates.filter { it.state.amount.token.product == currency } + val ofCurrency = cashStates.filter { it.state.data.amount.token.product == currency } if (onlyFromParties != null) - ofCurrency.filter { it.state.deposit.party in onlyFromParties } + ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } else ofCurrency } @@ -156,7 +156,7 @@ class Cash : FungibleAsset() { for (c in acceptableCoins) { if (gatheredAmount >= amount) break gathered.add(c) - gatheredAmount += Amount(c.state.amount.quantity, currency) + gatheredAmount += Amount(c.state.data.amount.quantity, currency) takeChangeFrom = c } @@ -164,30 +164,30 @@ class Cash : FungibleAsset() { throw InsufficientBalanceException(amount - gatheredAmount) val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.issuanceDef) + Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) } else { null } - val keysUsed = gathered.map { it.state.owner }.toSet() + val keysUsed = gathered.map { it.state.data.owner }.toSet() - val states = gathered.groupBy { it.state.deposit }.map { + val states = gathered.groupBy { it.state.data.deposit }.map { val (deposit, coins) = it - val totalAmount = coins.map { it.state.amount }.sumOrThrow() - State(totalAmount, to, coins.first().state.notary) + val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() + TransactionState(State(totalAmount, to), coins.first().state.notary) } val outputs = if (change != null) { // Just copy a key across as the change key. In real life of course, this works but leaks private data. // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow // value flows through the transaction graph. - val changeKey = gathered.first().state.owner + val changeKey = gathered.first().state.data.owner // Add a change output and adjust the last output downwards. states.subList(0, states.lastIndex) + - states.last().let { it.copy(amount = it.amount - change) } + - State(change, changeKey, gathered.last().state.notary) + states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } + + TransactionState(State(change, changeKey), gathered.last().state.notary) } else states - for (state in gathered) tx.addInputState(state.ref) + for (state in gathered) tx.addInputState(state) for (state in outputs) tx.addOutputState(state) // What if we already have a move command with the right keys? Filter it out here or in platform code? val keysList = keysUsed.toList() diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt index 417d16392e..a865e6bcd0 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -37,7 +37,6 @@ abstract class FungibleAsset : Contract { override val amount: Amount> /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey - override val notary: Party } // Just for grouping @@ -58,7 +57,7 @@ abstract class FungibleAsset : Contract { } /** This is the function EVERYONE runs */ - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible // and must be kept separated for bookkeeping purposes. val groups = tx.groupStates() { it: FungibleAsset.State -> it.issuanceDef } @@ -97,7 +96,7 @@ abstract class FungibleAsset : Contract { private fun verifyIssueCommand(inputs: List>, outputs: List>, - tx: TransactionForVerification, + tx: TransactionForContract, issueCommand: AuthenticatedObject, token: Issued, issuer: Party) { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index eb575cf7b1..e1d01a5079 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -9,10 +9,11 @@ import com.r3corda.core.contracts.DUMMY_PROGRAM_ID import com.r3corda.core.contracts.DummyContract import com.r3corda.core.contracts.PartyAndReference import com.r3corda.core.contracts.Issued +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.generateKeyPair -import com.r3corda.core.testing.DUMMY_NOTARY import java.security.PublicKey import java.util.* @@ -26,7 +27,7 @@ val TEST_PROGRAM_MAP: Map> = mapOf( IRS_PROGRAM_ID to InterestRateSwap::class.java ) -fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().nextInt(), notary) +fun generateState() = DummyContract.State(Random().nextInt()) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -50,8 +51,10 @@ fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().n infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner) infix fun Cash.State.`issued by`(party: Party) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party)))) infix fun Cash.State.`issued by`(deposit: PartyAndReference) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit))) +infix fun Cash.State.`with notary`(notary: Party) = TransactionState(this, notary) infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) +infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = @@ -62,7 +65,8 @@ val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public). /** Allows you to write 100.DOLLARS.CASH */ val Amount.CASH: Cash.State get() = Cash.State( Amount>(this.quantity, Issued(DUMMY_CASH_ISSUER, this.token)), - NullPublicKey, DUMMY_NOTARY) + NullPublicKey) -val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey, DUMMY_NOTARY) +val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey) +infix fun ContractState.`with notary`(notary: Party) = TransactionState(this, notary) diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 21bafe4fb0..b681362336 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -17,7 +17,7 @@ import com.r3corda.core.utilities.trace import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException -import java.util.Currency +import java.util.* /** * This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer @@ -129,7 +129,7 @@ object TwoPartyTradeProtocol { // This verifies that the transaction is contract-valid, even though it is missing signatures. serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)) - if (wtx.outputs.sumCashBy(myKeyPair.public) != price) + if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public) != price) throw IllegalArgumentException("Transaction is not sending us the right amount of cash") // There are all sorts of funny games a malicious secondary might play here, we should fix them: @@ -221,7 +221,7 @@ object TwoPartyTradeProtocol { progressTracker.currentStep = VERIFYING maybeTradeRequest.validate { // What is the seller trying to sell us? - val asset = it.assetForSale.state + val asset = it.assetForSale.state.data val assetTypeName = asset.javaClass.name logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" } @@ -266,21 +266,21 @@ object TwoPartyTradeProtocol { } private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() // Add input and output states for the movement of cash, by using the Cash contract to generate the states. val wallet = serviceHub.walletService.currentWallet val cashStates = wallet.statesOfType() val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) // Add inputs/outputs/a command for the movement of the asset. - ptx.addInputState(tradeRequest.assetForSale.ref) + ptx.addInputState(tradeRequest.assetForSale) // Just pick some new public key for now. This won't be linked with our identity in any way, which is what // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to // reveal who the owner actually is. The key management service is expected to derive a unique key from some // initial seed in order to provide privacy protection. val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public) - ptx.addOutputState(state) - ptx.addCommand(command, tradeRequest.assetForSale.state.owner) + val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public) + ptx.addOutputState(state, tradeRequest.assetForSale.state.notary) + ptx.addCommand(command, tradeRequest.assetForSale.state.data.owner) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 80d7a8814b..69fe21d40c 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -1,10 +1,7 @@ package com.r3corda.contracts -import com.r3corda.contracts.testing.CASH -import com.r3corda.contracts.testing.`issued by` -import com.r3corda.contracts.testing.`owned by` import com.r3corda.contracts.cash.Cash -import com.r3corda.contracts.testing.STATE +import com.r3corda.contracts.testing.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.days @@ -32,8 +29,7 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate { MEGA_CORP.ref(123), MEGA_CORP_PUBKEY, 1000.DOLLARS `issued by` MEGA_CORP.ref(123), - TEST_TX_TIME + 7.days, - DUMMY_NOTARY + TEST_TX_TIME + 7.days ) override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue() @@ -46,8 +42,7 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate { issuance = MEGA_CORP.ref(123), owner = MEGA_CORP_PUBKEY, faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), - maturityDate = TEST_TX_TIME + 7.days, - notary = DUMMY_NOTARY + maturityDate = TEST_TX_TIME + 7.days ) override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue() @@ -121,7 +116,7 @@ class CommercialPaperTestsGeneric { fun `issue cannot replace an existing state`() { transactionGroup { roots { - transaction(thisTest.getPaper() label "paper") + transaction(thisTest.getPaper() `with notary` DUMMY_NOTARY label "paper") } transaction { input("paper") @@ -144,9 +139,9 @@ class CommercialPaperTestsGeneric { trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed") } - fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { - val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256()) - return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) + fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { + val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.General()) + return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } @Test @@ -163,14 +158,14 @@ class CommercialPaperTestsGeneric { } val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( - 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY, - 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY, - 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY ) // Alice pays $9000 to MiniCorp to own some of their debt. val moveTX: LedgerTransaction = run { - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY) ptx.signWith(MINI_CORP_KEY) @@ -181,12 +176,12 @@ class CommercialPaperTestsGeneric { // Won't be validated. val (corpWalletTX, corpWallet) = cashOutputsToWallet( - 9000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY, - 4000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY + 9000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY, + 4000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY ) fun makeRedeemTX(time: Instant): LedgerTransaction { - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() ptx.setTime(time, DUMMY_NOTARY, 30.seconds) CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet) ptx.signWith(ALICE_KEY) @@ -213,8 +208,8 @@ class CommercialPaperTestsGeneric { val someProfits = 1200.DOLLARS `issued by` issuer return transactionGroupFor() { roots { - transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY label "alice's $900") - transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY label "some profits") + transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "alice's $900") + transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY `with notary` DUMMY_NOTARY label "some profits") } // Some CP is issued onto the ledger by MegaCorp. @@ -230,7 +225,7 @@ class CommercialPaperTestsGeneric { input("paper") input("alice's $900") output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output `owned by` ALICE_PUBKEY } + output("alice's paper") { "paper".output.data `owned by` ALICE_PUBKEY } arg(ALICE_PUBKEY) { Cash.Commands.Move() } arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } } @@ -244,7 +239,7 @@ class CommercialPaperTestsGeneric { output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } if (!destroyPaperAtRedemption) - output { "paper".output } + output { "paper".output.data } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index d1d8a64c50..77cc263fa8 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -97,7 +97,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") ) - InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY) + InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common) } 2 -> { // 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides) @@ -187,7 +187,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") ) - return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY) + return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common) } else -> TODO("IRS number $irsSelect not defined") @@ -204,8 +204,7 @@ class IRSTests { exampleIRS.fixedLeg, exampleIRS.floatingLeg, exampleIRS.calculation, - exampleIRS.common, - DUMMY_NOTARY + exampleIRS.common ) val outState = inState.copy() @@ -255,7 +254,7 @@ class IRSTests { * Utility so I don't have to keep typing this */ fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { - return generateIRSTxn(irsSelector).outputs.filterIsInstance().single() + return generateIRSTxn(irsSelector).outputs.map { it.data }.filterIsInstance().single() } /** @@ -287,7 +286,7 @@ class IRSTests { newCalculation = newCalculation.applyFixing(it.key, FixedRate(PercentageRatioUnit(it.value))) } - val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_NOTARY) + val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common) println(newIRS.exportIRSToCSV()) } @@ -306,13 +305,13 @@ class IRSTests { @Test fun generateIRSandFixSome() { var previousTXN = generateIRSTxn(1) - var currentIRS = previousTXN.outputs.filterIsInstance().single() + var currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance().single() println(currentIRS.prettyPrint()) while (true) { val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n") var fixTX: LedgerTransaction = run { - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() val fixing = Pair(nextFixingDate, FixedRate("0.052".percent)) InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing) with(tx) { @@ -323,7 +322,7 @@ class IRSTests { } tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) } - currentIRS = previousTXN.outputs.filterIsInstance().single() + currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance().single() println(currentIRS.prettyPrint()) previousTXN = fixTX } @@ -387,11 +386,11 @@ class IRSTests { transaction("Fix") { input("irs post agreement") output("irs post first fixing") { - "irs post agreement".output.copy( - "irs post agreement".output.fixedLeg, - "irs post agreement".output.floatingLeg, - "irs post agreement".output.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), - "irs post agreement".output.common + "irs post agreement".output.data.copy( + "irs post agreement".output.data.fixedLeg, + "irs post agreement".output.data.floatingLeg, + "irs post agreement".output.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), + "irs post agreement".output.data.common ) } arg(ORACLE_PUBKEY) { @@ -678,7 +677,6 @@ class IRSTests { irs.floatingLeg, irs.calculation, irs.common.copy(tradeID = "t1") - ) } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } @@ -692,7 +690,6 @@ class IRSTests { irs.floatingLeg, irs.calculation, irs.common.copy(tradeID = "t2") - ) } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } @@ -703,19 +700,19 @@ class IRSTests { input("irs post agreement1") input("irs post agreement2") output("irs post first fixing1") { - "irs post agreement1".output.copy( - "irs post agreement1".output.fixedLeg, - "irs post agreement1".output.floatingLeg, - "irs post agreement1".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - "irs post agreement1".output.common.copy(tradeID = "t1") + "irs post agreement1".output.data.copy( + "irs post agreement1".output.data.fixedLeg, + "irs post agreement1".output.data.floatingLeg, + "irs post agreement1".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + "irs post agreement1".output.data.common.copy(tradeID = "t1") ) } output("irs post first fixing2") { - "irs post agreement2".output.copy( - "irs post agreement2".output.fixedLeg, - "irs post agreement2".output.floatingLeg, - "irs post agreement2".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - "irs post agreement2".output.common.copy(tradeID = "t2") + "irs post agreement2".output.data.copy( + "irs post agreement2".output.data.fixedLeg, + "irs post agreement2".output.data.floatingLeg, + "irs post agreement2".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + "irs post agreement2".output.data.common.copy(tradeID = "t2") ) } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt index 2b7ee70510..163a1378db 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt @@ -4,6 +4,7 @@ import com.r3corda.core.contracts.DummyContract import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` import com.r3corda.contracts.testing.`with deposit` +import com.r3corda.contracts.testing.`with notary` import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash @@ -12,19 +13,14 @@ import com.r3corda.core.testing.* import org.junit.Test import java.security.PublicKey import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue +import kotlin.test.* class CashTests { val defaultRef = OpaqueBytes(ByteArray(1, {1})) val defaultIssuer = MEGA_CORP.ref(defaultRef) val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = DUMMY_PUBKEY_1, - notary = DUMMY_NOTARY + owner = DUMMY_PUBKEY_1 ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) @@ -71,7 +67,7 @@ class CashTests { fun issueMoney() { // Check we can't "move" money into existence. transaction { - input { DummyContract.State(notary = DUMMY_NOTARY) } + input { DummyContract.State() } output { outState } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } @@ -89,8 +85,7 @@ class CashTests { output { Cash.State( amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), - owner = DUMMY_PUBKEY_1, - notary = DUMMY_NOTARY + owner = DUMMY_PUBKEY_1 ) } tweak { @@ -102,10 +97,10 @@ class CashTests { } // Test generation works. - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(ptx.inputStates().isEmpty()) - val s = ptx.outputStates()[0] as Cash.State + val s = ptx.outputStates()[0].data as Cash.State assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) assertEquals(MINI_CORP, s.deposit.party) assertEquals(DUMMY_PUBKEY_1, s.owner) @@ -114,7 +109,7 @@ class CashTests { // Test issuance from the issuance definition val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) - val templatePtx = TransactionBuilder() + val templatePtx = TransactionType.General.Builder() Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(templatePtx.inputStates().isEmpty()) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) @@ -181,15 +176,15 @@ class CashTests { @Test(expected = IllegalStateException::class) fun `reject issuance with inputs`() { // Issue some cash - var ptx = TransactionBuilder() + var ptx = TransactionType.General.Builder() Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) ptx.signWith(MINI_CORP_KEY) val tx = ptx.toSignedTransaction() // Include the previously issued cash in a new issuance command - ptx = TransactionBuilder() - ptx.addInputState(tx.tx.outRef(0).ref) + ptx = TransactionType.General.Builder() + ptx.addInputState(tx.tx.outRef(0)) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) } @@ -359,7 +354,7 @@ class CashTests { fun multiCurrency() { // Check we can do an atomic currency trade tx. transaction { - val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), DUMMY_PUBKEY_2, DUMMY_NOTARY) + val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), DUMMY_PUBKEY_2) input { inState `owned by` DUMMY_PUBKEY_1 } input { pounds } output { inState `owned by` DUMMY_PUBKEY_2 } @@ -379,7 +374,7 @@ class CashTests { fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( - Cash.State(amount `issued by` corp.ref(depositRef), OUR_PUBKEY_1, DUMMY_NOTARY), + Cash.State(amount `issued by` corp.ref(depositRef), OUR_PUBKEY_1) `with notary` DUMMY_NOTARY, StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) @@ -391,7 +386,7 @@ class CashTests { ) fun makeSpend(amount: Amount, dest: PublicKey, corp: Party, depositRef: OpaqueBytes = defaultRef): WireTransaction { - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() Cash().generateSpend(tx, amount, dest, WALLET) return tx.toWireTransaction() } @@ -400,13 +395,13 @@ class CashTests { fun generateSimpleDirectSpend() { val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @Test fun generateSimpleSpendWithParties() { - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP)) assertEquals(WALLET[2].ref, tx.inputStates()[0]) } @@ -415,8 +410,8 @@ class CashTests { fun generateSimpleSpendWithChange() { val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) - assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(WALLET[0].state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -425,7 +420,7 @@ class CashTests { val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[1].ref, wtx.inputs[1]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -436,8 +431,8 @@ class CashTests { assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[1].ref, wtx.inputs[1]) assertEquals(WALLET[2].ref, wtx.inputs[2]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) - assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1]) + assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(WALLET[2].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -458,9 +453,9 @@ class CashTests { */ @Test fun aggregation() { - val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP_PUBKEY, DUMMY_NOTARY) - val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP_PUBKEY, DUMMY_NOTARY) - val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY, DUMMY_NOTARY) + val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP_PUBKEY) + val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP_PUBKEY) + val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY) // Obviously it must be possible to aggregate states with themselves assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef) @@ -474,7 +469,7 @@ class CashTests { // States cannot be aggregated if the currency differs assertNotEquals(oneThousandDollarsFromMini.issuanceDef, - Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY, DUMMY_NOTARY).issuanceDef) + Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).issuanceDef) // States cannot be aggregated if the reference differs assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef) @@ -484,9 +479,9 @@ class CashTests { @Test fun `summing by owner`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP_PUBKEY)) } @@ -494,8 +489,8 @@ class CashTests { @Test(expected = UnsupportedOperationException::class) fun `summing by owner throws`() { val states = listOf( - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) states.sumCashBy(MINI_CORP_PUBKEY) } @@ -516,9 +511,9 @@ class CashTests { @Test fun `summing a single currency`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) // Test that summing everything produces the total number of dollars var expected = 7000.DOLLARS `issued by` defaultIssuer @@ -529,8 +524,8 @@ class CashTests { @Test(expected = IllegalArgumentException::class) fun `summing multiple currencies`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), + Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) ) // Test that summing everything fails because we're mixing units states.sumCash() 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 75555e18b0..d7018b9c3e 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -92,7 +92,7 @@ fun List>.getTimestampByName(vararg names: Stri */ @Throws(IllegalArgumentException::class) // TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here? -inline fun verifyMoveCommands(inputs: List, tx: TransactionForVerification) { +inline fun verifyMoveCommands(inputs: List, tx: TransactionForContract) { // Now check the digital signatures on the move command. Every input has an owning public key, and we must // see a signature from each of those keys. The actual signatures have been verified against the transaction // data by the platform before execution. diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt index d2c1cf75e2..41038e128b 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt @@ -2,22 +2,40 @@ package com.r3corda.core.contracts import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash +import java.security.PublicKey // The dummy contract doesn't do anything useful. It exists for testing purposes. val DUMMY_PROGRAM_ID = DummyContract() class DummyContract : Contract { - class State(val magicNumber: Int = 0, - override val notary: Party) : ContractState { + data class State(val magicNumber: Int = 0) : ContractState { override val contract = DUMMY_PROGRAM_ID + override val participants: List + get() = emptyList() + } + + data class SingleOwnerState(val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState { + override val contract = DUMMY_PROGRAM_ID + override val participants: List + get() = listOf(owner) + + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + } + + data class MultiOwnerState(val magicNumber: Int = 0, + val owners: List) : ContractState { + override val contract = DUMMY_PROGRAM_ID + override val participants: List + get() = owners } interface Commands : CommandData { class Create : TypeOnlyCommandData(), Commands + class Move : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Always accepts. } @@ -25,7 +43,7 @@ class DummyContract : Contract { override val legalContractReference: SecureHash = SecureHash.sha256("") fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = State(magicNumber, notary) - return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + val state = SingleOwnerState(magicNumber, owner.party.owningKey) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) } } \ No newline at end of file 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 798e9616c4..4be3e957b9 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -1,9 +1,5 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.TransactionBuilder -import com.r3corda.core.contracts.TransactionForVerification -import com.r3corda.core.contracts.Fix -import com.r3corda.core.contracts.FixOf import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort @@ -31,8 +27,30 @@ interface ContractState { /** Contract by which the state belongs */ val contract: Contract - /** Identity of the notary that ensures this state is not used as an input to a transaction more than once */ - val notary: Party + /** + * A _participant_ is any party that is able to consume this state in a valid transaction. + * + * The list of participants is required for certain types of transactions. For example, when changing the notary + * for this state ([TransactionType.NotaryChange]), every participants has to be involved and approve the transaction + * so that they receive the updated state, and don't end up in a situation where they can no longer use a state + * they possess, since someone consumed that state during the notary change process. + * + * The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants + * list should just contain the owner. + */ + val participants: List +} + +/** A wrapper for [ContractState] containing additional platform-level state information. This is the state */ +data class TransactionState( + val data: T, + /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ + val notary: Party) { + /** + * Copies the underlying state, replacing the notary field with the new value. + * To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState] + */ + fun withNewNotary(newNotary: Party) = TransactionState(this.data, newNotary) } /** @@ -100,7 +118,7 @@ interface DealState : LinearState { * TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a * Contract instance from a ContractState are imminent, at which point we can move this out of here */ - fun generateAgreement(): TransactionBuilder + fun generateAgreement(notary: Party): TransactionBuilder } /** @@ -120,7 +138,7 @@ interface FixableDealState : DealState { * TODO: This would also likely move to methods on the Contract once the changes to reference * the Contract from the ContractState are in */ - fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) + fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) } /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ @@ -135,11 +153,11 @@ data class StateRef(val txhash: SecureHash, val index: Int) { } /** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */ -data class StateAndRef(val state: T, val ref: StateRef) +data class StateAndRef(val state: TransactionState, val ref: StateRef) /** Filters a list of [StateAndRef] objects according to the type of the states */ inline fun List>.filterStatesOfType(): List> { - return mapNotNull { if (it.state is T) StateAndRef(it.state, it.ref) else null } + return mapNotNull { if (it.state.data is T) StateAndRef(TransactionState(it.state.data, it.state.notary), it.ref) else null } } /** @@ -210,7 +228,7 @@ interface Contract { * existing contract code. */ @Throws(IllegalArgumentException::class) - fun verify(tx: TransactionForVerification) + fun verify(tx: TransactionForContract) /** * Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 820d9221f0..b66803898f 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -1,12 +1,6 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.WireTransaction -import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.DigitalSignature -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.crypto.signWithECDSA +import com.r3corda.core.crypto.* import com.r3corda.core.serialization.serialize import java.security.KeyPair import java.security.PublicKey @@ -16,14 +10,19 @@ import java.util.* /** * A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is - * intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set. - * Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from - * multiple parties. + * intended to be passed around contracts that may edit it by adding new states/commands. Then once the states + * and commands are right, this class can be used as a holding bucket to gather signatures from multiple parties. + * + * The builder can be customised for specific transaction types, e.g. where additional processing is needed + * before adding a state/command. */ -class TransactionBuilder(private val inputs: MutableList = arrayListOf(), - private val attachments: MutableList = arrayListOf(), - private val outputs: MutableList = arrayListOf(), - private val commands: MutableList = arrayListOf()) { +abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(), + protected val notary: Party? = null) { + protected val inputs: MutableList = arrayListOf() + protected val attachments: MutableList = arrayListOf() + protected val outputs: MutableList> = arrayListOf() + protected val commands: MutableList = arrayListOf() + protected val signers: MutableSet = mutableSetOf() val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() @@ -49,7 +48,8 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { when (t) { - is StateRef -> addInputState(t) + is StateAndRef<*> -> addInputState(t) + is TransactionState<*> -> addOutputState(t) is ContractState -> addOutputState(t) is Command -> addCommand(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") @@ -59,7 +59,7 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf } /** The signatures that have been collected so far - might be incomplete! */ - private val currentSigs = arrayListOf() + protected val currentSigs = arrayListOf() fun signWith(key: KeyPair) { check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" } @@ -96,22 +96,25 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf } fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands)) + ArrayList(outputs), ArrayList(commands), signers.toList(), type) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { if (checkSufficientSignatures) { val gotKeys = currentSigs.map { it.by }.toSet() - for (command in commands) { - if (!gotKeys.containsAll(command.signers)) - throw IllegalStateException("Missing signatures on the transaction for a ${command.value.javaClass.canonicalName} command") - } + val missing = signers - gotKeys + if (missing.isNotEmpty()) + throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.map { it.toStringShort() }}") } return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - fun addInputState(ref: StateRef) { + open fun addInputState(stateAndRef: StateAndRef<*>) { check(currentSigs.isEmpty()) - inputs.add(ref) + + val notaryKey = stateAndRef.state.notary.owningKey + signers.add(notaryKey) + + inputs.add(stateAndRef.ref) } fun addAttachment(attachment: Attachment) { @@ -119,14 +122,22 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf attachments.add(attachment.id) } - fun addOutputState(state: ContractState) { + fun addOutputState(state: TransactionState<*>) { check(currentSigs.isEmpty()) outputs.add(state) } + fun addOutputState(state: ContractState, notary: Party) = addOutputState(TransactionState(state, notary)) + + fun addOutputState(state: ContractState) { + checkNotNull(notary) { "Need to specify a Notary for the state, or set a default one on TransactionBuilder initialisation" } + addOutputState(state, notary!!) + } + fun addCommand(arg: Command) { check(currentSigs.isEmpty()) - // We should probably merge the lists of pubkeys for identical commands here. + // TODO: replace pubkeys in commands with 'pointers' to keys in signers + signers.addAll(arg.signers) commands.add(arg) } @@ -136,7 +147,7 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputs) - fun outputStates(): List = ArrayList(outputs) + fun outputStates(): List> = ArrayList(outputs) fun commands(): List = ArrayList(commands) fun attachments(): List = ArrayList(attachments) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt index d749e31e95..82b140c8b8 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -1,9 +1,5 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.AuthenticatedObject -import com.r3corda.core.contracts.LedgerTransaction -import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.node.services.AttachmentStorage import com.r3corda.core.node.services.IdentityService import java.io.FileNotFoundException @@ -22,7 +18,7 @@ fun WireTransaction.toLedgerTransaction(identityService: IdentityService, val attachments = attachments.map { attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } - return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id) + return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id, signers, type) } /** diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt new file mode 100644 index 0000000000..4216c8bf68 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -0,0 +1,115 @@ +package com.r3corda.core.contracts + +import com.r3corda.core.crypto.Party +import com.r3corda.core.noneOrSingle +import java.security.PublicKey + +/** Defines transaction build & validation logic for a specific transaction type */ +sealed class TransactionType { + override fun equals(other: Any?) = other?.javaClass == javaClass + override fun hashCode() = javaClass.name.hashCode() + + /** + * Check that the transaction is valid based on: + * - General platform rules + * - Rules for the specific transaction type + * + * Note: Presence of _signatures_ is not checked, only the public keys to be signed for. + */ + fun verify(tx: TransactionForVerification) { + + val missing = verifySigners(tx) + if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList()) + + verifyTransaction(tx) + } + + /** Check that the list of signers includes all the necessary keys */ + fun verifySigners(tx: TransactionForVerification): Set { + val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand } + val timestampKey = timestamp?.signers.orEmpty() + val notaryKey = (tx.inStates.map { it.notary.owningKey } + timestampKey).toSet() + if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx) + + val requiredKeys = getRequiredSigners(tx) + notaryKey + val missing = requiredKeys - tx.signers + + return missing + } + + /** + * Return the list of public keys that that require signatures for the transaction type. + * Note: the notary key is checked separately for all transactions and need not be included + */ + abstract fun getRequiredSigners(tx: TransactionForVerification): Set + + /** Implement type specific transaction validation logic */ + abstract fun verifyTransaction(tx: TransactionForVerification) + + /** A general transaction type where transaction validity is determined by custom contract code */ + class General : TransactionType() { + /** Just uses the default [TransactionBuilder] with no special logic */ + class Builder(notary: Party? = null) : TransactionBuilder(General(), notary) {} + + /** + * Check the transaction is contract-valid by running the verify() for each input and output state contract. + * If any contract fails to verify, the whole transaction is considered to be invalid + */ + override fun verifyTransaction(tx: TransactionForVerification) { + // TODO: Check that notary is unchanged + val ctx = tx.toTransactionForContract() + + val contracts = (ctx.inStates.map { it.contract } + ctx.outStates.map { it.contract }).toSet() + for (contract in contracts) { + try { + contract.verify(ctx) + } catch(e: Throwable) { + throw TransactionVerificationException.ContractRejection(tx, contract, e) + } + } + } + + override fun getRequiredSigners(tx: TransactionForVerification): Set { + val commandKeys = tx.commands.flatMap { it.signers }.toSet() + return commandKeys + } + } + + /** + * A special transaction type for reassigning a notary for a state. Validation does not involve running + * any contract code, it just checks that the states are unmodified apart from the notary field. + */ + class NotaryChange : TransactionType() { + /** + * A transaction builder that automatically sets the transaction type to [NotaryChange] + * and adds the list of participants to the signers set for every input state. + */ + class Builder(notary: Party? = null) : TransactionBuilder(NotaryChange(), notary) { + override fun addInputState(stateAndRef: StateAndRef<*>) { + signers.addAll(stateAndRef.state.data.participants) + super.addInputState(stateAndRef) + } + } + + /** + * Check that the difference between inputs and outputs is only the notary field, + * and that all required signing public keys are present + */ + override fun verifyTransaction(tx: TransactionForVerification) { + try { + tx.inStates.zip(tx.outStates).forEach { + check(it.first.data == it.second.data) + check(it.first.notary != it.second.notary) + } + check(tx.commands.isEmpty()) + } catch (e: IllegalStateException) { + throw TransactionVerificationException.InvalidNotaryChange(tx) + } + } + + override fun getRequiredSigners(tx: TransactionForVerification): Set { + val participantKeys = tx.inStates.flatMap { it.data.participants }.toSet() + return participantKeys + } + } +} \ No newline at end of file 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 551ca6734a..459a5c3eda 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -1,8 +1,8 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash +import java.security.PublicKey import java.util.* // TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts. @@ -30,7 +30,7 @@ class TransactionGroup(val transactions: Set, val nonVerified val resolved = HashSet(transactions.size) for (tx in transactions) { - val inputs = ArrayList(tx.inputs.size) + val inputs = ArrayList>(tx.inputs.size) for (ref in tx.inputs) { val conflict = refToConsumingTXMap[ref] if (conflict != null) @@ -42,74 +42,51 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the output in that transaction by index. inputs.add(ltx.outputs[ref.index]) } - resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id)) + resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.signers, tx.type)) } for (tx in resolved) tx.verify() return resolved } - } /** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */ -data class TransactionForVerification(val inStates: List, - val outStates: List, +data class TransactionForVerification(val inStates: List>, + val outStates: List>, val attachments: List, val commands: List>, - val origHash: SecureHash) { + val origHash: SecureHash, + val signers: List, + val type: TransactionType) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash /** - * Verifies that the transaction is valid: - * - Checks that the input states and the timestamp point to the same Notary - * - Runs the contracts for this transaction. If any contract fails to verify, the whole transaction - * is considered to be invalid + * Verifies that the transaction is valid by running type-specific validation logic. * * TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated. * - * @throws TransactionVerificationException if a contract throws an exception (the original is in the cause field) - * or the transaction has references to more than one Notary + * @throws TransactionVerificationException if validation logic fails or if a contract throws an exception + * (the original is in the cause field) */ @Throws(TransactionVerificationException::class) - fun verify() { - verifySingleNotary() - val contracts = (inStates.map { it.contract } + outStates.map { it.contract }).toSet() - for (contract in contracts) { - try { - contract.verify(this) - } catch(e: Throwable) { - throw TransactionVerificationException.ContractRejection(this, contract, e) - } - } - } + fun verify() = type.verify(this) - private fun verifySingleNotary() { - if (inStates.isEmpty()) return - val notary = inStates.first().notary - if (inStates.any { it.notary != notary }) throw TransactionVerificationException.MoreThanOneNotary(this) - val timestampCmd = commands.singleOrNull { it.value is TimestampCommand } ?: return - if (!timestampCmd.signers.contains(notary.owningKey)) throw TransactionVerificationException.MoreThanOneNotary(this) - } + fun toTransactionForContract() = TransactionForContract(inStates.map { it.data }, outStates.map { it.data }, attachments, commands, origHash) +} - /** - * Utilities for contract writers to incorporate into their logic. - */ - - /** - * A set of related inputs and outputs that are connected by some common attributes. An InOutGroup is calculated - * using [groupStates] and is useful for handling cases where a transaction may contain similar but unrelated - * state evolutions, for example, a transaction that moves cash in two different currencies. The numbers must add - * up on both sides of the transaction, but the values must be summed independently per currency. Grouping can - * be used to simplify this logic. - */ - data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) - - /** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */ - fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) - /** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */ - fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName) +/** + * A transaction to be passed as input to a contract verification function. Defines helper methods to + * simplify verification logic in contracts. + */ +data class TransactionForContract(val inStates: List, + val outStates: List, + val attachments: List, + val commands: List>, + val origHash: SecureHash) { + override fun hashCode() = origHash.hashCode() + override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash /** * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they @@ -161,6 +138,24 @@ data class TransactionForVerification(val inStates: List, return result } + + /** Utilities for contract writers to incorporate into their logic. */ + + /** + * A set of related inputs and outputs that are connected by some common attributes. An InOutGroup is calculated + * using [groupStates] and is useful for handling cases where a transaction may contain similar but unrelated + * state evolutions, for example, a transaction that moves cash in two different currencies. The numbers must add + * up on both sides of the transaction, but the values must be summed independently per currency. Grouping can + * be used to simplify this logic. + */ + data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) + + /** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */ + fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) + + /** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */ + fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName) + } class TransactionResolutionException(val hash: SecureHash) : Exception() @@ -169,4 +164,6 @@ class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTra sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) { class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause) 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/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt index 9d4260650d..7f5ea0f8f5 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -1,7 +1,6 @@ package com.r3corda.core.contracts import com.esotericsoftware.kryo.Kryo -import com.r3corda.core.contracts.* import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort @@ -44,8 +43,10 @@ import java.security.SignatureException /** Transaction ready for serialisation, without any signatures attached. */ data class WireTransaction(val inputs: List, val attachments: List, - val outputs: List, - val commands: List) : NamedByHash { + val outputs: List>, + val commands: List, + val signers: List, + val type: TransactionType) : NamedByHash { // Cache the serialised form of the transaction and its hash to give us fast access to it. @Volatile @Transient private var cachedBits: SerializedBytes? = null @@ -64,11 +65,11 @@ data class WireTransaction(val inputs: List, @Suppress("UNCHECKED_CAST") fun outRef(index: Int): StateAndRef { require(index >= 0 && index < outputs.size) - return StateAndRef(outputs[index] as T, StateRef(id, index)) + return StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) } /** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */ - fun outRef(state: ContractState): StateAndRef = outRef(outputs.indexOfOrThrow(state)) + fun outRef(state: ContractState): StateAndRef = outRef(outputs.map { it.data }.indexOfOrThrow(state)) override fun toString(): String { val buf = StringBuilder() @@ -110,7 +111,7 @@ data class SignedTransaction(val txBits: SerializedBytes, /** * Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains - * the set of pubkeys in the commands. If any signatures are missing, either throws an exception (by default) or + * the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or * returns the list of keys that have missing signatures, depending on the parameter. * * @throws SignatureException if a signature is invalid, does not match or if any signature is missing. @@ -131,15 +132,20 @@ data class SignedTransaction(val txBits: SerializedBytes, return copy(sigs = sigs + sig) } + fun withAdditionalSignatures(sigList: Iterable): SignedTransaction { + return copy(sigs = sigs + sigList) + } + /** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */ operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig) + operator fun plus(sigList: Collection) = withAdditionalSignatures(sigList) + /** - * Returns the set of missing signatures - a signature must be present for every command pub key - * and the Notary (if it is specified) + * Returns the set of missing signatures - a signature must be present for each signer public key */ fun getMissingSignatures(): Set { - val requiredKeys = tx.commands.flatMap { it.signers }.toSet() + val requiredKeys = tx.signers.toSet() val sigKeys = sigs.map { it.by }.toSet() if (sigKeys.containsAll(requiredKeys)) return emptySet() @@ -160,12 +166,14 @@ data class LedgerTransaction( /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */ val attachments: List, /** The states that will be generated by the execution of this transaction. */ - val outputs: List, + val outputs: List>, /** Arbitrary data passed to the program of each input state. */ val commands: List>, /** The hash of the original serialised WireTransaction */ - override val id: SecureHash + override val id: SecureHash, + val signers: List, + val type: TransactionType ) : NamedByHash { @Suppress("UNCHECKED_CAST") - fun outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(id, index)) + fun outRef(index: Int) = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt b/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt index a0e8c253bb..4f526b5b55 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt @@ -53,7 +53,7 @@ interface ServiceHub { * * @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction */ - fun loadState(stateRef: StateRef): ContractState { + fun loadState(stateRef: StateRef): TransactionState<*> { val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return definingTx.tx.outputs[stateRef.index] } diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index 5d2c106311..946208d72a 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -30,7 +30,7 @@ abstract class Wallet { abstract val states: List> @Suppress("UNCHECKED_CAST") - inline fun statesOfType() = states.filter { it.state is T } as List> + inline fun statesOfType() = states.filter { it.state.data is T } as List> /** * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for @@ -99,10 +99,10 @@ interface WalletService { /** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */ @Suppress("UNCHECKED_CAST") fun linearHeadsOfType_(stateType: Class): Map> { - return linearHeads.filterValues { stateType.isInstance(it.state) }.mapValues { StateAndRef(it.value.state as T, it.value.ref) } + return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState, it.value.ref) } } - fun statesForRefs(refs: List): Map { + fun statesForRefs(refs: List): Map?> { val refsToStates = currentWallet.states.associateBy { it.ref } return refs.associateBy({ it }, { refsToStates[it]?.state }) } diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt b/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt index 1ee6b76330..c54290e5ab 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/UniquenessProvider.kt @@ -1,8 +1,7 @@ package com.r3corda.core.node.services -import com.r3corda.core.crypto.Party import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.WireTransaction +import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash /** @@ -11,7 +10,7 @@ import com.r3corda.core.crypto.SecureHash */ interface UniquenessProvider { /** Commits all input states of the given transaction */ - fun commit(tx: WireTransaction, callerIdentity: Party) + fun commit(states: List, txId: SecureHash, callerIdentity: Party) /** Specifies the consuming transaction for every conflicting state */ data class Conflict(val stateHistory: Map) diff --git a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt index f7f5a1cff7..1d582082f4 100644 --- a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt @@ -29,6 +29,7 @@ import java.io.ObjectOutputStream import java.lang.reflect.InvocationTargetException import java.nio.file.Files import java.nio.file.Path +import java.security.PublicKey import java.time.Instant import java.util.* import javax.annotation.concurrent.ThreadSafe @@ -232,6 +233,8 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.attachments) kryo.writeClassAndObject(output, obj.outputs) kryo.writeClassAndObject(output, obj.commands) + kryo.writeClassAndObject(output, obj.signers) + kryo.writeClassAndObject(output, obj.type) } @Suppress("UNCHECKED_CAST") @@ -258,10 +261,12 @@ object WireTransactionSerializer : Serializer() { } else javaClass.classLoader kryo.useClassLoader(classLoader) { - val outputs = kryo.readClassAndObject(input) as List + val outputs = kryo.readClassAndObject(input) as List> val commands = kryo.readClassAndObject(input) as List + val signers = kryo.readClassAndObject(input) as List + val transactionType = kryo.readClassAndObject(input) as TransactionType - return WireTransaction(inputs, attachmentHashes, outputs, commands) + return WireTransaction(inputs, attachmentHashes, outputs, commands, signers, transactionType) } } } @@ -343,6 +348,7 @@ fun createKryo(k: Kryo = Kryo()): Kryo { // Work around a bug in Kryo handling nested generics register(Issued::class.java, ImmutableClassSerializer(Issued::class)) + register(TransactionState::class.java, ImmutableClassSerializer(TransactionState::class)) noReferencesWithin() } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index 604ebd6ad8..09ac70c4c5 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -4,12 +4,12 @@ package com.r3corda.core.testing import com.google.common.base.Throwables import com.google.common.net.HostAndPort -import com.r3corda.core.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.* -import com.r3corda.core.serialization.serialize import com.r3corda.core.node.services.testing.MockIdentityService import com.r3corda.core.node.services.testing.MockStorageService +import com.r3corda.core.seconds +import com.r3corda.core.serialization.serialize import java.net.ServerSocket import java.security.KeyPair import java.security.PublicKey @@ -96,20 +96,22 @@ fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) // // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block -class LabeledOutput(val label: String?, val state: ContractState) { +class LabeledOutput(val label: String?, val state: TransactionState<*>) { override fun toString() = state.toString() + (if (label != null) " ($label)" else "") override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state) override fun hashCode(): Int = state.hashCode() } -infix fun ContractState.label(label: String) = LabeledOutput(label, this) +infix fun TransactionState<*>.label(label: String) = LabeledOutput(label, this) abstract class AbstractTransactionForTest { protected val attachments = ArrayList() protected val outStates = ArrayList() protected val commands = ArrayList() + protected val signers = LinkedHashSet() + protected val type = TransactionType.General() - open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) } + open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) } protected fun commandsToAuthenticatedObjects(): List> { return commands.map { AuthenticatedObject(it.signers, it.signers.mapNotNull { MOCK_IDENTITY_SERVICE.partyFromKey(it) }, it.value) } @@ -121,7 +123,7 @@ abstract class AbstractTransactionForTest { fun arg(vararg key: PublicKey, c: () -> CommandData) { val keys = listOf(*key) - commands.add(Command(c(), keys)) + addCommand(Command(c(), keys)) } fun timestamp(time: Instant) { @@ -130,7 +132,12 @@ abstract class AbstractTransactionForTest { } fun timestamp(data: TimestampCommand) { - commands.add(Command(data, DUMMY_NOTARY.owningKey)) + addCommand(Command(data, DUMMY_NOTARY.owningKey)) + } + + fun addCommand(cmd: Command) { + signers.addAll(cmd.signers) + commands.add(cmd) } // Forbid patterns like: transaction { ... transaction { ... } } @@ -150,12 +157,15 @@ sealed class LastLineShouldTestForAcceptOrFailure { // Corresponds to the args to Contract.verify open class TransactionForTest : AbstractTransactionForTest() { - private val inStates = arrayListOf() - fun input(s: () -> ContractState) = inStates.add(s()) + private val inStates = arrayListOf>() + fun input(s: () -> ContractState) { + signers.add(DUMMY_NOTARY.owningKey) + inStates.add(TransactionState(s(), DUMMY_NOTARY)) + } protected fun runCommandsAndVerify(time: Instant) { val cmds = commandsToAuthenticatedObjects() - val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256()) + val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), signers.toList(), type) tx.verify() } @@ -210,6 +220,9 @@ open class TransactionForTest : AbstractTransactionForTest() { tx.inStates.addAll(inStates) tx.outStates.addAll(outStates) tx.commands.addAll(commands) + + tx.signers.addAll(tx.inStates.map { it.notary.owningKey }) + tx.signers.addAll(commands.flatMap { it.signers }) return tx.body() } @@ -240,16 +253,24 @@ class TransactionGroupDSL(private val stateType: Class) { private val inStates = ArrayList() fun input(label: String) { + val notaryKey = label.output.notary.owningKey + signers.add(notaryKey) inStates.add(label.outputRef) } - fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands) + fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, signers.toList(), type) } - val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found") + val String.output: TransactionState + get() = + labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found") val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"") - fun lookup(label: String) = StateAndRef(label.output as C, label.outputRef) + fun lookup(label: String): StateAndRef { + val output = label.output + val newOutput = TransactionState(output.data as C, output.notary) + return StateAndRef(newOutput, label.outputRef) + } private inner class InternalWireTransactionDSL : WireTransactionDSL() { fun finaliseAndInsertLabels(): WireTransaction { @@ -257,8 +278,8 @@ class TransactionGroupDSL(private val stateType: Class) { for ((index, labelledState) in outStates.withIndex()) { if (labelledState.label != null) { labelToRefs[labelledState.label] = StateRef(wtx.id, index) - if (stateType.isInstance(labelledState.state)) { - labelToOutputs[labelledState.label] = labelledState.state as T + if (stateType.isInstance(labelledState.state.data)) { + labelToOutputs[labelledState.label] = labelledState.state as TransactionState } outputsToLabels[labelledState.state] = labelledState.label } @@ -269,20 +290,20 @@ class TransactionGroupDSL(private val stateType: Class) { private val rootTxns = ArrayList() private val labelToRefs = HashMap() - private val labelToOutputs = HashMap() - private val outputsToLabels = HashMap() + private val labelToOutputs = HashMap>() + private val outputsToLabels = HashMap, String>() - fun labelForState(state: T): String? = outputsToLabels[state] + fun labelForState(output: TransactionState<*>): String? = outputsToLabels[output] inner class Roots { fun transaction(vararg outputStates: LabeledOutput) { val outs = outputStates.map { it.state } - val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList()) + val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), emptyList(), TransactionType.General()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! labelToRefs[label] = StateRef(wtx.id, index) outputsToLabels[state.state] = label - labelToOutputs[label] = state.state as T + labelToOutputs[label] = state.state as TransactionState } rootTxns.add(wtx) } @@ -357,7 +378,7 @@ class TransactionGroupDSL(private val stateType: Class) { fun signAll(txnsToSign: List = txns, vararg extraKeys: KeyPair): List { return txnsToSign.map { wtx -> - val allPubKeys = wtx.commands.flatMap { it.signers }.toMutableSet() + val allPubKeys = wtx.signers.toMutableSet() val bits = wtx.serialize() require(bits == wtx.serialized) val sigs = ArrayList() diff --git a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt index 49eed3cb0b..6345e4ecbb 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt @@ -163,7 +163,7 @@ object NotaryProtocol { private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) { try { - uniquenessProvider.commit(tx, reqIdentity) + uniquenessProvider.commit(tx.inputs, tx.id, reqIdentity) } catch (e: UniquenessException) { val conflictData = e.error.serialize() val signedConflict = SignedData(conflictData, sign(conflictData)) diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index 81e0a2bcaa..34b83f33f3 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -314,7 +314,7 @@ object TwoPartyDealProtocol { } override fun assembleSharedTX(handshake: Handshake): Pair> { - val ptx = handshake.payload.generateAgreement() + val ptx = handshake.payload.generateAgreement(notary) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. @@ -336,7 +336,7 @@ object TwoPartyDealProtocol { val dealToFix: StateAndRef, sessionID: Long, val replacementProgressTracker: ProgressTracker? = null) : Secondary(otherSide, notary, sessionID) { - private val ratesFixTracker = RatesFixProtocol.tracker(dealToFix.state.nextFixingOf()!!.name) + private val ratesFixTracker = RatesFixProtocol.tracker(dealToFix.state.data.nextFixingOf()!!.name) override val progressTracker: ProgressTracker = replacementProgressTracker ?: createTracker() @@ -358,22 +358,21 @@ object TwoPartyDealProtocol { @Suspendable override fun assembleSharedTX(handshake: Handshake): Pair> { - val fixOf = dealToFix.state.nextFixingOf()!! + val fixOf = dealToFix.state.data.nextFixingOf()!! // TODO Do we need/want to substitute in new public keys for the Parties? val myName = serviceHub.storageService.myLegalIdentity.name - val deal: T = dealToFix.state + val deal: T = dealToFix.state.data val myOldParty = deal.parties.single { it.name == myName } @Suppress("UNCHECKED_CAST") val newDeal = deal - val oldRef = dealToFix.ref - val ptx = TransactionBuilder() + val ptx = TransactionType.General.Builder() val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapCache.ratesOracleNodes[0], fixOf, BigDecimal.ZERO, BigDecimal.ONE) { @Suspendable override fun beforeSigning(fix: Fix) { - newDeal.generateFix(ptx, oldRef, fix) + newDeal.generateFix(ptx, dealToFix, fix) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. diff --git a/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt new file mode 100644 index 0000000000..9acebf7aa6 --- /dev/null +++ b/core/src/main/kotlin/protocols/NotaryChangeProtocol.kt @@ -0,0 +1,260 @@ +package protocols + +import co.paralleluniverse.fibers.Suspendable +import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.DigitalSignature +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.signWithECDSA +import com.r3corda.core.messaging.SingleMessageRecipient +import com.r3corda.core.node.NodeInfo +import com.r3corda.core.protocols.ProtocolLogic +import com.r3corda.core.random63BitValue +import com.r3corda.core.utilities.ProgressTracker +import com.r3corda.protocols.AbstractRequestMessage +import com.r3corda.protocols.NotaryProtocol +import com.r3corda.protocols.ResolveTransactionsProtocol +import java.security.PublicKey + +/** + * A protocol to be used for changing a state's Notary. This is required since all input states to a transaction + * must point to the same notary. + * + * The [Instigator] assembles the transaction for notary replacement and sends out change proposals to all participants + * ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction. + * Finally, [Instigator] sends the transaction containing all signatures back to each participant so they can record it and + * use the new updated state for future transactions. + */ +object NotaryChangeProtocol { + val TOPIC_INITIATE = "platform.notary.change.initiate" + val TOPIC_CHANGE = "platform.notary.change.execute" + + data class Proposal(val stateRef: StateRef, + val newNotary: Party, + val stx: SignedTransaction) + + class Handshake(val sessionIdForSend: Long, + replyTo: SingleMessageRecipient, + replySessionId: Long) : AbstractRequestMessage(replyTo, replySessionId) + + class Instigator(val originalState: StateAndRef, + val newNotary: Party, + override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic>() { + companion object { + + object SIGNING : ProgressTracker.Step("Requesting signatures from other parties") + + object NOTARY : ProgressTracker.Step("Requesting current Notary signature") + + fun tracker() = ProgressTracker(SIGNING, NOTARY) + } + + @Suspendable + override fun call(): StateAndRef { + val (stx, participants) = assembleTx() + + progressTracker.currentStep = SIGNING + + val myKey = serviceHub.storageService.myLegalIdentity.owningKey + val me = listOf(myKey) + + val signatures = if (participants == me) { + listOf(getNotarySignature(stx.tx)) + } else { + collectSignatures(participants - me, stx) + } + + val finalTx = stx + signatures + serviceHub.recordTransactions(listOf(finalTx)) + return finalTx.tx.outRef(0) + } + + private fun assembleTx(): Pair> { + val state = originalState.state + val newState = state.withNewNotary(newNotary) + val participants = state.data.participants + val tx = TransactionType.NotaryChange.Builder().withItems(originalState, newState) + tx.signWith(serviceHub.storageService.myLegalIdentityKey) + + val stx = tx.toSignedTransaction(false) + return Pair(stx, participants) + } + + @Suspendable + private fun collectSignatures(participants: List, stx: SignedTransaction): List { + val sessions = mutableMapOf() + + val participantSignatures = participants.map { + val participantNode = serviceHub.networkMapCache.getNodeByPublicKey(it) ?: + throw IllegalStateException("Participant $it to state $originalState not found on the network") + val sessionIdForSend = random63BitValue() + sessions[participantNode] = sessionIdForSend + + getParticipantSignature(participantNode, stx, sessionIdForSend) + } + + val allSignatures = participantSignatures + getNotarySignature(stx.tx) + sessions.forEach { send(TOPIC_CHANGE, it.key.address, it.value, allSignatures) } + + return allSignatures + } + + @Suspendable + private fun getParticipantSignature(node: NodeInfo, stx: SignedTransaction, sessionIdForSend: Long): DigitalSignature.WithKey { + val sessionIdForReceive = random63BitValue() + val proposal = Proposal(originalState.ref, newNotary, stx) + + val handshake = Handshake(sessionIdForSend, serviceHub.networkService.myAddress, sessionIdForReceive) + sendAndReceive(TOPIC_INITIATE, node.address, 0, sessionIdForReceive, handshake) + + val response = sendAndReceive(TOPIC_CHANGE, node.address, sessionIdForSend, sessionIdForReceive, proposal) + val participantSignature = response.validate { + if (it.sig == null) throw NotaryChangeException(it.error!!) + else { + check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" } + it.sig.verifyWithECDSA(stx.txBits) + it.sig + } + } + + return participantSignature + } + + @Suspendable + private fun getNotarySignature(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable { + progressTracker.currentStep = NOTARY + return subProtocol(NotaryProtocol.Client(wtx)) + } + } + + class Acceptor(val otherSide: SingleMessageRecipient, + val sessionIdForSend: Long, + val sessionIdForReceive: Long, + override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic() { + + companion object { + object VERIFYING : ProgressTracker.Step("Verifying Notary change proposal") + + object APPROVING : ProgressTracker.Step("Notary change approved") + + object REJECTING : ProgressTracker.Step("Notary change rejected") + + fun tracker() = ProgressTracker(VERIFYING, APPROVING, REJECTING) + } + + @Suspendable + override fun call() { + progressTracker.currentStep = VERIFYING + val proposal = receive(TOPIC_CHANGE, sessionIdForReceive).validate { it } + + try { + verifyProposal(proposal) + verifyTx(proposal.stx) + } catch(e: Exception) { + // TODO: catch only specific exceptions. However, there are numerous validation exceptions + // that might occur (tx validation/resolution, invalid proposal). Need to rethink how + // we manage exceptions and maybe introduce some platform exception hierarchy + val myIdentity = serviceHub.storageService.myLegalIdentity + val state = proposal.stateRef + val reason = NotaryChangeRefused(myIdentity, state, e.message) + + reject(reason) + return + } + + approve(proposal.stx) + } + + @Suspendable + private fun approve(stx: SignedTransaction) { + progressTracker.currentStep = APPROVING + + val mySignature = sign(stx) + val response = Result.noError(mySignature) + val swapSignatures = sendAndReceive>(TOPIC_CHANGE, otherSide, sessionIdForSend, sessionIdForReceive, response) + + val allSignatures = swapSignatures.validate { signatures -> + signatures.forEach { it.verifyWithECDSA(stx.txBits) } + signatures + } + + val finalTx = stx + allSignatures + finalTx.verify() + serviceHub.recordTransactions(listOf(finalTx)) + } + + @Suspendable + private fun reject(e: NotaryChangeRefused) { + progressTracker.currentStep = REJECTING + val response = Result.withError(e) + send(TOPIC_CHANGE, otherSide, sessionIdForSend, response) + } + + /** + * Check the notary change proposal. + * + * For example, if the proposed new notary has the same behaviour (e.g. both are non-validating) + * and is also in a geographically convenient location we can just automatically approve the change. + * TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal + */ + @Suspendable + private fun verifyProposal(proposal: NotaryChangeProtocol.Proposal) { + val newNotary = proposal.newNotary + val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary } + require(isNotary) { "The proposed node $newNotary does not run a Notary service " } + + val state = proposal.stateRef + val proposedTx = proposal.stx.tx + require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" } + + // An example requirement + val blacklist = listOf("Evil Notary") + require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" } + } + + @Suspendable + private fun verifyTx(stx: SignedTransaction) { + checkMySignatureRequired(stx.tx) + checkDependenciesValid(stx) + checkValid(stx) + } + + private fun checkMySignatureRequired(tx: WireTransaction) { + // TODO: use keys from the keyManagementService instead + val myKey = serviceHub.storageService.myLegalIdentity.owningKey + require(tx.signers.contains(myKey)) { "Party is not a participant for any of the input states of transaction ${tx.id}" } + } + + @Suspendable + private fun checkDependenciesValid(stx: SignedTransaction) { + val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet() + subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide)) + } + + private fun checkValid(stx: SignedTransaction) { + val ltx = stx.tx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) + serviceHub.verifyTransaction(ltx) + } + + private fun sign(stx: SignedTransaction): DigitalSignature.WithKey { + val myKeyPair = serviceHub.storageService.myLegalIdentityKey + return myKeyPair.signWithECDSA(stx.txBits) + } + } + + // TODO: similar classes occur in other places (NotaryProtocol), need to consolidate + data class Result private constructor(val sig: DigitalSignature.WithKey?, val error: NotaryChangeRefused?) { + companion object { + fun withError(error: NotaryChangeRefused) = Result(null, error) + fun noError(sig: DigitalSignature.WithKey) = Result(sig, null) + } + } +} + +/** Thrown when a participant refuses to change the notary of the state */ +class NotaryChangeRefused(val identity: Party, val state: StateRef, val cause: String?) { + override fun toString() = "A participant $identity refused to change the notary of state $state" +} + +class NotaryChangeException(val error: NotaryChangeRefused) : Exception() { + override fun toString() = "${super.toString()}: Notary change failed - ${error.toString()}" +} \ No newline at end of file diff --git a/core/src/main/resources/com/r3corda/core/node/isolated.jar b/core/src/main/resources/com/r3corda/core/node/isolated.jar index 974d8d182f..93f4c94cbd 100644 Binary files a/core/src/main/resources/com/r3corda/core/node/isolated.jar and b/core/src/main/resources/com/r3corda/core/node/isolated.jar differ diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt index 247e5e05c8..8fcf313900 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGraphSearchTests.kt @@ -26,13 +26,13 @@ class TransactionGraphSearchTests { * @param signer signer for the two transactions and their commands. */ fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage { - val originTx = TransactionBuilder().apply { - addOutputState(DummyContract.State(random31BitValue(), DUMMY_NOTARY)) + val originTx = TransactionType.General.Builder().apply { + addOutputState(DummyContract.State(random31BitValue()), DUMMY_NOTARY) addCommand(command, signer.public) signWith(signer) }.toSignedTransaction(false) - val inputTx = TransactionBuilder().apply { - addInputState(originTx.tx.outRef(0).ref) + val inputTx = TransactionType.General.Builder().apply { + addInputState(originTx.tx.outRef(0)) signWith(signer) }.toSignedTransaction(false) return GraphTransactionStorage(originTx, inputTx) diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt index c2e60ed365..84f595dc52 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt @@ -7,7 +7,7 @@ import com.r3corda.core.testing.* import org.junit.Test import java.security.PublicKey import java.security.SecureRandom -import java.util.Currency +import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals @@ -15,22 +15,25 @@ import kotlin.test.assertNotEquals val TEST_PROGRAM_ID = TransactionGroupTests.TestCash() class TransactionGroupTests { - val A_THOUSAND_POUNDS = TestCash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY, DUMMY_NOTARY) + val A_THOUSAND_POUNDS = TestCash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY) class TestCash : Contract { override val legalContractReference = SecureHash.sha256("TestCash") - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { } data class State( val deposit: PartyAndReference, val amount: Amount, - override val owner: PublicKey, - override val notary: Party) : OwnableState { + override val owner: PublicKey) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID + override val participants: List + get() = listOf(owner) + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } + interface Commands : CommandData { class Move() : TypeOnlyCommandData(), Commands data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands @@ -39,12 +42,13 @@ class TransactionGroupTests { } infix fun TestCash.State.`owned by`(owner: PublicKey) = copy(owner = owner) + infix fun TestCash.State.`with notary`(notary: Party) = TransactionState(this, notary) @Test fun success() { transactionGroup { roots { - transaction(A_THOUSAND_POUNDS label "£1000") + transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000") } transaction { @@ -117,17 +121,17 @@ class TransactionGroupTests { // We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that // points nowhere. - val input = generateStateRef() - tg.txns += TransactionBuilder().apply { + val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef()) + tg.txns += TransactionType.General.Builder().apply { addInputState(input) - addOutputState(A_THOUSAND_POUNDS) + addOutputState(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY) addCommand(TestCash.Commands.Move(), BOB_PUBKEY) }.toWireTransaction() val e = assertFailsWith(TransactionResolutionException::class) { tg.verify() } - assertEquals(e.hash, input.txhash) + assertEquals(e.hash, input.ref.txhash) } @Test @@ -135,7 +139,7 @@ class TransactionGroupTests { // Check that a transaction cannot refer to the same input more than once. transactionGroup { roots { - transaction(A_THOUSAND_POUNDS label "£1000") + transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000") } transaction { diff --git a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt index b984c62436..a375fcebfb 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt @@ -13,6 +13,7 @@ import org.junit.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.net.URLClassLoader +import java.security.PublicKey import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.test.assertEquals @@ -32,16 +33,17 @@ class AttachmentClassLoaderTests { } class AttachmentDummyContract : Contract { - class State(val magicNumber: Int = 0, - override val notary: Party) : ContractState { + data class State(val magicNumber: Int = 0) : ContractState { override val contract = ATTACHMENT_TEST_PROGRAM_ID + override val participants: List + get() = listOf() } interface Commands : CommandData { class Create : TypeOnlyCommandData(), Commands } - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { // Always accepts. } @@ -49,8 +51,8 @@ class AttachmentClassLoaderTests { override val legalContractReference: SecureHash = SecureHash.sha256("") fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = State(magicNumber, notary) - return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey)) + val state = State(magicNumber) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) } } @@ -215,7 +217,7 @@ class AttachmentClassLoaderTests { val copiedWireTransaction = bytes.deserialize() assertEquals(1, copiedWireTransaction.outputs.size) - assertEquals(42, (copiedWireTransaction.outputs[0] as AttachmentDummyContract.State).magicNumber) + assertEquals(42, (copiedWireTransaction.outputs[0].data as AttachmentDummyContract.State).magicNumber) } @Test @@ -245,8 +247,8 @@ class AttachmentClassLoaderTests { val copiedWireTransaction = bytes.deserialize(kryo2) assertEquals(1, copiedWireTransaction.outputs.size) - val contract2 = copiedWireTransaction.outputs[0].contract as DummyContractBackdoor - assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0])) + val contract2 = copiedWireTransaction.outputs[0].data.contract as DummyContractBackdoor + assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0].data)) } @Test diff --git a/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt b/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt index 437f8ecf73..8905950d2d 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/WalletUpdateTests.kt @@ -5,6 +5,7 @@ import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.Wallet import com.r3corda.core.testing.DUMMY_NOTARY import org.junit.Test +import java.security.PublicKey import kotlin.test.assertEquals @@ -12,14 +13,15 @@ class WalletUpdateTests { object DummyContract : Contract { - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { } override val legalContractReference: SecureHash = SecureHash.sha256("") } private class DummyState : ContractState { - override val notary = DUMMY_NOTARY + override val participants: List + get() = emptyList() override val contract = WalletUpdateTests.DummyContract } @@ -29,11 +31,11 @@ class WalletUpdateTests { private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3) private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4) - private val stateAndRef0 = StateAndRef(DummyState(), stateRef0) - private val stateAndRef1 = StateAndRef(DummyState(), stateRef1) - private val stateAndRef2 = StateAndRef(DummyState(), stateRef2) - private val stateAndRef3 = StateAndRef(DummyState(), stateRef3) - private val stateAndRef4 = StateAndRef(DummyState(), stateRef4) + private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef0) + private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef1) + private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef2) + private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef3) + private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_NOTARY), stateRef4) @Test fun `nothing plus nothing is nothing`() { diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index 1ab212e742..23cf58b655 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -1,7 +1,6 @@ package com.r3corda.core.serialization import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.seconds @@ -11,7 +10,7 @@ import org.junit.Test import java.security.PublicKey import java.security.SecureRandom import java.security.SignatureException -import java.util.Currency +import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -21,15 +20,17 @@ class TransactionSerializationTests { class TestCash : Contract { override val legalContractReference = SecureHash.sha256("TestCash") - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { } data class State( val deposit: PartyAndReference, val amount: Amount, - override val owner: PublicKey, - override val notary: Party) : OwnableState { + override val owner: PublicKey) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID + override val participants: List + get() = listOf(owner) + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } interface Commands : CommandData { @@ -39,24 +40,27 @@ class TransactionSerializationTests { } } - // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). + // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). // It refers to a fake TX/state that we don't bother creating here. val depositRef = MINI_CORP.ref(1) - val outputState = TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY) - val changeState = TestCash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY) - val fakeStateRef = generateStateRef() + val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY), fakeStateRef) + val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY) + val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, TestUtils.keypair.public), DUMMY_NOTARY) + + lateinit var tx: TransactionBuilder @Before fun setup() { - tx = TransactionBuilder().withItems( - fakeStateRef, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) + tx = TransactionType.General.Builder().withItems( + inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) ) } @Test fun signWireTX() { + tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(TestUtils.keypair) val signedTX = tx.toSignedTransaction() @@ -78,6 +82,7 @@ class TransactionSerializationTests { } tx.signWith(TestUtils.keypair) + tx.signWith(DUMMY_NOTARY_KEY) val signedTX = tx.toSignedTransaction() // Cannot construct with an empty sigs list. @@ -87,8 +92,9 @@ class TransactionSerializationTests { // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { - val tx2 = TransactionBuilder().withItems(fakeStateRef, outputState, changeState, + val tx2 = TransactionType.General.Builder().withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), TestUtils.keypair2.public)) + tx2.signWith(DUMMY_NOTARY_KEY) tx2.signWith(TestUtils.keypair2) signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() diff --git a/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt b/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt index 0c2331244f..7e3979c16a 100644 --- a/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt +++ b/node/src/main/kotlin/com/r3corda/node/api/APIServer.kt @@ -1,10 +1,7 @@ package com.r3corda.node.api +import com.r3corda.core.contracts.* import com.r3corda.node.api.StatesQuery -import com.r3corda.core.contracts.ContractState -import com.r3corda.core.contracts.SignedTransaction -import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.SecureHash import com.r3corda.core.serialization.SerializedBytes @@ -43,7 +40,7 @@ interface APIServer { */ fun queryStates(query: StatesQuery): List - fun fetchStates(states: List): Map + fun fetchStates(states: List): Map?> /** * Query for immutable transactions (results can be cached indefinitely by their id/hash). diff --git a/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt b/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt index bf9075f67e..4a8fd1f038 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/APIServerImpl.kt @@ -27,7 +27,7 @@ class APIServerImpl(val node: AbstractNode) : APIServer { return states.values.map { it.ref } } else if (query.criteria is StatesQuery.Criteria.Deal) { val states = node.services.walletService.linearHeadsOfType().filterValues { - it.state.ref == query.criteria.ref + it.state.data.ref == query.criteria.ref } return states.values.map { it.ref } } @@ -35,7 +35,7 @@ class APIServerImpl(val node: AbstractNode) : APIServer { return emptyList() } - override fun fetchStates(states: List): Map { + override fun fetchStates(states: List): Map?> { return node.services.walletService.statesForRefs(states) } diff --git a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt index 6a8eb1fd67..8a6e05c45f 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt @@ -16,6 +16,7 @@ import com.r3corda.core.seconds import com.r3corda.core.serialization.deserialize import com.r3corda.core.serialization.serialize import com.r3corda.node.api.APIServer +import com.r3corda.node.services.NotaryChangeService import com.r3corda.node.services.api.AcceptsFileUpload import com.r3corda.node.services.api.CheckpointStorage import com.r3corda.node.services.api.MonitoringService @@ -139,6 +140,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, // This object doesn't need to be referenced from this class because it registers handlers on the network // service and so that keeps it from being collected. DataVendingService(net, storage) + NotaryChangeService(net, smm) buildAdvertisedServices() diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt index 225f1a5d16..dc5c80a1cd 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt @@ -85,7 +85,7 @@ class IRSSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork val theDealRef: StateAndRef = swaps.values.single() // Do we have any more days left in this deal's lifetime? If not, return. - val nextFixingDate = theDealRef.state.calculation.nextFixingDate() ?: return null + val nextFixingDate = theDealRef.state.data.calculation.nextFixingDate() ?: return null extraNodeLabels[node1] = "Fixing event on $nextFixingDate" extraNodeLabels[node2] = "Fixing event on $nextFixingDate" diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt index 05966b6540..703a038f62 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/TestUtils.kt @@ -1,7 +1,10 @@ package com.r3corda.node.internal.testing +import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.DummyContract import com.r3corda.core.contracts.StateRef +import com.r3corda.core.contracts.TransactionState +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.crypto.Party import com.r3corda.core.seconds import com.r3corda.core.testing.DUMMY_NOTARY @@ -10,20 +13,34 @@ import com.r3corda.node.internal.AbstractNode import java.time.Instant import java.util.* -fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef { - val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) +fun issueState(node: AbstractNode): StateAndRef<*> { + val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY) tx.signWith(node.storage.myLegalIdentityKey) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() node.services.recordTransactions(listOf(stx)) - return StateRef(stx.id, 0) + return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } -fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef { +fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode): StateAndRef { + val state = TransactionState(DummyContract.MultiOwnerState(0, + listOf(nodeA.info.identity.owningKey, nodeB.info.identity.owningKey)), DUMMY_NOTARY) + val tx = TransactionType.NotaryChange.Builder().withItems(state) + tx.signWith(nodeA.storage.myLegalIdentityKey) + tx.signWith(nodeB.storage.myLegalIdentityKey) + tx.signWith(DUMMY_NOTARY_KEY) + val stx = tx.toSignedTransaction() + nodeA.services.recordTransactions(listOf(stx)) + nodeB.services.recordTransactions(listOf(stx)) + val stateAndRef = StateAndRef(state, StateRef(stx.id, 0)) + return stateAndRef +} + +fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) tx.setTime(Instant.now(), notary, 30.seconds) tx.signWith(node.storage.myLegalIdentityKey) val stx = tx.toSignedTransaction(false) node.services.recordTransactions(listOf(stx)) - return StateRef(stx.id, 0) + return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt index 538360bdfe..615766135c 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt @@ -3,7 +3,7 @@ package com.r3corda.node.internal.testing import com.r3corda.contracts.cash.Cash import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Issued -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.crypto.Party import com.r3corda.core.node.ServiceHub import com.r3corda.core.serialization.OpaqueBytes @@ -34,7 +34,7 @@ object WalletFiller { // this field as there's no other database or source of truth we need to sync with. val depositRef = myIdentity.ref(ref) - val issuance = TransactionBuilder() + val issuance = TransactionType.General.Builder() val freshKey = services.keyManagementService.freshKey() cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary) issuance.signWith(myKey) diff --git a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt new file mode 100644 index 0000000000..7b13f01e44 --- /dev/null +++ b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt @@ -0,0 +1,27 @@ +package com.r3corda.node.services + +import com.r3corda.core.messaging.MessagingService +import com.r3corda.core.messaging.SingleMessageRecipient +import com.r3corda.node.services.api.AbstractNodeService +import com.r3corda.node.services.statemachine.StateMachineManager +import protocols.NotaryChangeProtocol + +/** + * A service that monitors the network for requests for changing the notary of a state, + * and immediately runs the [NotaryChangeProtocol] if the auto-accept criteria are met. + */ +class NotaryChangeService(net: MessagingService, val smm: StateMachineManager) : AbstractNodeService(net) { + init { + addMessageHandler(NotaryChangeProtocol.TOPIC_INITIATE, + { req: NotaryChangeProtocol.Handshake -> handleChangeNotaryRequest(req) } + ) + } + + private fun handleChangeNotaryRequest(req: NotaryChangeProtocol.Handshake) { + val protocol = NotaryChangeProtocol.Acceptor( + req.replyTo as SingleMessageRecipient, + req.sessionID!!, + req.sessionIdForSend) + smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) + } +} diff --git a/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt b/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt index faa447b7b2..4b9192444b 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/transactions/InMemoryUniquenessProvider.kt @@ -2,8 +2,8 @@ package com.r3corda.node.services.transactions import com.r3corda.core.ThreadBox import com.r3corda.core.contracts.StateRef -import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.UniquenessException import com.r3corda.core.node.services.UniquenessProvider import java.util.* @@ -15,12 +15,10 @@ class InMemoryUniquenessProvider() : UniquenessProvider { /** For each input state store the consuming transaction information */ private val committedStates = ThreadBox(HashMap()) - // TODO: the uniqueness provider shouldn't be able to see all tx outputs and commands - override fun commit(tx: WireTransaction, callerIdentity: Party) { - val inputStates = tx.inputs + override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { committedStates.locked { val conflictingStates = LinkedHashMap() - for (inputState in inputStates) { + for (inputState in states) { val consumingTx = get(inputState) if (consumingTx != null) conflictingStates[inputState] = consumingTx } @@ -28,8 +26,8 @@ class InMemoryUniquenessProvider() : UniquenessProvider { val conflict = UniquenessProvider.Conflict(conflictingStates) throw UniquenessException(conflict) } else { - inputStates.forEachIndexed { i, stateRef -> - put(stateRef, UniquenessProvider.ConsumingTx(tx.id, i, callerIdentity)) + states.forEachIndexed { i, stateRef -> + put(stateRef, UniquenessProvider.ConsumingTx(txId, i, callerIdentity)) } } diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt index b51d8a0f3f..c9a45975e0 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt @@ -51,7 +51,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer */ override val linearHeads: Map> get() = mutex.locked { wallet }.let { wallet -> - wallet.states.filterStatesOfType().associateBy { it.state.thread }.mapValues { it.value } + wallet.states.filterStatesOfType().associateBy { it.state.data.thread }.mapValues { it.value } } override fun notifyAll(txns: Iterable): Wallet { @@ -103,8 +103,8 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer private fun Wallet.update(tx: WireTransaction, ourKeys: Set): Pair { val ourNewStates = tx.outputs. - filter { isRelevant(it, ourKeys) }. - map { tx.outRef(it) } + filter { isRelevant(it.data, ourKeys) }. + map { tx.outRef(it.data) } // Now calculate the states that are being spent by this transaction. val consumed: Set = states.map { it.ref }.intersect(tx.inputs) diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt index fe6fdd03a5..745237ecfc 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt @@ -24,7 +24,7 @@ class WalletImpl(override val states: List>) : Wallet */ override val cashBalances: Map> get() = states. // Select the states we own which are cash, ignore the rest, take the amounts. - mapNotNull { (it.state as? Cash.State)?.amount }. + mapNotNull { (it.state.data as? Cash.State)?.amount }. // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } groupBy { it.token.product }. // Collapse to Map by summing all the amounts of the same currency together. diff --git a/node/src/main/resources/com/r3corda/node/internal/testing/trade.json b/node/src/main/resources/com/r3corda/node/internal/testing/trade.json index 0d5d125897..c2b7d51a3f 100644 --- a/node/src/main/resources/com/r3corda/node/internal/testing/trade.json +++ b/node/src/main/resources/com/r3corda/node/internal/testing/trade.json @@ -99,6 +99,5 @@ "dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360", "tradeID": "tradeXXX", "hashLegalDocs": "put hash here" - }, - "notary": "Notary Service" + } } 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 2d45aa3030..a82246bbaa 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -468,7 +468,7 @@ class TwoPartyTradeProtocolTests { attachmentID: SecureHash?): Pair> { val ap = transaction { output("alice's paper") { - CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days, notary) + CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days) } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } if (!withError) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt index 0ce88da61e..4062170da1 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt @@ -4,13 +4,15 @@ import com.r3corda.contracts.cash.Cash import com.r3corda.contracts.testing.CASH import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` +import com.r3corda.contracts.testing.`with notary` import com.r3corda.core.bd import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.Fix -import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.generateKeyPair +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.testing.ALICE_PUBKEY +import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.MEGA_CORP import com.r3corda.core.testing.MEGA_CORP_KEY import com.r3corda.core.utilities.BriefLogFormatter @@ -102,7 +104,7 @@ class NodeInterestRatesTest { val (n1, n2) = net.createTwoNodes() n2.interestRatesService.oracle.knownFixes = TEST_DATA - val tx = TransactionBuilder() + val tx = TransactionType.General.Builder() val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val protocol = RatesFixProtocol(tx, n2.info, fixOf, "0.675".bd, "0.1".bd) BriefLogFormatter.initVerbose("rates") @@ -117,5 +119,5 @@ class NodeInterestRatesTest { assertEquals("0.678".bd, fix.value) } - private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY)) + private fun makeTX() = TransactionType.General.Builder().withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY) } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt index 129d42ac5d..a64927640d 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt @@ -3,7 +3,7 @@ package com.r3corda.node.services import com.r3corda.contracts.cash.Cash import com.r3corda.core.contracts.`issued by` import com.r3corda.core.contracts.DOLLARS -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.contracts.USD import com.r3corda.core.contracts.verifyToLedgerTransaction import com.r3corda.core.node.ServiceHub @@ -51,14 +51,14 @@ class NodeWalletServiceTest { val w = wallet.currentWallet assertEquals(3, w.states.size) - val state = w.states[0].state as Cash.State + val state = w.states[0].state.data as Cash.State val myIdentity = services.storageService.myLegalIdentity val myPartyRef = myIdentity.ref(ref) assertEquals(29.01.DOLLARS `issued by` myPartyRef, state.amount) assertEquals(ALICE_PUBKEY, state.owner) - assertEquals(33.34.DOLLARS `issued by` myPartyRef, (w.states[2].state as Cash.State).amount) - assertEquals(35.61.DOLLARS `issued by` myPartyRef, (w.states[1].state as Cash.State).amount) + assertEquals(33.34.DOLLARS `issued by` myPartyRef, (w.states[2].state.data as Cash.State).amount) + assertEquals(35.61.DOLLARS `issued by` myPartyRef, (w.states[1].state.data as Cash.State).amount) } @Test @@ -67,22 +67,24 @@ class NodeWalletServiceTest { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() - val usefulTX = TransactionBuilder().apply { + val usefulTX = TransactionType.General.Builder().apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef(0) // A tx that spends our money. - val spendTX = TransactionBuilder().apply { + val spendTX = TransactionType.General.Builder().apply { Cash().generateSpend(this, 80.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_PUBKEY, listOf(myOutput)) signWith(freshKey) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() // A tx that doesn't send us anything. - val irrelevantTX = TransactionBuilder().apply { + val irrelevantTX = TransactionType.General.Builder().apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) + signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() assertNull(wallet.cashBalances[USD]) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt index 47b3f3aeae..559df095a7 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt @@ -1,7 +1,7 @@ package com.r3corda.node.services import com.r3corda.core.contracts.TimestampCommand -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.seconds import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY @@ -38,7 +38,7 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction with a valid timestamp`() { val inputState = issueState(clientNode) - val tx = TransactionBuilder().withItems(inputState) + val tx = TransactionType.General.Builder().withItems(inputState) tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds) val wtx = tx.toWireTransaction() @@ -52,7 +52,7 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction without a timestamp`() { val inputState = issueState(clientNode) - val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() + val wtx = TransactionType.General.Builder().withItems(inputState).toWireTransaction() val protocol = NotaryProtocol.Client(wtx) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) @@ -64,7 +64,7 @@ class NotaryServiceTests { @Test fun `should report error for transaction with an invalid timestamp`() { val inputState = issueState(clientNode) - val tx = TransactionBuilder().withItems(inputState) + val tx = TransactionType.General.Builder().withItems(inputState) tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds) val wtx = tx.toWireTransaction() @@ -79,7 +79,7 @@ class NotaryServiceTests { @Test fun `should report error for transaction with more than one timestamp`() { val inputState = issueState(clientNode) - val tx = TransactionBuilder().withItems(inputState) + 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) @@ -96,7 +96,7 @@ class NotaryServiceTests { @Test fun `should report conflict for a duplicate transaction`() { val inputState = issueState(clientNode) - val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() + val wtx = TransactionType.General.Builder().withItems(inputState).toWireTransaction() val firstSpend = NotaryProtocol.Client(wtx) val secondSpend = NotaryProtocol.Client(wtx) diff --git a/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt b/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt index f7b76fbc32..4146bee109 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/UniquenessProviderTests.kt @@ -1,6 +1,6 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.UniquenessException import com.r3corda.core.testing.MEGA_CORP import com.r3corda.core.testing.generateStateRef @@ -11,27 +11,27 @@ import kotlin.test.assertFailsWith class UniquenessProviderTests { val identity = MEGA_CORP + val txID = SecureHash.randomSHA256() @Test fun `should commit a transaction with unused inputs without exception`() { val provider = InMemoryUniquenessProvider() val inputState = generateStateRef() - val tx = TransactionBuilder().withItems(inputState).toWireTransaction() - provider.commit(tx, identity) + + provider.commit(listOf(inputState), txID, identity) } @Test fun `should report a conflict for a transaction with previously used inputs`() { val provider = InMemoryUniquenessProvider() val inputState = generateStateRef() - val tx1 = TransactionBuilder().withItems(inputState).toWireTransaction() - provider.commit(tx1, identity) + val inputs = listOf(inputState) + provider.commit(inputs, txID, identity) - val tx2 = TransactionBuilder().withItems(inputState).toWireTransaction() - val ex = assertFailsWith { provider.commit(tx2, identity) } + val ex = assertFailsWith { provider.commit(inputs, txID, identity) } val consumingTx = ex.error.stateHistory[inputState]!! - assertEquals(consumingTx.id, tx1.id) - assertEquals(consumingTx.inputIndex, tx1.inputs.indexOf(inputState)) + assertEquals(consumingTx.id, txID) + assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState)) assertEquals(consumingTx.requestingParty, identity) } } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt index 9897bf2972..9906a0128a 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ValidatingNotaryServiceTests.kt @@ -1,6 +1,6 @@ package com.r3corda.node.services -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.TransactionType import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.node.internal.testing.MockNetwork @@ -34,7 +34,7 @@ class ValidatingNotaryServiceTests { @Test fun `should report error for invalid transaction dependency`() { val inputState = issueInvalidState(clientNode) - val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() + val wtx = TransactionType.General.Builder().withItems(inputState).toWireTransaction() val protocol = NotaryProtocol.Client(wtx) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) diff --git a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt index a36aa7731b..9cb29a4e18 100644 --- a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt +++ b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt @@ -2,6 +2,7 @@ package com.r3corda.node.visualiser import com.r3corda.core.contracts.CommandData import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.crypto.SecureHash import com.r3corda.core.testing.TransactionGroupDSL import org.graphstream.graph.Edge @@ -30,7 +31,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { val node = graph.addNode(tx.outRef(outIndex).ref.toString()) val state = tx.outputs[outIndex] node.label = stateToLabel(state) - node.styleClass = stateToCSSClass(state) + ",state" + node.styleClass = stateToCSSClass(state.data) + ",state" node.setAttribute("state", state) val edge = graph.addEdge("tx$txIndex-out$outIndex", txNode, node, true) edge.weight = 0.7 @@ -55,8 +56,8 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { return graph } - private fun stateToLabel(state: ContractState): String { - return dsl.labelForState(state) ?: stateToTypeName(state) + private fun stateToLabel(state: TransactionState<*>): String { + return dsl.labelForState(state) ?: stateToTypeName(state.data) } private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.') diff --git a/node/src/test/kotlin/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/node/services/NotaryChangeTests.kt new file mode 100644 index 0000000000..4059a1dcd9 --- /dev/null +++ b/node/src/test/kotlin/node/services/NotaryChangeTests.kt @@ -0,0 +1,93 @@ +package node.services + +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.generateKeyPair +import com.r3corda.core.testing.DUMMY_NOTARY +import com.r3corda.core.testing.DUMMY_NOTARY_KEY +import com.r3corda.node.internal.testing.MockNetwork +import com.r3corda.node.internal.testing.issueMultiPartyState +import com.r3corda.node.internal.testing.issueState +import com.r3corda.node.services.network.NetworkMapService +import com.r3corda.node.services.transactions.SimpleNotaryService +import org.junit.Before +import org.junit.Test +import protocols.NotaryChangeException +import protocols.NotaryChangeProtocol +import protocols.NotaryChangeProtocol.Instigator +import protocols.NotaryChangeRefused +import java.util.concurrent.ExecutionException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class NotaryChangeTests { + lateinit var net: MockNetwork + lateinit var oldNotaryNode: MockNetwork.MockNode + lateinit var newNotaryNode: MockNetwork.MockNode + lateinit var clientNodeA: MockNetwork.MockNode + lateinit var clientNodeB: MockNetwork.MockNode + + @Before + fun setup() { + net = MockNetwork() + oldNotaryNode = net.createNode( + legalName = DUMMY_NOTARY.name, + keyPair = DUMMY_NOTARY_KEY, + advertisedServices = *arrayOf(NetworkMapService.Type, SimpleNotaryService.Type)) + clientNodeA = net.createNode(networkMapAddress = oldNotaryNode.info) + clientNodeB = net.createNode(networkMapAddress = oldNotaryNode.info) + newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info, advertisedServices = SimpleNotaryService.Type) + + net.runNetwork() // Clear network map registration messages + } + + @Test + fun `should change notary for a state with single participant`() { + val state = issueState(clientNodeA) + val newNotary = newNotaryNode.info.identity + val protocol = Instigator(state, newNotary) + val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) + + net.runNetwork() + + val newState = future.get() + assertEquals(newState.state.notary, newNotary) + } + + @Test + fun `should change notary for a state with multiple participants`() { + val state = issueMultiPartyState(clientNodeA, clientNodeB) + val newNotary = newNotaryNode.info.identity + val protocol = Instigator(state, newNotary) + val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) + + net.runNetwork() + + val newState = future.get() + assertEquals(newState.state.notary, newNotary) + val loadedStateA = clientNodeA.services.loadState(newState.ref) + val loadedStateB = clientNodeB.services.loadState(newState.ref) + assertEquals(loadedStateA, loadedStateB) + } + + @Test + fun `should throw when a participant refuses to change Notary`() { + val state = issueMultiPartyState(clientNodeA, clientNodeB) + val newEvilNotary = Party("Evil Notary", generateKeyPair().public) + val protocol = Instigator(state, newEvilNotary) + val future = clientNodeA.smm.add(NotaryChangeProtocol.TOPIC_CHANGE, protocol) + + net.runNetwork() + + val ex = assertFailsWith(ExecutionException::class) { future.get() } + val error = (ex.cause as NotaryChangeException).error + assertTrue(error is NotaryChangeRefused) + } + + // TODO: Add more test cases once we have a general protocol/service exception handling mechanism: + // - A participant is offline/can't be found on the network + // - The requesting party is not a participant + // - The requesting party wants to change additional state fields + // - Multiple states in a single "notary change" transaction + // - Transaction contains additional states and commands with business logic +} \ No newline at end of file diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index 41fb88f3f9..e8d981e047 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -1,10 +1,7 @@ package com.r3corda.demos import com.r3corda.contracts.cash.Cash -import com.r3corda.core.contracts.DOLLARS -import com.r3corda.core.contracts.FixOf -import com.r3corda.core.contracts.`issued by` -import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.logElapsedTime import com.r3corda.core.node.NodeInfo @@ -86,8 +83,8 @@ fun main(args: Array) { val notary = node.services.networkMapCache.notaryNodes[0] // Make a garbage transaction that includes a rate fix. - val tx = TransactionBuilder() - tx.addOutputState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public, notary.identity)) + val tx = TransactionType.General.Builder() + tx.addOutputState(TransactionState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public), notary.identity)) val protocol = RatesFixProtocol(tx, oracleNode, fixOf, expectedRate, rateTolerance) node.smm.add("demo.ratefix", protocol).get() node.stop() diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index a7ccff583f..f4e65d2e16 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -356,7 +356,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, // Sign it as ourselves. tx.signWith(keyPair) - // Get the notary to sign it, thus committing the outputs. + // Get the notary to sign the timestamp val notarySig = subProtocol(NotaryProtocol.Client(tx.toWireTransaction())) tx.addSignatureUnchecked(notarySig) @@ -369,7 +369,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, // Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works. val move: SignedTransaction = run { - val builder = TransactionBuilder() + val builder = TransactionType.General.Builder() CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) builder.signWith(keyPair) builder.addSignatureUnchecked(subProtocol(NotaryProtocol.Client(builder.toWireTransaction()))) diff --git a/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt b/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt index a9b206234f..9197216da7 100644 --- a/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt +++ b/src/main/kotlin/com/r3corda/demos/api/InterestRateSwapAPI.kt @@ -44,14 +44,14 @@ class InterestRateSwapAPI(val api: APIServer) { private fun getDealByRef(ref: String): InterestRateSwap.State? { val states = api.queryStates(StatesQuery.selectDeal(ref)) return if (states.isEmpty()) null else { - val deals = api.fetchStates(states).values.map { it as InterestRateSwap.State }.filterNotNull() + val deals = api.fetchStates(states).values.map { it?.data as InterestRateSwap.State }.filterNotNull() return if (deals.isEmpty()) null else deals[0] } } private fun getAllDeals(): Array { val states = api.queryStates(StatesQuery.selectAllDeals()) - val swaps = api.fetchStates(states).values.map { it as InterestRateSwap.State }.filterNotNull().toTypedArray() + val swaps = api.fetchStates(states).values.map { it?.data as InterestRateSwap.State }.filterNotNull().toTypedArray() return swaps } diff --git a/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt b/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt index e32bc610a3..452e315871 100644 --- a/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt +++ b/src/main/kotlin/com/r3corda/demos/protocols/UpdateBusinessDayProtocol.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import com.r3corda.contracts.InterestRateSwap import com.r3corda.core.contracts.DealState import com.r3corda.core.contracts.StateAndRef +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.services.linearHeadsOfType import com.r3corda.core.protocols.ProtocolLogic @@ -13,6 +14,7 @@ import com.r3corda.core.utilities.ProgressTracker import com.r3corda.demos.DemoClock import com.r3corda.node.internal.Node import com.r3corda.node.services.network.MockNetworkMapCache +import com.r3corda.node.utilities.ANSIProgressRenderer import com.r3corda.protocols.TwoPartyDealProtocol import java.time.LocalDate @@ -41,12 +43,12 @@ object UpdateBusinessDayProtocol { // Get deals progressTracker.currentStep = FETCHING val dealStateRefs = serviceHub.walletService.linearHeadsOfType() - val otherPartyToDeals = dealStateRefs.values.groupBy { otherParty(it.state) } + val otherPartyToDeals = dealStateRefs.values.groupBy { otherParty(it.state.data) } // TODO we need to process these in parallel to stop there being an ordering problem across more than two nodes val sortedParties = otherPartyToDeals.keys.sortedBy { it.identity.name } for (party in sortedParties) { - val sortedDeals = otherPartyToDeals[party]!!.sortedBy { it.state.ref } + val sortedDeals = otherPartyToDeals[party]!!.sortedBy { it.state.data.ref } for (deal in sortedDeals) { progressTracker.currentStep = ITERATING_DEALS processDeal(party, deal, date, sessionID) @@ -64,9 +66,9 @@ object UpdateBusinessDayProtocol { // TODO we should make this more object oriented when we can ask a state for it's contract @Suspendable fun processDeal(party: NodeInfo, deal: StateAndRef, date: LocalDate, sessionID: Long) { - val s = deal.state + val s = deal.state.data when (s) { - is InterestRateSwap.State -> processInterestRateSwap(party, StateAndRef(s, deal.ref), date, sessionID) + is InterestRateSwap.State -> processInterestRateSwap(party, StateAndRef(TransactionState(s, deal.state.notary), deal.ref), date, sessionID) } } @@ -74,7 +76,7 @@ object UpdateBusinessDayProtocol { @Suspendable fun processInterestRateSwap(party: NodeInfo, deal: StateAndRef, date: LocalDate, sessionID: Long) { var dealStateAndRef: StateAndRef? = deal - var nextFixingDate = deal.state.calculation.nextFixingDate() + var nextFixingDate = deal.state.data.calculation.nextFixingDate() while (nextFixingDate != null && !nextFixingDate.isAfter(date)) { progressTracker.currentStep = ITERATING_FIXINGS /* @@ -83,12 +85,12 @@ object UpdateBusinessDayProtocol { * One of the parties needs to take the lead in the coordination and this is a reliable deterministic way * to do it. */ - if (party.identity.name == deal.state.fixedLeg.fixedRatePayer.name) { + if (party.identity.name == deal.state.data.fixedLeg.fixedRatePayer.name) { dealStateAndRef = nextFixingFloatingLeg(dealStateAndRef!!, party, sessionID) } else { dealStateAndRef = nextFixingFixedLeg(dealStateAndRef!!, party, sessionID) } - nextFixingDate = dealStateAndRef?.state?.calculation?.nextFixingDate() + nextFixingDate = dealStateAndRef?.state?.data?.calculation?.nextFixingDate() } } @@ -98,7 +100,7 @@ object UpdateBusinessDayProtocol { progressTracker.currentStep = FIXING val myName = serviceHub.storageService.myLegalIdentity.name - val deal: InterestRateSwap.State = dealStateAndRef.state + val deal: InterestRateSwap.State = dealStateAndRef.state.data val myOldParty = deal.parties.single { it.name == myName } val keyPair = serviceHub.keyManagementService.toKeyPair(myOldParty.owningKey) val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.notaryNodes[0], dealStateAndRef,