diff --git a/build.gradle b/build.gradle index b2215b4c97..c4478eac3f 100644 --- a/build.gradle +++ b/build.gradle @@ -198,7 +198,7 @@ applicationDistribution.into("bin") { fileMode = 0755 } -task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') { +task buildCordaJAR(type: FatCapsule, dependsOn: ['quasarScan', 'buildCertSigningRequestUtilityJAR']) { applicationClass 'com.r3corda.node.MainKt' archiveName 'corda.jar' applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class') @@ -212,6 +212,15 @@ task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') { } } +task buildCertSigningRequestUtilityJAR(type: FatCapsule, dependsOn: project.jar) { + applicationClass 'com.r3corda.node.utilities.certsigning.CertificateSignerKt' + archiveName 'certSigningRequestUtility.jar' + capsuleManifest { + systemProperties['log4j.configuration'] = 'log4j2.xml' + minJavaVersion = '1.8.0' + } +} + task installTemplateNodes(dependsOn: 'buildCordaJAR') << { copy { from buildCordaJAR.outputs.getFiles() diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt b/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt new file mode 100644 index 0000000000..15581a7228 --- /dev/null +++ b/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt @@ -0,0 +1,12 @@ +package com.r3corda.contracts.tradefinance + +import java.security.PublicKey +import java.util.* + +/** + * A notice which can be attached to a receivable. + */ +sealed class Notice(val id: UUID, val owner: PublicKey) { + class OwnershipInterest(id: UUID, owner: PublicKey) : Notice(id, owner) + class Objection(id: UUID, owner: PublicKey) : Notice(id, owner) +} \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt b/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt new file mode 100644 index 0000000000..cddf5da957 --- /dev/null +++ b/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt @@ -0,0 +1,305 @@ +package com.r3corda.contracts.tradefinance + +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.* +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.isOrderedAndUnique +import com.r3corda.core.random63BitValue +import com.r3corda.core.serialization.OpaqueBytes +import com.r3corda.core.utilities.NonEmptySet +import java.security.PublicKey +import java.time.Instant +import java.time.LocalDate +import java.time.ZonedDateTime +import java.util.* + +/** + * Contract for managing lifecycle of a receivable which is recorded on the distributed ledger. These are entered by + * a third party (typically a potential creditor), and then shared by the trade finance registry, allowing others to + * attach/detach notices of ownership interest/objection. + * + * States of this contract *are not* fungible, and as such special rules apply. States must be unique within the + * inputs/outputs, and strictly ordered, in order to make it easy to verify that outputs match the inputs except where + * commands mean there are changes. + */ +class Receivable : Contract { + data class State(override val linearId: UniqueIdentifier = UniqueIdentifier(), + val created: ZonedDateTime, // When the underlying receivable was raised + val registered: Instant, // When the receivable was added to the registry + val payer: Party, + val payee: Party, + val payerRef: OpaqueBytes?, + val payeeRef: OpaqueBytes?, + val value: Amount>, + val attachments: Set, + val notices: List, + override val owner: PublicKey) : OwnableState, LinearState { + override val contract: Contract = Receivable() + override val participants: List = listOf(owner) + override fun isRelevant(ourKeys: Set): Boolean + = ourKeys.contains(payer.owningKey) || ourKeys.contains(payee.owningKey) || ourKeys.contains(owner) + override fun withNewOwner(newOwner: PublicKey): Pair + = Pair(Commands.Move(null, mapOf(Pair(linearId, newOwner))), copy(owner = newOwner)) + } + + interface Commands : CommandData { + val changed: Iterable + data class Issue(override val changed: NonEmptySet, + override val nonce: Long = random63BitValue()) : IssueCommand, Commands + data class Move(override val contractHash: SecureHash?, val changes: Map) : MoveCommand, Commands { + override val changed: Iterable = changes.keys + } + data class Note(val changes: Map>) : Commands { + override val changed: Iterable = changes.keys + } + // TODO: Write Amend clause, possibly to merge into Move + /* data class Amend(val id: UniqueIdentifier, + val payer: Party, + val payee: Party, + val payerRef: OpaqueBytes?, + val payeeRef: OpaqueBytes?, + val value: Amount>, + val attachments: Set) : Commands */ + data class Exit(override val changed: NonEmptySet) : Commands + } + + data class Diff(val added: List, val removed: List) + + interface Clauses { + /** + * Assert that each input/output state is unique within that list of states, and that states are ordered. There + * should never be the same receivable twice in a transaction. Uniqueness is also enforced by the notary, + * but we get the check as a side-effect of comparing states, so the duplication is acceptable. + */ + class StatesAreOrderedAndUnique : Clause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + // Enforce that states are ordered, so that the transaction can only be assembled in one way + requireThat { + "input receivables are ordered and unique" by inputs.isOrderedAndUnique { linearId } + "output receivables are ordered and unique" by outputs.isOrderedAndUnique { linearId } + } + return emptySet() + } + } + + /** + * Check that all inputs are present as outputs, and that all owners for new outputs have signed the command. + */ + class Issue : Clause() { + override val requiredCommands: Set> = setOf(Commands.Issue::class.java) + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + require(groupingKey == null) + // TODO: Take in matched commands as a parameter + val command = commands.requireSingleCommand() + val timestamp = tx.timestamp + + // Records for receivables are never fungible, so we just want to make sure all inputs exist as + // outputs, and there are new outputs. + requireThat { + "there are more output states than input states" by (outputs.size > inputs.size) + // TODO: Should timestamps perhaps be enforced on all receivable transactions? + "the transaction has a timestamp" by (timestamp != null) + } + + val expectedOutputs = ArrayList(inputs) + val keysThatSigned = command.signers + val owningPubKeys = HashSet() + outputs + .filter { it.linearId in command.value.changed } + .forEach { state -> + val registrationInLocalZone = state.registered.atZone(state.created.zone) + requireThat { + "the receivable is registered after it was created" by (state.created < registrationInLocalZone) + // TODO: Should narrow the window on how long ago the registration can be compared to the transaction + "the receivable is registered before the transaction date" by (state.registered < timestamp?.before) + } + owningPubKeys.add(state.owner) + expectedOutputs.add(state) + } + // Re-sort the outputs now we've finished changing them + expectedOutputs.sortBy { state -> state.linearId } + requireThat { + "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) + "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) + } + + return setOf(command.value as Commands) + } + } + + /** + * Check that inputs and outputs are exactly the same, except for ownership changes specified in the command. + * The command must be signed by the previous owners of all changed input states. + */ + class Move : Clause() { + override val requiredCommands: Set> = setOf(Commands.Move::class.java) + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + require(groupingKey == null) + // TODO: Take in matched commands as a parameter + val moveCommand = commands.requireSingleCommand() + val changes = moveCommand.value.changes + // Rebuild the outputs we expect, then compare. Receivables are not fungible, so inputs and outputs + // must match one to one + val expectedOutputs: List = inputs.map { input -> + val newOwner = changes[input.linearId] + if (newOwner != null) { + input.copy(owner = newOwner) + } else { + input + } + } + requireThat { + "inputs are not empty" by inputs.isNotEmpty() + "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) + } + // Do standard move command checks including the signature checks + verifyMoveCommand(inputs, commands) + return setOf(moveCommand.value as Commands) + } + } + + /** + * Add and/or remove notices on receivables. All input states must match output states, except for the + * changed notices. + */ + class Note : Clause() { + override val requiredCommands: Set> = setOf(Commands.Note::class.java) + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + require(groupingKey == null) + // TODO: Take in matched commands as a parameter + val command = commands.requireSingleCommand() + // Rebuild the outputs we expect, then compare. Receivables are not fungible, so inputs and outputs + // must match one to one + val (expectedOutputs, owningPubKeys) = deriveOutputStates(inputs, command) + val keysThatSigned = command.signers + requireThat { + "inputs are not empty" by inputs.isNotEmpty() + "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) + "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) + } + return setOf(command.value as Commands) + } + + fun deriveOutputStates(inputs: List, + command: AuthenticatedObject): Pair, Set> { + val changes = command.value.changes + val seenNotices = HashSet() + val outputs = inputs.map { input -> + val stateChanges = changes[input.linearId] + if (stateChanges != null) { + val notices = ArrayList(input.notices) + stateChanges.added.forEach { notice -> + require(!seenNotices.contains(notice)) { "Notices can only appear once in the add and/or remove lists" } + require(!notices.contains(notice)) { "Notice is already present on the receivable" } + seenNotices.add(notice) + notices.add(notice) + } + stateChanges.removed.forEach { notice -> + require(!seenNotices.contains(notice)) { "Notices can only appear once in the add and/or remove lists" } + require(notices.remove(notice)) { "Notice is not present on the receivable" } + seenNotices.add(notice) + } + input.copy(notices = notices) + } else { + input + } + } + return Pair(outputs, seenNotices.map { it.owner }.toSet() ) + } + } + + /** + * Remove a receivable from the ledger. This can only be done once all notices have been removed. + */ + class Exit : Clause() { + override val requiredCommands: Set> = setOf(Commands.Exit::class.java) + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + require(groupingKey == null) + // TODO: Take in matched commands as a parameter + val command = commands.requireSingleCommand() + val unmatchedIds = HashSet(command.value.changed) + val owningPubKeys = HashSet() + val expectedOutputs = inputs.filter { input -> + if (unmatchedIds.contains(input.linearId)) { + requireThat { + "there are no notices on receivables to be removed from the ledger" by input.notices.isEmpty() + } + unmatchedIds.remove(input.linearId) + owningPubKeys.add(input.owner) + false + } else { + true + } + } + val keysThatSigned = command.signers + requireThat { + "inputs are not empty" by inputs.isNotEmpty() + "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) + "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) + } + return setOf(command.value as Commands) + } + } + + // TODO: Amend clause, which replaces the Move clause + + /** + * Default clause, which checks the inputs and outputs match. Normally this wouldn't be expected to trigger, + * as other commands would handle the transaction, but this exists in case the states need to be witnessed by + * other contracts within the transaction but not modified. + */ + class InputsAndOutputsMatch : Clause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + require(groupingKey == null) + require(inputs.equals(outputs)) { "Inputs and outputs must match unless commands indicate otherwise" } + return emptySet() + } + } + } + + override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/receivables.html") + fun extractCommands(commands: Collection>): List> + = commands.select() + override fun verify(tx: TransactionForContract) + = verifyClause(tx, FilterOn( + AllComposition( + Clauses.StatesAreOrderedAndUnique(), // TODO: This is varient of the LinearState.ClauseVerifier, and we should move it up there + FirstComposition( + Clauses.Issue(), + Clauses.Exit(), + Clauses.Note(), + Clauses.Move(), + Clauses.InputsAndOutputsMatch() + ) + ), { states -> states.filterIsInstance() }), + extractCommands(tx.commands)) +} diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt new file mode 100644 index 0000000000..cc1246d961 --- /dev/null +++ b/contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt @@ -0,0 +1,153 @@ +package com.r3corda.contracts.tradefinance + +import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER +import com.r3corda.core.contracts.* +import com.r3corda.core.serialization.OpaqueBytes +import com.r3corda.core.utilities.NonEmptySet +import com.r3corda.core.utilities.TEST_TX_TIME +import com.r3corda.testing.* +import org.junit.Test +import java.time.Duration +import java.time.ZoneId +import java.util.* + +class ReceivableTests { + val inStates = arrayOf( + Receivable.State( + UniqueIdentifier.fromString("9e688c58-a548-3b8e-af69-c9e1005ad0bf"), + (TEST_TX_TIME - Duration.ofDays(2)).atZone(ZoneId.of("UTC")), + TEST_TX_TIME - Duration.ofDays(1), + ALICE, + BOB, + OpaqueBytes(ByteArray(1, { 1 })), + OpaqueBytes(ByteArray(1, { 2 })), + Amount>(1000L, USD `issued by` DUMMY_CASH_ISSUER), + emptySet(), + emptyList(), + MEGA_CORP_PUBKEY + ), + Receivable.State( + UniqueIdentifier.fromString("55a54008-ad1b-3589-aa21-0d2629c1df41"), + (TEST_TX_TIME - Duration.ofDays(2)).atZone(ZoneId.of("UTC")), + TEST_TX_TIME - Duration.ofDays(1), + ALICE, + BOB, + OpaqueBytes(ByteArray(1, { 3 })), + OpaqueBytes(ByteArray(1, { 4 })), + Amount>(2000L, GBP `issued by` DUMMY_CASH_ISSUER), + emptySet(), + emptyList(), + MEGA_CORP_PUBKEY + ) + ) + + @Test + fun trivial() { + transaction { + input { inStates[0] } + timestamp(TEST_TX_TIME) + this `fails with` "Inputs and outputs must match unless commands indicate otherwise" + + tweak { + output { inStates[0] } + verifies() + } + + tweak { + output { inStates[1] } + this `fails with` "Inputs and outputs must match unless commands indicate otherwise" + } + } + + transaction { + output { inStates[0] } + timestamp(TEST_TX_TIME) + this `fails with` "Inputs and outputs must match unless commands indicate otherwise" + } + } + + @Test + fun `order and uniqueness is enforced`() { + transaction { + input { inStates[0] } + input { inStates[1] } + output { inStates[0] } + output { inStates[1] } + timestamp(TEST_TX_TIME) + verifies() + } + + transaction { + input { inStates[1] } + input { inStates[0] } + output { inStates[0] } + output { inStates[1] } + timestamp(TEST_TX_TIME) + this `fails with` "receivables are ordered and unique" + } + + transaction { + input { inStates[0] } + input { inStates[0] } + output { inStates[0] } + output { inStates[0] } + timestamp(TEST_TX_TIME) + this `fails with` "receivables are ordered and unique" + } + } + + @Test + fun `issue`() { + // Testing that arbitrary new outputs are rejected is covered in trivial() + transaction { + output { inStates[0] } + command(MEGA_CORP_PUBKEY, Receivable.Commands.Issue(NonEmptySet(inStates[0].linearId))) + timestamp(TEST_TX_TIME) + verifies() + } + transaction { + output { inStates[0] } + command(ALICE_PUBKEY, Receivable.Commands.Issue(NonEmptySet(inStates[0].linearId))) + timestamp(TEST_TX_TIME) + this `fails with` "the owning keys are the same as the signing keys" + } + } + + @Test + fun `move`() { + transaction { + input { inStates[0] } + output { inStates[0].copy(owner = MINI_CORP_PUBKEY) } + timestamp(TEST_TX_TIME) + this `fails with` "Inputs and outputs must match unless commands indicate otherwise" + tweak { + command(MEGA_CORP_PUBKEY, Receivable.Commands.Move(null, mapOf(Pair(inStates[0].linearId, MINI_CORP_PUBKEY)))) + verifies() + } + // Test that moves enforce the correct new owner + tweak { + command(MEGA_CORP_PUBKEY, Receivable.Commands.Move(null, mapOf(Pair(inStates[0].linearId, ALICE_PUBKEY)))) + this `fails with` "outputs match inputs with expected changes applied" + } + } + } + + @Test + fun `exit`() { + // Testing that arbitrary disappearing outputs are rejected is covered in trivial() + transaction { + input { inStates[0] } + timestamp(TEST_TX_TIME) + command(MEGA_CORP_PUBKEY, Receivable.Commands.Exit(NonEmptySet(inStates[0].linearId))) + verifies() + } + transaction { + input { inStates[0] } + timestamp(TEST_TX_TIME) + command(ALICE_PUBKEY, Receivable.Commands.Exit(NonEmptySet(inStates[0].linearId))) + this `fails with` "the owning keys are the same as the signing keys" + } + } + + // TODO: Test adding and removing notices +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/Utils.kt b/core/src/main/kotlin/com/r3corda/core/Utils.kt index fc2d40768b..9a81635d75 100644 --- a/core/src/main/kotlin/com/r3corda/core/Utils.kt +++ b/core/src/main/kotlin/com/r3corda/core/Utils.kt @@ -276,4 +276,21 @@ fun Observable.bufferUntilSubscribed(): Observable { val subject = UnicastSubject.create() val subscription = subscribe(subject) return subject.doOnUnsubscribe { subscription.unsubscribe() } +} + +/** + * Determine if an iterable data type's contents are ordered and unique, based on their [Comparable].compareTo + * function. + */ +fun > Iterable.isOrderedAndUnique(extractId: T.() -> I): Boolean { + var last: I? = null + return all { it -> + val lastLast = last + last = extractId(it) + if (lastLast == null) { + true + } else { + lastLast.compareTo(extractId(it)) < 0 + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt index 1d91ddfee1..46f6c0fac6 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt @@ -448,13 +448,38 @@ data class Commodity(val commodityCode: String, /** * This class provides a truly unique identifier of a trade, state, or other business object. - * @param externalId If there is an existing weak identifer e.g. trade reference id. + * + * @param externalId If there is an existing weak identifier e.g. trade reference id. * This should be set here the first time a UniqueIdentifier identifier is created as part of an issue, * or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong id. * @param id Should never be set by user code and left as default initialised. * So that the first time a state is issued this should be given a new UUID. * Subsequent copies and evolutions of a state should just copy the externalId and Id fields unmodified. */ -data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) { +data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString() -} \ No newline at end of file + companion object { + fun fromString(name: String) : UniqueIdentifier + = UniqueIdentifier(null, UUID.fromString(name)) + } + + override fun compareTo(other: UniqueIdentifier): Int { + val idCompare = id.compareTo(other.id) + return if (idCompare == 0) + compareExternalIds(other) + else + idCompare + } + + private fun compareExternalIds(other: UniqueIdentifier): Int + = if (other.externalId == null) + if (externalId == null) + 0 + else + 1 + else + if (externalId == null) + -1 + else + externalId.compareTo(externalId) +} diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index db426ac830..56f7f15e11 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -238,18 +238,18 @@ interface LinearState: ContractState { /** * Standard clause to verify the LinearState safety properties. */ - class ClauseVerifier(val stateClass: Class) : Clause() { + class ClauseVerifier() : Clause() { override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - val filteredInputs = inputs.filterIsInstance(stateClass) - val inputIds = filteredInputs.map { it.linearId }.distinct() - require(inputIds.count() == filteredInputs.count()) { "LinearStates cannot be merged" } - val filteredOutputs = outputs.filterIsInstance(stateClass) - val outputIds = filteredOutputs.map { it.linearId }.distinct() - require(outputIds.count() == filteredOutputs.count()) { "LinearStates cannot be split" } + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + val inputIds = inputs.map { it.linearId }.distinct() + val outputIds = outputs.map { it.linearId }.distinct() + requireThat { + "LinearStates are not merged" by (inputIds.count() == inputs.count()) + "LinearStates are not split" by (outputIds.count() == outputs.count()) + } return emptySet() } } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt index 100234ed32..aace2abae5 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt @@ -24,8 +24,10 @@ class FirstComposition(val firstCla clauses.addAll(remainingClauses) } - override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set - = matchedClauses(commands).single().verify(tx, inputs, outputs, commands, groupingKey) + override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set { + val clause = matchedClauses(commands).singleOrNull() ?: throw IllegalStateException("No delegate clause matched in first composition") + return clause.verify(tx, inputs, outputs, commands, groupingKey) + } override fun toString() = "First: ${clauses.toList()}" } \ No newline at end of file diff --git a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt index 337ea56d33..4c6139b966 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt @@ -115,7 +115,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMap return smm.add(loggerName, logic).resultFuture } - override fun

> registerProtocolInitiator(markerClass: KClass, protocolFactory: (Party) -> ProtocolLogic<*>) { + override fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) { require(markerClass !in protocolFactories) { "${markerClass.java.name} has already been used to register a protocol" } log.debug { "Registering ${markerClass.java.name}" } protocolFactories[markerClass.java] = protocolFactory diff --git a/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt index 86d9a1c738..f087fcca94 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt @@ -76,12 +76,12 @@ abstract class ServiceHubInternal : ServiceHub { * marker class has been registered then the corresponding factory will be used to create the protocol which will * communicate with the other side. If there is no mapping then the session attempt is rejected. * @param markerClass The marker [KClass] present in a session initiation attempt, which is a 1:1 mapping to a [Class] - * using the

::class
construct. Any marker class can be used, with the default being the class of the initiating - * protocol. This enables the registration to be of the form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol) + * using the
::class
construct. Conventionally this is a [ProtocolLogic] subclass, however any class can + * be used, with the default being the class of the initiating protocol. This enables the registration to be of the + * form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol) * @param protocolFactory The protocol factory generating the initiated protocol. - * @param R the return type of the protocol logic */ - abstract fun

> registerProtocolInitiator(markerClass: KClass, protocolFactory: (Party) -> ProtocolLogic<*>) + abstract fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) /** * Return the protocol factory that has been registered with [markerClass], or null if no factory is found. diff --git a/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt index 09fd6498a6..1c9cee6625 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt @@ -15,6 +15,7 @@ import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigRenderOptions import org.slf4j.LoggerFactory +import java.net.URL import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -60,7 +61,6 @@ interface NodeConfiguration : NodeSSLConfiguration { val exportJMXto: String val dataSourceProperties: Properties get() = Properties() val devMode: Boolean - val certificateSigningService: HostAndPort companion object { val log = LoggerFactory.getLogger("NodeConfiguration") @@ -96,6 +96,7 @@ operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { Instant::class.java -> Instant.parse(getString(metadata.name)) as T HostAndPort::class.java -> HostAndPort.fromString(getString(metadata.name)) as T Path::class.java -> Paths.get(getString(metadata.name)) as T + URL::class.java -> URL(getString(metadata.name)) as T Properties::class.java -> getProperties(metadata.name) as T else -> throw IllegalArgumentException("Unsupported type ${metadata.returnType}") } @@ -133,7 +134,6 @@ class NodeConfigurationFromConfig(val config: Config = ConfigFactory.load()) : N override val trustStorePassword: String by config override val dataSourceProperties: Properties by config override val devMode: Boolean by config.getOrElse { false } - override val certificateSigningService: HostAndPort by config } class FullNodeConfiguration(conf: Config) : NodeConfiguration { @@ -146,7 +146,6 @@ class FullNodeConfiguration(conf: Config) : NodeConfiguration { override val trustStorePassword: String by conf override val dataSourceProperties: Properties by conf override val devMode: Boolean by conf.getOrElse { false } - override val certificateSigningService: HostAndPort by conf val useHTTPS: Boolean by conf val artemisAddress: HostAndPort by conf val webAddress: HostAndPort by conf diff --git a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt index 52437e07b7..fe31681727 100644 --- a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt +++ b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt @@ -9,9 +9,11 @@ import com.r3corda.core.crypto.X509Utilities.addOrReplaceKey import com.r3corda.core.div import com.r3corda.core.minutes import com.r3corda.core.utilities.loggerFor -import com.r3corda.node.services.config.FullNodeConfiguration import com.r3corda.node.services.config.NodeConfiguration +import com.r3corda.node.services.config.NodeConfigurationFromConfig +import com.r3corda.node.services.config.getValue import joptsimple.OptionParser +import java.net.URL import java.nio.file.Files import java.nio.file.Paths import java.security.KeyPair @@ -113,8 +115,8 @@ class CertificateSigner(val config: NodeConfiguration, val certService: Certific object ParamsSpec { val parser = OptionParser() - val baseDirectoryArg = parser.accepts("base-dir", "The directory to put all key stores under").withRequiredArg() - val configFileArg = parser.accepts("config-file", "The path to the config file").withRequiredArg() + val baseDirectoryArg = parser.accepts("base-dir", "Working directory of Corda Node.").withRequiredArg().defaultsTo(".") + val configFileArg = parser.accepts("config-file", "The path to the config file.").withRequiredArg() } fun main(args: Array) { @@ -122,12 +124,19 @@ fun main(args: Array) { ParamsSpec.parser.parse(*args) } catch (ex: Exception) { CertificateSigner.log.error("Unable to parse args", ex) + ParamsSpec.parser.printHelpOn(System.out) exitProcess(1) } - val baseDirectoryPath = Paths.get(cmdlineOptions.valueOf(ParamsSpec.baseDirectoryArg) ?: throw IllegalArgumentException("Please provide Corda node base directory path")) + val baseDirectoryPath = Paths.get(cmdlineOptions.valueOf(ParamsSpec.baseDirectoryArg)) val configFile = if (cmdlineOptions.has(ParamsSpec.configFileArg)) Paths.get(cmdlineOptions.valueOf(ParamsSpec.configFileArg)) else null - val conf = FullNodeConfiguration(NodeConfiguration.loadConfig(baseDirectoryPath, configFile, allowMissingConfig = true)) + + val config = NodeConfiguration.loadConfig(baseDirectoryPath, configFile, allowMissingConfig = true).let { config -> + object : NodeConfiguration by NodeConfigurationFromConfig(config) { + val certificateSigningService: URL by config + } + } + // TODO: Use HTTPS instead - CertificateSigner(conf, HTTPCertificateSigningService(conf.certificateSigningService)).buildKeyStore() + CertificateSigner(config, HTTPCertificateSigningService(config.certificateSigningService)).buildKeyStore() } diff --git a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt index 1ca9323699..a18c9725fe 100644 --- a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt +++ b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt @@ -1,6 +1,5 @@ package com.r3corda.node.utilities.certsigning -import com.google.common.net.HostAndPort import org.apache.commons.io.IOUtils import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.io.IOException @@ -11,7 +10,7 @@ import java.security.cert.CertificateFactory import java.util.* import java.util.zip.ZipInputStream -class HTTPCertificateSigningService(val server: HostAndPort) : CertificateSigningService { +class HTTPCertificateSigningService(val server: URL) : CertificateSigningService { companion object { // TODO: Propagate version information from gradle val clientVersion = "1.0" @@ -19,7 +18,7 @@ class HTTPCertificateSigningService(val server: HostAndPort) : CertificateSignin override fun retrieveCertificates(requestId: String): Array? { // Poll server to download the signed certificate once request has been approved. - val url = URL("http://$server/api/certificate/$requestId") + val url = URL("$server/api/certificate/$requestId") val conn = url.openConnection() as HttpURLConnection conn.requestMethod = "GET" @@ -42,7 +41,7 @@ class HTTPCertificateSigningService(val server: HostAndPort) : CertificateSignin override fun submitRequest(request: PKCS10CertificationRequest): String { // Post request to certificate signing server via http. - val conn = URL("http://$server/api/certificate").openConnection() as HttpURLConnection + val conn = URL("$server/api/certificate").openConnection() as HttpURLConnection conn.doOutput = true conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/octet-stream") diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 02e75d25f3..70414423bb 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -11,5 +11,5 @@ dataSourceProperties = { "dataSource.password" = "" } devMode = true -certificateSigningService = "localhost:0" +certificateSigningService = "https://cordaci-netperm.corda.r3cev.com" useHTTPS = false \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt index 3ec543bd48..86bb3c5eba 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt @@ -51,8 +51,6 @@ class ArtemisMessagingTests { override val exportJMXto: String = "" override val keyStorePassword: String = "testpass" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) - } } diff --git a/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt index bb38499543..657d23a79b 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt @@ -75,7 +75,7 @@ open class MockServiceHubInternal( return smm.add(loggerName, logic).resultFuture } - override fun

> registerProtocolInitiator(markerClass: KClass, protocolFactory: (Party) -> ProtocolLogic<*>) { + override fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) { protocolFactories[markerClass.java] = protocolFactory } diff --git a/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt b/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt index 606dc15073..1101eeeff1 100644 --- a/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt @@ -1,6 +1,5 @@ package com.r3corda.node.utilities.certsigning -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock @@ -46,7 +45,6 @@ class CertificateSignerTest { override val exportJMXto: String = "" override val keyStorePassword: String = "testpass" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) } assertFalse(Files.exists(config.keyStorePath)) diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index 3b90583aa2..3d8a7a50ce 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -71,7 +71,6 @@ fun main(args: Array) { override val keyStorePassword: String = "cordacadevpass" override val trustStorePassword: String = "trustpass" override val dataSourceProperties: Properties = makeTestDataSourceProperties() - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) } diff --git a/src/main/kotlin/com/r3corda/simulation/Simulation.kt b/src/main/kotlin/com/r3corda/simulation/Simulation.kt index 4864b69dd8..85aa576948 100644 --- a/src/main/kotlin/com/r3corda/simulation/Simulation.kt +++ b/src/main/kotlin/com/r3corda/simulation/Simulation.kt @@ -1,6 +1,5 @@ package com.r3corda.simulation -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.r3corda.core.crypto.generateKeyPair @@ -70,7 +69,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val exportJMXto: String = "" override val keyStorePassword: String = "dummy" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) override val dataSourceProperties = makeTestDataSourceProperties() } return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) @@ -100,7 +98,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val exportJMXto: String = "" override val keyStorePassword: String = "dummy" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) override val dataSourceProperties = makeTestDataSourceProperties() } @@ -123,7 +120,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val exportJMXto: String = "" override val keyStorePassword: String = "dummy" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) override val dataSourceProperties = makeTestDataSourceProperties() } return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) @@ -145,7 +141,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val exportJMXto: String = "" override val keyStorePassword: String = "dummy" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) override val dataSourceProperties = makeTestDataSourceProperties() } @@ -173,7 +168,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val exportJMXto: String = "" override val keyStorePassword: String = "dummy" override val trustStorePassword: String = "trustpass" - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) override val dataSourceProperties = makeTestDataSourceProperties() } diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/DummyLinearState.kt b/test-utils/src/main/kotlin/com/r3corda/testing/DummyLinearState.kt index b5503079be..24697f0b64 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/DummyLinearState.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/DummyLinearState.kt @@ -2,6 +2,7 @@ package com.r3corda.testing import com.r3corda.core.contracts.* import com.r3corda.core.contracts.clauses.Clause +import com.r3corda.core.contracts.clauses.FilterOn import com.r3corda.core.contracts.clauses.verifyClause import com.r3corda.core.crypto.SecureHash import java.security.PublicKey @@ -9,9 +10,9 @@ import java.security.PublicKey class DummyLinearContract: Contract { override val legalContractReference: SecureHash = SecureHash.sha256("Test") - val clause: Clause = LinearState.ClauseVerifier(State::class.java) + val clause: Clause = LinearState.ClauseVerifier() override fun verify(tx: TransactionForContract) = verifyClause(tx, - clause, + FilterOn(clause, { states -> states.filterIsInstance() }), emptyList()) class State( diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt index a7f371c0ac..98e283ea27 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt @@ -1,7 +1,6 @@ package com.r3corda.testing.node import com.google.common.jimfs.Jimfs -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.r3corda.core.crypto.Party @@ -194,7 +193,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override val keyStorePassword: String = "dummy" override val trustStorePassword: String = "trustpass" override val dataSourceProperties: Properties get() = makeTestDataSourceProperties("node_${id}_net_$networkId") - override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0) } val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair) if (start) {