From 88fbb47f67c5468eeaf1ddefefd7562e701af90c Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Mon, 26 Nov 2018 16:02:32 +0000 Subject: [PATCH 1/8] ENT-2320 state contract identification (#4285) * Enforce state/contract agreement validation * Fix some broken tests * Ascertain targetVersion by inspecting the jar source of the ContractState * Docs added and rebased against master * contextLogger doesn't work here * Java examples in docs * Label IRSState with owning contract * Fix rst formatting * Add @BelongsToContract annotation to PortfolioState --- core-deterministic/build.gradle | 1 + .../rules/TargetVersionDependentRules.kt | 12 ++++ .../TransactionVerificationException.kt | 32 ++++++++++- .../rules/TargetVersionDependentRules.kt | 55 +++++++++++++++++++ .../core/transactions/LedgerTransaction.kt | 54 +++++++++++------- .../flows/WithReferencedStatesFlowTests.kt | 8 +-- .../transactions/ReferenceInputStateTests.kt | 13 ++++- docs/source/api-contract-constraints.rst | 52 ++++++++++++++++++ .../finance/contracts/asset/CashTests.kt | 7 ++- .../contracts/asset/ObligationTests.kt | 8 ++- .../net/corda/vega/contracts/IRSState.kt | 6 +- .../corda/vega/contracts/PortfolioState.kt | 1 + .../testing/internal/vault/VaultFiller.kt | 1 + 13 files changed, 217 insertions(+), 33 deletions(-) create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index f7cdb65e9c..a8162e2c18 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -51,6 +51,7 @@ task patchCore(type: Zip, dependsOn: coreJarTask) { exclude 'net/corda/core/internal/*ToggleField*.class' exclude 'net/corda/core/serialization/*SerializationFactory*.class' exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' + exclude 'net/corda/core/internal/rules/*.class' } reproducibleFileOrder = true diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt new file mode 100644 index 0000000000..87fa115261 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt @@ -0,0 +1,12 @@ +package net.corda.core.internal.rules + +import net.corda.core.contracts.ContractState + +// This file provides rules that depend on the targetVersion of the current Contract or Flow. +// In core, this is determined by means which are unavailable in the DJVM, +// so we must provide deterministic alternatives here. + +@Suppress("unused") +object StateContractValidationEnforcementRule { + fun shouldEnforce(state: ContractState): Boolean = true +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 3b57766508..18531dfe67 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -149,7 +149,7 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S "is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " + "a full cycle. Offending indices $nonMatching", null) - /** + /**HEAD * All encumbered states should be assigned to the same notary. This is due to the fact that multi-notary * transactions are not supported and thus two encumbered states with different notaries cannot be consumed * in the same transaction. @@ -159,6 +159,36 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S : TransactionVerificationException(txId, "Encumbered output states assigned to different notaries found. " + "Output state with index $encumberedIndex is assigned to notary [$encumberedNotary], while its encumbrance with index $encumbranceIndex is assigned to notary [$encumbranceNotary]", null) + /** + * If a state is identified as belonging to a contract, either because the state class is defined as an inner class + * of the contract class or because the state class is annotated with [BelongsToContract], then it must not be + * bundled in a [TransactionState] with a different contract. + * + * @param state The [TransactionState] whose bundled state and contract are in conflict. + * @param requiredContractClassName The class name of the contract to which the state belongs. + */ + @KeepForDJVM + class TransactionContractConflictException(txId: SecureHash, state: TransactionState, requiredContractClassName: String) + : TransactionVerificationException(txId, + """ + State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but + is bundled in TransactionState with ${state.contract}. + + For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement + """.trimIndent().replace('\n', ' '), null) + + // TODO: add reference to documentation + @KeepForDJVM + class TransactionRequiredContractUnspecifiedException(txId: SecureHash, state: TransactionState) + : TransactionVerificationException(txId, + """ + State of class ${state.data::class.java.typeName} does not have a specified owning contract. + Add the @BelongsToContract annotation to this class to ensure that it can only be bundled in a TransactionState + with the correct contract. + + For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement + """.trimIndent(), null) + /** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */ @CordaSerializable @KeepForDJVM diff --git a/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt b/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt new file mode 100644 index 0000000000..5cb3cc5ada --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt @@ -0,0 +1,55 @@ +package net.corda.core.internal.rules + +import net.corda.core.contracts.ContractState +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.warnOnce +import org.slf4j.LoggerFactory +import java.net.URL +import java.util.concurrent.ConcurrentHashMap +import java.util.jar.JarInputStream +import java.util.jar.Manifest + +// This file provides rules that depend on the targetVersion of the current Contract or Flow. +// Rules defined in this package are automatically removed from the DJVM in core-deterministic, +// and must be replaced by a deterministic alternative defined within that module. + +/** + * Rule which determines whether [ContractState]s must declare the [Contract] to which they belong (e.g. via the + * [BelongsToContract] annotation), and must be bundled together with that contract in any [TransactionState]. + * + * This rule is consulted during validation by [LedgerTransaction]. + */ +object StateContractValidationEnforcementRule { + + private val logger = LoggerFactory.getLogger(StateContractValidationEnforcementRule::class.java) + + private val targetVersionCache = ConcurrentHashMap() + + fun shouldEnforce(state: ContractState): Boolean { + val jarLocation = state::class.java.protectionDomain.codeSource.location + + if (jarLocation == null) { + logger.warnOnce(""" + Unable to determine JAR location for contract state class ${state::class.java.name}, + and consequently unable to determine target platform version. + Enforcing state/contract agreement validation by default. + + For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement + """.trimIndent().replace("\n", " ")) + return true + } + + val targetVersion = targetVersionCache.computeIfAbsent(jarLocation) { + jarLocation.openStream().use { inputStream -> + JarInputStream(inputStream).manifest?.targetPlatformVersion ?: 1 + } + } + + return targetVersion >= 4 + } +} + +private val Manifest.targetPlatformVersion: Int get() { + val minPlatformVersion = mainAttributes.getValue("Min-Platform-Version")?.toInt() ?: 1 + return mainAttributes.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 90e09c86b8..c347df08c0 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -3,10 +3,14 @@ package net.corda.core.transactions import net.corda.core.CordaInternal import net.corda.core.KeepForDJVM import net.corda.core.contracts.* +import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException +import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.rules.StateContractValidationEnforcementRule +import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.CordaSerializable @@ -133,7 +137,37 @@ private constructor( } /** - * Verify that package ownership is respected. + * For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the + * wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class. + * + * If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log + * if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown. + */ + private fun validateStatesAgainstContract(internalTx: LedgerTransaction) = + internalTx.allStates.forEach(::validateStateAgainstContract) + + private fun validateStateAgainstContract(state: TransactionState) { + val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data) + + val requiredContractClassName = state.data.requiredContractClassName ?: + if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(id, state) + else return + + if (state.contract != requiredContractClassName) + if (shouldEnforce) { + throw TransactionContractConflictException(id, state, requiredContractClassName) + } else { + logger.warnOnce(""" + State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but + is bundled in TransactionState with ${state.contract}. + + For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement + """.trimIndent().replace('\n', ' ')) + } + } + + /** + * Verify that for each contract the network wide package owner is respected. * * TODO - revisit once transaction contains network parameters. */ @@ -156,24 +190,6 @@ private constructor( } } - /** - * For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the - * wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class. - * - * A warning will be written to the log if any mismatch is detected. - */ - private fun validateStatesAgainstContract(internalTx: LedgerTransaction) = internalTx.allStates.forEach { validateStateAgainstContract(it) } - - private fun validateStateAgainstContract(state: TransactionState) { - state.data.requiredContractClassName?.let { requiredContractClassName -> - if (state.contract != requiredContractClassName) - logger.warnOnce(""" - State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but - is bundled in TransactionState with ${state.contract}. - """.trimIndent().replace('\n', ' ')) - } - } - /** * Enforces the validity of the actual constraints. * * Constraints should be one of the valid supported ones. diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index deb07d0089..e40bb4453f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -14,8 +14,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.contracts.DummyContract -import net.corda.testing.contracts.DummyState import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.cordappsForPackages @@ -106,16 +104,16 @@ internal class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic(query).states.single() + val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { addReferenceState(referenceState.referenced()) - addOutputState(DummyState(), DummyContract.PROGRAM_ID) - addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey)) + addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID) + addCommand(RefState.Create(), listOf(ourIdentity.owningKey)) }) return subFlow(FinalityFlow(stx, emptyList())) } } - class WithReferencedStatesFlowTests { companion object { @JvmStatic diff --git a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt index 8507de2834..913057168a 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt @@ -58,10 +58,21 @@ class ReferenceStateTests { // check might not be present in other contracts, like Cash, for example. Cash might have a command // called "Share" that allows a party to prove to another that they own over a certain amount of cash. // As such, cash can be added to the references list with a "Share" command. + @BelongsToContract(ExampleContract::class) data class ExampleState(val creator: Party, val data: String) : ContractState { override val participants: List get() = listOf(creator) } + // This state has only been created to serve reference data so it cannot ever be used as an input or + // output when it is being referred to. However, we might want all states to be referable, so this + // check might not be present in other contracts, like Cash, for example. Cash might have a command + // called "Share" that allows a party to prove to another that they own over a certain amount of cash. + // As such, cash can be added to the references list with a "Share" command. + @BelongsToContract(Cash::class) + data class ExampleCashState(val creator: Party, val data: String) : ContractState { + override val participants: List get() = listOf(creator) + } + class ExampleContract : Contract { interface Commands : CommandData class Create : Commands @@ -169,7 +180,7 @@ class ReferenceStateTests { transaction { input("REF DATA") command(ALICE_PUBKEY, ExampleContract.Update()) - output(Cash.PROGRAM_ID, "UPDATED REF DATA", "REF DATA".output().copy(data = "NEW STUFF!")) + output(ExampleContract::class.java.typeName, "UPDATED REF DATA", "REF DATA".output().copy(data = "NEW STUFF!")) verifies() } // Try to use the old one. diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index d288ef5eb1..90f9992822 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -62,6 +62,58 @@ consumes notary and ledger resources, and is just in general more complex. .. _implicit_constraint_types: +Contract/State Agreement +------------------------ + +Starting with Corda 4, ``ContractState``s must explicitly indicate which ``Contract`` they belong to. When a transaction is +verified, the contract bundled with each state in the transaction must be its "owning" contract, otherwise we cannot guarantee that +the transition of the ``ContractState`` will be verified against the business rules that should apply to it. + +There are two mechanisms for indicating ownership. One is to annotate the ``ContractState`` with the ``BelongsToContract`` annotation, +indicating the ``Contract`` class to which it is tied: + +.. sourcecode:: java + + @BelongToContract(MyContract.class) + public class MyState implements ContractState { + // implementation goes here + } + + +.. sourcecode:: kotlin + + @BelongsToContract(MyContract::class) + data class MyState(val value: Int) : ContractState { + // implementation goes here + } + + +The other is to define the ``ContractState`` class as an inner class of the ``Contract`` class + +.. sourcecode:: java + + public class MyContract implements Contract { + + public static class MyState implements ContractState { + // state implementation goes here + } + + // contract implementation goes here + } + + +.. sourcecode:: kotlin + + class MyContract : Contract { + data class MyState(val value: Int) : ContractState + } + + +If a ``ContractState``'s owning ``Contract`` cannot be identified by either of these mechanisms, and the ``targetVersion`` of the +CorDapp is 4 or greater, then transaction verification will fail with a ``TransactionRequiredContractUnspecifiedException``. If +the owning ``Contract`` _can_ be identified, but the ``ContractState`` has been bundled with a different contract, then +transaction verification will fail with a ``TransactionContractConflictException``. + How constraints work -------------------- diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index a271d13af2..f3afb354d2 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -160,12 +160,17 @@ class CashTests { } } + @BelongsToContract(Cash::class) + object DummyState: ContractState { + override val participants: List = emptyList() + } + @Test fun `issue by move`() { // Check we can't "move" money into existence. transaction { attachment(Cash.PROGRAM_ID) - input(Cash.PROGRAM_ID, DummyState()) + input(Cash.PROGRAM_ID, DummyState) output(Cash.PROGRAM_ID, outState) command(miniCorp.publicKey, Cash.Commands.Move()) this `fails with` "there is at least one cash input for this group" diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 4b1bd358ef..59f209e2fe 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -21,7 +21,6 @@ import net.corda.finance.contracts.NetType import net.corda.finance.contracts.asset.Obligation.Lifecycle import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.contracts.DummyContract -import net.corda.testing.contracts.DummyState import net.corda.testing.core.* import net.corda.testing.dsl.* import net.corda.testing.internal.TEST_TX_TIME @@ -145,12 +144,17 @@ class ObligationTests { } } + @BelongsToContract(DummyContract::class) + object DummyState: ContractState { + override val participants: List = emptyList() + } + @Test fun `issue debt`() { // Check we can't "move" debt into existence. transaction { attachments(DummyContract.PROGRAM_ID, Obligation.PROGRAM_ID) - input(DummyContract.PROGRAM_ID, DummyState()) + input(DummyContract.PROGRAM_ID, DummyState) output(Obligation.PROGRAM_ID, outState) command(MINI_CORP_PUBKEY, Obligation.Commands.Move()) this `fails with` "at least one obligation input" diff --git a/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/IRSState.kt b/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/IRSState.kt index 73537a02d3..e4dd6a7291 100644 --- a/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/IRSState.kt +++ b/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/IRSState.kt @@ -1,9 +1,6 @@ package net.corda.vega.contracts -import net.corda.core.contracts.Command -import net.corda.core.contracts.ContractClassName -import net.corda.core.contracts.StateAndContract -import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder @@ -16,6 +13,7 @@ const val IRS_PROGRAM_ID: ContractClassName = "net.corda.vega.contracts.OGTrade" * * TODO: Merge with the existing demo IRS code. */ +@BelongsToContract(OGTrade::class) data class IRSState(val swap: SwapData, val buyer: AbstractParty, val seller: AbstractParty, diff --git a/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt b/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt index 119bc683e4..02daba646a 100644 --- a/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt +++ b/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt @@ -17,6 +17,7 @@ const val PORTFOLIO_SWAP_PROGRAM_ID = "net.corda.vega.contracts.PortfolioSwap" * Represents an aggregate set of trades agreed between two parties and a possible valuation of that portfolio at a * given point in time. This state can be consumed to create a new state with a mutated valuation or portfolio. */ +@BelongsToContract(PortfolioSwap::class) data class PortfolioState(val portfolio: List, val _parties: Pair, val valuationDate: LocalDate, diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index baba1b0cfe..af7f452444 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -310,6 +310,7 @@ class VaultFiller @JvmOverloads constructor( /** A state representing a commodity claim against some party */ +@BelongsToContract(Obligation::class) data class CommodityState( override val amount: Amount>, From d399e3c2423ff1c0b8352a7838905c6953e58def Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Mon, 26 Nov 2018 16:03:06 +0000 Subject: [PATCH 2/8] CORDA-2232 external id to pub key mapping - fixes Fixes in PersistentKeyManagementService.kt, VaultSchema.kt, and vault-schema.changelog-v8.xml. (#4295) --- docs/source/node-database.rst | 6 ++++++ .../keys/PersistentKeyManagementService.kt | 8 +++++--- .../corda/node/services/vault/VaultSchema.kt | 15 +++++++------- .../migration/vault-schema.changelog-v8.xml | 20 ++++++++++++++----- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index e40c3bf520..2f078cbaf5 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -102,6 +102,10 @@ By default, the node database has the following tables: +-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | NODE_TRANSACTIONS | TX_ID, TRANSACTION_VALUE, STATE_MACHINE_RUN_ID | +-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| PK_HASH_TO_EXT_ID_MAP | ID, EXTERNAL_ID, PUBLIC_KEY_HASH | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| STATE_PARTY | OUTPUT_INDEX, TRANSACTION_ID, ID, PUBLIC_KEY_HASH, X500_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | VAULT_FUNGIBLE_STATES | OUTPUT_INDEX, TRANSACTION_ID, ISSUER_NAME, ISSUER_REF, OWNER_NAME, QUANTITY | +-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | VAULT_FUNGIBLE_STATES_PARTS | OUTPUT_INDEX, TRANSACTION_ID, PARTICIPANTS | @@ -114,3 +118,5 @@ By default, the node database has the following tables: +-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | VAULT_TRANSACTION_NOTES | SEQ_NO, NOTE, TRANSACTION_ID | +-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| V_PKEY_HASH_EX_ID_MAP | ID, PUBLIC_KEY_HASH, TRANSACTION_ID, OUTPUT_INDEX, EXTERNAL_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 0cbaa360de..c7934a5c02 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -11,6 +11,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY import org.bouncycastle.operator.ContentSigner +import org.hibernate.annotations.Type import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey @@ -50,13 +51,14 @@ class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identi @Id @GeneratedValue @Column(name = "id", unique = true, nullable = false) - var key: Long? = null, + val key: Long?, @Column(name = "external_id", nullable = false) - var externalId: UUID, + @Type(type = "uuid-char") + val externalId: UUID, @Column(name = "public_key_hash", nullable = false) - var publicKeyHash: String + val publicKeyHash: String ) { constructor(accountId: UUID, publicKey: PublicKey) : this(null, accountId, publicKey.toStringShort()) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index b2cf297557..586ea4bfdf 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -167,14 +167,14 @@ object VaultSchemaV1 : MappedSchema( @Id @GeneratedValue @Column(name = "id", unique = true, nullable = false) - var id: Long? = null, + val id: Long?, // Foreign key. @Column(name = "state_ref") - var stateRef: PersistentStateRef, + val stateRef: PersistentStateRef, @Column(name = "public_key_hash", nullable = false) - var publicKeyHash: String, + val publicKeyHash: String, @Column(name = "x500_name", nullable = true) var x500Name: AbstractParty? = null @@ -190,17 +190,18 @@ object VaultSchemaV1 : MappedSchema( @Id @GeneratedValue @Column(name = "id", unique = true, nullable = false) - var id: Long? = null, + val id: Long, // Foreign key. @Column(name = "state_ref") - var stateRef: PersistentStateRef, + val stateRef: PersistentStateRef, @Column(name = "public_key_hash") - var publicKeyHash: String, + val publicKeyHash: String, @Column(name = "external_id") - var externalId: UUID + @Type(type = "uuid-char") + val externalId: UUID ) : StatePersistable } diff --git a/node/src/main/resources/migration/vault-schema.changelog-v8.xml b/node/src/main/resources/migration/vault-schema.changelog-v8.xml index 3f7adda2c2..b52eb27d92 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-v8.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-v8.xml @@ -6,17 +6,27 @@ - - + + + + + + - - - + + + + + + + + + From b7d04b1c6ef47b79f1045a8680001678e70fb397 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Mon, 26 Nov 2018 17:11:05 +0000 Subject: [PATCH 3/8] [CORDA-2235]: Add overrides for network parameters via command line and file (#4279) * Temp commit * Print the error message first by default, makes error output more natural. * Polishing * Further modifications after testing * Documentation updates * Couple of fixes after review * Removing unnecessary tests * Fix broken test * Add interface to bootstrapper for testign * Added unit tests * Remove unused class * Fix up bootstrapper unit tests and add a couple more * Refactor the tests slightly * Review comments * Couple of minor tweaks --- build.gradle | 1 + .../parsing/internal/Configuration.kt | 2 +- .../net/corda/core/node/NetworkParameters.kt | 20 +- docs/source/corda-configuration-file.rst | 8 +- docs/source/network-bootstrapper.rst | 230 +++++++++++----- .../internal/network/NetworkBootstrapper.kt | 110 ++++---- .../network/NetworkBootstrapperTest.kt | 82 ++++-- .../net/corda/node/NodeCmdLineOptions.kt | 1 - .../net/corda/node/internal/NodeStartup.kt | 14 +- .../node/internal/NetworkParametersTest.kt | 2 +- testing/test-utils/build.gradle | 2 +- .../testing/core/JarSignatureTestUtils.kt | 6 + tools/bootstrapper/build.gradle | 3 + .../kotlin/net/corda/bootstrapper/Main.kt | 151 +++++------ .../NetworkParameterOverridesSpec.kt | 103 +++++++ ...kBootstrapperBackwardsCompatibilityTest.kt | 5 + .../NetworkBootstrapperRunnerTest.kt | 5 - .../NetworkBootstrapperRunnerTests.kt | 255 ++++++++++++++++++ .../bootstrapper/PackageOwnerParsingTest.kt | 164 ----------- .../src/test/resources/alice-network.conf | 8 + .../src/test/resources/correct-network.conf | 4 + .../src/test/resources/package-overlap.conf | 14 + .../net/corda/cliutils/CordaCliWrapper.kt | 10 +- .../cliutils/InstallShellExtensionsParser.kt | 2 +- 24 files changed, 787 insertions(+), 415 deletions(-) create mode 100644 tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkParameterOverridesSpec.kt create mode 100644 tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperBackwardsCompatibilityTest.kt delete mode 100644 tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt create mode 100644 tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt delete mode 100644 tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt create mode 100644 tools/bootstrapper/src/test/resources/alice-network.conf create mode 100644 tools/bootstrapper/src/test/resources/correct-network.conf create mode 100644 tools/bootstrapper/src/test/resources/package-overlap.conf diff --git a/build.gradle b/build.gradle index b06d4c0b21..23797293b1 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ buildscript { ext.fileupload_version = '1.3.3' ext.junit_version = '4.12' ext.mockito_version = '2.18.3' + ext.mockito_kotlin_version = '1.5.0' ext.hamkrest_version = '1.4.2.2' ext.jopt_simple_version = '5.0.2' ext.jansi_version = '1.14' diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt index 9169f18fad..7883f82586 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt @@ -448,7 +448,7 @@ object Configuration { override fun toString(): String { - return "(keyName='$keyName', typeName='$typeName', path=$path, message='$message')" + return "$message: (keyName='$keyName', typeName='$typeName', path=$path)" } /** diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index d43311b07f..8add8d07df 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -107,8 +107,8 @@ data class NetworkParameters( private fun owns(packageName: String, fullClassName: String) = fullClassName.startsWith("$packageName.", ignoreCase = true) // Make sure that packages don't overlap so that ownership is clear. - private fun noOverlap(packages: Collection) = packages.all { currentPackage -> - packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.startsWith("${currentPackage}.") } + fun noOverlap(packages: Collection) = packages.all { currentPackage -> + packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.startsWith("$currentPackage.") } } private fun KProperty1.isAutoAcceptable(): Boolean { @@ -117,14 +117,14 @@ data class NetworkParameters( } init { - require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" } + require(minimumPlatformVersion > 0) { "Minimum platform level must be at least 1" } require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" } - require(epoch > 0) { "epoch must be at least 1" } - require(maxMessageSize > 0) { "maxMessageSize must be at least 1" } - require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" } - require(!eventHorizon.isNegative) { "eventHorizon must be positive value" } + require(epoch > 0) { "Epoch must be at least 1" } + require(maxMessageSize > 0) { "Maximum message size must be at least 1" } + require(maxTransactionSize > 0) { "Maximum transaction size must be at least 1" } + require(!eventHorizon.isNegative) { "Event Horizon must be a positive value" } packageOwnership.keys.forEach(::requirePackageValid) - require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." } + require(noOverlap(packageOwnership.keys)) { "Multiple packages added to the packageOwnership overlap." } } fun copy(minimumPlatformVersion: Int, @@ -217,3 +217,7 @@ data class NotaryInfo(val identity: Party, val validating: Boolean) * version. */ class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) + +private fun KProperty1.isAutoAcceptable(): Boolean { + return this.findAnnotation() != null +} \ No newline at end of file diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index bbf5dc8006..420e8a7f89 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -288,7 +288,7 @@ Configuring a node where the Corda Compatibility Zone's registration and Network .. literalinclude:: example-code/src/main/resources/example-node-with-networkservices.conf -Fields Override +Fields override --------------- JVM options or environmental variables prefixed with ``corda.`` can override ``node.conf`` fields. Provided system properties can also set values for absent fields in ``node.conf``. @@ -299,7 +299,7 @@ This is an example of adding/overriding the keyStore password : java -Dcorda.rpcSettings.ssl.keyStorePassword=mypassword -jar node.jar -CRL Configuration +CRL configuration ----------------- The Corda Network provides an endpoint serving an empty certificate revocation list for the TLS-level certificates. This is intended for deployments that do not provide a CRL infrastructure but still require a strict CRL mode checking. @@ -318,7 +318,9 @@ Together with the above configuration `tlsCertCrlIssuer` option needs to be set This set-up ensures that the TLS-level certificates are embedded with the CRL distribution point referencing the CRL issued by R3. In cases where a proprietary CRL infrastructure is provided those values need to be changed accordingly. -Hiding Sensitive Data +.. _corda-configuration-hiding-sensitive-data: + +Hiding sensitive data --------------------- A frequent requirement is that configuration files must not expose passwords to unauthorised readers. By leveraging environment variables, it is possible to hide passwords and other similar fields. diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index a8bc5b0c97..d386afc0eb 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -15,7 +15,7 @@ In addition to the network map, all the nodes must also use the same set of netw which guarantee interoperability between the nodes. The HTTP network map distributes the network parameters which are downloaded automatically by the nodes. In the absence of this the network parameters must be generated locally. -For these reasons, test deployments can avail themselves of the network bootstrapper. This is a tool that scans all the +For these reasons, test deployments can avail themselves of the Network Bootstrapper. This is a tool that scans all the node configurations from a common directory to generate the network parameters file, which is then copied to all the nodes' directories. It also copies each node's node-info file to every other node so that they can all be visible to each other. @@ -41,7 +41,7 @@ For example running the command on a directory containing these files: └── partyb_node.conf // Party B's node.conf file will generate directories containing three nodes: ``notary``, ``partya`` and ``partyb``. They will each use the ``corda.jar`` -that comes with the bootstrapper. If a different version of Corda is required then simply place that ``corda.jar`` file +that comes with the Network Bootstrapper. If a different version of Corda is required then simply place that ``corda.jar`` file alongside the configuration files in the directory. You can also have the node directories containing their "node.conf" files already laid out. The previous example would be: @@ -56,7 +56,7 @@ You can also have the node directories containing their "node.conf" files alread └── partyb └── node.conf -Similarly, each node directory may contain its own ``corda.jar``, which the bootstrapper will use instead. +Similarly, each node directory may contain its own ``corda.jar``, which the Bootstrapper will use instead. Providing CorDapps to the Network Bootstrapper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -87,7 +87,7 @@ Any CorDapps provided when bootstrapping a network will be scanned for contracts The CorDapp JARs will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`). -By default the bootstrapper will whitelist all the contracts found in the unsigned CorDapp JARs (a JAR file not signed by jarSigner tool). +By default the Bootstrapper will whitelist all the contracts found in the unsigned CorDapp JARs (a JAR file not signed by jarSigner tool). Whitelisted contracts are checked by `Zone constraints`, while contract classes from signed JARs will be checked by `Signature constraints`. To prevent certain contracts from unsigned JARs from being whitelisted, add their fully qualified class name in the ``exclude_whitelist.txt``. These will instead use the more restrictive ``HashAttachmentConstraint``. @@ -103,7 +103,7 @@ For example: Modifying a bootstrapped network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The network bootstrapper is provided as a development tool for setting up Corda networks for development and testing. +The Network Bootstrapper is provided as a development tool for setting up Corda networks for development and testing. There is some limited functionality which can be used to make changes to a network, but for anything more complicated consider using a :doc:`network-map` server. @@ -115,7 +115,7 @@ the nodes are being run on different machines you need to do the following: * Run the Network Bootstrapper from the root directory * Copy each individual node's directory back to the original machine -The network bootstrapper cannot dynamically update the network if an existing node has changed something in their node-info, +The Network Bootstrapper cannot dynamically update the network if an existing node has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed in the other nodes' ``additional-node-infos`` directory. If the nodes are located on different machines, then a utility such as `rsync `_ can be used so that the nodes can share node-infos. @@ -123,11 +123,11 @@ so that the nodes can share node-infos. Adding a new node to the network -------------------------------- -Running the bootstrapper again on the same network will allow a new node to be added and its +Running the Bootstrapper again on the same network will allow a new node to be added and its node-info distributed to the existing nodes. As an example, if we have an existing bootstrapped network, with a Notary and PartyA and we want to add a PartyB, we -can use the network bootstrapper on the following network structure: +can use the Network Bootstrapper on the following network structure: .. sourcecode:: none @@ -148,7 +148,7 @@ can use the network bootstrapper on the following network structure: │ └── node-info-partya └── partyb_node.conf // the node.conf for the node to be added -Then run the network bootstrapper again from the root dir: +Then run the Network Bootstrapper again from the root dir: ``java -jar network-bootstrapper-VERSION.jar --dir `` @@ -182,19 +182,19 @@ Which will give the following: ├── node-info-partya └── node-info-partyb -The bootstrapper will generate a directory and the ``node-info`` file for PartyB, and will also make sure a copy of each +The Bootstrapper will generate a directory and the ``node-info`` file for PartyB, and will also make sure a copy of each nodes' ``node-info`` file is in the ``additional-node-info`` directory of every node. Any other files in the existing nodes, such a generated keys, will be unaffected. -.. note:: The bootstrapper is provided for test deployments and can only generate information for nodes collected on - the same machine. If a network needs to be updated using the bootstrapper once deployed, the nodes will need +.. note:: The Network Bootstrapper is provided for test deployments and can only generate information for nodes collected on + the same machine. If a network needs to be updated using the Bootstrapper once deployed, the nodes will need collecting back together. Updating the contract whitelist for bootstrapped networks --------------------------------------------------------- If the network already has a set of network parameters defined (i.e. the node directories all contain the same network-parameters -file) then the bootstrapper can be used to append contracts from new CorDapps to the current whitelist. +file) then the Network Bootstrapper can be used to append contracts from new CorDapps to the current whitelist. For example, with the following pre-generated network: .. sourcecode:: none @@ -217,7 +217,7 @@ For example, with the following pre-generated network: │ └── cordapp-a.jar └── cordapp-b.jar // The new cordapp to add to the existing nodes -Then run the network bootstrapper again from the root dir: +Then run the Network Bootstrapper again from the root dir: ``java -jar network-bootstrapper-VERSION.jar --dir `` @@ -247,81 +247,183 @@ To give the following: .. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed. -Package namespace ownership ----------------------------- +Modifying the network parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Package namespace ownership is a Corda security feature that allows a compatibility zone to give ownership of parts of the Java package namespace to registered users (e.g. CorDapp development organisations). -The exact mechanism used to claim a namespace is up to the zone operator. A typical approach would be to accept an SSL -certificate with the domain in it as proof of domain ownership, or to accept an email from that domain. +The Network Bootstrapper creates a network parameters file when bootstrapping a network, using a set of sensible defaults. However, if you would like +to override these defaults when testing, there are two ways of doing this. Options can be overridden via the command line or by supplying a configuration +file. If the same parameter is overridden both by a command line argument and in the configuration file, the command line value +will take precedence. + +Overriding network parameters via command line +---------------------------------------------- + +The ``--minimum-platform-version``, ``--max-message-size``, ``--max-transaction-size`` and ``--event-horizon`` command line parameters can +be used to override the default network parameters. See `Command line options`_ for more information. + +Overriding network parameters via a file +---------------------------------------- + +You can provide a network parameters overrides file using the following syntax: + +``java -jar network-bootstrapper-VERSION.jar --network-parameters-overrides=`` + +Or alternatively, by using the short form version: + +``java -jar network-bootstrapper-VERSION.jar -n=`` + +The network parameter overrides file is a HOCON file with the following fields, all of which are optional. Any field that is not provided will be +ignored. If a field is not provided and you are bootstrapping a new network, a sensible default value will be used. If a field is not provided and you +are updating an existing network, the value in the existing network parameters file will be used. + +.. note:: All fields can be used with placeholders for environment variables. For example: ``${KEY_STORE_PASSWORD}`` would be replaced by the contents of environment +variable ``KEY_STORE_PASSWORD``. See: :ref:`corda-configuration-hiding-sensitive-data` . + +The available configuration fields are listed below: + +:minimumPlatformVersion: The minimum supported version of the Corda platform that is required for nodes in the network. + +:maxMessageSize: The maximum permitted message size, in bytes. This is currently ignored but will be used in a future release. + +:maxTransactionSize: The maximum permitted transaction size, in bytes. + +:eventHorizon: The time after which nodes will be removed from the network map if they have not been seen during this period. This parameter uses + the ``parse`` function on the ``java.time.Duration`` class to interpret the data. See `here `_ + for information on valid inputs. + +:packageOwnership: A list of package owners. See `Package namespace ownership`_ for more information. For each package owner, the following fields + are required: + + :packageName: Java package name (e.g `com.my_company` ). + + :keystore: The path of the keystore file containing the signed certificate. + + :keystorePassword: The password for the given keystore (not to be confused with the key password). + + :keystoreAlias: The alias for the name associated with the certificate to be associated with the package namespace. + +An example configuration file: + +.. parsed-literal:: + + minimumPlatformVersion=4 + maxMessageSize=10485760 + maxTransactionSize=524288000 + eventHorizon="30 days" + packageOwnership=[ + { + packageName="com.example" + keystore="myteststore" + keystorePassword="MyStorePassword" + keystoreAlias="MyKeyAlias" + } + ] + +Package namespace ownership +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Package namespace ownership is a Corda security feature that allows a compatibility zone to give ownership of parts of the Java package +namespace to registered users (e.g. a CorDapp development organisation). The exact mechanism used to claim a namespace is up to the zone +operator. A typical approach would be to accept an SSL certificate with the domain in it as proof of domain ownership, or to accept an email from that domain. .. note:: Read more about *Package ownership* :doc:`here`. A Java package namespace is case insensitive and cannot be a sub-package of an existing registered namespace. See `Naming a Package `_ and `Naming Conventions `_ for guidelines on naming conventions. -Registration of a java package namespace requires creation of a signed certificate as generated by the +The registration of a Java package namespace requires the creation of a signed certificate as generated by the `Java keytool `_. -The following four items are passed as a semi-colon separated string to the ``--register-package-owner`` command: +The packages can be registered by supplying a network parameters override config file via the command line, using the ``--network-parameters-overrides`` command. - 1. Java package name (e.g `com.my_company` ). - 2. Keystore file refers to the full path of the file containing the signed certificate. - 3. Password refers to the key store password (not to be confused with the key password). - 4. Alias refers to the name associated with a certificate containing the public key to be associated with the package namespace. +For each package to be registered, the following are required: -Let's use the `Example CorDapp `_ to initialise a simple network, and then register and unregister a package namespace. +:packageName: Java package name (e.g `com.my_company` ). + +:keystore: The path of the keystore file containing the signed certificate. If a relative path is provided, it is assumed to be relative to the + location of the configuration file. + +:keystorePassword: The password for the given keystore (not to be confused with the key password). + +:keystoreAlias: The alias for the name associated with the certificate to be associated with the package namespace. + +Using the `Example CorDapp `_ as an example, we will initialise a simple network and then register and unregister a package namespace. Checkout the Example CorDapp and follow the instructions to build it `here `_. .. note:: You can point to any existing bootstrapped corda network (this will have the effect of updating the associated network parameters file). -1. Create a new public key to use for signing the java package namespace we wish to register: +#. Create a new public key to use for signing the Java package namespace we wish to register: + + .. code-block:: shell + + $JAVA_HOME/bin/keytool -genkeypair -keystore _teststore -storepass MyStorePassword -keyalg RSA -alias MyKeyAlias -keypass MyKeyPassword -dname "O=Alice Corp, L=Madrid, C=ES" + + This will generate a key store file called ``_teststore`` in the current directory. + +#. Create a ``network-parameters.conf`` file in the same directory, with the following information: + + .. parsed-literal:: + + packageOwnership=[ + { + packageName="com.example" + keystore="_teststore" + keystorePassword="MyStorePassword" + keystoreAlias="MyKeyAlias" + } + ] + +#. Register the package namespace to be claimed by the public key generated above: + + .. code-block:: shell + + # Register the Java package namespace using the Network Bootstrapper + java -jar network-bootstrapper.jar --dir build/nodes --network-parameter-overrides=network-parameters.conf + + +#. To unregister the package namespace, edit the ``network-parameters.conf`` file to remove the package: + + .. parsed-literal:: + + packageOwnership=[] + +#. Unregister the package namespace: + + .. code-block:: shell + + # Unregister the Java package namespace using the Network Bootstrapper + java -jar network-bootstrapper.jar --dir build/nodes --network-parameter-overrides=network-parameters.conf + +Command line options +~~~~~~~~~~~~~~~~~~~~ + +The Network Bootstrapper can be started with the following command line options: .. code-block:: shell - $JAVA_HOME/bin/keytool -genkeypair -keystore _teststore -storepass MyStorePassword -keyalg RSA -alias MyKeyAlias -keypass MyKeyPassword -dname "O=Alice Corp, L=Madrid, C=ES" - -This will generate a key store file called ``_teststore`` in the current directory. - -2. Register the package namespace to be claimed by the public key generated above: - -.. code-block:: shell - - # Register the java package namespace using the bootstrapper tool - java -jar network-bootstrapper.jar --dir build/nodes --register-package-owner com.example;./_teststore;MyStorePassword;MyKeyAlias - -3. Unregister the package namespace: - -.. code-block:: shell - - # Unregister the java package namespace using the bootstrapper tool - java -jar network-bootstrapper.jar --dir build/nodes --unregister-package-owner com.example - -Command-line options --------------------- - -The network bootstrapper can be started with the following command-line options: - -.. code-block:: shell - - bootstrapper [-hvV] [--no-copy] [--dir=] [--logging-level=] - [--minimum-platform-version=] - [--register-package-owner java-package-namespace=keystore-file:password:alias] - [--unregister-package-owner java-package-namespace] - [COMMAND] + bootstrapper [-hvV] [--no-copy] [--dir=] [--event-horizon=] + [--logging-level=] + [--max-message-size=] + [--max-transaction-size=] + [--minimum-platform-version=] + [-n=] [COMMAND] * ``--dir=``: Root directory containing the node configuration files and CorDapp JARs that will form the test network. - It may also contain existing node directories. Defaults to the current directory. + It may also contain existing node directories. Defaults to the current directory. * ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. * ``--help``, ``-h``: Show this help message and exit. * ``--version``, ``-V``: Print version information and exit. -* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters. -* ``--register-package-owner``: Register a java package namespace with its owners public key. -* ``--unregister-package-owner``: Unregister a java package namespace. +* ``--minimum-platform-version``: The minimum platform version to use in the network-parameters. +* ``--max-message-size``: The maximum message size to use in the network-parameters, in bytes. +* ``--max-transaction-size``: The maximum transaction size to use in the network-parameters, in bytes. +* ``--event-horizon``: The event horizon to use in the network-parameters. +* ``--network-parameter-overrides=``, ``-n=`: Overrides the default network parameters with those + in the given file. See `Overriding network parameters via a file`_ for more information. + Sub-commands -^^^^^^^^^^^^ - -``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. +------------ +``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 11f04dc97b..92d5e72751 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -3,7 +3,6 @@ package net.corda.nodeapi.internal.network import com.typesafe.config.Config import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory -import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* @@ -19,7 +18,6 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.days -import net.corda.core.utilities.filterNotNullValues import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.* @@ -36,6 +34,7 @@ import java.nio.file.FileAlreadyExistsException import java.nio.file.Path import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.security.PublicKey +import java.time.Duration import java.time.Instant import java.util.* import java.util.concurrent.Executors @@ -56,7 +55,7 @@ class NetworkBootstrapper internal constructor(private val initSerEnv: Boolean, private val embeddedCordaJar: () -> InputStream, private val nodeInfosGenerator: (List) -> List, - private val contractsJarConverter: (Path) -> ContractsJar) { + private val contractsJarConverter: (Path) -> ContractsJar) : NetworkBootstrapperWithOverridableParameters { constructor() : this( initSerEnv = true, @@ -128,6 +127,9 @@ internal constructor(private val initSerEnv: Boolean, throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.") } } + + const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760 + const val DEFAULT_MAX_TRANSACTION_SIZE: Int = 524288000 } sealed class NotaryCluster { @@ -189,14 +191,15 @@ internal constructor(private val initSerEnv: Boolean, } /** Entry point for the tool */ - fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership: Map = emptyMap()) { - require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" } - // Don't accidently include the bootstrapper jar as a CorDapp! + override fun bootstrap(directory: Path, copyCordapps: Boolean, networkParameterOverrides: NetworkParametersOverrides) { + require(networkParameterOverrides.minimumPlatformVersion == null || networkParameterOverrides.minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" } + // Don't accidentally include the bootstrapper jar as a CorDapp! val bootstrapperJar = javaClass.location.toPath() val cordappJars = directory.list { paths -> - paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList() + paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" } + .toList() } - bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion, packageOwnership = packageOwnership) + bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, networkParametersOverrides = networkParameterOverrides) } private fun bootstrap( @@ -204,8 +207,7 @@ internal constructor(private val initSerEnv: Boolean, cordappJars: List, copyCordapps: Boolean, fromCordform: Boolean, - minimumPlatformVersion: Int = PLATFORM_VERSION, - packageOwnership: Map = emptyMap() + networkParametersOverrides: NetworkParametersOverrides = NetworkParametersOverrides() ) { directory.createDirectories() println("Bootstrapping local test network in $directory") @@ -250,8 +252,9 @@ internal constructor(private val initSerEnv: Boolean, println("Gathering notary identities") val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs) println("Generating contract implementations whitelist") + // Only add contracts to the whitelist from unsigned jars val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter)) - val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion, packageOwnership) + val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, networkParametersOverrides) if (newNetParams != existingNetParams) { println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams") } else { @@ -283,7 +286,8 @@ internal constructor(private val initSerEnv: Boolean, println("Generating node directory for $nodeName") val nodeDir = (directory / nodeName).createDirectories() confFile.copyTo(nodeDir / "node.conf", REPLACE_EXISTING) - webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.copyTo(nodeDir / "web-server.conf", REPLACE_EXISTING) + webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName } + ?.copyTo(nodeDir / "web-server.conf", REPLACE_EXISTING) cordaJar.copyToDirectory(nodeDir, REPLACE_EXISTING) } @@ -378,50 +382,42 @@ internal constructor(private val initSerEnv: Boolean, throw IllegalStateException(msg.toString()) } + private fun defaultNetworkParametersWith(notaryInfos: List, + whitelist: Map>): NetworkParameters { + return NetworkParameters( + minimumPlatformVersion = PLATFORM_VERSION, + notaries = notaryInfos, + modifiedTime = Instant.now(), + maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE, + maxTransactionSize = DEFAULT_MAX_TRANSACTION_SIZE, + whitelistedContractImplementations = whitelist, + packageOwnership = emptyMap(), + epoch = 1, + eventHorizon = 30.days + ) + } + private fun installNetworkParameters( notaryInfos: List, whitelist: Map>, existingNetParams: NetworkParameters?, nodeDirs: List, - minimumPlatformVersion: Int, - packageOwnership: Map + networkParametersOverrides: NetworkParametersOverrides ): NetworkParameters { - // TODO Add config for maxMessageSize and maxTransactionSize val netParams = if (existingNetParams != null) { - if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos && - existingNetParams.packageOwnership.entries.containsAll(packageOwnership.entries)) { - existingNetParams - } else { - var updatePackageOwnership = mutableMapOf(*existingNetParams.packageOwnership.map { Pair(it.key, it.value) }.toTypedArray()) - packageOwnership.forEach { key, value -> - if (value == null) { - if (updatePackageOwnership.remove(key) != null) - println("Unregistering package $key") - } else { - if (updatePackageOwnership.put(key, value) == null) - println("Registering package $key for owner ${value.toStringShort()}") - } - } - existingNetParams.copy( - notaries = notaryInfos, + val newNetParams = existingNetParams + .copy(notaries = notaryInfos, whitelistedContractImplementations = whitelist) + .overrideWith(networkParametersOverrides) + if (newNetParams != existingNetParams) { + newNetParams.copy( modifiedTime = Instant.now(), - whitelistedContractImplementations = whitelist, - packageOwnership = updatePackageOwnership, epoch = existingNetParams.epoch + 1 ) + } else { + existingNetParams } } else { - NetworkParameters( - minimumPlatformVersion = minimumPlatformVersion, - notaries = notaryInfos, - modifiedTime = Instant.now(), - maxMessageSize = 10485760, - maxTransactionSize = 524288000, - whitelistedContractImplementations = whitelist, - packageOwnership = packageOwnership.filterNotNullValues(), - epoch = 1, - eventHorizon = 30.days - ) + defaultNetworkParametersWith(notaryInfos, whitelist).overrideWith(networkParametersOverrides) } val copier = NetworkParametersCopier(netParams, overwriteFile = true) nodeDirs.forEach(copier::install) @@ -435,7 +431,7 @@ internal constructor(private val initSerEnv: Boolean, // Nodes which are part of a distributed notary have a second identity which is the composite identity of the // cluster and is shared by all the other members. This is the notary identity. 2 -> legalIdentities[1] - else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") + else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenario: $this") } } @@ -464,3 +460,27 @@ internal constructor(private val initSerEnv: Boolean, } } } + +fun NetworkParameters.overrideWith(override: NetworkParametersOverrides): NetworkParameters { + return this.copy( + minimumPlatformVersion = override.minimumPlatformVersion ?: this.minimumPlatformVersion, + maxMessageSize = override.maxMessageSize ?: this.maxMessageSize, + maxTransactionSize = override.maxTransactionSize ?: this.maxTransactionSize, + eventHorizon = override.eventHorizon ?: this.eventHorizon, + packageOwnership = override.packageOwnership?.map { it.javaPackageName to it.publicKey }?.toMap() ?: this.packageOwnership + ) +} + +data class PackageOwner(val javaPackageName: String, val publicKey: PublicKey) + +data class NetworkParametersOverrides( + val minimumPlatformVersion: Int? = null, + val maxMessageSize: Int? = null, + val maxTransactionSize: Int? = null, + val packageOwnership: List? = null, + val eventHorizon: Duration? = null +) + +interface NetworkBootstrapperWithOverridableParameters { + fun bootstrap(directory: Path, copyCordapps: Boolean, networkParameterOverrides: NetworkParametersOverrides = NetworkParametersOverrides()) +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index ac5e5b7104..6590ead935 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -11,9 +11,12 @@ import net.corda.core.serialization.serialize import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.core.internal.NODE_INFO_DIRECTORY +import net.corda.core.utilities.days import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig +import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_MESSAGE_SIZE +import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_TRANSACTION_SIZE import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX import net.corda.testing.core.* import net.corda.testing.internal.createNodeInfoAndSigned @@ -26,6 +29,7 @@ import org.junit.rules.ExpectedException import org.junit.rules.TemporaryFolder import java.nio.file.Path import java.security.PublicKey +import java.time.Duration import kotlin.streams.toList class NetworkBootstrapperTest { @@ -210,6 +214,24 @@ class NetworkBootstrapperTest { assertThat(networkParameters.epoch).isEqualTo(2) } + @Test + fun `network parameters overrides`() { + createNodeConfFile("alice", aliceConfig) + val minimumPlatformVersion = 2 + val maxMessageSize = 10000 + val maxTransactionSize = 20000 + val eventHorizon = 7.days + bootstrap(minimumPlatformVerison = minimumPlatformVersion, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + eventHorizon = eventHorizon) + val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig) + assertThat(networkParameters.minimumPlatformVersion).isEqualTo(minimumPlatformVersion) + assertThat(networkParameters.maxMessageSize).isEqualTo(maxMessageSize) + assertThat(networkParameters.maxTransactionSize).isEqualTo(maxTransactionSize) + assertThat(networkParameters.eventHorizon).isEqualTo(eventHorizon) + } + private val ALICE = TestIdentity(ALICE_NAME, 70) private val BOB = TestIdentity(BOB_NAME, 80) @@ -230,7 +252,7 @@ class NetworkBootstrapperTest { assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) // register additional package name createNodeConfFile("bob", bobConfig) - bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey))) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) assertContainsPackageOwner("bob", mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) } @@ -243,8 +265,8 @@ class NetworkBootstrapperTest { // register overlapping package name createNodeConfFile("bob", bobConfig) expectedEx.expect(IllegalArgumentException::class.java) - expectedEx.expectMessage("multiple packages added to the packageOwnership overlap.") - bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey))) + expectedEx.expectMessage("Multiple packages added to the packageOwnership overlap.") + bootstrap(packageOwnership = mapOf(Pair(greedyNamespace, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) } @Test @@ -253,7 +275,7 @@ class NetworkBootstrapperTest { bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey))) assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) // unregister package name - bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null))) + bootstrap(packageOwnership = emptyMap()) assertContainsPackageOwner("alice", emptyMap()) } @@ -262,8 +284,8 @@ class NetworkBootstrapperTest { createNodeConfFile("alice", aliceConfig) bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) // unregister package name - bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null))) - assertContainsPackageOwner("alice", mapOf(Pair(bobPackageName, BOB.publicKey))) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey))) + assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) } @Test @@ -271,19 +293,10 @@ class NetworkBootstrapperTest { createNodeConfFile("alice", aliceConfig) bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) // unregister all package names - bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(bobPackageName, null))) + bootstrap(packageOwnership = emptyMap()) assertContainsPackageOwner("alice", emptyMap()) } - @Test - fun `register and unregister sample package namespace in network`() { - createNodeConfFile("alice", aliceConfig) - bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(alicePackageName, null))) - assertContainsPackageOwner("alice", emptyMap()) - bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(alicePackageName, ALICE.publicKey))) - assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) - } - private val rootDir get() = tempFolder.root.toPath() private fun fakeFileBytes(writeToFile: Path? = null): ByteArray { @@ -292,9 +305,20 @@ class NetworkBootstrapperTest { return bytes } - private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map = emptyMap()) { + private fun bootstrap(copyCordapps: Boolean = true, + packageOwnership: Map? = emptyMap(), + minimumPlatformVerison: Int? = PLATFORM_VERSION, + maxMessageSize: Int? = DEFAULT_MAX_MESSAGE_SIZE, + maxTransactionSize: Int? = DEFAULT_MAX_TRANSACTION_SIZE, + eventHorizon: Duration? = 30.days) { providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null } - bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership) + bootstrapper.bootstrap(rootDir, copyCordapps, NetworkParametersOverrides( + minimumPlatformVersion = minimumPlatformVerison, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + eventHorizon = eventHorizon, + packageOwnership = packageOwnership?.map { PackageOwner(it.key, it.value!!) } + )) } private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) { @@ -320,19 +344,23 @@ class NetworkBootstrapperTest { return cordappBytes } - private val Path.networkParameters: NetworkParameters get() { - return (this / NETWORK_PARAMS_FILE_NAME).readObject().verifiedNetworkParametersCert(DEV_ROOT_CA.certificate) - } + private val Path.networkParameters: NetworkParameters + get() { + return (this / NETWORK_PARAMS_FILE_NAME).readObject() + .verifiedNetworkParametersCert(DEV_ROOT_CA.certificate) + } - private val Path.nodeInfoFile: Path get() { - return list { it.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.toList() }.single() - } + private val Path.nodeInfoFile: Path + get() { + return list { it.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.toList() }.single() + } private val Path.nodeInfo: NodeInfo get() = nodeInfoFile.readObject().verified() - private val Path.fakeNodeConfig: FakeNodeConfig get() { - return ConfigFactory.parseFile((this / "node.conf").toFile()).parseAs(FakeNodeConfig::class) - } + private val Path.fakeNodeConfig: FakeNodeConfig + get() { + return ConfigFactory.parseFile((this / "node.conf").toFile()).parseAs(FakeNodeConfig::class) + } private fun assertBootstrappedNetwork(cordaJar: ByteArray, vararg nodes: Pair): NetworkParameters { val networkParameters = (rootDir / nodes[0].first).networkParameters diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index 5a9a0788ae..c4e3d64813 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -49,7 +49,6 @@ open class SharedNodeCmdLineOptions { var devMode: Boolean? = null open fun parseConfiguration(configuration: Config): Valid { - val option = Configuration.Validation.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL) return configuration.parseAsNodeConfiguration(option) } diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 2c9344a087..eeaa5140f0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -143,21 +143,19 @@ open class NodeStartup : NodeStartupLogging { val configuration = cmdLineOptions.parseConfiguration(rawConfig).doIfValid { logRawConfig(rawConfig) }.doOnErrors(::logConfigurationErrors).optional ?: return ExitCodes.FAILURE // Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization. - attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success - ?: return ExitCodes.FAILURE + if (attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } !is Try.Success) return ExitCodes.FAILURE // Step 7. Any actions required before starting up the Corda network layer. - attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success - ?: return ExitCodes.FAILURE + if (attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) !is Try.Success) return ExitCodes.FAILURE // Step 8. Log startup info. logStartupInfo(versionInfo, configuration) // Step 9. Start node: create the node, check for other command-line options, add extra logging etc. - attempt { - cmdLineOptions.baseDirectory.createDirectories() - afterNodeInitialisation.run(createNode(configuration, versionInfo)) - }.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE + if (attempt { + cmdLineOptions.baseDirectory.createDirectories() + afterNodeInitialisation.run(createNode(configuration, versionInfo)) + }.doOnException(::handleStartError) !is Try.Success) return ExitCodes.FAILURE return ExitCodes.SUCCESS } diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index 6546ab887c..875f3c6715 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -109,7 +109,7 @@ class NetworkParametersTest { "com.example.stuff" to key2 ) ) - }.withMessage("multiple packages added to the packageOwnership overlap.") + }.withMessage("Multiple packages added to the packageOwnership overlap.") val params = NetworkParameters(1, emptyList(), diff --git a/testing/test-utils/build.gradle b/testing/test-utils/build.gradle index 715c795317..05266a7ef4 100644 --- a/testing/test-utils/build.gradle +++ b/testing/test-utils/build.gradle @@ -24,7 +24,7 @@ dependencies { // Unit testing helpers. compile "junit:junit:$junit_version" compile 'org.hamcrest:hamcrest-library:1.3' - compile 'com.nhaarman:mockito-kotlin:1.5.0' + compile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version" compile "org.mockito:mockito-core:$mockito_version" compile "org.assertj:assertj-core:$assertj_version" compile "com.natpryce:hamkrest:$hamkrest_version" diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt index fb13a38de8..535fae2c24 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt @@ -3,6 +3,7 @@ package net.corda.testing.core import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.div import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.testing.core.JarSignatureTestUtils.signJar import java.io.FileInputStream import java.nio.file.Path import java.nio.file.Paths @@ -44,6 +45,11 @@ object JarSignatureTestUtils { return ks.getCertificate(alias).publicKey } + fun Path.getPublicKey(alias: String, storePassword: String) : PublicKey { + val ks = loadKeyStore(this.resolve("_teststore"), storePassword) + return ks.getCertificate(alias).publicKey + } + fun Path.getJarSigners(fileName: String) = JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners) } diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle index dc98459edd..3e1a4c8ab3 100644 --- a/tools/bootstrapper/build.gradle +++ b/tools/bootstrapper/build.gradle @@ -7,6 +7,7 @@ description 'Network bootstrapper' dependencies { compile project(':node-api') compile project(':tools:cliutils') + compile project(':common-configuration-parsing') compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" testCompile(project(':test-utils')) { @@ -14,6 +15,8 @@ dependencies { } testCompile(project(':test-cli')) + testCompile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version" + testCompile "org.mockito:mockito-core:$mockito_version" } processResources { diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 0ec24440fd..38dfb7221c 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -1,109 +1,96 @@ package net.corda.bootstrapper +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.ExitCodes +import net.corda.cliutils.printError import net.corda.cliutils.start +import net.corda.common.configuration.parsing.internal.Configuration import net.corda.core.internal.PLATFORM_VERSION -import net.corda.core.internal.requirePackageValid -import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.core.internal.exists import net.corda.nodeapi.internal.network.NetworkBootstrapper -import picocli.CommandLine +import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_MESSAGE_SIZE +import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_TRANSACTION_SIZE +import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridableParameters +import net.corda.nodeapi.internal.network.NetworkParametersOverrides import picocli.CommandLine.Option -import java.io.IOException +import java.io.FileNotFoundException import java.nio.file.Path import java.nio.file.Paths -import java.security.KeyStoreException -import java.security.PublicKey +import java.time.Duration fun main(args: Array) { NetworkBootstrapperRunner().start(args) } -class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") { - @Option( - names = ["--dir"], - description = [ - "Root directory containing the node configuration files and CorDapp JARs that will form the test network.", - "It may also contain existing node directories." - ] - ) +class NetworkBootstrapperRunner(private val bootstrapper: NetworkBootstrapperWithOverridableParameters = NetworkBootstrapper()) : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") { + @Option(names = ["--dir"], + description = [ "Root directory containing the node configuration files and CorDapp JARs that will form the test network.", + "It may also contain existing node directories."]) var dir: Path = Paths.get(".") @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) var noCopy: Boolean = false - @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."]) - var minimumPlatformVersion = PLATFORM_VERSION + @Option(names = ["--minimum-platform-version"], description = ["The minimum platform version to use in the network-parameters. Current default is $PLATFORM_VERSION."]) + var minimumPlatformVersion: Int? = null - @Option(names = ["--register-package-owner"], - converter = [PackageOwnerConverter::class], - description = [ - "Register owner of Java package namespace in the network-parameters.", - "Format: [java-package-namespace;keystore-file;password;alias]", - " `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace", - " `keystore-file` refers to the location of key store file containing the signed certificate as generated by the Java 'keytool' tool (see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html)", - " `password` to open the key store", - " `alias` refers to the name associated with a certificate containing the public key to be associated with the package namespace" - ]) - var registerPackageOwnership: List = mutableListOf() + @Option(names = ["--max-message-size"], description = ["The maximum message size to use in the network-parameters, in bytes. Current default is $DEFAULT_MAX_MESSAGE_SIZE."]) + var maxMessageSize: Int? = null - @Option(names = ["--unregister-package-owner"], - description = [ - "Unregister owner of Java package namespace in the network-parameters.", - "Format: [java-package-namespace]", - " `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace" - ]) - var unregisterPackageOwnership: List = mutableListOf() + @Option(names = ["--max-transaction-size"], description = ["The maximum transaction size to use in the network-parameters, in bytes. Current default is $DEFAULT_MAX_TRANSACTION_SIZE."]) + var maxTransactionSize: Int? = null + + @Option(names = ["--event-horizon"], description = ["The event horizon to use in the network-parameters. Default is 30 days."]) + var eventHorizon: Duration? = null + + @Option(names = ["--network-parameter-overrides", "-n"], description = ["Overrides the default network parameters with those in the given file."]) + var networkParametersFile: Path? = null + + + private fun verifyInputs() { + require(minimumPlatformVersion == null || minimumPlatformVersion ?: 0 > 0) { "The --minimum-platform-version parameter must be at least 1" } + require(eventHorizon == null || eventHorizon?.isNegative == false) { "The --event-horizon parameter must be a positive value" } + require(maxTransactionSize == null || maxTransactionSize ?: 0 > 0) { "The --max-transaction-size parameter must be at least 1" } + require(maxMessageSize == null || maxMessageSize ?: 0 > 0) { "The --max-message-size parameter must be at least 1" } + } + + private fun commandLineOverrides(): Map { + val overrides = mutableMapOf() + overrides += minimumPlatformVersion?.let { mapOf("minimumPlatformVersion" to minimumPlatformVersion!!) } ?: mutableMapOf() + overrides += maxMessageSize?.let { mapOf("maxMessageSize" to maxMessageSize!!) } ?: emptyMap() + overrides += maxTransactionSize?.let { mapOf("maxTransactionSize" to maxTransactionSize!!) } ?: emptyMap() + overrides += eventHorizon?.let { mapOf("eventHorizon" to eventHorizon!!) } ?: emptyMap() + return overrides + } + + private fun getNetworkParametersOverrides(): Valid { + val parseOptions = ConfigParseOptions.defaults() + val config = if (networkParametersFile == null) { + ConfigFactory.empty() + } else { + if (networkParametersFile?.exists() != true) throw FileNotFoundException("Unable to find specified network parameters config file at $networkParametersFile") + ConfigFactory.parseFile(networkParametersFile!!.toFile(), parseOptions) + } + val finalConfig = ConfigFactory.parseMap(commandLineOverrides()).withFallback(config).resolve() + return finalConfig.parseAsNetworkParametersConfiguration() + } + + private fun Collection.pluralise() = if (this.count() > 1) "s" else "" + + private fun reportErrors(errors: Set) { + printError("Error${errors.pluralise()} found parsing the network parameter overrides file at $networkParametersFile:") + errors.forEach { printError("Error parsing ${it.pathAsString}: ${it.message}") } + } override fun runProgram(): Int { - NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), + verifyInputs() + val networkParameterOverrides = getNetworkParametersOverrides().doOnErrors(::reportErrors).optional ?: return ExitCodes.FAILURE + bootstrapper.bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy, - minimumPlatformVersion = minimumPlatformVersion, - packageOwnership = registerPackageOwnership.map { Pair(it.javaPackageName, it.publicKey) }.toMap() - .plus(unregisterPackageOwnership.map { Pair(it, null) }) + networkParameterOverrides = networkParameterOverrides ) - return 0 //exit code - } -} - - -data class PackageOwner(val javaPackageName: String, val publicKey: PublicKey) - -/** - * Converter from String to PackageOwner (String and PublicKey) - */ -class PackageOwnerConverter : CommandLine.ITypeConverter { - override fun convert(packageOwner: String): PackageOwner { - if (!packageOwner.isBlank()) { - val packageOwnerSpec = packageOwner.split(";") - if (packageOwnerSpec.size < 4) - throw IllegalArgumentException("Package owner must specify 4 elements separated by semi-colon: 'java-package-namespace;keyStorePath;keyStorePassword;alias'") - - // java package name validation - val javaPackageName = packageOwnerSpec[0] - requirePackageValid(javaPackageName) - - // cater for passwords that include the argument delimiter field - val keyStorePassword = - if (packageOwnerSpec.size > 4) - packageOwnerSpec.subList(2, packageOwnerSpec.size-1).joinToString(";") - else packageOwnerSpec[2] - try { - val ks = loadKeyStore(Paths.get(packageOwnerSpec[1]), keyStorePassword) - try { - val publicKey = ks.getCertificate(packageOwnerSpec[packageOwnerSpec.size-1]).publicKey - return PackageOwner(javaPackageName,publicKey) - } - catch(kse: KeyStoreException) { - throw IllegalArgumentException("Keystore has not been initialized for alias ${packageOwnerSpec[3]}") - } - } - catch(kse: KeyStoreException) { - throw IllegalArgumentException("Password is incorrect or the key store is damaged for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword") - } - catch(e: IOException) { - throw IllegalArgumentException("Error reading the key store from the file for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword") - } - } - else throw IllegalArgumentException("Must specify package owner argument: 'java-package-namespace;keyStorePath;keyStorePassword;alias'") + return ExitCodes.SUCCESS } } diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkParameterOverridesSpec.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkParameterOverridesSpec.kt new file mode 100644 index 0000000000..ca98d8254f --- /dev/null +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkParameterOverridesSpec.kt @@ -0,0 +1,103 @@ +package net.corda.bootstrapper + +import com.typesafe.config.Config +import net.corda.common.configuration.parsing.internal.Configuration +import net.corda.common.configuration.parsing.internal.get +import net.corda.common.configuration.parsing.internal.mapValid +import net.corda.common.configuration.parsing.internal.nested +import net.corda.common.validation.internal.Validated +import net.corda.core.internal.div +import net.corda.core.internal.requirePackageValid +import net.corda.core.node.NetworkParameters +import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.nodeapi.internal.network.NetworkParametersOverrides +import net.corda.nodeapi.internal.network.PackageOwner +import java.io.IOException +import java.nio.file.InvalidPathException +import java.nio.file.Path +import java.nio.file.Paths +import java.security.KeyStoreException + +internal typealias Valid = Validated + +fun Config.parseAsNetworkParametersConfiguration(options: Configuration.Validation.Options = Configuration.Validation.Options(strict = false)): + Valid = NetworkParameterOverridesSpec.parse(this, options) + +internal fun badValue(msg: String): Valid = Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of(msg)).toSet()) +internal fun valid(value: T): Valid = Validated.valid(value) + +internal object NetworkParameterOverridesSpec : Configuration.Specification("DefaultNetworkParameters") { + private val minimumPlatformVersion by int().mapValid(::parsePositiveInteger).optional() + private val maxMessageSize by int().mapValid(::parsePositiveInteger).optional() + private val maxTransactionSize by int().mapValid(::parsePositiveInteger).optional() + private val packageOwnership by nested(PackageOwnershipSpec).list().optional() + private val eventHorizon by duration().optional() + + internal object PackageOwnershipSpec : Configuration.Specification("PackageOwners") { + private val packageName by string().mapValid(::toPackageName) + private val keystore by string().mapValid(::toPath) + private val keystorePassword by string() + private val keystoreAlias by string() + + override fun parseValid(configuration: Config): Validated { + val suppliedKeystorePath = configuration[keystore] + val keystorePassword = configuration[keystorePassword] + return try { + val javaPackageName = configuration[packageName] + val absoluteKeystorePath = if (suppliedKeystorePath.isAbsolute) { + suppliedKeystorePath + } else { + //If a relative path is supplied, make it relative to the location of the config file + Paths.get(configuration.origin().filename()).resolveSibling(suppliedKeystorePath.toString()) + }.toAbsolutePath() + val ks = loadKeyStore(absoluteKeystorePath, keystorePassword) + return try { + val publicKey = ks.getCertificate(configuration[keystoreAlias]).publicKey + valid(PackageOwner(javaPackageName, publicKey)) + } catch (kse: KeyStoreException) { + badValue("Keystore has not been initialized for alias ${configuration[keystoreAlias]}") + } + } catch (kse: KeyStoreException) { + badValue("Password is incorrect or the key store is damaged for keyStoreFilePath: $suppliedKeystorePath and keyStorePassword: $keystorePassword") + } catch (e: IOException) { + badValue("Error reading the key store from the file for keyStoreFilePath: $suppliedKeystorePath and keyStorePassword: $keystorePassword ${e.message}") + } + } + + private fun toPackageName(rawValue: String): Validated { + return try { + requirePackageValid(rawValue) + valid(rawValue) + } catch (e: Exception) { + return badValue(e.message ?: e.toString()) + } + } + + private fun toPath(rawValue: String): Validated { + return try { + valid(Paths.get(rawValue)) + } catch (e: InvalidPathException) { + return badValue("Path $rawValue not found") + } + } + } + + override fun parseValid(configuration: Config): Valid { + val packageOwnership = configuration[packageOwnership] + if (packageOwnership != null && !NetworkParameters.noOverlap(packageOwnership.map { it.javaPackageName })) { + return Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of("Package namespaces must not overlap", keyName = "packageOwnership", containingPath = listOf())).toSet()) + } + return valid(NetworkParametersOverrides( + minimumPlatformVersion = configuration[minimumPlatformVersion], + maxMessageSize = configuration[maxMessageSize], + maxTransactionSize = configuration[maxTransactionSize], + packageOwnership = packageOwnership, + eventHorizon = configuration[eventHorizon] + )) + } + + private fun parsePositiveInteger(rawValue: Int): Valid { + if (rawValue > 0) return valid(rawValue) + return badValue("The value must be at least 1") + } +} \ No newline at end of file diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperBackwardsCompatibilityTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperBackwardsCompatibilityTest.kt new file mode 100644 index 0000000000..4c288a6390 --- /dev/null +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperBackwardsCompatibilityTest.kt @@ -0,0 +1,5 @@ +package net.corda.bootstrapper + +import net.corda.testing.CliBackwardsCompatibleTest + +class NetworkBootstrapperBackwardsCompatibilityTest : CliBackwardsCompatibleTest(NetworkBootstrapperRunner::class.java) \ No newline at end of file diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt deleted file mode 100644 index 8322c5862f..0000000000 --- a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt +++ /dev/null @@ -1,5 +0,0 @@ -package net.corda.bootstrapper - -import net.corda.testing.CliBackwardsCompatibleTest - -class NetworkBootstrapperRunnerTest : CliBackwardsCompatibleTest(NetworkBootstrapperRunner::class.java) \ No newline at end of file diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt new file mode 100644 index 0000000000..300c9182fd --- /dev/null +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt @@ -0,0 +1,255 @@ +package net.corda.bootstrapper + +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import net.corda.core.internal.copyTo +import net.corda.core.internal.deleteRecursively +import net.corda.core.internal.div +import net.corda.core.utilities.days +import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridableParameters +import net.corda.nodeapi.internal.network.NetworkParametersOverrides +import net.corda.nodeapi.internal.network.PackageOwner +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.JarSignatureTestUtils.generateKey +import net.corda.testing.core.JarSignatureTestUtils.getPublicKey +import org.junit.* +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.io.PrintStream +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.security.PublicKey +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class NetworkBootstrapperRunnerTests { + private val outContent = ByteArrayOutputStream() + private val errContent = ByteArrayOutputStream() + private val originalOut = System.out + private val originalErr = System.err + + @Before + fun setUpStreams() { + System.setOut(PrintStream(outContent)) + System.setErr(PrintStream(errContent)) + } + + @After + fun restoreStreams() { + System.setOut(originalOut) + System.setErr(originalErr) + } + + companion object { + private const val ALICE = "alice" + private const val ALICE_PASS = "alicepass" + + private const val aliceConfigFile = "alice-network.conf" + private const val correctNetworkFile = "correct-network.conf" + private const val packageOverlapConfigFile = "package-overlap.conf" + + private val dirAlice = Files.createTempDirectory(ALICE) + private val dirAliceEC = Files.createTempDirectory("sdfsdfds") + private val dirAliceDSA = Files.createTempDirectory(ALICE) + + private lateinit var alicePublicKey: PublicKey + private lateinit var alicePublicKeyEC: PublicKey + private lateinit var alicePublicKeyDSA: PublicKey + + private val resourceDirectory = Paths.get(".") / "src" / "test" / "resources" + + private fun String.copyToTestDir(dir: Path = dirAlice): Path { + return (resourceDirectory / this).copyTo(dir / this) + } + + @BeforeClass + @JvmStatic + fun beforeClass() { + dirAlice.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString()) + dirAliceEC.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString(), "EC") + dirAliceDSA.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString(), "DSA") + alicePublicKey = dirAlice.getPublicKey(ALICE, ALICE_PASS) + alicePublicKeyEC = dirAliceEC.getPublicKey(ALICE, ALICE_PASS) + alicePublicKeyDSA = dirAliceDSA.getPublicKey(ALICE, ALICE_PASS) + } + + @AfterClass + @JvmStatic + fun afterClass() { + dirAlice.deleteRecursively() + } + } + + private fun getRunner(): Pair { + val mockBootstrapper = mock() + return Pair(NetworkBootstrapperRunner(mockBootstrapper), mockBootstrapper) + } + + @Test + fun `test when defaults are run bootstrapper is called correctly`() { + val (runner, mockBootstrapper) = getRunner() + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides()) + assertEquals(0, exitCode) + } + + @Test + fun `test when base directory is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + val tempDir = createTempDir() + runner.dir = tempDir.toPath() + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(tempDir.toPath().toAbsolutePath().normalize(), true, NetworkParametersOverrides()) + assertEquals(0, exitCode) + } + + @Test + fun `test when copy cordapps is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + runner.noCopy = true + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), false, NetworkParametersOverrides()) + assertEquals(0, exitCode) + } + + @Test + fun `test when min platform version is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + runner.minimumPlatformVersion = 1 + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(minimumPlatformVersion = 1)) + assertEquals(0, exitCode) + } + + @Test + fun `test when min platform version is invalid it fails to run with a sensible error message`() { + val runner = getRunner().first + runner.minimumPlatformVersion = 0 + val exception = assertFailsWith { runner.runProgram() } + assertEquals("The --minimum-platform-version parameter must be at least 1", exception.message) + } + + @Test + fun `test when max message size is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + runner.maxMessageSize = 1 + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(maxMessageSize = 1)) + assertEquals(0, exitCode) + } + + @Test + fun `test when max message size is invalid it fails to run with a sensible error message`() { + val runner = getRunner().first + runner.maxMessageSize = 0 + val exception = assertFailsWith { runner.runProgram() } + assertEquals("The --max-message-size parameter must be at least 1", exception.message) + } + + @Test + fun `test when max transaction size is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + runner.maxTransactionSize = 1 + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(maxTransactionSize = 1)) + assertEquals(0, exitCode) + } + + @Test + fun `test when max transaction size is invalid it fails to run with a sensible error message`() { + val runner = getRunner().first + runner.maxTransactionSize = 0 + val exception = assertFailsWith { runner.runProgram() } + assertEquals("The --max-transaction-size parameter must be at least 1", exception.message) + } + + @Test + fun `test when event horizon is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + runner.eventHorizon = 7.days + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(eventHorizon = 7.days)) + assertEquals(0, exitCode) + } + + @Test + fun `test when event horizon is invalid it fails to run with a sensible error message`() { + val runner = getRunner().first + runner.eventHorizon = (-7).days + val exception = assertFailsWith { runner.runProgram() } + assertEquals("The --event-horizon parameter must be a positive value", exception.message) + } + + @Test + fun `test when a network parameters is specified the values are passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + val conf = correctNetworkFile.copyToTestDir() + runner.networkParametersFile = conf + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + maxMessageSize = 10000, + maxTransactionSize = 2000, + eventHorizon = 5.days, + minimumPlatformVersion = 2 + )) + assertEquals(0, exitCode) + } + + @Test + fun `test when a package is specified in the network parameters file it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + val conf = aliceConfigFile.copyToTestDir() + runner.networkParametersFile = conf + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKey)) + )) + assertEquals(0, exitCode) + } + + @Test + fun `test when a package is specified in the network parameters file it is passed through to the bootstrapper EC`() { + val (runner, mockBootstrapper) = getRunner() + val conf = aliceConfigFile.copyToTestDir(dirAliceEC) + runner.networkParametersFile = conf + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKeyEC)) + )) + assertEquals(0, exitCode) + } + + @Test + fun `test when a package is specified in the network parameters file it is passed through to the bootstrapper DSA`() { + val (runner, mockBootstrapper) = getRunner() + val conf = aliceConfigFile.copyToTestDir(dirAliceDSA) + runner.networkParametersFile = conf + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKeyDSA)) + )) + assertEquals(0, exitCode) + } + + @Test + fun `test when packages overlap that the bootstrapper fails with a sensible message`() { + val (runner, mockBootstrapper) = getRunner() + val conf = packageOverlapConfigFile.copyToTestDir() + runner.networkParametersFile = conf + val exitCode = runner.runProgram() + val output = errContent.toString() + assert(output.contains("Error parsing packageOwnership: Package namespaces must not overlap")) + assertEquals(1, exitCode) + } + + @Test + fun `test when keyfile does not exist then bootstrapper fails with a sensible message`() { + val (runner, mockBootstrapper) = getRunner() + runner.networkParametersFile = dirAlice / "filename-that-doesnt-exist" + val exception = assertFailsWith { runner.runProgram() } + assert(exception.message!!.startsWith("Unable to find specified network parameters config file at")) + } +} \ No newline at end of file diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt deleted file mode 100644 index b0df16c391..0000000000 --- a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt +++ /dev/null @@ -1,164 +0,0 @@ -package net.corda.bootstrapper - -import net.corda.core.internal.deleteRecursively -import net.corda.core.internal.div -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.CHARLIE_NAME -import net.corda.testing.core.JarSignatureTestUtils.generateKey -import org.assertj.core.api.Assertions.assertThat -import org.junit.* -import org.junit.rules.ExpectedException -import picocli.CommandLine -import java.nio.file.Files - -class PackageOwnerParsingTest { - - @Rule - @JvmField - val expectedEx: ExpectedException = ExpectedException.none() - - companion object { - - private const val ALICE = "alice" - private const val ALICE_PASS = "alicepass" - private const val BOB = "bob" - private const val BOB_PASS = "bobpass" - private const val CHARLIE = "charlie" - private const val CHARLIE_PASS = "charliepass" - - private val dirAlice = Files.createTempDirectory(ALICE) - private val dirBob = Files.createTempDirectory(BOB) - private val dirCharlie = Files.createTempDirectory(CHARLIE) - - val networkBootstrapper = NetworkBootstrapperRunner() - val commandLine = CommandLine(networkBootstrapper) - - @BeforeClass - @JvmStatic - fun beforeClass() { - dirAlice.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString()) - dirBob.generateKey(BOB, BOB_PASS, BOB_NAME.toString(), "EC") - dirCharlie.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "DSA") - } - - @AfterClass - @JvmStatic - fun afterClass() { - dirAlice.deleteRecursively() - } - } - - @Test - fun `parse registration request with single mapping`() { - val aliceKeyStorePath = dirAlice / "_teststore" - val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE") - commandLine.parse(*args) - assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo("com.example.stuff") - } - - @Test - fun `parse registration request with invalid arguments`() { - val args = arrayOf("--register-package-owner", "com.!example.stuff") - expectedEx.expect(CommandLine.ParameterException::class.java) - expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon") - commandLine.parse(*args) - } - - @Test - fun `parse registration request with incorrect keystore specification`() { - val aliceKeyStorePath = dirAlice / "_teststore" - val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath$ALICE_PASS") - expectedEx.expect(CommandLine.ParameterException::class.java) - expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon") - commandLine.parse(*args) - } - - @Test - fun `parse registration request with invalid java package name`() { - val args = arrayOf("--register-package-owner", "com.!example.stuff;A;B;C") - expectedEx.expect(CommandLine.ParameterException::class.java) - expectedEx.expectMessage("Invalid Java package name") - commandLine.parse(*args) - } - - @Test - fun `parse registration request with invalid keystore file`() { - val args = arrayOf("--register-package-owner", "com.example.stuff;NONSENSE;B;C") - expectedEx.expect(CommandLine.ParameterException::class.java) - expectedEx.expectMessage("Error reading the key store from the file") - commandLine.parse(*args) - } - - @Test - fun `parse registration request with invalid keystore password`() { - val aliceKeyStorePath = dirAlice / "_teststore" - val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;BAD_PASSWORD;$ALICE") - expectedEx.expect(CommandLine.ParameterException::class.java) - expectedEx.expectMessage("Error reading the key store from the file") - commandLine.parse(*args) - } - - @Test - fun `parse registration request with invalid keystore alias`() { - val aliceKeyStorePath = dirAlice / "_teststore" - val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;BAD_ALIAS") - expectedEx.expect(CommandLine.ParameterException::class.java) - expectedEx.expectMessage("must not be null") - commandLine.parse(*args) - } - - @Test - fun `parse registration request with multiple arguments`() { - val aliceKeyStorePath = dirAlice / "_teststore" - val bobKeyStorePath = dirBob / "_teststore" - val charlieKeyStorePath = dirCharlie / "_teststore" - val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE", - "--register-package-owner", "com.example.more.stuff;$bobKeyStorePath;$BOB_PASS;$BOB", - "--register-package-owner", "com.example.even.more.stuff;$charlieKeyStorePath;$CHARLIE_PASS;$CHARLIE") - commandLine.parse(*args) - assertThat(networkBootstrapper.registerPackageOwnership).hasSize(3) - } - - @Ignore("Ignoring this test as the delimiters don't work correctly, see CORDA-2191") - @Test - fun `parse registration request with delimiter inclusive passwords`() { - val aliceKeyStorePath1 = dirAlice / "_alicestore1" - dirAlice.generateKey("${ALICE}1", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore1") - val aliceKeyStorePath2 = dirAlice / "_alicestore2" - dirAlice.generateKey("${ALICE}2", "\"passw;rd\"", ALICE_NAME.toString(), storeName = "_alicestore2") - val aliceKeyStorePath3 = dirAlice / "_alicestore3" - dirAlice.generateKey("${ALICE}3", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore3") - val aliceKeyStorePath4 = dirAlice / "_alicestore4" - dirAlice.generateKey("${ALICE}4", "\'passw;rd\'", ALICE_NAME.toString(), storeName = "_alicestore4") - val aliceKeyStorePath5 = dirAlice / "_alicestore5" - dirAlice.generateKey("${ALICE}5", "\"\"passw;rd\"\"", ALICE_NAME.toString(), storeName = "_alicestore5") - val packageOwnerSpecs = listOf("net.something0;$aliceKeyStorePath1;passw;rd;${ALICE}1", - "net.something1;$aliceKeyStorePath2;\"passw;rd\";${ALICE}2", - "\"net.something2;$aliceKeyStorePath3;passw;rd;${ALICE}3\"", - "net.something3;$aliceKeyStorePath4;\'passw;rd\';${ALICE}4", - "net.something4;$aliceKeyStorePath5;\"\"passw;rd\"\";${ALICE}5") - packageOwnerSpecs.forEachIndexed { i, packageOwnerSpec -> - commandLine.parse(*arrayOf("--register-package-owner", packageOwnerSpec)) - assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo("net.something$i") - } - } - - @Test - fun `parse unregister request with single mapping`() { - val args = arrayOf("--unregister-package-owner", "com.example.stuff") - commandLine.parse(*args) - assertThat(networkBootstrapper.unregisterPackageOwnership).contains("com.example.stuff") - } - - @Test - fun `parse mixed register and unregister request`() { - val aliceKeyStorePath = dirAlice / "_teststore" - val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE", - "--unregister-package-owner", "com.example.stuff2") - commandLine.parse(*args) - assertThat(networkBootstrapper.registerPackageOwnership.map { it.javaPackageName }).contains("com.example.stuff") - assertThat(networkBootstrapper.unregisterPackageOwnership).contains("com.example.stuff2") - } -} - diff --git a/tools/bootstrapper/src/test/resources/alice-network.conf b/tools/bootstrapper/src/test/resources/alice-network.conf new file mode 100644 index 0000000000..cc15960f82 --- /dev/null +++ b/tools/bootstrapper/src/test/resources/alice-network.conf @@ -0,0 +1,8 @@ +packageOwnership=[ + { + packageName="com.example.stuff" + keystore="_teststore" + keystorePassword="alicepass" + keystoreAlias="alice" + } +] \ No newline at end of file diff --git a/tools/bootstrapper/src/test/resources/correct-network.conf b/tools/bootstrapper/src/test/resources/correct-network.conf new file mode 100644 index 0000000000..d2b302a230 --- /dev/null +++ b/tools/bootstrapper/src/test/resources/correct-network.conf @@ -0,0 +1,4 @@ +minimumPlatformVersion=2 +maxMessageSize=10000 +maxTransactionSize=2000 +eventHorizon="5 days" \ No newline at end of file diff --git a/tools/bootstrapper/src/test/resources/package-overlap.conf b/tools/bootstrapper/src/test/resources/package-overlap.conf new file mode 100644 index 0000000000..db23acf01e --- /dev/null +++ b/tools/bootstrapper/src/test/resources/package-overlap.conf @@ -0,0 +1,14 @@ +packageOwnership=[ + { + packageName="com.example" + keystore="_teststore" + keystorePassword="alicepass" + keystoreAlias="alice" + } + { + packageName="com.example.overlap" + keystore="_teststore" + keystorePassword="alicepass" + keystoreAlias="alice" + } +] \ No newline at end of file diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 0da2f9e44e..209262fc4a 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -53,6 +53,7 @@ object CordaSystemUtils { object ShellConstants { const val RED = "\u001B[31m" + const val YELLOW = "\u001B[33m" const val RESET = "\u001B[0m" } @@ -86,8 +87,8 @@ fun CordaCliWrapper.start(args: Array) { if (this.verbose || this.subCommands().any { it.verbose }) { throwable.printStackTrace() } else { - System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") } + printError(throwable.rootMessage ?: "Use --verbose for more details") exitProcess(ExitCodes.FAILURE) } } @@ -185,11 +186,12 @@ abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperB fun printHelp() = cmd.usage(System.out) - fun printlnErr(message: String) = System.err.println(message) - - fun printlnWarn(message: String) = System.err.println(message) } +fun printWarning(message: String) = System.err.println("${ShellConstants.YELLOW}$message${ShellConstants.RESET}") +fun printError(message: String) = System.err.println("${ShellConstants.RED}$message${ShellConstants.RESET}") + + /** * Useful commonly used constants applicable to many CLI tools */ diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt index 17f5bbe876..2759ca5700 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt @@ -94,7 +94,7 @@ private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { val semanticParts = declaredBashVersion().split(".") semanticParts.firstOrNull()?.toIntOrNull()?.let { major -> if (major < minSupportedBashVersion) { - parent.printlnWarn("Cannot install shell extension for bash major version earlier than $minSupportedBashVersion. Please upgrade your bash version. Aliases should still work.") + printWarning("Cannot install shell extension for bash major version earlier than $minSupportedBashVersion. Please upgrade your bash version. Aliases should still work.") generateAutoCompleteFile = false } } From 88ee343e9561ead81fed07785d39b80d51f83085 Mon Sep 17 00:00:00 2001 From: Milen Dobrinov <16443261+milen-dobrinov@users.noreply.github.com> Date: Mon, 26 Nov 2018 19:31:31 +0200 Subject: [PATCH 4/8] [CORDA-2189] Fix non deterministic manifest file timestamp (#4301) --- .../net/corda/testing/node/internal/TestCordappsUtils.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt index 241a5cc7e3..471142ce60 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt @@ -4,9 +4,11 @@ import io.github.classgraph.ClassGraph import net.corda.core.internal.outputStream import net.corda.node.internal.cordapp.createTestManifest import net.corda.testing.node.TestCordapp +import java.io.BufferedOutputStream import java.nio.file.Path import java.nio.file.attribute.FileTime import java.time.Instant +import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.reflect.KClass @@ -65,8 +67,13 @@ fun TestCordappImpl.packageAsJar(file: Path) { scanResult.use { val manifest = createTestManifest(name, title, version, vendor, targetVersion) - JarOutputStream(file.outputStream(), manifest).use { jos -> + JarOutputStream(file.outputStream()).use { jos -> val time = FileTime.from(Instant.EPOCH) + val manifestEntry = ZipEntry(JarFile.MANIFEST_NAME).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time) + jos.putNextEntry(manifestEntry) + manifest.write(BufferedOutputStream(jos)) + jos.closeEntry() + // The same resource may be found in different locations (this will happen when running from gradle) so just // pick the first one found. scanResult.allResources.asMap().forEach { path, resourceList -> From b881fbd330661a133739048f563ee9c4eb4f458b Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Mon, 26 Nov 2018 19:15:06 +0000 Subject: [PATCH 5/8] Throughput is private in PersistentUniquenessProvider (#4302) --- .../node/services/transactions/PersistentUniquenessProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 71b871109a..56bf0cde4c 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -114,7 +114,7 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste */ private val throughputHistory = SlidingWindowReservoir(100) @Volatile - var throughput: Double = 0.0 + private var throughput: Double = 0.0 /** * Estimated time of request processing. From c533e0ab0ca38d39d055aa9112052213cb69fe7b Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Mon, 26 Nov 2018 20:28:18 +0000 Subject: [PATCH 6/8] CORDA-2255 reduce noise from serialisation warnings (#4299) Suppress warnings when constructing NonConstructible types that are wrapped by Opaque, and reduce warning level to info. Construct wrapped LocalTypeInformation for Opaque types immediately, rather than deferring with a thunk. --- .../amqp/custom/ThrowableSerializer.kt | 3 +- .../internal/model/LocalTypeInformation.kt | 19 +--------- .../model/LocalTypeInformationBuilder.kt | 37 ++++++++++--------- .../internal/model/TypeIdentifier.kt | 4 +- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index b3082a629e..ba93143cd4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -7,7 +7,6 @@ import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.model.LocalConstructorInformation -import net.corda.serialization.internal.model.LocalPropertyInformation import net.corda.serialization.internal.model.LocalTypeInformation import java.io.NotSerializableException @@ -26,7 +25,7 @@ class ThrowableSerializer(factory: LocalSerializerFactory) : CustomSerializer.Pr is LocalTypeInformation.NonComposable -> constructor ?: throw NotSerializableException("$this has no deserialization constructor") is LocalTypeInformation.Composable -> constructor - is LocalTypeInformation.Opaque -> expand.constructor + is LocalTypeInformation.Opaque -> wrapped.constructor else -> throw NotSerializableException("$this has no deserialization constructor") } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt index 9c815fe84f..0d4c555269 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt @@ -91,7 +91,7 @@ sealed class LocalTypeInformation { is LocalTypeInformation.Abstract -> properties is LocalTypeInformation.AnInterface -> properties is LocalTypeInformation.NonComposable -> properties - is LocalTypeInformation.Opaque -> expand.propertiesOrEmptyMap + is LocalTypeInformation.Opaque -> wrapped.propertiesOrEmptyMap else -> emptyMap() } @@ -154,22 +154,7 @@ sealed class LocalTypeInformation { * May in fact be a more complex class, but is treated as if atomic, i.e. we don't further expand its properties. */ data class Opaque(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier, - private val _expand: () -> LocalTypeInformation) : LocalTypeInformation() { - /** - * In some rare cases, e.g. during Exception serialisation, we may want to "look inside" an opaque type. - */ - val expand: LocalTypeInformation by lazy { _expand() } - - // Custom equals / hashcode because otherwise the "expand" lambda makes equality harder to reason about. - override fun equals(other: Any?): Boolean = - other is Cycle && - other.observedType == observedType && - other.typeIdentifier == typeIdentifier - - override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier) - - override fun toString(): String = "Opaque($observedType, $typeIdentifier)" - } + val wrapped: LocalTypeInformation) : LocalTypeInformation() /** * Represents a scalar type such as [Int]. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt index 5e596d6465..b99abb9bda 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt @@ -94,12 +94,11 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, buildInterfaceInformation(type)) type.isInterface -> buildInterface(type, typeIdentifier, emptyList()) type.isAbstractClass -> buildAbstract(type, typeIdentifier, emptyList()) - else -> when { - isOpaque -> LocalTypeInformation.Opaque(type, typeIdentifier) { - buildNonAtomic(type, type, typeIdentifier, emptyList()) - } - else -> buildNonAtomic(type, type, typeIdentifier, emptyList()) - } + isOpaque -> LocalTypeInformation.Opaque( + type, + typeIdentifier, + buildNonAtomic(type, type, typeIdentifier, emptyList(), true)) + else -> buildNonAtomic(type, type, typeIdentifier, emptyList()) } } @@ -118,12 +117,10 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, } rawType.isInterface -> buildInterface(type, typeIdentifier, buildTypeParameterInformation(type)) rawType.isAbstractClass -> buildAbstract(type, typeIdentifier, buildTypeParameterInformation(type)) - else -> when { - isOpaque -> LocalTypeInformation.Opaque(rawType, typeIdentifier) { - buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type)) - } - else -> buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type)) - } + isOpaque -> LocalTypeInformation.Opaque(rawType, + typeIdentifier, + buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type), true)) + else -> buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type)) } } @@ -159,13 +156,15 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, * Rather than throwing an exception if a type is [NonComposable], we capture its type information so that it can * still be used to _serialize_ values, or as the basis for deciding on an evolution strategy. */ - private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List): LocalTypeInformation { + private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List, suppressWarning: Boolean = false): LocalTypeInformation { val superclassInformation = buildSuperclassInformation(type) val interfaceInformation = buildInterfaceInformation(type) val observedConstructor = constructorForDeserialization(type) if (observedConstructor == null) { - logger.warn("No unique deserialisation constructor found for class $rawType, type is marked as non-composable") + if (!suppressWarning) { + logger.info("No unique deserialisation constructor found for class $rawType, type is marked as non-composable") + } return LocalTypeInformation.NonComposable(type, typeIdentifier, null, buildReadOnlyProperties(rawType), superclassInformation, interfaceInformation, typeParameterInformation) } @@ -176,10 +175,12 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val hasNonComposableProperties = properties.values.any { it.type is LocalTypeInformation.NonComposable } if (!propertiesSatisfyConstructor(constructorInformation, properties) || hasNonComposableProperties) { - if (hasNonComposableProperties) { - logger.warn("Type ${type.typeName} has non-composable properties and has been marked as non-composable") - } else { - logger.warn("Properties of type ${type.typeName} do not satisfy its constructor, type has been marked as non-composable") + if (!suppressWarning) { + if (hasNonComposableProperties) { + logger.info("Type ${type.typeName} has non-composable properties and has been marked as non-composable") + } else { + logger.info("Properties of type ${type.typeName} do not satisfy its constructor, type has been marked as non-composable") + } } return LocalTypeInformation.NonComposable(type, typeIdentifier, constructorInformation, properties, superclassInformation, interfaceInformation, typeParameterInformation) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt index 95e4a6132a..5777c96cea 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -90,7 +90,9 @@ sealed class TypeIdentifier { (type.rawType as Class<*>).name, type.ownerType?.let { forGenericType(it) }, type.actualTypeArguments.map { - forGenericType(it.resolveAgainst(resolutionContext)) + val resolved = it.resolveAgainst(resolutionContext) + // Avoid cycles, e.g. Enum where E resolves to Enum + if (resolved == type) UnknownType else forGenericType(resolved) }) is Class<*> -> forClass(type) is GenericArrayType -> ArrayOf(forGenericType(type.genericComponentType.resolveAgainst(resolutionContext))) From 8784e51a8f18918b33a6169038db80bc340ab415 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 27 Nov 2018 09:53:44 +0000 Subject: [PATCH 7/8] CORDA-2248 fix rpc classloader issue (#4288) --- .../java/net/corda/tools/shell/FlowShellCommand.java | 2 +- .../java/net/corda/tools/shell/RunShellCommand.java | 4 ++-- .../java/net/corda/tools/shell/StartShellCommand.java | 2 +- .../kotlin/net/corda/tools/shell/InteractiveShell.kt | 4 ++++ .../net/corda/tools/shell/InteractiveShellCommand.kt | 11 ++++++++++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java index 6fbf4cb003..53bb921d3d 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java @@ -37,7 +37,7 @@ public class FlowShellCommand extends InteractiveShellCommand { @Usage("The data to pass as input") @Argument(unquote = false) List input ) { logger.info("Executing command \"flow start {} {}\",", name, (input != null) ? input.stream().collect(joining(" ")) : ""); - startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper()); + startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper(null)); } // TODO Limit number of flows shown option? diff --git a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java index 763d9181be..0597c2aa61 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java @@ -38,13 +38,13 @@ public class RunShellCommand extends InteractiveShellCommand { @Usage("runs a method from the CordaRPCOps interface on the node.") public Object main(InvocationContext context, @Usage("The command to run") @Argument(unquote = false) List command) { logger.info("Executing command \"run {}\",", (command != null) ? command.stream().collect(joining(" ")) : ""); - StringToMethodCallParser parser = new StringToMethodCallParser<>(CordaRPCOps.class, objectMapper()); + StringToMethodCallParser parser = new StringToMethodCallParser<>(CordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader())); if (command == null) { emitHelp(context, parser); return null; } - return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper()); + return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(InteractiveShell.getCordappsClassloader())); } private void emitHelp(InvocationContext context, StringToMethodCallParser parser) { diff --git a/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java index c37d4b78c3..8a1a1c47c6 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java @@ -23,6 +23,6 @@ public class StartShellCommand extends InteractiveShellCommand { logger.info("Executing command \"start {} {}\",", name, (input != null) ? input.stream().collect(joining(" ")) : ""); ANSIProgressRenderer ansiProgressRenderer = ansiProgressRenderer(); - FlowShellCommand.startFlow(name, input, out, ops(), ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), objectMapper()); + FlowShellCommand.startFlow(name, input, out, ops(), ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), objectMapper(null)); } } \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 8ed572a14a..062d39b878 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -78,6 +78,10 @@ object InteractiveShell { private var classLoader: ClassLoader? = null private lateinit var shellConfiguration: ShellConfiguration private var onExit: () -> Unit = {} + + @JvmStatic + fun getCordappsClassloader() = classLoader + /** * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node * internals. diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt index 6253be2172..5a8aa5d443 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt @@ -1,5 +1,7 @@ package net.corda.tools.shell +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.type.TypeFactory import org.crsh.command.BaseCommand import org.crsh.shell.impl.command.CRaSHSession @@ -9,6 +11,13 @@ import org.crsh.shell.impl.command.CRaSHSession open class InteractiveShellCommand : BaseCommand() { fun ops() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).rpcOps fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer - fun objectMapper() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).yamlInputMapper + fun objectMapper(classLoader: ClassLoader?): ObjectMapper { + val om = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).yamlInputMapper + if (classLoader != null) { + om.typeFactory = TypeFactory.defaultInstance().withClassLoader(classLoader) + } + return om + } + fun isSsh() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).isSsh } From 759fd9b5bbcaca7cc2ecffef852f85b60573eb7c Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Tue, 27 Nov 2018 11:41:05 +0000 Subject: [PATCH 8/8] Update to corda-gradle-plugins v4.0.36 (#4292) * Update to corda-gradle-plugins v4.0.36 (new development key for JAR signing). * Changed maven repo from https://dl.bintray.com/kotlin/kotlin-eap/ to https://kotlin.bintray.com/kotlinx. --- build.gradle | 3 +-- constants.properties | 2 +- samples/simm-valuation-demo/contracts-states/build.gradle | 4 ++++ samples/simm-valuation-demo/flows/build.gradle | 4 ++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 23797293b1..5d94ccfa75 100644 --- a/build.gradle +++ b/build.gradle @@ -86,9 +86,8 @@ buildscript { mavenLocal() mavenCentral() jcenter() - // This repository is needed for Dokka until 0.9.16 is released. maven { - url 'https://dl.bintray.com/kotlin/kotlin-eap/' + url 'https://kotlin.bintray.com/kotlinx' } maven { url "$artifactory_contextUrl/corda-releases" diff --git a/constants.properties b/constants.properties index d0817917ec..f3223f4c44 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.34 +gradlePluginsVersion=4.0.36 kotlinVersion=1.2.71 # ***************************************************************# # When incrementing platformVersion make sure to update # diff --git a/samples/simm-valuation-demo/contracts-states/build.gradle b/samples/simm-valuation-demo/contracts-states/build.gradle index 2849ad30ff..023b072628 100644 --- a/samples/simm-valuation-demo/contracts-states/build.gradle +++ b/samples/simm-valuation-demo/contracts-states/build.gradle @@ -13,6 +13,10 @@ cordapp { // but the jar signer doesn't support that yet. enabled false } + sealing { + // Cannot seal JAR because other module also defines classes in the package net.corda.vega.analytics + enabled false + } } configurations { diff --git a/samples/simm-valuation-demo/flows/build.gradle b/samples/simm-valuation-demo/flows/build.gradle index 4520f321dd..1989c8fb41 100644 --- a/samples/simm-valuation-demo/flows/build.gradle +++ b/samples/simm-valuation-demo/flows/build.gradle @@ -9,6 +9,10 @@ cordapp { signing { enabled false } + sealing { + // Cannot seal JAR because other module also defines classes in the package net.corda.vega.analytics + enabled false + } } dependencies {