From 5f9e1405456b9df3357aaef48df00468e5d64f6b Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 16:50:05 +0100 Subject: [PATCH 01/19] Put "buyer" and "seller" directories in .gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ca5a51b4d5..d97c406cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ TODO /build/ /docs/build/doctrees -alpha -beta +buyer +seller ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio From 1bc57085c8a604be37f0671d3e3b90be4238584f Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 16:50:43 +0100 Subject: [PATCH 02/19] Rename ContractStateRef -> StateRef. Rename craft* methods to generate* --- .../java/contracts/JavaCommercialPaper.java | 2 +- src/main/kotlin/contracts/Cash.kt | 6 ++--- src/main/kotlin/contracts/CommercialPaper.kt | 8 +++---- src/main/kotlin/contracts/CrowdFund.kt | 8 +++---- .../protocols/TwoPartyTradeProtocol.kt | 2 +- src/main/kotlin/core/Structures.kt | 9 ++++---- .../kotlin/core/TransactionVerification.kt | 4 ++-- src/main/kotlin/core/Transactions.kt | 22 +++++++++---------- .../kotlin/core/node/E2ETestWalletService.kt | 4 ++-- src/main/kotlin/core/node/TraderDemo.kt | 2 +- src/test/kotlin/contracts/CashTests.kt | 22 +++++++++---------- .../kotlin/contracts/CommercialPaperTests.kt | 14 ++++++------ src/test/kotlin/contracts/CrowdFundTests.kt | 10 ++++----- src/test/kotlin/core/TransactionGroupTests.kt | 4 ++-- .../core/node/TimestamperNodeServiceTest.kt | 4 ++-- .../TransactionSerializationTests.kt | 2 +- src/test/kotlin/core/testutils/TestUtils.kt | 10 ++++----- 17 files changed, 66 insertions(+), 67 deletions(-) diff --git a/src/main/java/contracts/JavaCommercialPaper.java b/src/main/java/contracts/JavaCommercialPaper.java index 3c67277552..faa4e54dbe 100644 --- a/src/main/java/contracts/JavaCommercialPaper.java +++ b/src/main/java/contracts/JavaCommercialPaper.java @@ -226,7 +226,7 @@ public class JavaCommercialPaper implements Contract { } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { - new Cash().craftSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null); + new Cash().generateSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null); tx.addInputState(paper.getRef()); tx.addCommand(new Command( new Commands.Redeem(), paper.getState().getOwner())); } diff --git a/src/main/kotlin/contracts/Cash.kt b/src/main/kotlin/contracts/Cash.kt index 977ef609c5..4b2769321f 100644 --- a/src/main/kotlin/contracts/Cash.kt +++ b/src/main/kotlin/contracts/Cash.kt @@ -152,7 +152,7 @@ class Cash : Contract { /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun craftIssue(tx: TransactionBuilder, amount: Amount, at: PartyReference, owner: PublicKey) { + fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyReference, owner: PublicKey) { check(tx.inputStates().isEmpty()) check(tx.outputStates().sumCashOrNull() == null) tx.addOutputState(Cash.State(at, amount, owner)) @@ -168,8 +168,8 @@ class Cash : Contract { * about which type of cash claims they are willing to accept. */ @Throws(InsufficientBalanceException::class) - fun craftSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, - cashStates: List>, onlyFromParties: Set? = null): List { + fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, + cashStates: List>, onlyFromParties: Set? = null): List { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. diff --git a/src/main/kotlin/contracts/CommercialPaper.kt b/src/main/kotlin/contracts/CommercialPaper.kt index 16344b95b2..bf905f7db1 100644 --- a/src/main/kotlin/contracts/CommercialPaper.kt +++ b/src/main/kotlin/contracts/CommercialPaper.kt @@ -125,7 +125,7 @@ class CommercialPaper : Contract { * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction * at the moment: this restriction is not fundamental and may be lifted later. */ - fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { + fun generateIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate) return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } @@ -133,7 +133,7 @@ class CommercialPaper : Contract { /** * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. */ - fun craftMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { + 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) @@ -147,9 +147,9 @@ class CommercialPaper : Contract { * @throws InsufficientBalanceException if the wallet doesn't contain enough money to pay the redeemer */ @Throws(InsufficientBalanceException::class) - fun craftRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) + Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet) tx.addInputState(paper.ref) tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner) } diff --git a/src/main/kotlin/contracts/CrowdFund.kt b/src/main/kotlin/contracts/CrowdFund.kt index 6b98fd92ec..c6e796b881 100644 --- a/src/main/kotlin/contracts/CrowdFund.kt +++ b/src/main/kotlin/contracts/CrowdFund.kt @@ -142,7 +142,7 @@ class CrowdFund : Contract { * Returns a transaction that registers a crowd-funding campaing, owned by the issuing institution's key. Does not update * an existing transaction because it's not possible to register multiple campaigns in a single transaction */ - fun craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): TransactionBuilder { + fun generateRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): TransactionBuilder { val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime) val state = State(campaign) return TransactionBuilder().withItems(state, Command(Commands.Register(), owner.party.owningKey)) @@ -151,7 +151,7 @@ class CrowdFund : Contract { /** * Updates the given partial transaction with an input/output/command to fund the opportunity. */ - fun craftPledge(tx: TransactionBuilder, campaign: StateAndRef, subscriber: PublicKey) { + fun generatePledge(tx: TransactionBuilder, campaign: StateAndRef, subscriber: PublicKey) { tx.addInputState(campaign.ref) tx.addOutputState(campaign.state.copy( pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS) @@ -159,14 +159,14 @@ class CrowdFund : Contract { tx.addCommand(Commands.Pledge(), subscriber) } - fun craftClose(tx: TransactionBuilder, campaign: StateAndRef, wallet: List>) { + fun generateClose(tx: TransactionBuilder, campaign: StateAndRef, wallet: List>) { tx.addInputState(campaign.ref) tx.addOutputState(campaign.state.copy(closed = true)) tx.addCommand(Commands.Close(), campaign.state.campaign.owner) // If campaign target has not been met, compose cash returns if (campaign.state.pledgedAmount < campaign.state.campaign.target) { for (pledge in campaign.state.pledges) { - Cash().craftSpend(tx, pledge.amount, pledge.owner, wallet) + Cash().generateSpend(tx, pledge.amount, pledge.owner, wallet) } } } diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index f438ef489f..14ac83b13b 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -162,7 +162,7 @@ object TwoPartyTradeProtocol { // 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().craftSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) + 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) // Just pick some new public key for now. This won't be linked with our identity in any way, which is what diff --git a/src/main/kotlin/core/Structures.kt b/src/main/kotlin/core/Structures.kt index 66c92740d2..2d955e3801 100644 --- a/src/main/kotlin/core/Structures.kt +++ b/src/main/kotlin/core/Structures.kt @@ -39,17 +39,16 @@ interface OwnableState : ContractState { /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits) -// TODO: Give this a shorter name. /** - * A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which transaction - * defined the state and where in that transaction it was. + * A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which + * transaction defined the state and where in that transaction it was. */ -data class ContractStateRef(val txhash: SecureHash, val index: Int) { +data class StateRef(val txhash: SecureHash, val index: Int) { override fun toString() = "$txhash($index)" } /** 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: ContractStateRef) +data class StateAndRef(val state: T, val ref: StateRef) /** A [Party] is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */ data class Party(val name: String, val owningKey: PublicKey) { diff --git a/src/main/kotlin/core/TransactionVerification.kt b/src/main/kotlin/core/TransactionVerification.kt index af879c47d5..4aa79a80e1 100644 --- a/src/main/kotlin/core/TransactionVerification.kt +++ b/src/main/kotlin/core/TransactionVerification.kt @@ -11,7 +11,7 @@ package core import java.util.* class TransactionResolutionException(val hash: SecureHash) : Exception() -class TransactionConflictException(val conflictRef: ContractStateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() +class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() /** * A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then @@ -33,7 +33,7 @@ class TransactionGroup(val transactions: Set, val nonVerified check(transactions.intersect(nonVerifiedRoots).isEmpty()) val hashToTXMap: Map> = (transactions + nonVerifiedRoots).groupBy { it.hash } - val refToConsumingTXMap = hashMapOf() + val refToConsumingTXMap = hashMapOf() val resolved = HashSet(transactions.size) for (tx in transactions) { diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index 2c9974204e..a7ce3337bc 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -51,7 +51,7 @@ import java.util.* */ /** Transaction ready for serialisation, without any signatures attached. */ -data class WireTransaction(val inputStates: List, +data class WireTransaction(val inputStates: List, val outputStates: List, val commands: List) { fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { @@ -124,7 +124,7 @@ data class SignedWireTransaction(val txBits: SerializedBytes, v } /** A mutable transaction that's in the process of being built, before all signatures are present. */ -class TransactionBuilder(private val inputStates: MutableList = arrayListOf(), +class TransactionBuilder(private val inputStates: MutableList = arrayListOf(), private val outputStates: MutableList = arrayListOf(), private val commands: MutableList = arrayListOf()) { @@ -152,7 +152,7 @@ class TransactionBuilder(private val inputStates: MutableList public fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { when (t) { - is ContractStateRef -> inputStates.add(t) + is StateRef -> inputStates.add(t) is ContractState -> outputStates.add(t) is Command -> commands.add(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") @@ -224,7 +224,7 @@ class TransactionBuilder(private val inputStates: MutableList return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - fun addInputState(ref: ContractStateRef) { + fun addInputState(ref: StateRef) { check(currentSigs.isEmpty()) inputStates.add(ref) } @@ -245,7 +245,7 @@ class TransactionBuilder(private val inputStates: MutableList fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) // Accessors that yield immutable snapshots. - fun inputStates(): List = ArrayList(inputStates) + fun inputStates(): List = ArrayList(inputStates) fun outputStates(): List = ArrayList(outputStates) fun commands(): List = ArrayList(commands) } @@ -256,17 +256,17 @@ class TransactionBuilder(private val inputStates: MutableList * with the commands from the wire, and verified/looked up. */ data class LedgerTransaction( - /** The input states which will be consumed/invalidated by the execution of this transaction. */ - val inStateRefs: List, - /** The states that will be generated by the execution of this transaction. */ + /** The input states which will be consumed/invalidated by the execution of this transaction. */ + val inStateRefs: List, + /** The states that will be generated by the execution of this transaction. */ val outStates: List, - /** Arbitrary data passed to the program of each input state. */ + /** Arbitrary data passed to the program of each input state. */ val commands: List>, - /** The hash of the original serialised SignedTransaction */ + /** The hash of the original serialised SignedTransaction */ val hash: SecureHash ) { @Suppress("UNCHECKED_CAST") - fun outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index)) + fun outRef(index: Int) = StateAndRef(outStates[index] as T, StateRef(hash, index)) fun outRef(state: T): StateAndRef { val i = outStates.indexOf(state) diff --git a/src/main/kotlin/core/node/E2ETestWalletService.kt b/src/main/kotlin/core/node/E2ETestWalletService.kt index f626188311..351cd46119 100644 --- a/src/main/kotlin/core/node/E2ETestWalletService.kt +++ b/src/main/kotlin/core/node/E2ETestWalletService.kt @@ -53,14 +53,14 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService { val issuance = TransactionBuilder() val freshKey = services.keyManagementService.freshKey() - cash.craftIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public) + cash.generateIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public) issuance.signWith(myKey) return@map issuance.toSignedTransaction(true) } val statesAndRefs = transactions.map { - StateAndRef(it.tx.outputStates[0] as OwnableState, ContractStateRef(it.id, 0)) + StateAndRef(it.tx.outputStates[0] as OwnableState, StateRef(it.id, 0)) } mutex.locked { diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 91eb2bff69..0620755af6 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -178,7 +178,7 @@ fun makeFakeCommercialPaper(ownedBy: PublicKey): StateAndRef>> { val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256()) - return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) }) + return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.hash, index)) }) } @Test fun `issue move and then redeem`() { // MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val issueTX: LedgerTransaction = run { - val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { + val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) signWith(MINI_CORP_KEY) timestamp(DUMMY_TIMESTAMPER) @@ -187,8 +187,8 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to MiniCorp to own some of their debt. val moveTX: LedgerTransaction = run { val ptx = TransactionBuilder() - Cash().craftSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) - CommercialPaper().craftMove(ptx, issueTX.outRef(0), ALICE) + Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) + CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE) ptx.signWith(MINI_CORP_KEY) ptx.signWith(ALICE_KEY) val stx = ptx.toSignedTransaction() @@ -204,7 +204,7 @@ class CommercialPaperTestsGeneric { fun makeRedeemTX(time: Instant): LedgerTransaction { val ptx = TransactionBuilder() ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds) - CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet) + CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet) ptx.signWith(ALICE_KEY) ptx.signWith(MINI_CORP_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt index 8880e141a5..0aed163b67 100644 --- a/src/test/kotlin/contracts/CrowdFundTests.kt +++ b/src/test/kotlin/contracts/CrowdFundTests.kt @@ -99,7 +99,7 @@ class CrowdFundTests { fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256()) - return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) }) + return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.hash, index)) }) } @Test @@ -107,7 +107,7 @@ class CrowdFundTests { // MiniCorp registers a crowdfunding of $1,000, to close in 7 days. val registerTX: LedgerTransaction = run { // craftRegister returns a partial transaction - val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply { + val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply { setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) signWith(MINI_CORP_KEY) timestamp(DUMMY_TIMESTAMPER) @@ -126,8 +126,8 @@ class CrowdFundTests { // Alice pays $1000 to MiniCorp to fund their campaign. val pledgeTX: LedgerTransaction = run { val ptx = TransactionBuilder() - CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE) - Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet) + CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE) + Cash().generateSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet) ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) ptx.signWith(ALICE_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) @@ -145,7 +145,7 @@ class CrowdFundTests { fun makeFundedTX(time: Instant): LedgerTransaction { val ptx = TransactionBuilder() ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds) - CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet) + CrowdFund().generateClose(ptx, pledgeTX.outRef(0), miniCorpWallet) ptx.signWith(MINI_CORP_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) val stx = ptx.toSignedTransaction() diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt index dd3575c37a..9be61293e5 100644 --- a/src/test/kotlin/core/TransactionGroupTests.kt +++ b/src/test/kotlin/core/TransactionGroupTests.kt @@ -73,7 +73,7 @@ class TransactionGroupTests { val e = assertFailsWith(TransactionConflictException::class) { verify() } - assertEquals(ContractStateRef(t.hash, 0), e.conflictRef) + assertEquals(StateRef(t.hash, 0), e.conflictRef) assertEquals(setOf(conflict1, conflict2), setOf(e.tx1, e.tx2)) } } @@ -95,7 +95,7 @@ class TransactionGroupTests { // We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that // points nowhere. - val ref = ContractStateRef(SecureHash.randomSHA256(), 0) + val ref = StateRef(SecureHash.randomSHA256(), 0) tg.txns.add(LedgerTransaction( listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move())), SecureHash.randomSHA256()) ) diff --git a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt index 8204763a8f..3e6189c5d4 100644 --- a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt +++ b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt @@ -31,7 +31,7 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() { lateinit var service: TimestamperNodeService val ptx = TransactionBuilder().apply { - addInputState(ContractStateRef(SecureHash.randomSHA256(), 0)) + addInputState(StateRef(SecureHash.randomSHA256(), 0)) addOutputState(100.DOLLARS.CASH) } @@ -62,7 +62,7 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() { override fun call(): Boolean { val client = TimestamperClient(this, server) val ptx = TransactionBuilder().apply { - addInputState(ContractStateRef(SecureHash.randomSHA256(), 0)) + addInputState(StateRef(SecureHash.randomSHA256(), 0)) addOutputState(100.DOLLARS.CASH) } ptx.addCommand(TimestampCommand(now - 20.seconds, now + 20.seconds), server.identity.owningKey) diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index fc95173c94..7944500cab 100644 --- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -24,7 +24,7 @@ class TransactionSerializationTests { val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1) val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public) - val fakeStateRef = ContractStateRef(SecureHash.sha256("fake tx id"), 0) + val fakeStateRef = StateRef(SecureHash.sha256("fake tx id"), 0) lateinit var tx: TransactionBuilder @Before diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index 37219b93ec..37fc7b2d88 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -201,7 +201,7 @@ fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().appl class TransactionGroupDSL(private val stateType: Class) { open inner class LedgerTransactionDSL : AbstractTransactionForTest() { - private val inStates = ArrayList() + private val inStates = ArrayList() fun input(label: String) { inStates.add(label.outputRef) @@ -219,7 +219,7 @@ class TransactionGroupDSL(private val stateType: Class) { } val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found") - val String.outputRef: ContractStateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"") + val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"") fun lookup(label: String) = StateAndRef(label.output as C, label.outputRef) @@ -228,7 +228,7 @@ class TransactionGroupDSL(private val stateType: Class) { val ltx = toLedgerTransaction() for ((index, labelledState) in outStates.withIndex()) { if (labelledState.label != null) { - labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index) + labelToRefs[labelledState.label] = StateRef(ltx.hash, index) if (stateType.isInstance(labelledState.state)) { labelToOutputs[labelledState.label] = labelledState.state as T } @@ -240,7 +240,7 @@ class TransactionGroupDSL(private val stateType: Class) { } private val rootTxns = ArrayList() - private val labelToRefs = HashMap() + private val labelToRefs = HashMap() private val labelToOutputs = HashMap() private val outputsToLabels = HashMap() @@ -253,7 +253,7 @@ class TransactionGroupDSL(private val stateType: Class) { val ltx = wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! - labelToRefs[label] = ContractStateRef(ltx.hash, index) + labelToRefs[label] = StateRef(ltx.hash, index) outputsToLabels[state.state] = label labelToOutputs[label] = state.state as T } From b023e570fce31eb54de70a4a4386f50ccdaa9957 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 9 Feb 2016 15:08:10 +0100 Subject: [PATCH 03/19] Docs: update contracts tutorial to talk about generation instead of crafting --- docs/source/tutorial.rst | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index dd5faf2b73..7ca1b6dbe2 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -636,22 +636,22 @@ again to ensure the third transaction fails with a message that contains "must h the exact message). -Adding a crafting API to your contract +Adding a generation API to your contract -------------------------------------- Contract classes **must** provide a verify function, but they may optionally also provide helper functions to simplify -their usage. A simple class of functions most contracts provide are *crafting functions*, which either generate or +their usage. A simple class of functions most contracts provide are *generation functions*, which either create or modify a transaction to perform certain actions (an action is normally mappable 1:1 to a command, but doesn't have to be so). -Crafting may involve complex logic. For example, the cash contract has a ``craftSpend`` method that is given a set of +Generation may involve complex logic. For example, the cash contract has a ``generateSpend`` method that is given a set of cash states and chooses a way to combine them together to satisfy the amount of money that is being sent. In the immutable-state model that we are using ledger entries (states) can only be created and deleted, but never modified. Therefore to send $1200 when we have only $900 and $500 requires combining both states together, and then creating two new output states of $1200 and $200 back to ourselves. This latter state is called the *change* and is a concept that should be familiar to anyone who has worked with Bitcoin. -As another example, we can imagine code that implements a netting algorithm may craft complex transactions that must +As another example, we can imagine code that implements a netting algorithm may generate complex transactions that must be signed by many people. Whilst such code might be too big for a single utility method (it'd probably be sized more like a module), the basic concept is the same: preparation of a transaction using complex logic. @@ -662,7 +662,7 @@ a method to wrap up the issuance process: .. sourcecode:: kotlin - fun craftIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { + fun generateIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate) return TransactionBuilder(state, WireCommand(Commands.Issue, issuance.party.owningKey)) } @@ -673,7 +673,7 @@ returns a ``TransactionBuilder``. A ``TransactionBuilder`` is one of the few mut It allows you to add inputs, outputs and commands to it and is designed to be passed around, potentially between multiple contracts. -.. note:: Crafting methods should ideally be written to compose with each other, that is, they should take a +.. note:: Generation methods should ideally be written to compose with each other, that is, they should take a ``TransactionBuilder`` as an argument instead of returning one, unless you are sure it doesn't make sense to combine this type of transaction with others. In this case, issuing CP at the same time as doing other things would just introduce complexity that isn't likely to be worth it, so we return a fresh object each time: instead, @@ -697,7 +697,7 @@ What about moving the paper, i.e. reassigning ownership to someone else? .. sourcecode:: kotlin - fun craftMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { tx.addInputState(paper.ref) tx.addOutputState(paper.state.copy(owner = newOwner)) tx.addArg(WireCommand(Commands.Move, paper.state.owner)) @@ -705,7 +705,7 @@ What about moving the paper, i.e. reassigning ownership to someone else? Here, the method takes a pre-existing ``TransactionBuilder`` and adds to it. This is correct because typically you will want to combine a sale of CP atomically with the movement of some other asset, such as cash. So both -craft methods should operate on the same transaction. You can see an example of this being done in the unit tests +generate methods should operate on the same transaction. You can see an example of this being done in the unit tests for the commercial paper contract. The paper is given to us as a ``StateAndRef`` object. This is exactly what it sounds like: @@ -719,9 +719,9 @@ Finally, we can do redemption. .. sourcecode:: kotlin @Throws(InsufficientBalanceException::class) - fun craftRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) + Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet) tx.addInputState(paper.ref) tx.addArg(WireCommand(CommercialPaper.Commands.Redeem, paper.state.owner)) } @@ -744,10 +744,9 @@ A ``TransactionBuilder`` is not by itself ready to be used anywhere, so first, w is recognised by the network. The most important next step is for the participating entities to sign it using the ``signWith()`` method. This takes a keypair, serialises the transaction, signs the serialised form and then stores the signature inside the ``TransactionBuilder``. Once all parties have signed, you can call ``TransactionBuilder.toSignedTransaction()`` -to get a ``SignedWireTransaction`` object. This is an immutable form of the transaction that's ready for *timestamping*. - -.. note:: Timestamping and passing around of partial transactions for group signing is not yet fully implemented. - This tutorial will be updated once it is. +to get a ``SignedWireTransaction`` object. This is an immutable form of the transaction that's ready for *timestamping*, +which can be done using a ``TimestamperClient``. To learn more about that, please refer to the +:doc:`protocol-state-machines` document. You can see how transactions flow through the different stages of construction by examining the commercial paper unit tests. From 262124385dea6bf6cca306d642c86d94d3186734 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 16:54:45 +0100 Subject: [PATCH 04/19] Move Crypto.kt into core/crypto/CryptoUtilities.kt --- src/main/java/contracts/JavaCommercialPaper.java | 5 +++-- src/main/java/core/crypto/Base58.java | 4 +--- .../core/crypto/CryptoUtilities.kt} | 15 ++++++++------- src/main/kotlin/contracts/Cash.kt | 2 ++ src/main/kotlin/contracts/CommercialPaper.kt | 3 +++ src/main/kotlin/contracts/CrowdFund.kt | 1 + src/main/kotlin/contracts/DummyContract.kt | 2 +- .../contracts/protocols/TwoPartyTradeProtocol.kt | 1 + src/main/kotlin/core/Services.kt | 1 + src/main/kotlin/core/Structures.kt | 2 ++ src/main/kotlin/core/TransactionVerification.kt | 1 + src/main/kotlin/core/Transactions.kt | 3 +++ src/main/kotlin/core/messaging/InMemoryNetwork.kt | 2 +- src/main/kotlin/core/messaging/StateMachines.kt | 4 ++-- .../kotlin/core/node/TimestamperNodeService.kt | 2 ++ src/main/kotlin/core/node/TraderDemo.kt | 1 + src/main/kotlin/core/serialization/Kryo.kt | 4 ++-- src/test/kotlin/contracts/CashTests.kt | 1 + src/test/kotlin/contracts/CommercialPaperTests.kt | 1 + src/test/kotlin/contracts/CrowdFundTests.kt | 1 + src/test/kotlin/core/MockServices.kt | 2 ++ src/test/kotlin/core/TransactionGroupTests.kt | 1 + .../core/node/TimestamperNodeServiceTest.kt | 1 + .../TransactionSerializationTests.kt | 1 + src/test/kotlin/core/testutils/TestUtils.kt | 3 +++ .../core/visualiser/GroupToGraphConversion.kt | 2 +- 26 files changed, 47 insertions(+), 19 deletions(-) rename src/main/{kotlin/core/Crypto.kt => java/core/crypto/CryptoUtilities.kt} (90%) diff --git a/src/main/java/contracts/JavaCommercialPaper.java b/src/main/java/contracts/JavaCommercialPaper.java index faa4e54dbe..a2b69e02e3 100644 --- a/src/main/java/contracts/JavaCommercialPaper.java +++ b/src/main/java/contracts/JavaCommercialPaper.java @@ -10,6 +10,7 @@ package contracts; import core.*; import core.TransactionForVerification.*; +import core.crypto.*; import org.jetbrains.annotations.*; import java.security.*; @@ -26,7 +27,7 @@ import static kotlin.collections.CollectionsKt.*; * */ public class JavaCommercialPaper implements Contract { - public static SecureHash JCP_PROGRAM_ID = SecureHash.Companion.sha256("java commercial paper (this should be a bytecode hash)"); + public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)"); public static class State implements ContractState, ICommercialPaperState { private PartyReference issuance; @@ -217,7 +218,7 @@ public class JavaCommercialPaper implements Contract { @Override public SecureHash getLegalContractReference() { // TODO: Should return hash of the contract's contents, not its URI - return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); + return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); } public TransactionBuilder generateIssue(@NotNull PartyReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate) { diff --git a/src/main/java/core/crypto/Base58.java b/src/main/java/core/crypto/Base58.java index a943d2500d..c7c7a3ca48 100644 --- a/src/main/java/core/crypto/Base58.java +++ b/src/main/java/core/crypto/Base58.java @@ -8,8 +8,6 @@ package core.crypto; -import core.*; - import java.math.*; import java.util.*; @@ -145,7 +143,7 @@ public class Base58 { throw new AddressFormatException("Input too short"); byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4); byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length); - byte[] actualChecksum = Arrays.copyOfRange(SecureHash.Companion.sha256Twice(data).getBits(), 0, 4); + byte[] actualChecksum = Arrays.copyOfRange(SecureHash.sha256Twice(data).getBits(), 0, 4); if (!Arrays.equals(checksum, actualChecksum)) throw new AddressFormatException("Checksum does not validate"); return data; diff --git a/src/main/kotlin/core/Crypto.kt b/src/main/java/core/crypto/CryptoUtilities.kt similarity index 90% rename from src/main/kotlin/core/Crypto.kt rename to src/main/java/core/crypto/CryptoUtilities.kt index 57900d756e..01d8ff5d4d 100644 --- a/src/main/kotlin/core/Crypto.kt +++ b/src/main/java/core/crypto/CryptoUtilities.kt @@ -6,10 +6,10 @@ * All other rights reserved. */ -package core +package core.crypto import com.google.common.io.BaseEncoding -import core.crypto.Base58 +import core.Party import core.serialization.OpaqueBytes import java.math.BigInteger import java.security.* @@ -28,18 +28,19 @@ sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) { // Like static methods in Java, except the 'companion' is a singleton that can have state. companion object { + @JvmStatic fun parse(str: String) = BaseEncoding.base16().decode(str.toLowerCase()).let { when (it.size) { - 32 -> SecureHash.SHA256(it) + 32 -> SHA256(it) else -> throw IllegalArgumentException("Provided string is not 32 bytes in base 16 (hex): $str") } } - fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits)) - fun sha256Twice(bits: ByteArray) = sha256(sha256(bits).bits) - fun sha256(str: String) = sha256(str.toByteArray()) + @JvmStatic fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits)) + @JvmStatic fun sha256Twice(bits: ByteArray) = sha256(sha256(bits).bits) + @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) - fun randomSHA256() = sha256(SecureRandom.getInstanceStrong().generateSeed(32)) + @JvmStatic fun randomSHA256() = sha256(SecureRandom.getInstanceStrong().generateSeed(32)) } abstract val signatureAlgorithmName: String diff --git a/src/main/kotlin/contracts/Cash.kt b/src/main/kotlin/contracts/Cash.kt index 4b2769321f..fc75f2a904 100644 --- a/src/main/kotlin/contracts/Cash.kt +++ b/src/main/kotlin/contracts/Cash.kt @@ -9,6 +9,8 @@ package contracts import core.* +import core.crypto.SecureHash +import core.crypto.toStringShort import core.utilities.Emoji import java.security.PublicKey import java.security.SecureRandom diff --git a/src/main/kotlin/contracts/CommercialPaper.kt b/src/main/kotlin/contracts/CommercialPaper.kt index bf905f7db1..91645c2a7d 100644 --- a/src/main/kotlin/contracts/CommercialPaper.kt +++ b/src/main/kotlin/contracts/CommercialPaper.kt @@ -9,6 +9,9 @@ package contracts import core.* +import core.crypto.NullPublicKey +import core.crypto.SecureHash +import core.crypto.toStringShort import core.utilities.Emoji import java.security.PublicKey import java.time.Instant diff --git a/src/main/kotlin/contracts/CrowdFund.kt b/src/main/kotlin/contracts/CrowdFund.kt index c6e796b881..5294e049ae 100644 --- a/src/main/kotlin/contracts/CrowdFund.kt +++ b/src/main/kotlin/contracts/CrowdFund.kt @@ -9,6 +9,7 @@ package contracts import core.* +import core.crypto.SecureHash import java.security.PublicKey import java.time.Instant import java.util.* diff --git a/src/main/kotlin/contracts/DummyContract.kt b/src/main/kotlin/contracts/DummyContract.kt index dd3bc4528c..45c0705f47 100644 --- a/src/main/kotlin/contracts/DummyContract.kt +++ b/src/main/kotlin/contracts/DummyContract.kt @@ -10,7 +10,7 @@ package contracts import core.Contract import core.ContractState -import core.SecureHash +import core.crypto.SecureHash import core.TransactionForVerification // The dummy contract doesn't do anything useful. It exists for testing purposes. diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index 14ac83b13b..87da3f432c 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -13,6 +13,7 @@ import com.google.common.util.concurrent.ListenableFuture import contracts.Cash import contracts.sumCashBy import core.* +import core.crypto.signWithECDSA import core.messaging.LegallyIdentifiableNode import core.messaging.ProtocolStateMachine import core.messaging.SingleMessageRecipient diff --git a/src/main/kotlin/core/Services.kt b/src/main/kotlin/core/Services.kt index 8630bd4509..4f2d18160e 100644 --- a/src/main/kotlin/core/Services.kt +++ b/src/main/kotlin/core/Services.kt @@ -9,6 +9,7 @@ package core import co.paralleluniverse.fibers.Suspendable +import core.crypto.DigitalSignature import core.messaging.MessagingService import core.messaging.NetworkMap import core.serialization.SerializedBytes diff --git a/src/main/kotlin/core/Structures.kt b/src/main/kotlin/core/Structures.kt index 2d955e3801..2195f0564c 100644 --- a/src/main/kotlin/core/Structures.kt +++ b/src/main/kotlin/core/Structures.kt @@ -8,6 +8,8 @@ package core +import core.crypto.SecureHash +import core.crypto.toStringShort import core.serialization.OpaqueBytes import core.serialization.serialize import java.security.PublicKey diff --git a/src/main/kotlin/core/TransactionVerification.kt b/src/main/kotlin/core/TransactionVerification.kt index 4aa79a80e1..6f038205a8 100644 --- a/src/main/kotlin/core/TransactionVerification.kt +++ b/src/main/kotlin/core/TransactionVerification.kt @@ -8,6 +8,7 @@ package core +import core.crypto.SecureHash import java.util.* class TransactionResolutionException(val hash: SecureHash) : Exception() diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index a7ce3337bc..e9c443a3b4 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -9,6 +9,9 @@ package core import co.paralleluniverse.fibers.Suspendable +import core.crypto.DigitalSignature +import core.crypto.SecureHash +import core.crypto.signWithECDSA import core.node.TimestampingError import core.serialization.SerializedBytes import core.serialization.deserialize diff --git a/src/main/kotlin/core/messaging/InMemoryNetwork.kt b/src/main/kotlin/core/messaging/InMemoryNetwork.kt index 143d1a2d7e..bf7795b6d4 100644 --- a/src/main/kotlin/core/messaging/InMemoryNetwork.kt +++ b/src/main/kotlin/core/messaging/InMemoryNetwork.kt @@ -12,8 +12,8 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import core.Party +import core.crypto.sha256 import core.node.TimestamperNodeService -import core.sha256 import core.utilities.loggerFor import java.security.KeyPairGenerator import java.time.Instant diff --git a/src/main/kotlin/core/messaging/StateMachines.kt b/src/main/kotlin/core/messaging/StateMachines.kt index 6576972788..853a3e5937 100644 --- a/src/main/kotlin/core/messaging/StateMachines.kt +++ b/src/main/kotlin/core/messaging/StateMachines.kt @@ -17,13 +17,13 @@ import com.esotericsoftware.kryo.io.Output import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture -import core.SecureHash +import core.crypto.SecureHash import core.ServiceHub import core.serialization.THREAD_LOCAL_KRYO import core.serialization.createKryo import core.serialization.deserialize import core.serialization.serialize -import core.sha256 +import core.crypto.sha256 import core.utilities.trace import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/src/main/kotlin/core/node/TimestamperNodeService.kt b/src/main/kotlin/core/node/TimestamperNodeService.kt index 535698727e..d0e37b1c1a 100644 --- a/src/main/kotlin/core/node/TimestamperNodeService.kt +++ b/src/main/kotlin/core/node/TimestamperNodeService.kt @@ -11,6 +11,8 @@ package core.node import co.paralleluniverse.common.util.VisibleForTesting import co.paralleluniverse.fibers.Suspendable import core.* +import core.crypto.DigitalSignature +import core.crypto.signWithECDSA import core.messaging.LegallyIdentifiableNode import core.messaging.MessageRecipients import core.messaging.MessagingService diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 0620755af6..339dc034a4 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -12,6 +12,7 @@ import com.google.common.net.HostAndPort import contracts.CommercialPaper import contracts.protocols.TwoPartyTradeProtocol import core.* +import core.crypto.SecureHash import core.messaging.LegallyIdentifiableNode import core.messaging.SingleMessageRecipient import core.messaging.runOnNextMessage diff --git a/src/main/kotlin/core/serialization/Kryo.kt b/src/main/kotlin/core/serialization/Kryo.kt index e5acdd7abb..45c2f4f91e 100644 --- a/src/main/kotlin/core/serialization/Kryo.kt +++ b/src/main/kotlin/core/serialization/Kryo.kt @@ -16,9 +16,9 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.JavaSerializer -import core.SecureHash +import core.crypto.SecureHash import core.SignedWireTransaction -import core.sha256 +import core.crypto.sha256 import de.javakaffee.kryoserializers.ArraysAsListSerializer import org.objenesis.strategy.StdInstantiatorStrategy import java.io.ByteArrayOutputStream diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index d049982b62..38c2159f3f 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -14,6 +14,7 @@ import contracts.Cash import contracts.DummyContract import contracts.InsufficientBalanceException import core.* +import core.crypto.SecureHash import core.serialization.OpaqueBytes import core.testutils.* import org.junit.Test diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt index 28bf54d6c4..e8b212553d 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTests.kt @@ -9,6 +9,7 @@ package contracts import core.* +import core.crypto.SecureHash import core.node.TimestampingError import core.testutils.* import org.junit.Test diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt index 0aed163b67..bf8a4528ef 100644 --- a/src/test/kotlin/contracts/CrowdFundTests.kt +++ b/src/test/kotlin/contracts/CrowdFundTests.kt @@ -9,6 +9,7 @@ package contracts import core.* +import core.crypto.SecureHash import core.testutils.* import org.junit.Test import java.time.Instant diff --git a/src/test/kotlin/core/MockServices.kt b/src/test/kotlin/core/MockServices.kt index ec31ad3b4d..6a61ab7178 100644 --- a/src/test/kotlin/core/MockServices.kt +++ b/src/test/kotlin/core/MockServices.kt @@ -8,6 +8,8 @@ package core +import core.crypto.DigitalSignature +import core.crypto.signWithECDSA import core.messaging.MessagingService import core.messaging.MockNetworkMap import core.messaging.NetworkMap diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt index 9be61293e5..cfbfeb8502 100644 --- a/src/test/kotlin/core/TransactionGroupTests.kt +++ b/src/test/kotlin/core/TransactionGroupTests.kt @@ -9,6 +9,7 @@ package core import contracts.Cash +import core.crypto.SecureHash import core.testutils.* import org.junit.Test import kotlin.test.assertEquals diff --git a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt index 3e6189c5d4..375e0db3d1 100644 --- a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt +++ b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt @@ -10,6 +10,7 @@ package core.node import co.paralleluniverse.fibers.Suspendable import core.* +import core.crypto.SecureHash import core.messaging.* import core.serialization.serialize import core.testutils.ALICE diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index 7944500cab..6b7fc24e6c 100644 --- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -10,6 +10,7 @@ package core.serialization import contracts.Cash import core.* +import core.crypto.SecureHash import core.testutils.* import org.junit.Before import org.junit.Test diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index 37fc7b2d88..f9c97148a8 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -12,6 +12,9 @@ package core.testutils import contracts.* import core.* +import core.crypto.DummyPublicKey +import core.crypto.NullPublicKey +import core.crypto.SecureHash import core.visualiser.GraphVisualiser import java.security.KeyPairGenerator import java.security.PublicKey diff --git a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt index 7a3149499d..03f81f8ce2 100644 --- a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt +++ b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt @@ -10,7 +10,7 @@ package core.visualiser import core.CommandData import core.ContractState -import core.SecureHash +import core.crypto.SecureHash import core.testutils.TransactionGroupDSL import org.graphstream.graph.Edge import org.graphstream.graph.Node From 7a70cdd4dea3977dcb4c5945a517990875120359 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 16:56:51 +0100 Subject: [PATCH 05/19] Minor: rename InMemoryNetwork.Node to InMemoryNetwork.InMemoryNode Makes IDE class navigation easier by avoiding having two classes with the same name. --- .../kotlin/core/messaging/InMemoryNetwork.kt | 28 +++++++++---------- .../core/messaging/InMemoryMessagingTests.kt | 4 +-- .../core/node/TimestamperNodeServiceTest.kt | 4 +-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/core/messaging/InMemoryNetwork.kt b/src/main/kotlin/core/messaging/InMemoryNetwork.kt index bf7795b6d4..377f9dc4c0 100644 --- a/src/main/kotlin/core/messaging/InMemoryNetwork.kt +++ b/src/main/kotlin/core/messaging/InMemoryNetwork.kt @@ -25,8 +25,8 @@ import javax.annotation.concurrent.ThreadSafe import kotlin.concurrent.thread /** - * An in-memory network allows you to manufacture [Node]s for a set of participants. Each - * [Node] maintains a queue of messages it has received, and a background thread that dispatches + * An in-memory network allows you to manufacture [InMemoryNode]s for a set of participants. Each + * [InMemoryNode] maintains a queue of messages it has received, and a background thread that dispatches * messages one by one to registered handlers. Alternatively, a messaging system may be manually pumped, in which * case no thread is created and a caller is expected to force delivery one at a time (this is useful for unit * testing). @@ -34,26 +34,26 @@ import kotlin.concurrent.thread @ThreadSafe public class InMemoryNetwork { private var counter = 0 // -1 means stopped. - private val handleNodeMap = HashMap() + private val handleNodeMap = HashMap() // All messages are kept here until the messages are pumped off the queue by a caller to the node class. // Queues are created on-demand when a message is sent to an address: the receiving node doesn't have to have // been created yet. If the node identified by the given handle has gone away/been shut down then messages // stack up here waiting for it to come back. The intent of this is to simulate a reliable messaging network. private val messageQueues = HashMap>() - val nodes: List @Synchronized get() = handleNodeMap.values.toList() + val nodes: List @Synchronized get() = handleNodeMap.values.toList() /** * Creates a node and returns the new object that identifies its location on the network to senders, and the - * [Node] that the recipient/in-memory node uses to receive messages and send messages itself. + * [InMemoryNode] that the recipient/in-memory node uses to receive messages and send messages itself. * - * If [manuallyPumped] is set to true, then you are expected to call the [Node.pump] method on the [Node] + * If [manuallyPumped] is set to true, then you are expected to call the [InMemoryNode.pump] method on the [InMemoryNode] * in order to cause the delivery of a single message, which will occur on the thread of the caller. If set to false * then this class will set up a background thread to deliver messages asynchronously, if the handler specifies no * executor. */ @Synchronized - fun createNode(manuallyPumped: Boolean): Pair> { + fun createNode(manuallyPumped: Boolean): Pair> { check(counter >= 0) { "In memory network stopped: please recreate. "} val builder = createNodeWithID(manuallyPumped, counter) as Builder counter++ @@ -62,7 +62,7 @@ public class InMemoryNetwork { } /** Creates a node at the given address: useful if you want to recreate a node to simulate a restart */ - fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder { + fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder { return Builder(manuallyPumped, Handle(id)) } @@ -103,10 +103,10 @@ public class InMemoryNetwork { messageQueues.clear() } - inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder { - override fun start(): ListenableFuture { + inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder { + override fun start(): ListenableFuture { synchronized(this@InMemoryNetwork) { - val node = Node(manuallyPumped, id) + val node = InMemoryNode(manuallyPumped, id) handleNodeMap[id] = node return Futures.immediateFuture(node) } @@ -122,7 +122,7 @@ public class InMemoryNetwork { private var timestampingAdvert: LegallyIdentifiableNode? = null @Synchronized - fun setupTimestampingNode(manuallyPumped: Boolean): Pair { + fun setupTimestampingNode(manuallyPumped: Boolean): Pair { check(timestampingAdvert == null) val (handle, builder) = createNode(manuallyPumped) val node = builder.start().get() @@ -134,13 +134,13 @@ public class InMemoryNetwork { } /** - * An [Node] provides a [MessagingService] that isn't backed by any kind of network or disk storage + * An [InMemoryNode] provides a [MessagingService] that isn't backed by any kind of network or disk storage * system, but just uses regular queues on the heap instead. It is intended for unit testing and developer convenience * when all entities on 'the network' are being simulated in-process. * * An instance can be obtained by creating a builder and then using the start method. */ - inner class Node(private val manuallyPumped: Boolean, private val handle: Handle): MessagingService { + inner class InMemoryNode(private val manuallyPumped: Boolean, private val handle: Handle): MessagingService { inner class Handler(val executor: Executor?, val topic: String, val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration @GuardedBy("this") diff --git a/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt b/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt index 5d4b523095..e6cdc5ae43 100644 --- a/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt +++ b/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt @@ -21,10 +21,10 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue open class TestWithInMemoryNetwork { - val nodes: MutableMap = HashMap() + val nodes: MutableMap = HashMap() lateinit var network: InMemoryNetwork - fun makeNode(inBackground: Boolean = false): Pair { + fun makeNode(inBackground: Boolean = false): Pair { // The manuallyPumped = true bit means that we must call the pump method on the system in order to val (address, builder) = network.createNode(!inBackground) val node = builder.start().get() diff --git a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt index 375e0db3d1..42a08dbae8 100644 --- a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt +++ b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt @@ -27,8 +27,8 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue class TimestamperNodeServiceTest : TestWithInMemoryNetwork() { - lateinit var myNode: Pair - lateinit var serviceNode: Pair + lateinit var myNode: Pair + lateinit var serviceNode: Pair lateinit var service: TimestamperNodeService val ptx = TransactionBuilder().apply { From 5d0d926568c7db07d04ea7f0fb572b0c6e21acdc Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 17:32:00 +0100 Subject: [PATCH 06/19] Node: if the node is started twice, abort and tell the sysadmin the PID of the other instance. --- src/main/kotlin/core/node/Node.kt | 53 +++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt index 1ad56c0106..b9709aa764 100644 --- a/src/main/kotlin/core/node/Node.kt +++ b/src/main/kotlin/core/node/Node.kt @@ -14,8 +14,12 @@ import core.messaging.* import core.serialization.deserialize import core.serialization.serialize import core.utilities.loggerFor +import java.io.RandomAccessFile +import java.lang.management.ManagementFactory +import java.nio.channels.FileLock import java.nio.file.Files import java.nio.file.Path +import java.nio.file.StandardOpenOption import java.security.KeyPair import java.security.KeyPairGenerator import java.util.* @@ -65,17 +69,27 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon override val identityService: IdentityService get() = identity } - // TODO: Implement mutual exclusion so we can't start the node twice by accident. - - val storage = makeStorageService(dir) - val smm = StateMachineManager(services, serverThread) - val net = ArtemisMessagingService(dir, myNetAddr) - val wallet: WalletService = E2ETestWalletService(services) - val keyManagement = E2ETestKeyManagementService() + val storage: StorageService + val smm: StateMachineManager + val net: ArtemisMessagingService + val wallet: WalletService + val keyManagement: E2ETestKeyManagementService val inNodeTimestampingService: TimestamperNodeService? val identity: IdentityService + // Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us + // when our process shuts down, but we try in stop() anyway just to be nice. + private var nodeFileLock: FileLock? = null + init { + alreadyRunningNodeCheck() + + storage = makeStorageService(dir) + smm = StateMachineManager(services, serverThread) + net = ArtemisMessagingService(dir, myNetAddr) + wallet = E2ETestWalletService(services) + keyManagement = E2ETestKeyManagementService() + // Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are // given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping. val tsid = if (timestamperAddress != null) { @@ -101,6 +115,7 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon fun stop() { net.stop() serverThread.shutdownNow() + nodeFileLock!!.release() } fun makeStorageService(dir: Path): StorageService { @@ -152,6 +167,30 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon } } + private fun alreadyRunningNodeCheck() { + // Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a + // file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already + // exists, we try to take the file lock first before replacing it and if that fails it means we're being started + // twice with the same directory: that's a user error and we should bail out. + val pidPath = dir.resolve("process-id") + val file = pidPath.toFile() + if (file.exists()) { + val f = RandomAccessFile(file, "rw") + val l = f.channel.tryLock() + if (l == null) { + println("It appears there is already a node running with the specified data directory $dir") + println("Shut that other node down and try again. It may have process ID ${file.readText()}") + System.exit(1) + } + nodeFileLock = l + } + val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0] + Files.write(pidPath, ourProcessID.toByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + pidPath.toFile().deleteOnExit() + if (nodeFileLock == null) + nodeFileLock = RandomAccessFile(file, "rw").channel.lock() + } + companion object { val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PUBLIC_IDENTITY_FILE_NAME = "identity-public" From 6f3b07a600c659fedc2edcf4d2b5bcea0bf2685f Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 17:35:27 +0100 Subject: [PATCH 07/19] Minor: move and api doc the Node.DEFAULT_PORT field --- src/main/kotlin/core/node/Node.kt | 5 +++-- src/main/kotlin/core/node/TraderDemo.kt | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt index b9709aa764..bc7f0ddaed 100644 --- a/src/main/kotlin/core/node/Node.kt +++ b/src/main/kotlin/core/node/Node.kt @@ -25,8 +25,6 @@ import java.security.KeyPairGenerator import java.util.* import java.util.concurrent.Executors -val DEFAULT_PORT = 31337 - class ConfigurationException(message: String) : Exception(message) // TODO: Split this into a regression testing environment @@ -194,5 +192,8 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon companion object { val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PUBLIC_IDENTITY_FILE_NAME = "identity-public" + + /** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */ + val DEFAULT_PORT = 31337 } } \ No newline at end of file diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 339dc034a4..61a47be4d1 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -89,11 +89,11 @@ fun main(args: Array) { val config = loadConfigFile(configFile) - val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(DEFAULT_PORT) + val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT) val listening = options.has(serviceFakeTradesArg) val timestamperId = if (options.has(timestamperIdentityFile)) { - val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(DEFAULT_PORT) + val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(Node.DEFAULT_PORT) val path = Paths.get(options.valueOf(timestamperIdentityFile)) val party = Files.readAllBytes(path).deserialize(includeClassName = true) LegallyIdentifiableNode(ArtemisMessagingService.makeRecipient(addr), party) @@ -143,7 +143,7 @@ fun main(args: Array) { println("Need the --fake-trade-with command line argument") System.exit(1) } - val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(DEFAULT_PORT) + val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT) val otherSide = ArtemisMessagingService.makeRecipient(peerAddr) node.net.runOnNextMessage("test.junktrade.initiate") { msg -> val sessionID = msg.data.deserialize() From 8d1b318370684f98d8bcfae6668240672b7cea42 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 17:40:36 +0100 Subject: [PATCH 08/19] Minor: some more renamings for concision and consistency --- .../protocols/TwoPartyTradeProtocol.kt | 2 +- .../kotlin/core/TransactionVerification.kt | 8 ++--- src/main/kotlin/core/Transactions.kt | 36 +++++++++---------- .../kotlin/core/node/E2ETestWalletService.kt | 2 +- src/test/kotlin/contracts/CashTests.kt | 26 +++++++------- .../TransactionSerializationTests.kt | 9 +++-- .../core/visualiser/GroupToGraphConversion.kt | 6 ++-- 7 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index 87da3f432c..cac04e6f23 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -87,7 +87,7 @@ object TwoPartyTradeProtocol { val wtx: WireTransaction = partialTX.txBits.deserialize() requireThat { - "transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(myKeyPair.public) == price) + "transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price) // There are all sorts of funny games a malicious secondary might play here, we should fix them: // // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if diff --git a/src/main/kotlin/core/TransactionVerification.kt b/src/main/kotlin/core/TransactionVerification.kt index 6f038205a8..4ba13566d0 100644 --- a/src/main/kotlin/core/TransactionVerification.kt +++ b/src/main/kotlin/core/TransactionVerification.kt @@ -38,8 +38,8 @@ class TransactionGroup(val transactions: Set, val nonVerified val resolved = HashSet(transactions.size) for (tx in transactions) { - val inputs = ArrayList(tx.inStateRefs.size) - for (ref in tx.inStateRefs) { + val inputs = ArrayList(tx.inputs.size) + for (ref in tx.inputs) { val conflict = refToConsumingTXMap[ref] if (conflict != null) throw TransactionConflictException(ref, tx, conflict) @@ -48,9 +48,9 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the connecting transaction. val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash) // Look up the output in that transaction by index. - inputs.add(ltx.outStates[ref.index]) + inputs.add(ltx.outputs[ref.index]) } - resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.hash)) + resolved.add(TransactionForVerification(inputs, tx.outputs, tx.commands, tx.hash)) } for (tx in resolved) diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index e9c443a3b4..9c1de7b931 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -54,22 +54,22 @@ import java.util.* */ /** Transaction ready for serialisation, without any signatures attached. */ -data class WireTransaction(val inputStates: List, - val outputStates: List, +data class WireTransaction(val inputs: List, + val outputs: List, val commands: List) { fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { val authenticatedArgs = commands.map { val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } AuthenticatedObject(it.pubkeys, institutions, it.data) } - return LedgerTransaction(inputStates, outputStates, authenticatedArgs, originalHash) + return LedgerTransaction(inputs, outputs, authenticatedArgs, originalHash) } override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") - for (input in inputStates) buf.appendln("${Emoji.rightArrow}INPUT: $input") - for (output in outputStates) buf.appendln("${Emoji.leftArrow}OUTPUT: $output") + for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input") + for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output") for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command") return buf.toString() } @@ -127,8 +127,8 @@ data class SignedWireTransaction(val txBits: SerializedBytes, v } /** A mutable transaction that's in the process of being built, before all signatures are present. */ -class TransactionBuilder(private val inputStates: MutableList = arrayListOf(), - private val outputStates: MutableList = arrayListOf(), +class TransactionBuilder(private val inputs: MutableList = arrayListOf(), + private val outputs: MutableList = arrayListOf(), private val commands: MutableList = arrayListOf()) { val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull() @@ -155,8 +155,8 @@ class TransactionBuilder(private val inputStates: MutableList = arrayL public fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { when (t) { - is StateRef -> inputStates.add(t) - is ContractState -> outputStates.add(t) + is StateRef -> inputs.add(t) + is ContractState -> outputs.add(t) is Command -> commands.add(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } @@ -214,7 +214,7 @@ class TransactionBuilder(private val inputStates: MutableList = arrayL currentSigs.add(sig) } - fun toWireTransaction() = WireTransaction(ArrayList(inputStates), ArrayList(outputStates), ArrayList(commands)) + fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(outputs), ArrayList(commands)) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction { if (checkSufficientSignatures) { @@ -229,12 +229,12 @@ class TransactionBuilder(private val inputStates: MutableList = arrayL fun addInputState(ref: StateRef) { check(currentSigs.isEmpty()) - inputStates.add(ref) + inputs.add(ref) } fun addOutputState(state: ContractState) { check(currentSigs.isEmpty()) - outputStates.add(state) + outputs.add(state) } fun addCommand(arg: Command) { @@ -248,8 +248,8 @@ class TransactionBuilder(private val inputStates: MutableList = arrayL fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) // Accessors that yield immutable snapshots. - fun inputStates(): List = ArrayList(inputStates) - fun outputStates(): List = ArrayList(outputStates) + fun inputStates(): List = ArrayList(inputs) + fun outputStates(): List = ArrayList(outputs) fun commands(): List = ArrayList(commands) } @@ -260,19 +260,19 @@ class TransactionBuilder(private val inputStates: MutableList = arrayL */ data class LedgerTransaction( /** The input states which will be consumed/invalidated by the execution of this transaction. */ - val inStateRefs: List, + val inputs: List, /** The states that will be generated by the execution of this transaction. */ - val outStates: List, + val outputs: List, /** Arbitrary data passed to the program of each input state. */ val commands: List>, /** The hash of the original serialised SignedTransaction */ val hash: SecureHash ) { @Suppress("UNCHECKED_CAST") - fun outRef(index: Int) = StateAndRef(outStates[index] as T, StateRef(hash, index)) + fun outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index)) fun outRef(state: T): StateAndRef { - val i = outStates.indexOf(state) + val i = outputs.indexOf(state) if (i == -1) throw IllegalArgumentException("State not found in this transaction") return outRef(i) diff --git a/src/main/kotlin/core/node/E2ETestWalletService.kt b/src/main/kotlin/core/node/E2ETestWalletService.kt index 351cd46119..8bdd61c90c 100644 --- a/src/main/kotlin/core/node/E2ETestWalletService.kt +++ b/src/main/kotlin/core/node/E2ETestWalletService.kt @@ -60,7 +60,7 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService { } val statesAndRefs = transactions.map { - StateAndRef(it.tx.outputStates[0] as OwnableState, StateRef(it.id, 0)) + StateAndRef(it.tx.outputs[0] as OwnableState, StateRef(it.id, 0)) } mutex.locked { diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index 38c2159f3f..f60285b6ca 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -321,8 +321,8 @@ class CashTests { @Test fun generateSimpleDirectSpend() { val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[0]) + assertEquals(WALLET[0].ref, wtx.inputs[0]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @@ -336,29 +336,29 @@ class CashTests { @Test fun generateSimpleSpendWithChange() { val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputStates[0]) - assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputStates[1]) + assertEquals(WALLET[0].ref, wtx.inputs[0]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0]) + assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @Test fun generateSpendWithTwoInputs() { val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[1].ref, wtx.inputStates[1]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0]) + 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), wtx.outputs[0]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @Test fun generateSpendMixedDeposits() { val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[1].ref, wtx.inputStates[1]) - assertEquals(WALLET[2].ref, wtx.inputStates[2]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0]) - assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[1]) + 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), wtx.outputs[0]) + assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index 6b7fc24e6c..58179a2e8e 100644 --- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -11,7 +11,10 @@ package core.serialization import contracts.Cash import core.* import core.crypto.SecureHash -import core.testutils.* +import core.testutils.DUMMY_PUBKEY_1 +import core.testutils.MINI_CORP +import core.testutils.TEST_TX_TIME +import core.testutils.TestUtils import org.junit.Before import org.junit.Test import java.security.SignatureException @@ -92,8 +95,8 @@ class TransactionSerializationTests { val stx = tx.toSignedTransaction() val ltx = stx.verifyToLedgerTransaction(MockIdentityService) assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value }) - assertEquals(tx.inputStates(), ltx.inStateRefs) - assertEquals(tx.outputStates(), ltx.outStates) + assertEquals(tx.inputStates(), ltx.inputs) + assertEquals(tx.outputStates(), ltx.outputs) assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint) } } \ No newline at end of file diff --git a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt index 03f81f8ce2..3cdd05d274 100644 --- a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt +++ b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt @@ -34,9 +34,9 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { txNode.styleClass = "tx" // Now create a vertex for each output state. - for (outIndex in tx.outStates.indices) { + for (outIndex in tx.outputs.indices) { val node = graph.addNode(tx.outRef(outIndex).ref.toString()) - val state = tx.outStates[outIndex] + val state = tx.outputs[outIndex] node.label = stateToLabel(state) node.styleClass = stateToCSSClass(state) + ",state" node.setAttribute("state", state) @@ -55,7 +55,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { } // And now all states and transactions were mapped to graph nodes, hook up the input edges. for ((txIndex, tx) in tg.transactions.withIndex()) { - for ((inputIndex, ref) in tx.inStateRefs.withIndex()) { + for ((inputIndex, ref) in tx.inputs.withIndex()) { val edge = graph.addEdge("tx$txIndex-in$inputIndex", ref.toString(), "tx$txIndex", true) edge.weight = 1.2 } From 0665492645053b7388370f6cad420701dcf760a4 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 17:48:08 +0100 Subject: [PATCH 09/19] Minor: introduce a generateKeyPair() function that wraps the Java API, so we can switch algorithm more easily later. --- src/main/java/core/crypto/CryptoUtilities.kt | 5 ++++- src/main/kotlin/core/Services.kt | 4 ++-- src/main/kotlin/core/messaging/InMemoryNetwork.kt | 4 ++-- .../kotlin/core/node/E2ETestKeyManagementService.kt | 4 ++-- src/main/kotlin/core/node/Node.kt | 4 ++-- src/main/kotlin/core/node/TraderDemo.kt | 4 ++-- src/main/kotlin/core/serialization/Kryo.kt | 6 +++--- src/test/kotlin/core/MockServices.kt | 6 +++--- src/test/kotlin/core/testutils/TestUtils.kt | 10 +++++----- 9 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/main/java/core/crypto/CryptoUtilities.kt b/src/main/java/core/crypto/CryptoUtilities.kt index 01d8ff5d4d..e0d468ad20 100644 --- a/src/main/java/core/crypto/CryptoUtilities.kt +++ b/src/main/java/core/crypto/CryptoUtilities.kt @@ -123,4 +123,7 @@ fun PublicKey.toStringShort(): String { // Allow Kotlin destructuring: val (private, public) = keypair operator fun KeyPair.component1() = this.private -operator fun KeyPair.component2() = this.public \ No newline at end of file +operator fun KeyPair.component2() = this.public + +/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */ +fun generateKeyPair() = KeyPairGenerator.getInstance("EC").genKeyPair() \ No newline at end of file diff --git a/src/main/kotlin/core/Services.kt b/src/main/kotlin/core/Services.kt index 4f2d18160e..d9dd1e6f68 100644 --- a/src/main/kotlin/core/Services.kt +++ b/src/main/kotlin/core/Services.kt @@ -10,11 +10,11 @@ package core import co.paralleluniverse.fibers.Suspendable import core.crypto.DigitalSignature +import core.crypto.generateKeyPair import core.messaging.MessagingService import core.messaging.NetworkMap import core.serialization.SerializedBytes import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey @@ -92,7 +92,7 @@ interface TimestamperService { // We define a dummy authority here to allow to us to develop prototype contracts in the absence of a real authority. // The timestamper itself is implemented in the unit test part of the code (in TestUtils.kt). object DummyTimestampingAuthority { - val key = KeyPairGenerator.getInstance("EC").genKeyPair() + val key = generateKeyPair() val identity = Party("The dummy timestamper", key.public) } diff --git a/src/main/kotlin/core/messaging/InMemoryNetwork.kt b/src/main/kotlin/core/messaging/InMemoryNetwork.kt index 377f9dc4c0..80180384ec 100644 --- a/src/main/kotlin/core/messaging/InMemoryNetwork.kt +++ b/src/main/kotlin/core/messaging/InMemoryNetwork.kt @@ -12,10 +12,10 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import core.Party +import core.crypto.generateKeyPair import core.crypto.sha256 import core.node.TimestamperNodeService import core.utilities.loggerFor -import java.security.KeyPairGenerator import java.time.Instant import java.util.* import java.util.concurrent.Executor @@ -126,7 +126,7 @@ public class InMemoryNetwork { check(timestampingAdvert == null) val (handle, builder) = createNode(manuallyPumped) val node = builder.start().get() - val key = KeyPairGenerator.getInstance("EC").genKeyPair() + val key = generateKeyPair() val identity = Party("Unit test timestamping authority", key.public) TimestamperNodeService(node, identity, key) timestampingAdvert = LegallyIdentifiableNode(handle, identity) diff --git a/src/main/kotlin/core/node/E2ETestKeyManagementService.kt b/src/main/kotlin/core/node/E2ETestKeyManagementService.kt index efebd5bdc1..9943454736 100644 --- a/src/main/kotlin/core/node/E2ETestKeyManagementService.kt +++ b/src/main/kotlin/core/node/E2ETestKeyManagementService.kt @@ -10,8 +10,8 @@ package core.node import core.KeyManagementService import core.ThreadBox +import core.crypto.generateKeyPair import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey import java.util.* @@ -39,7 +39,7 @@ class E2ETestKeyManagementService : KeyManagementService { override val keys: Map get() = mutex.locked { HashMap(keys) } override fun freshKey(): KeyPair { - val keypair = KeyPairGenerator.getInstance("EC").genKeyPair() + val keypair = generateKeyPair() mutex.locked { keys[keypair.public] = keypair.private } diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt index bc7f0ddaed..89e3d885f6 100644 --- a/src/main/kotlin/core/node/Node.kt +++ b/src/main/kotlin/core/node/Node.kt @@ -10,6 +10,7 @@ package core.node import com.google.common.net.HostAndPort import core.* +import core.crypto.generateKeyPair import core.messaging.* import core.serialization.deserialize import core.serialization.serialize @@ -21,7 +22,6 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardOpenOption import java.security.KeyPair -import java.security.KeyPairGenerator import java.util.* import java.util.concurrent.Executors @@ -127,7 +127,7 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon val (identity, keypair) = if (!Files.exists(privKeyFile)) { log.info("Identity key not found, generating fresh key!") - val keypair: KeyPair = KeyPairGenerator.getInstance("EC").genKeyPair() + val keypair: KeyPair = generateKeyPair() keypair.serialize().writeToFile(privKeyFile) val myIdentity = Party(configuration.myLegalName, keypair.public) // We include the Party class with the file here to help catch mixups when admins provide files of the diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 61a47be4d1..576cf6df1b 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -13,6 +13,7 @@ import contracts.CommercialPaper import contracts.protocols.TwoPartyTradeProtocol import core.* import core.crypto.SecureHash +import core.crypto.generateKeyPair import core.messaging.LegallyIdentifiableNode import core.messaging.SingleMessageRecipient import core.messaging.runOnNextMessage @@ -24,7 +25,6 @@ import joptsimple.OptionParser import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.security.KeyPairGenerator import java.security.PublicKey import java.time.Instant import java.util.* @@ -176,7 +176,7 @@ fun main(args: Array) { fun makeFakeCommercialPaper(ownedBy: PublicKey): StateAndRef { // Make a fake company that's issued its own paper. - val party = Party("MegaCorp, Inc", KeyPairGenerator.getInstance("EC").genKeyPair().public) + val party = Party("MegaCorp, Inc", generateKeyPair().public) // ownedBy here is the random key that gives us control over it. val paper = CommercialPaper.State(party.ref(1,2,3), ownedBy, 1100.DOLLARS, Instant.now() + 10.days) val randomRef = StateRef(SecureHash.randomSHA256(), 0) diff --git a/src/main/kotlin/core/serialization/Kryo.kt b/src/main/kotlin/core/serialization/Kryo.kt index 45c2f4f91e..0cbb28054d 100644 --- a/src/main/kotlin/core/serialization/Kryo.kt +++ b/src/main/kotlin/core/serialization/Kryo.kt @@ -16,8 +16,9 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.JavaSerializer -import core.crypto.SecureHash import core.SignedWireTransaction +import core.crypto.SecureHash +import core.crypto.generateKeyPair import core.crypto.sha256 import de.javakaffee.kryoserializers.ArraysAsListSerializer import org.objenesis.strategy.StdInstantiatorStrategy @@ -25,7 +26,6 @@ import java.io.ByteArrayOutputStream import java.lang.reflect.InvocationTargetException import java.nio.file.Files import java.nio.file.Path -import java.security.KeyPairGenerator import java.time.Instant import java.util.* import kotlin.reflect.* @@ -198,7 +198,7 @@ fun createKryo(k: Kryo = Kryo()): Kryo { // Some things where the JRE provides an efficient custom serialisation. val ser = JavaSerializer() - val keyPair = KeyPairGenerator.getInstance("EC").genKeyPair() + val keyPair = generateKeyPair() register(keyPair.public.javaClass, ser) register(keyPair.private.javaClass, ser) register(Instant::class.java, ser) diff --git a/src/test/kotlin/core/MockServices.kt b/src/test/kotlin/core/MockServices.kt index 6a61ab7178..a65b6e104f 100644 --- a/src/test/kotlin/core/MockServices.kt +++ b/src/test/kotlin/core/MockServices.kt @@ -9,6 +9,7 @@ package core import core.crypto.DigitalSignature +import core.crypto.generateKeyPair import core.crypto.signWithECDSA import core.messaging.MessagingService import core.messaging.MockNetworkMap @@ -19,7 +20,6 @@ import core.serialization.deserialize import core.testutils.TEST_KEYS_TO_CORP_MAP import core.testutils.TEST_TX_TIME import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey import java.time.Clock @@ -53,7 +53,7 @@ object MockIdentityService : IdentityService { class MockKeyManagementService( override val keys: Map, - val nextKeys: MutableList = arrayListOf(KeyPairGenerator.getInstance("EC").genKeyPair()) + val nextKeys: MutableList = arrayListOf(generateKeyPair()) ) : KeyManagementService { override fun freshKey() = nextKeys.removeAt(nextKeys.lastIndex) } @@ -64,7 +64,7 @@ class MockWalletService(val states: List>) : WalletSer @ThreadSafe class MockStorageService : StorageService { - override val myLegalIdentityKey: KeyPair = KeyPairGenerator.getInstance("EC").genKeyPair() + override val myLegalIdentityKey: KeyPair = generateKeyPair() override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public) private val tables = HashMap>() diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index f9c97148a8..5156e422a3 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -15,8 +15,8 @@ import core.* import core.crypto.DummyPublicKey import core.crypto.NullPublicKey import core.crypto.SecureHash +import core.crypto.generateKeyPair import core.visualiser.GraphVisualiser -import java.security.KeyPairGenerator import java.security.PublicKey import java.time.Instant import java.util.* @@ -25,8 +25,8 @@ import kotlin.test.assertFailsWith import kotlin.test.fail object TestUtils { - val keypair = KeyPairGenerator.getInstance("EC").genKeyPair() - val keypair2 = KeyPairGenerator.getInstance("EC").genKeyPair() + val keypair = generateKeyPair() + val keypair2 = generateKeyPair() } // A few dummy values for testing. @@ -36,9 +36,9 @@ val MINI_CORP_KEY = TestUtils.keypair2 val MINI_CORP_PUBKEY = MINI_CORP_KEY.public val DUMMY_PUBKEY_1 = DummyPublicKey("x1") val DUMMY_PUBKEY_2 = DummyPublicKey("x2") -val ALICE_KEY = KeyPairGenerator.getInstance("EC").genKeyPair() +val ALICE_KEY = generateKeyPair() val ALICE = ALICE_KEY.public -val BOB_KEY = KeyPairGenerator.getInstance("EC").genKeyPair() +val BOB_KEY = generateKeyPair() val BOB = BOB_KEY.public val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY) val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY) From 55646104b4544a4927e4293b9f3ce19020e523e0 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 5 Feb 2016 18:14:25 +0100 Subject: [PATCH 10/19] Minor: tweak StateMachineManager to work around a cosmetic issue in Quasar, where it prints exception details to stderr twice. We'd rather log it ourselves. --- .../kotlin/core/messaging/StateMachines.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/core/messaging/StateMachines.kt b/src/main/kotlin/core/messaging/StateMachines.kt index 853a3e5937..a3baf5ecc4 100644 --- a/src/main/kotlin/core/messaging/StateMachines.kt +++ b/src/main/kotlin/core/messaging/StateMachines.kt @@ -17,13 +17,13 @@ import com.esotericsoftware.kryo.io.Output import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture -import core.crypto.SecureHash import core.ServiceHub +import core.crypto.SecureHash +import core.crypto.sha256 import core.serialization.THREAD_LOCAL_KRYO import core.serialization.createKryo import core.serialization.deserialize import core.serialization.serialize -import core.crypto.sha256 import core.utilities.trace import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -166,19 +166,12 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) psm.prepareForResumeWith(serviceHub, obj, logger, onSuspend) - try { - // Now either start or carry on with the protocol from where it left off (or at the start). - resumeFunc(psm) + resumeFunc(psm) - // We're back! Check if the fiber is finished and if so, clean up. - if (psm.isTerminated) { - _stateMachines.remove(psm) - checkpointsMap.remove(prevCheckpointKey) - } - } catch (t: Throwable) { - // TODO: Quasar is logging exceptions by itself too, find out where and stop it. - logger.error("Caught error whilst invoking protocol state machine", t) - throw t + // We're back! Check if the fiber is finished and if so, clean up. + if (psm.isTerminated) { + _stateMachines.remove(psm) + checkpointsMap.remove(prevCheckpointKey) } } @@ -234,6 +227,12 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch @Transient protected lateinit var logger: Logger @Transient private var _resultFuture: SettableFuture? = SettableFuture.create() + init { + setDefaultUncaughtExceptionHandler { strand, throwable -> + logger.error("Caught error whilst running protocol state machine ${this.javaClass.name}", throwable) + } + } + /** This future will complete when the call method returns. */ val resultFuture: ListenableFuture get() { return _resultFuture ?: run { From e426e784b44d7c1d685ea52d9331342d3b817986 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 8 Feb 2016 20:13:32 +0100 Subject: [PATCH 11/19] Minor: Use exitProcess rather than System.exit, as the former interacts better with flow typing. --- src/main/kotlin/core/node/TraderDemo.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 576cf6df1b..5d681912cd 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -28,6 +28,7 @@ import java.nio.file.Paths import java.security.PublicKey import java.time.Instant import java.util.* +import kotlin.system.exitProcess // TRADING DEMO // @@ -74,8 +75,7 @@ fun main(args: Array) { } catch (e: Exception) { println(e.message) printHelp() - System.exit(1) - throw Exception() // TODO: Remove when upgrading to Kotlin 1.0 RC + exitProcess(1) } BriefLogFormatter.initVerbose("platform.trade") @@ -141,7 +141,7 @@ fun main(args: Array) { // Grab a session ID for the fake trade from the other side, then kick off the seller and sell them some junk. if (!options.has(fakeTradeWithArg)) { println("Need the --fake-trade-with command line argument") - System.exit(1) + exitProcess(1) } val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT) val otherSide = ArtemisMessagingService.makeRecipient(peerAddr) @@ -188,7 +188,7 @@ private fun loadConfigFile(configFile: Path): NodeConfiguration { println() println("This is the first run, so you should edit the config file in $configFile and then start the node again.") println() - System.exit(1) + exitProcess(1) } val defaultLegalName = "Global MegaCorp, Ltd." From 52172c5f958a6be3eb0e573758252b210bebdc2e Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 8 Feb 2016 20:32:25 +0100 Subject: [PATCH 12/19] Minor: in TraderDemo comments point to docsite instead of duplicating --- src/main/kotlin/core/node/TraderDemo.kt | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 5d681912cd..01c84d6fcd 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -32,24 +32,7 @@ import kotlin.system.exitProcess // TRADING DEMO // -// This demo app can be run in one of two modes. In the listening mode it will buy commercial paper from a selling node -// that connects to it, using IOU cash it issued to itself. It also runs a timestamping service in this mode. In the -// non-listening mode, it will connect to the specified listening node and sell some commercial paper in return for -// cash. There's currently no UI so all you can see is log messages. -// -// Please note that the software currently assumes every node has a unique DNS name. Thus you cannot name both nodes -// "localhost". This might get fixed in future, but for now to run the listening node, alias "alpha" to "localhost" -// in your /etc/hosts file and then try a command line like this: -// -// --dir=alpha --service-fake-trades --network-address=alpha -// -// To run the node that initiates a trade, alias "beta" to "localhost" in your /etc/hosts file and then try a command -// line like this: -// -// --dir=beta --fake-trade-with=alpha --network-address=beta:31338 -// --timestamper-identity-file=alpha/identity-public --timestamper-address=alpha -// -// Alternatively, +// Please see docs/build/html/running-the-trading-demo.html fun main(args: Array) { From bd8d05646698eaa16546f44af460b5728c37dc25 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 8 Feb 2016 20:34:31 +0100 Subject: [PATCH 13/19] Minor: remove duplicated copyright header from CashTests.kt --- src/test/kotlin/contracts/CashTests.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index f60285b6ca..ea503c53b7 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -6,10 +6,6 @@ * All other rights reserved. */ -/* - * Copyright 2015, R3 CEV. All rights reserved. - */ - import contracts.Cash import contracts.DummyContract import contracts.InsufficientBalanceException From 28daae5bd4f2860386e16c2a42f5cb27e79681cb Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 9 Feb 2016 12:02:50 +0100 Subject: [PATCH 14/19] Minor: in CashTests make editIssuedBy into an infix fun with a space in the name, like `owned by`. Makes the code read a bit more like English. --- src/test/kotlin/contracts/CashTests.kt | 15 +++++++-------- src/test/kotlin/core/testutils/TestUtils.kt | 6 ++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index ea503c53b7..d238bccc54 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -28,7 +28,6 @@ class CashTests { ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) - fun Cash.State.editInstitution(party: Party) = copy(deposit = deposit.copy(party = party)) fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref))) @Test @@ -53,7 +52,7 @@ class CashTests { } tweak { output { outState } - output { outState.editInstitution(MINI_CORP) } + output { outState `issued by` MINI_CORP } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "at least one cash input" } @@ -153,7 +152,7 @@ class CashTests { // Can't change issuer. transaction { input { inState } - output { outState.editInstitution(MINI_CORP) } + output { outState `issued by` MINI_CORP } this `fails requirement` "at issuer MegaCorp the amounts balance" } // Can't change deposit reference when splitting. @@ -184,7 +183,7 @@ class CashTests { // Can't have superfluous input states from different issuers. transaction { input { inState } - input { inState.editInstitution(MINI_CORP) } + input { inState `issued by` MINI_CORP } output { outState } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "at issuer MiniCorp the amounts balance" @@ -224,9 +223,9 @@ class CashTests { // Multi-issuer case. transaction { input { inState } - input { inState.editInstitution(MINI_CORP) } + input { inState `issued by` MINI_CORP } - output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) } + output { inState.copy(amount = inState.amount - 200.DOLLARS) `issued by` MINI_CORP } output { inState.copy(amount = inState.amount - 200.DOLLARS) } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } @@ -246,7 +245,7 @@ class CashTests { transaction { // Gather 2000 dollars from two different issuers. input { inState } - input { inState.editInstitution(MINI_CORP) } + input { inState `issued by` MINI_CORP } // Can't merge them together. tweak { @@ -262,7 +261,7 @@ class CashTests { // This works. output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) } + output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this.accepts() } diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index 5156e422a3..adc53c694c 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -82,7 +82,9 @@ val TEST_PROGRAM_MAP: Map = mapOf( // // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block -infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) +infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner) +infix fun Cash.State.`issued by`(party: Party) = copy(deposit = deposit.copy(party = party)) +infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) // Allows you to write 100.DOLLARS.CASH @@ -320,4 +322,4 @@ class TransactionGroupDSL(private val stateType: Class) { } inline fun transactionGroupFor(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(T::class.java).apply { this.body() } -fun transactionGroup(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() } \ No newline at end of file +fun transactionGroup(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() } From d98a3871dae8ad03b1c34f80c5aa1806b2225dc6 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 9 Feb 2016 15:11:17 +0100 Subject: [PATCH 15/19] Refactor the 2-party trading protocol - Fix a security bug/TODO by having seller send back the signatures rather than a full blown transaction (which would allow a malicious seller to try and confuse the buyer by sending back a completely different TX to the one he proposed) - Introduce an UntrustworthyData wrapper as an (inefficient) form of taint tracking, to make it harder to forget that data has come from an untrustworthy source and may be malicious. - Split the giant {Buyer, Seller}.call() methods into a set of smaller methods that make it easier to unit test various kinds of failure/skip bits in tests that aren't needed. --- docs/source/protocol-state-machines.rst | 248 +++++++++++------- .../protocols/TwoPartyTradeProtocol.kt | 232 +++++++++------- src/main/kotlin/core/Transactions.kt | 5 +- .../kotlin/core/messaging/StateMachines.kt | 34 ++- .../core/node/TimestamperNodeService.kt | 7 +- .../messaging/TwoPartyTradeProtocolTests.kt | 24 +- 6 files changed, 337 insertions(+), 213 deletions(-) diff --git a/docs/source/protocol-state-machines.rst b/docs/source/protocol-state-machines.rst index 57c54148aa..be3cf0c2ef 100644 --- a/docs/source/protocol-state-machines.rst +++ b/docs/source/protocol-state-machines.rst @@ -125,6 +125,16 @@ each side. return buyer.resultFuture } + // This object is serialised to the network and is the first protocol message the seller sends to the buyer. + class SellerTradeInfo( + val assetForSale: StateAndRef, + val price: Amount, + val sellerOwnerKey: PublicKey, + val sessionID: Long + ) + + class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey) + class Seller(val otherSide: SingleMessageRecipient, val timestampingAuthority: LegallyIdentifiableNode, val assetToSell: StateAndRef, @@ -137,21 +147,11 @@ each side. } } - // This object is serialised to the network and is the first protocol message the seller sends to the buyer. - private class SellerTradeInfo( - val assetForSale: StateAndRef, - val price: Amount, - val sellerOwnerKey: PublicKey, - val sessionID: Long - ) - - class UnacceptablePriceException(val givenPrice: Amount) : Exception() class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } - // The buyer's side of the protocol. See note above Seller to learn about the caveats here. class Buyer(val otherSide: SingleMessageRecipient, val timestampingAuthority: Party, val acceptablePrice: Amount, @@ -167,7 +167,7 @@ each side. Let's unpack what this code does: - It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` singleton, and a couple of methods, one - to run the buyer side of the protocol and one to run the seller side. + to run the buyer side of the protocol and one to run the seller side. Some of the classes are simply protocol messages. - It defines the "trade topic", which is just a string that namespaces this protocol. The prefix "platform." is reserved by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation. - The ``runBuyer`` and ``runSeller`` methods take a number of parameters that specialise the protocol for this run, @@ -205,7 +205,8 @@ to either runBuyer or runSeller, depending on who we are, and then call ``.get() block the calling thread until the protocol has finished. Or we could register a callback on the returned future that will be invoked when it's done, where we could e.g. update a user interface. -Finally, we define a couple of exceptions, and a class that will be used as a protocol message called ``SellerTradeInfo``. +Finally, we define a couple of exceptions, and two classes that will be used as a protocol message called +``SellerTradeInfo`` and ``SignaturesFromSeller``. Suspendable methods ------------------- @@ -244,13 +245,57 @@ Let's implement the ``Seller.call`` method. This will be invoked by the platform .. sourcecode:: kotlin - val sessionID = random63BitValue() + val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction() - // Make the first message we'll send to kick off the protocol. - val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) + // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. + val ourSignature = signWithOurKey(partialTX) + val tsaSig = timestamp(partialTX) - val partialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) - logger().trace { "Received partially signed transaction" } + val ledgerTX = sendSignatures(partialTX, ourSignature, tsaSig) + + return Pair(partialTX.tx, ledgerTX) + +Here we see the outline of the procedure. We receive a proposed trade transaction from the buyer and check that it's +valid. Then we sign with our own key, request a timestamping authority to assert with another signature that the +timestamp in the transaction (if any) is valid, and finally we send back both our signature and the TSA's signature. +Finally, we hand back to the code that invoked the protocol the finished transaction in a couple of different forms. + +Let's fill out the ``receiveAndCheckProposedTransaction()`` method. + +.. container:: codeset + + .. sourcecode:: kotlin + + @Suspendable + open fun receiveAndCheckProposedTransaction(): SignedWireTransaction { + val sessionID = random63BitValue() + + // Make the first message we'll send to kick off the protocol. + val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) + + val maybePartialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) + val partialTX = maybePartialTX.validate { + it.verifySignatures() + logger.trace { "Received partially signed transaction" } + val wtx: WireTransaction = it.tx + + requireThat { + "transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price) + // There are all sorts of funny games a malicious secondary might play here, we should fix them: + // + // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if + // we're reusing keys! So don't reuse keys! + // - This tx may not be valid according to the contracts of the input states, so we must resolve + // and fully audit the transaction chains to convince ourselves that it is actually valid. + // - This tx may include output states that impose odd conditions on the movement of the cash, + // once we implement state pairing. + // + // but the goal of this code is not to be fully secure, but rather, just to find good ways to + // express protocol state machines on top of the messaging layer. + } + } + return partialTX + } That's pretty straightforward. We generate a session ID to identify what's happening on the seller side, fill out the initial protocol message, and then call ``sendAndReceive``. This function takes a few arguments: @@ -260,6 +305,11 @@ the initial protocol message, and then call ``sendAndReceive``. This function ta - The thing to send. It'll be serialised and sent automatically. - Finally a type argument, which is the kind of object we're expecting to receive from the other side. +It returns a simple wrapper class, ``UntrustworthyData``, which is just a marker class that reminds +us that the data came from a potentially malicious external source and may have been tampered with or be unexpected in +other ways. It doesn't add any functionality, but acts as a reminder to "scrub" the data before use. Here, our scrubbing +simply involves checking the signatures on it. Then we could go ahead and do some more involved checks. + Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we'll do a log message. The buyer is supposed to send us a transaction with all the right inputs/outputs/commands in return, with their cash put into the transaction and their signature on it authorising the movement of the cash. @@ -273,51 +323,45 @@ cash put into the transaction and their signature on it authorising the movement doing things like creating threads from inside these calls would be a bad idea. They should only contain business logic. -OK, let's keep going: +Here's the rest of the code: .. container:: codeset .. sourcecode:: kotlin - partialTX.verifySignatures() - val wtx = partialTX.txBits.deserialize() + open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits) - requireThat { - "transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price) - // There are all sorts of funny games a malicious secondary might play here, we should fix them: - // - // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if - // we're reusing keys! So don't reuse keys! - // - This tx may not be valid according to the contracts of the input states, so we must resolve - // and fully audit the transaction chains to convince ourselves that it is actually valid. - // - This tx may include output states that impose odd conditions on the movement of the cash, - // once we implement state pairing. + @Suspendable + open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable { + return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits) } - val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits) - val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature) - fullySigned.verify() - val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService) - logger().trace { "Built finished transaction, sending back to secondary!" } + @Suspendable + open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey, + tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction { + val fullySigned = partialTX + tsaSig + ourSignature + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) - send(TRADE_TOPIC, sessionID, timestamped) + // TODO: We should run it through our full TransactionGroup of all transactions here. - return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)) + logger.trace { "Built finished transaction, sending back to secondary!" } -Here, we see some assertions and signature checking to satisfy ourselves that we're not about to sign something -incorrect. Once we're happy, we calculate a signature over the transaction to authorise the movement of the asset -we are selling, and then we verify things to make sure it's all OK. Finally, we request timestamping of the -transaction, in case the contracts governing the asset we're selling require it, and send the now finalised and -validated transaction back to the buyer. + send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature)) + return ltx + } + +It's should be all pretty straightforward: here, ``txBits`` is the raw byte array representing the transaction. + +In ``sendSignatures``, we take the two signatures we calculated, then add them to the partial transaction we were sent +and verify that the signatures all make sense. This should never fail: it's just a sanity check. Finally, we wrap the +two signatures in a simple wrapper message class and send it back. The send won't block waiting for an acknowledgement, +but the underlying message queue software will retry delivery if the other side has gone away temporarily. .. warning:: This code is **not secure**. Other than not checking for all possible invalid constructions, if the seller stops before sending the finalised transaction to the buyer, the seller is left with a valid transaction but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing will be fixed in a future version of the code. -Finally, the call function returns with the result of the protocol: in our case, the final transaction in two different -forms. - Implementing the buyer ---------------------- @@ -328,41 +372,54 @@ OK, let's do the same for the buyer side: .. sourcecode:: kotlin @Suspendable - override fun call(): Pair { + override fun call(): Pair { + val tradeRequest = receiveAndValidateTradeRequest() + val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest) + val stx = signWithOurKeys(cashSigningPubKeys, ptx) + val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID) + + logger.trace { "Got signatures from seller, verifying ... "} + val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) + + logger.trace { "Fully signed transaction was valid. Trade complete! :-)" } + return Pair(fullySigned.tx, ltx) + } + + @Suspendable + open fun receiveAndValidateTradeRequest(): SellerTradeInfo { // Wait for a trade request to come in on our pre-provided session ID. - val tradeRequest = receive(TRADE_TOPIC, args.sessionID, SellerTradeInfo::class.java) + val maybeTradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java) - // What is the seller trying to sell us? - val assetTypeName = tradeRequest.assetForSale.state.javaClass.name - logger().trace { "Got trade request for a $assetTypeName" } + val tradeRequest = maybeTradeRequest.validate { + // What is the seller trying to sell us? + val assetTypeName = it.assetForSale.state.javaClass.name + logger.trace { "Got trade request for a $assetTypeName" } - // Check the start message for acceptability. - check(tradeRequest.sessionID > 0) - if (tradeRequest.price > acceptablePrice) - throw UnacceptablePriceException(tradeRequest.price) - if (!typeToBuy.isInstance(tradeRequest.assetForSale.state)) - throw AssetMismatchException(typeToBuy.name, assetTypeName) + // Check the start message for acceptability. + check(it.sessionID > 0) + if (it.price > acceptablePrice) + throw UnacceptablePriceException(it.price) + if (!typeToBuy.isInstance(it.assetForSale.state)) + throw AssetMismatchException(typeToBuy.name, assetTypeName) + } - // TODO: Either look up the stateref here in our local db, or accept a long chain - // of states and validate them to audit the other side and ensure it actually owns - // the state we are being offered! For now, just assume validity! + // TODO: Either look up the stateref here in our local db, or accept a long chain of states and + // validate them to audit the other side and ensure it actually owns the state we are being offered! + // For now, just assume validity! + return tradeRequest + } - // Generate the shared transaction that both sides will sign, using the data we have. - val ptx = TransactionBuilder() - // 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().craftSpend(ptx, tradeRequest.price, - tradeRequest.sellerOwnerKey, cashStates) - // Add inputs/outputs/a command for the movement of the asset. - ptx.addInputState(tradeRequest.assetForSale.ref) - // Just pick some new public key for now. - val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public) - ptx.addOutputState(state) - ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner)) + @Suspendable + open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller { + logger.trace { "Sending partially signed transaction to seller" } + // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. + + return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {} + } + + open fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedWireTransaction { // Now sign the transaction with whatever keys we need to move the cash. for (k in cashSigningPubKeys) { val priv = serviceHub.keyManagementService.toPrivate(k) @@ -374,36 +431,43 @@ OK, let's do the same for the buyer side: // TODO: Could run verify() here to make sure the only signature missing is the sellers. - logger().trace { "Sending partially signed transaction to seller" } + return stx + } - // TODO: Protect against the buyer terminating here and leaving us in the lurch without - // the final tx. - // TODO: Protect against a malicious buyer sending us back a different transaction to - // the one we built. - val fullySigned = sendAndReceive(TRADE_TOPIC, tradeRequest.sessionID, sessionID, stx, - TimestampedWireTransaction::class.java) + open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { + val ptx = TransactionBuilder() + // 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) + // 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) - logger().trace { "Got fully signed transaction, verifying ... "} - - val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, - serviceHub.identityService) - - logger().trace { "Fully signed transaction was valid. Trade complete! :-)" } - - return Pair(fullySigned, ltx) + // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt + // to have one. + ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds) + return Pair(ptx, cashSigningPubKeys) } This code is longer but still fairly straightforward. Here are some things to pay attention to: 1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered. -2. We create a cash spend in the normal way, by using ``Cash().craftSpend``. See the contracts tutorial if this isn't +2. We create a cash spend in the normal way, by using ``Cash().generateSpend``. See the contracts tutorial if this isn't clear. 3. We access the *service hub* when we need it to access things that are transient and may change or be recreated whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may be suspended when it waits to receive a message across node or computer restarts, so objects representing a service or data which may frequently change should be accessed 'just in time'. -4. Finally, we send the unfinsished, invalid transaction to the seller so they can sign it. They are expected to send - back to us a ``TimestampedWireTransaction``, which once we verify it, should be the final outcome of the trade. +4. Finally, we send the unfinished, invalid transaction to the seller so they can sign it. They are expected to send + back to us a ``SignaturesFromSeller``, which once we verify it, should be the final outcome of the trade. As you can see, the protocol logic is straightforward and does not contain any callbacks or network glue code, despite the fact that it takes minimal resources and can survive node restarts. diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index cac04e6f23..9843d9b97b 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -13,13 +13,13 @@ import com.google.common.util.concurrent.ListenableFuture import contracts.Cash import contracts.sumCashBy import core.* +import core.crypto.DigitalSignature import core.crypto.signWithECDSA import core.messaging.LegallyIdentifiableNode import core.messaging.ProtocolStateMachine import core.messaging.SingleMessageRecipient import core.messaging.StateMachineManager import core.node.TimestamperClient -import core.serialization.deserialize import core.utilities.trace import java.security.KeyPair import java.security.PublicKey @@ -67,98 +67,162 @@ object TwoPartyTradeProtocol { return buyer.resultFuture } - class Seller(val otherSide: SingleMessageRecipient, - val timestampingAuthority: LegallyIdentifiableNode, - val assetToSell: StateAndRef, - val price: Amount, - val myKeyPair: KeyPair, - val buyerSessionID: Long) : ProtocolStateMachine>() { - @Suspendable - override fun call(): Pair { - val sessionID = random63BitValue() - - // Make the first message we'll send to kick off the protocol. - val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) - - val partialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) - logger.trace { "Received partially signed transaction" } - - partialTX.verifySignatures() - val wtx: WireTransaction = partialTX.txBits.deserialize() - - requireThat { - "transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price) - // There are all sorts of funny games a malicious secondary might play here, we should fix them: - // - // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if - // we're reusing keys! So don't reuse keys! - // - This tx may not be valid according to the contracts of the input states, so we must resolve - // and fully audit the transaction chains to convince ourselves that it is actually valid. - // - This tx may include output states that impose odd conditions on the movement of the cash, - // once we implement state pairing. - // - // but the goal of this code is not to be fully secure, but rather, just to find good ways to - // express protocol state machines on top of the messaging layer. - } - - // Sign with our key and get the timestamping authorities key as well. - // These two steps could be done in parallel, in theory. - val ourSignature = myKeyPair.signWithECDSA(partialTX.txBits) - val tsaSig = TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits) - val fullySigned = partialTX.withAdditionalSignature(tsaSig).withAdditionalSignature(ourSignature) - val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) - - // We should run it through our full TransactionGroup of all transactions here. - - logger.trace { "Built finished transaction, sending back to secondary!" } - - send(TRADE_TOPIC, otherSide, buyerSessionID, fullySigned) - - return Pair(wtx, ltx) - } - } - // This object is serialised to the network and is the first protocol message the seller sends to the buyer. - private class SellerTradeInfo( + class SellerTradeInfo( val assetForSale: StateAndRef, val price: Amount, val sellerOwnerKey: PublicKey, val sessionID: Long ) + class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey) + + open class Seller(val otherSide: SingleMessageRecipient, + val timestampingAuthority: LegallyIdentifiableNode, + val assetToSell: StateAndRef, + val price: Amount, + val myKeyPair: KeyPair, + val buyerSessionID: Long) : ProtocolStateMachine>() { + @Suspendable + override fun call(): Pair { + val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction() + + // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. + val ourSignature = signWithOurKey(partialTX) + val tsaSig = timestamp(partialTX) + + val ledgerTX = sendSignatures(partialTX, ourSignature, tsaSig) + + return Pair(partialTX.tx, ledgerTX) + } + + @Suspendable + open fun receiveAndCheckProposedTransaction(): SignedWireTransaction { + val sessionID = random63BitValue() + + // Make the first message we'll send to kick off the protocol. + val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) + + val maybePartialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) + val partialTX = maybePartialTX.validate { + it.verifySignatures() + logger.trace { "Received partially signed transaction" } + val wtx: WireTransaction = it.tx + + requireThat { + "transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price) + // There are all sorts of funny games a malicious secondary might play here, we should fix them: + // + // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if + // we're reusing keys! So don't reuse keys! + // - This tx may not be valid according to the contracts of the input states, so we must resolve + // and fully audit the transaction chains to convince ourselves that it is actually valid. + // - This tx may include output states that impose odd conditions on the movement of the cash, + // once we implement state pairing. + // + // but the goal of this code is not to be fully secure, but rather, just to find good ways to + // express protocol state machines on top of the messaging layer. + } + } + return partialTX + } + + open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits) + + @Suspendable + open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable { + return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits) + } + + @Suspendable + open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey, + tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction { + val fullySigned = partialTX + tsaSig + ourSignature + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) + + // TODO: We should run it through our full TransactionGroup of all transactions here. + + logger.trace { "Built finished transaction, sending back to secondary!" } + + send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature)) + return ltx + } + } class UnacceptablePriceException(val givenPrice: Amount) : Exception() class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } - // The buyer's side of the protocol. See note above Seller to learn about the caveats here. - class Buyer(val otherSide: SingleMessageRecipient, - val timestampingAuthority: Party, - val acceptablePrice: Amount, - val typeToBuy: Class, - val sessionID: Long) : ProtocolStateMachine>() { + open class Buyer(val otherSide: SingleMessageRecipient, + val timestampingAuthority: Party, + val acceptablePrice: Amount, + val typeToBuy: Class, + val sessionID: Long) : ProtocolStateMachine>() { @Suspendable override fun call(): Pair { + val tradeRequest = receiveAndValidateTradeRequest() + val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest) + val stx = signWithOurKeys(cashSigningPubKeys, ptx) + val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID) + + logger.trace { "Got signatures from seller, verifying ... "} + val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) + + logger.trace { "Fully signed transaction was valid. Trade complete! :-)" } + return Pair(fullySigned.tx, ltx) + } + + @Suspendable + open fun receiveAndValidateTradeRequest(): SellerTradeInfo { // Wait for a trade request to come in on our pre-provided session ID. - val tradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java) + val maybeTradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java) - // What is the seller trying to sell us? - val assetTypeName = tradeRequest.assetForSale.state.javaClass.name - logger.trace { "Got trade request for a $assetTypeName" } + val tradeRequest = maybeTradeRequest.validate { + // What is the seller trying to sell us? + val assetTypeName = it.assetForSale.state.javaClass.name + logger.trace { "Got trade request for a $assetTypeName" } - // Check the start message for acceptability. - check(tradeRequest.sessionID > 0) - if (tradeRequest.price > acceptablePrice) - throw UnacceptablePriceException(tradeRequest.price) - if (!typeToBuy.isInstance(tradeRequest.assetForSale.state)) - throw AssetMismatchException(typeToBuy.name, assetTypeName) + // Check the start message for acceptability. + check(it.sessionID > 0) + if (it.price > acceptablePrice) + throw UnacceptablePriceException(it.price) + if (!typeToBuy.isInstance(it.assetForSale.state)) + throw AssetMismatchException(typeToBuy.name, assetTypeName) + } // TODO: Either look up the stateref here in our local db, or accept a long chain of states and // validate them to audit the other side and ensure it actually owns the state we are being offered! // For now, just assume validity! + return tradeRequest + } - // Generate the shared transaction that both sides will sign, using the data we have. + @Suspendable + open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller { + logger.trace { "Sending partially signed transaction to seller" } + + // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. + + return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {} + } + + open fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedWireTransaction { + // Now sign the transaction with whatever keys we need to move the cash. + for (k in cashSigningPubKeys) { + val priv = serviceHub.keyManagementService.toPrivate(k) + ptx.signWith(KeyPair(k, priv)) + } + + val stx = ptx.toSignedTransaction(checkSufficientSignatures = false) + stx.verifySignatures() // Verifies that we generated a signed transaction correctly. + + // TODO: Could run verify() here to make sure the only signature missing is the sellers. + + return stx + } + + open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { val ptx = TransactionBuilder() // Add input and output states for the movement of cash, by using the Cash contract to generate the states. val wallet = serviceHub.walletService.currentWallet @@ -178,33 +242,7 @@ object TwoPartyTradeProtocol { // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds) - - // Now sign the transaction with whatever keys we need to move the cash. - for (k in cashSigningPubKeys) { - val priv = serviceHub.keyManagementService.toPrivate(k) - ptx.signWith(KeyPair(k, priv)) - } - - val stx = ptx.toSignedTransaction(checkSufficientSignatures = false) - stx.verifySignatures() // Verifies that we generated a signed transaction correctly. - - // TODO: Could run verify() here to make sure the only signature missing is the sellers. - - logger.trace { "Sending partially signed transaction to seller" } - - // TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx. - // TODO: Protect against a malicious buyer sending us back a different transaction to the one we built. - - val fullySigned = sendAndReceive(TRADE_TOPIC, otherSide, tradeRequest.sessionID, - sessionID, stx, SignedWireTransaction::class.java) - - logger.trace { "Got fully signed transaction, verifying ... "} - - val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) - - logger.trace { "Fully signed transaction was valid. Trade complete! :-)" } - - return Pair(fullySigned.tx, ltx) + return Pair(ptx, cashSigningPubKeys) } } } \ No newline at end of file diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index 9c1de7b931..4c4fc96170 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -79,7 +79,7 @@ data class WireTransaction(val inputs: List, data class SignedWireTransaction(val txBits: SerializedBytes, val sigs: List) { init { check(sigs.isNotEmpty()) } - // Lazily calculated access to the deserialised/hashed transaction data. + /** Lazily calculated access to the deserialised/hashed transaction data. */ val tx: WireTransaction by lazy { txBits.deserialize() } /** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */ @@ -124,6 +124,9 @@ data class SignedWireTransaction(val txBits: SerializedBytes, v /** Returns the same transaction but with an additional (unchecked) signature */ fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig) + + /** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */ + operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig) } /** A mutable transaction that's in the process of being built, before all signatures are present. */ diff --git a/src/main/kotlin/core/messaging/StateMachines.kt b/src/main/kotlin/core/messaging/StateMachines.kt index a3baf5ecc4..9046e5ffc7 100644 --- a/src/main/kotlin/core/messaging/StateMachines.kt +++ b/src/main/kotlin/core/messaging/StateMachines.kt @@ -118,8 +118,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) } /** - * Kicks off a brand new state machine of the given class. It will log with the named logger, and the - * [initialArgs] object will be passed to the call method of the [ProtocolStateMachine] object. + * Kicks off a brand new state machine of the given class. It will log with the named logger. * The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is * restarted with checkpointed state machines in the storage service. */ @@ -262,7 +261,7 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch } @Suspendable @Suppress("UNCHECKED_CAST") - private fun suspendAndExpectReceive(with: FiberRequest): T { + private fun suspendAndExpectReceive(with: FiberRequest): UntrustworthyData { Fiber.parkAndSerialize { fiber, serializer -> // We don't use the passed-in serializer here, because we need to use our own augmented Kryo. val deserializer = Fiber.getFiberSerializer() as KryoSerializer @@ -275,18 +274,18 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch } val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something") resumeWithObject = null - return tmp as T + return UntrustworthyData(tmp as T) } @Suspendable @Suppress("UNCHECKED_CAST") fun sendAndReceive(topic: String, destination: MessageRecipients, sessionIDForSend: Long, sessionIDForReceive: Long, - obj: Any, recvType: Class): T { + obj: Any, recvType: Class): UntrustworthyData { val result = FiberRequest.ExpectingResponse(topic, destination, sessionIDForSend, sessionIDForReceive, obj, recvType) return suspendAndExpectReceive(result) } @Suspendable - fun receive(topic: String, sessionIDForReceive: Long, recvType: Class): T { + fun receive(topic: String, sessionIDForReceive: Long, recvType: Class): UntrustworthyData { val result = FiberRequest.ExpectingResponse(topic, null, -1, sessionIDForReceive, null, recvType) return suspendAndExpectReceive(result) } @@ -298,6 +297,29 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch } } +/** + * A small utility to approximate taint tracking: if a method gives you back one of these, it means the data came from + * a remote source that may be incentivised to pass us junk that violates basic assumptions and thus must be checked + * first. The wrapper helps you to avoid forgetting this vital step. Things you might want to check are: + * + * - Is this object the one you actually expected? Did the other side hand you back something technically valid but + * not what you asked for? + * - Is the object disobeying its own invariants? + * - Are any objects *reachable* from this object mismatched or not what you expected? + * - Is it suspiciously large or small? + */ +class UntrustworthyData(private val fromUntrustedWorld: T) { + val data: T + @Deprecated("Accessing the untrustworthy data directly without validating it first is a bad idea") + get() = fromUntrustedWorld + + @Suppress("DEPRECATION") + inline fun validate(validator: (T) -> Unit): T { + validator(data) + return data + } +} + // TODO: Clean this up open class FiberRequest(val topic: String, val destination: MessageRecipients?, val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) { diff --git a/src/main/kotlin/core/node/TimestamperNodeService.kt b/src/main/kotlin/core/node/TimestamperNodeService.kt index d0e37b1c1a..433cc7a480 100644 --- a/src/main/kotlin/core/node/TimestamperNodeService.kt +++ b/src/main/kotlin/core/node/TimestamperNodeService.kt @@ -117,10 +117,13 @@ class TimestamperClient(private val psm: ProtocolStateMachine<*>, private val no val sessionID = random63BitValue() val replyTopic = "${TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC}.$sessionID" val req = TimestampingMessages.Request(wtxBytes, psm.serviceHub.networkService.myAddress, replyTopic) - val signature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0, + + val maybeSignature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0, sessionID, req, DigitalSignature.LegallyIdentifiable::class.java) + // Check that the timestamping authority gave us back a valid signature and didn't break somehow - signature.verifyWithECDSA(wtxBytes) + val signature = maybeSignature.validate { it.verifyWithECDSA(wtxBytes) } + return signature } } diff --git a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt index 26ba415f25..ad95e27654 100644 --- a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt +++ b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt @@ -14,14 +14,12 @@ import contracts.CommercialPaper import contracts.protocols.TwoPartyTradeProtocol import core.* import core.testutils.* +import core.utilities.BriefLogFormatter import org.junit.After import org.junit.Before import org.junit.Test +import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import java.util.logging.Formatter -import java.util.logging.Level -import java.util.logging.LogRecord -import java.util.logging.Logger import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -32,24 +30,21 @@ import kotlin.test.assertTrue * We assume that Alice and Bob already found each other via some market, and have agreed the details already. */ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() { + lateinit var backgroundThread: ExecutorService + @Before - fun initLogging() { - Logger.getLogger("").handlers[0].level = Level.ALL - Logger.getLogger("").handlers[0].formatter = object : Formatter() { - override fun format(record: LogRecord) = "${record.threadID} ${record.loggerName}: ${record.message}\n" - } - Logger.getLogger("com.r3cev.protocols.trade").level = Level.ALL + fun before() { + backgroundThread = Executors.newSingleThreadExecutor() + BriefLogFormatter.initVerbose("platform.trade") } @After - fun stopLogging() { - Logger.getLogger("com.r3cev.protocols.trade").level = Level.INFO + fun after() { + backgroundThread.shutdown() } @Test fun cashForCP() { - val backgroundThread = Executors.newSingleThreadExecutor() - transactionGroupFor { // Bob (Buyer) has some cash, Alice (Seller) has some commercial paper she wants to sell to Bob. roots { @@ -96,7 +91,6 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() { txns.add(aliceResult.get().second) verify() } - backgroundThread.shutdown() } @Test From 2ccbd5db3e0b906d8124b250f86d6f0e5167a9fc Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 9 Feb 2016 15:18:34 +0100 Subject: [PATCH 16/19] Minor: code cleanup on ArtemisMessagingService. - Use fixes in Kotlin 1.0 RC to clean up property access a bit (fewer set* calls) - Note that we currently won't notice if Artemis throws an exception during startup, as it happens async. There is a fix to Artemis pending. --- .../core/node/ArtemisMessagingService.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/core/node/ArtemisMessagingService.kt b/src/main/kotlin/core/node/ArtemisMessagingService.kt index 54c4cdaf91..1eb835eec5 100644 --- a/src/main/kotlin/core/node/ArtemisMessagingService.kt +++ b/src/main/kotlin/core/node/ArtemisMessagingService.kt @@ -111,11 +111,16 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) secConfig.addUser("internal", password) secConfig.addRole("internal", "internal") secConfig.defaultUser = "internal" - config.setSecurityRoles(mapOf( + config.securityRoles = mapOf( "#" to setOf(Role("internal", true, true, true, true, true, true, true)) - )) + ) val secManager = ActiveMQJAASSecurityManager(InVMLoginModule::class.java.name, secConfig) mq.setSecurityManager(secManager) + + // Currently we cannot find out if something goes wrong during startup :( This is bug ARTEMIS-388 filed by me. + // The fix should be in the 1.3.0 release: + // + // https://issues.apache.org/jira/browse/ARTEMIS-388 mq.start() // Connect to our in-memory server. @@ -242,19 +247,19 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) hostAndPort.hostText, hostAndPort.port)) mq.activeMQServer.deployBridge(BridgeConfiguration().apply { setName(name) - setQueueName(name) - setForwardingAddress(name) - setStaticConnectors(listOf(name)) - setConfirmationWindowSize(100000) // a guess + queueName = name + forwardingAddress = name + staticConnectors = listOf(name) + confirmationWindowSize = 100000 // a guess }) } } private fun setConfigDirectories(config: Configuration, dir: Path) { config.apply { - setBindingsDirectory(dir.resolve("bindings").toString()) - setJournalDirectory(dir.resolve("journal").toString()) - setLargeMessagesDirectory(dir.resolve("largemessages").toString()) + bindingsDirectory = dir.resolve("bindings").toString() + journalDirectory = dir.resolve("journal").toString() + largeMessagesDirectory = dir.resolve("largemessages").toString() } } @@ -262,11 +267,9 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) val config = ConfigurationImpl() setConfigDirectories(config, directory) // We will be talking to our server purely in memory. - config.setAcceptorConfigurations( - setOf( - tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port), - TransportConfiguration(InVMAcceptorFactory::class.java.name) - ) + config.acceptorConfigurations = setOf( + tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port), + TransportConfiguration(InVMAcceptorFactory::class.java.name) ) return config } From 31964f869514792d57dd3ad921e54a348024416c Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 9 Feb 2016 19:36:52 +0100 Subject: [PATCH 17/19] Introduce ContractFactory to replace Map. It allows for lazy loading of contracts. --- src/main/kotlin/contracts/DummyContract.kt | 4 ++-- src/main/kotlin/core/Structures.kt | 13 +++++++++++++ .../kotlin/core/TransactionVerification.kt | 6 +++--- .../kotlin/contracts/CommercialPaperTests.kt | 4 ++-- src/test/kotlin/contracts/CrowdFundTests.kt | 4 ++-- src/test/kotlin/core/MockServices.kt | 10 ++++++++++ src/test/kotlin/core/testutils/TestUtils.kt | 18 +++++++++--------- 7 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/contracts/DummyContract.kt b/src/main/kotlin/contracts/DummyContract.kt index 45c0705f47..52956d71c5 100644 --- a/src/main/kotlin/contracts/DummyContract.kt +++ b/src/main/kotlin/contracts/DummyContract.kt @@ -10,14 +10,14 @@ package contracts import core.Contract import core.ContractState -import core.crypto.SecureHash import core.TransactionForVerification +import core.crypto.SecureHash // The dummy contract doesn't do anything useful. It exists for testing purposes. val DUMMY_PROGRAM_ID = SecureHash.sha256("dummy") -object DummyContract : Contract { +class DummyContract : Contract { class State : ContractState { override val programRef: SecureHash = DUMMY_PROGRAM_ID } diff --git a/src/main/kotlin/core/Structures.kt b/src/main/kotlin/core/Structures.kt index 2195f0564c..498e4ea4ef 100644 --- a/src/main/kotlin/core/Structures.kt +++ b/src/main/kotlin/core/Structures.kt @@ -134,3 +134,16 @@ interface Contract { */ val legalContractReference: SecureHash } + +/** A contract factory knows how to lazily load and instantiate contract objects. */ +interface ContractFactory { + /** + * Loads, instantiates and returns a contract object from its class bytecodes, given the hash of that bytecode. + * + * @throws UnknownContractException if the hash doesn't map to any known contract. + * @throws ClassCastException if the hash mapped to a contract, but it was not of type T + */ + operator fun get(hash: SecureHash): T +} + +class UnknownContractException : Exception() \ No newline at end of file diff --git a/src/main/kotlin/core/TransactionVerification.kt b/src/main/kotlin/core/TransactionVerification.kt index 4ba13566d0..69e5f4079a 100644 --- a/src/main/kotlin/core/TransactionVerification.kt +++ b/src/main/kotlin/core/TransactionVerification.kt @@ -27,7 +27,7 @@ class TransactionGroup(val transactions: Set, val nonVerified /** * Verifies the group and returns the set of resolved transactions. */ - fun verify(programMap: Map): Set { + fun verify(programMap: ContractFactory): Set { // Check that every input can be resolved to an output. // Check that no output is referenced by more than one input. // Cycles should be impossible due to the use of hashes as pointers. @@ -73,12 +73,12 @@ data class TransactionForVerification(val inStates: List, * @throws IllegalStateException if a state refers to an unknown contract. */ @Throws(TransactionVerificationException::class, IllegalStateException::class) - fun verify(programMap: Map) { + fun verify(programMap: ContractFactory) { // For each input and output state, locate the program to run. Then execute the verification function. If any // throws an exception, the entire transaction is invalid. val programHashes = (inStates.map { it.programRef } + outStates.map { it.programRef }).toSet() for (hash in programHashes) { - val program = programMap[hash] ?: throw IllegalStateException("Unknown program hash $hash") + val program: Contract = programMap[hash] try { program.verify(this) } catch(e: Throwable) { diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt index e8b212553d..40b0df4fe7 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTests.kt @@ -216,11 +216,11 @@ class CommercialPaperTestsGeneric { val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) val e = assertFailsWith(TransactionVerificationException::class) { - TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(MockContractFactory) } assertTrue(e.cause!!.message!!.contains("paper must have matured")) - TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(MockContractFactory) } // Generate a trade lifecycle with various parameters. diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt index bf8a4528ef..d8292537b6 100644 --- a/src/test/kotlin/contracts/CrowdFundTests.kt +++ b/src/test/kotlin/contracts/CrowdFundTests.kt @@ -157,11 +157,11 @@ class CrowdFundTests { val validClose = makeFundedTX(TEST_TX_TIME + 8.days) val e = assertFailsWith(TransactionVerificationException::class) { - TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify(MockContractFactory) } assertTrue(e.cause!!.message!!.contains("the closing date has past")) // This verification passes - TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(MockContractFactory) } } \ No newline at end of file diff --git a/src/test/kotlin/core/MockServices.kt b/src/test/kotlin/core/MockServices.kt index a65b6e104f..18854cd2dd 100644 --- a/src/test/kotlin/core/MockServices.kt +++ b/src/test/kotlin/core/MockServices.kt @@ -9,6 +9,7 @@ package core import core.crypto.DigitalSignature +import core.crypto.SecureHash import core.crypto.generateKeyPair import core.crypto.signWithECDSA import core.messaging.MessagingService @@ -18,6 +19,7 @@ import core.node.TimestampingError import core.serialization.SerializedBytes import core.serialization.deserialize import core.testutils.TEST_KEYS_TO_CORP_MAP +import core.testutils.TEST_PROGRAM_MAP import core.testutils.TEST_TX_TIME import java.security.KeyPair import java.security.PrivateKey @@ -77,6 +79,14 @@ class MockStorageService : StorageService { } } +object MockContractFactory : ContractFactory { + override operator fun get(hash: SecureHash): T { + val clazz = TEST_PROGRAM_MAP[hash] ?: throw UnknownContractException() + @Suppress("UNCHECKED_CAST") + return clazz.newInstance() as T + } +} + class MockServices( val wallet: WalletService? = null, val keyManagement: KeyManagementService? = null, diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index adc53c694c..1dccf00ef3 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -52,13 +52,13 @@ val TEST_KEYS_TO_CORP_MAP: Map = mapOf( val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") // In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside -// a sandbox. For now we just instantiate right at the start of the program. -val TEST_PROGRAM_MAP: Map = mapOf( - CASH_PROGRAM_ID to Cash(), - CP_PROGRAM_ID to CommercialPaper(), - JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper(), - CROWDFUND_PROGRAM_ID to CrowdFund(), - DUMMY_PROGRAM_ID to DummyContract +// a sandbox. For unit tests we just have a hard-coded list. +val TEST_PROGRAM_MAP: Map> = mapOf( + CASH_PROGRAM_ID to Cash::class.java, + CP_PROGRAM_ID to CommercialPaper::class.java, + JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java, + CROWDFUND_PROGRAM_ID to CrowdFund::class.java, + DUMMY_PROGRAM_ID to DummyContract::class.java ) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -135,7 +135,7 @@ open class TransactionForTest : AbstractTransactionForTest() { protected fun run(time: Instant) { val cmds = commandsToAuthenticatedObjects() val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256()) - tx.verify(TEST_PROGRAM_MAP) + tx.verify(MockContractFactory) } fun accepts(time: Instant = TEST_TX_TIME) = run(time) @@ -297,7 +297,7 @@ class TransactionGroupDSL(private val stateType: Class) { fun verify() { val group = toTransactionGroup() try { - group.verify(TEST_PROGRAM_MAP) + group.verify(MockContractFactory) } catch (e: TransactionVerificationException) { // Let the developer know the index of the transaction that failed. val ltx: LedgerTransaction = txns.find { it.hash == e.tx.origHash }!! From ae6309a11337e17c525d6e9789df77852ad1ac41 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 9 Feb 2016 20:19:10 +0100 Subject: [PATCH 18/19] Minor: make SecureHash c'tor private so it's not possible to instantiate directly. --- src/main/java/core/crypto/CryptoUtilities.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/core/crypto/CryptoUtilities.kt b/src/main/java/core/crypto/CryptoUtilities.kt index e0d468ad20..2ecd0d35fc 100644 --- a/src/main/java/core/crypto/CryptoUtilities.kt +++ b/src/main/java/core/crypto/CryptoUtilities.kt @@ -16,7 +16,7 @@ import java.security.* import java.security.interfaces.ECPublicKey // "sealed" here means there can't be any subclasses other than the ones defined here. -sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) { +sealed class SecureHash private constructor(bits: ByteArray) : OpaqueBytes(bits) { class SHA256(bits: ByteArray) : SecureHash(bits) { init { require(bits.size == 32) } override val signatureAlgorithmName: String get() = "SHA256withECDSA" From 5b473be79c7bc88dbcc643bae20de86374345bdf Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 10 Feb 2016 17:33:57 +0100 Subject: [PATCH 19/19] Minor: fix test breakage under Gradle due to some horrible stuff Gradle does with injecting things into our environment. --- src/main/kotlin/core/messaging/StateMachines.kt | 15 +++++++++++++-- .../core/messaging/TwoPartyTradeProtocolTests.kt | 4 ++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/core/messaging/StateMachines.kt b/src/main/kotlin/core/messaging/StateMachines.kt index 9046e5ffc7..44e8357d96 100644 --- a/src/main/kotlin/core/messaging/StateMachines.kt +++ b/src/main/kotlin/core/messaging/StateMachines.kt @@ -59,6 +59,15 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) // property. private val _stateMachines = Collections.synchronizedList(ArrayList>()) + // This is a workaround for something Gradle does to us during unit tests. It replaces stderr with its own + // class that inserts itself into a ThreadLocal. That then gets caught in fiber serialisation, which we don't + // want because it can't get recreated properly. It turns out there's no good workaround for this! All the obvious + // approaches fail. Pending resolution of https://github.com/puniverse/quasar/issues/153 we just disable + // checkpointing when unit tests are run inside Gradle. The right fix is probably to make Quasar's + // bit-too-clever-for-its-own-good ThreadLocal serialisation trick. It already wasted far more time than it can + // ever recover. + val checkpointing: Boolean get() = !System.err.javaClass.name.contains("LinePerThreadBufferingOutputStream") + /** Returns a snapshot of the currently registered state machines. */ val stateMachines: List> get() { synchronized(_stateMachines) { @@ -82,7 +91,8 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) ) init { - restoreCheckpoints() + if (checkpointing) + restoreCheckpoints() } /** Reads the database map and resurrects any serialised state machines. */ @@ -179,7 +189,8 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) serialisedFiber: ByteArray) { val checkpoint = Checkpoint(serialisedFiber, logger.name, topic, responseType.name) val curPersistedBytes = checkpoint.serialize().bits - persistCheckpoint(prevCheckpointKey, curPersistedBytes) + if (checkpointing) + persistCheckpoint(prevCheckpointKey, curPersistedBytes) val newCheckpointKey = curPersistedBytes.sha256() net.runOnNextMessage(topic, runInThread) { netMsg -> val obj: Any = THREAD_LOCAL_KRYO.get().readObject(Input(netMsg.data), responseType) diff --git a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt index ad95e27654..88f3b96220 100644 --- a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt +++ b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt @@ -121,6 +121,10 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() { val smmBuyer = StateMachineManager(bobsServices, MoreExecutors.directExecutor()) + // Horrible Gradle/Kryo/Quasar FUBAR workaround: just skip these tests when run under Gradle for now. + if (!smmBuyer.checkpointing) + return + val buyerSessionID = random63BitValue() TwoPartyTradeProtocol.runSeller(