From 5d2ad46553441b055cc2e26d0dcc495949de1e47 Mon Sep 17 00:00:00 2001 From: Katarzyna Streich Date: Tue, 27 Nov 2018 15:48:32 +0000 Subject: [PATCH] CORDA-2089 - network parameters tags - part (#4228) * CORDA-2089 - network parameters tags - part Data structures changes, storage and notarisation. Tag transactions with network parameters hash that was in force when tx was created. Add component group on all core transactions and resolved parameters on full transactions. The hash should be always visible on the filtered versions of transactions. Add notarisation check that the parameters are current. Implement network parameters storage on services for resolution. This is only part of the work, next PR will include changes to ResolveTransactionsFlow to make sure that parameters in the transaction graph are ordered (this is to prevent the downgrade attack, when the malicious notary and participants sign transaction that shouldn't be notarised otherwise). Probably on network services side we need the default parameters endpoint for the transactions that were created before this change - for now it's default to the current ones. * Make parameters storage agnostic to cert hierarchy Test fixes * Address most PR comments * Fixes after rebase * Fixes. Add epoch column to parameters storage. * Address part of review comments * Some more comments * Hopefully fixing what I broke doing rebse * Address Kostas comments * Further fixes * Save all parameters from updates to storage * Fix integration test * Address comments * Fixes after rebase * Fix test * Fixes * Add wrapper for filtering around parameters hash * API stability fixes * Add NetworkParametersStorageInternal * Rename --- .../client/jackson/internal/CordaModule.kt | 9 +- .../client/jackson/JacksonSupportTest.kt | 10 +- .../data/TransactionGenerator.kt | 6 +- .../TransactionVerificationRequest.kt | 13 +- .../core/contracts/ComponentGroupEnum.kt | 3 +- .../corda/core/flows/CollectSignaturesFlow.kt | 5 + .../net/corda/core/flows/NotaryChangeFlow.kt | 3 +- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 13 +- .../core/internal/ContractUpgradeUtils.kt | 4 +- .../net/corda/core/internal/FetchDataFlow.kt | 2 +- .../corda/core/internal/TransactionUtils.kt | 14 +- .../core/internal/notary/NotaryServiceFlow.kt | 20 ++- .../kotlin/net/corda/core/node/ServiceHub.kt | 8 +- .../node/services/NetworkParametersStorage.kt | 30 ++++ .../core/transactions/BaseTransactions.kt | 15 +- .../ContractUpgradeTransactions.kt | 25 ++- .../core/transactions/LedgerTransaction.kt | 22 ++- .../core/transactions/MerkleTransaction.kt | 15 ++ .../transactions/NotaryChangeTransactions.kt | 87 +++++++++- .../core/transactions/SignedTransaction.kt | 2 + .../core/transactions/TransactionBuilder.kt | 4 +- .../core/transactions/WireTransaction.kt | 29 ++-- .../core/contracts/DummyContractV2Tests.kt | 0 .../core/crypto/PartialMerkleTreeTest.kt | 2 + .../core/internal/TopologicalSortTest.kt | 2 + .../CompatibleTransactionTests.kt | 37 +++- .../transactions/TransactionBuilderTest.kt | 9 +- .../core/transactions/TransactionTests.kt | 2 +- .../nodeapi/internal/ContractsScanning.kt | 2 +- ...tachmentsClassLoaderStaticContractTests.kt | 18 +- .../node/services/AttachmentLoadingTests.kt | 10 +- .../net/corda/node/internal/AbstractNode.kt | 10 +- .../internal/DBNetworkParametersStorage.kt | 159 ++++++++++++++++++ .../node/internal/NetworkParametersReader.kt | 7 +- .../internal/ServicesForResolutionImpl.kt | 10 +- .../services/network/NetworkMapUpdater.kt | 5 +- .../node/services/schema/NodeSchemaService.kt | 2 + .../transactions/NonValidatingNotaryFlow.kt | 5 +- .../transactions/ValidatingNotaryFlow.kt | 2 +- .../corda/node/utilities/NodeNamedCache.kt | 1 + .../node/messaging/TwoPartyTradeFlowTests.kt | 50 ++---- .../network/DBNetworkParametersStorageTest.kt | 132 +++++++++++++++ .../services/network/NetworkMapUpdaterTest.kt | 5 +- .../persistence/ExposeJpaToFlowsTests.kt | 39 +++-- .../persistence/NodeAttachmentServiceTest.kt | 12 +- .../transactions/MaxTransactionSizeTests.kt | 1 + .../transactions/NotaryServiceTests.kt | 43 ++++- .../transactions/ResolveStatePointersTest.kt | 22 ++- .../ValidatingNotaryServiceTests.kt | 30 +++- .../services/vault/ExternalIdMappingTest.kt | 31 ++-- .../services/vault/NodeVaultServiceTest.kt | 12 +- .../vault/VaultQueryExceptionsTests.kt | 8 +- .../node/services/vault/VaultQueryTests.kt | 1 - .../net/corda/testing/node/MockServices.kt | 20 ++- .../internal/MockNetworkParametersStorage.kt | 27 +++ .../testing/internal/InternalTestUtils.kt | 2 +- 56 files changed, 855 insertions(+), 202 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/node/services/NetworkParametersStorage.kt delete mode 100644 core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/DBNetworkParametersStorage.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/network/DBNetworkParametersStorageTest.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNetworkParametersStorage.kt diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index ae68b568a0..094294fd60 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -189,7 +189,8 @@ private class WireTransactionSerializer : JsonSerializer() { value.timeWindow, value.attachments, value.references, - value.privacySalt + value.privacySalt, + value.networkParametersHash )) } } @@ -204,7 +205,8 @@ private class WireTransactionDeserializer : JsonDeserializer() wrapper.attachments, wrapper.notary, wrapper.timeWindow, - wrapper.references + wrapper.references, + wrapper.networkParametersHash ) return WireTransaction(componentGroups, wrapper.privacySalt) } @@ -218,7 +220,8 @@ private class WireTransactionJson(val id: SecureHash, val timeWindow: TimeWindow?, val attachments: List, val references: List, - val privacySalt: PrivacySalt) + val privacySalt: PrivacySalt, + val networkParametersHash: SecureHash?) private interface TransactionStateMixin { @get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index e4aa6ff84f..fbdb10c67a 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -24,6 +24,7 @@ import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.NetworkParametersStorage import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.serialize @@ -92,8 +93,13 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: } services = rigorousMock() cordappProvider = rigorousMock() + val networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + val networkParametersStorage = rigorousMock().also { + doReturn(networkParameters.serialize().hash).whenever(it).currentHash + } + doReturn(networkParametersStorage).whenever(services).networkParametersStorage doReturn(cordappProvider).whenever(services).cordappProvider - doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters + doReturn(networkParameters).whenever(services).networkParameters doReturn(attachments).whenever(services).attachments } @@ -254,7 +260,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: println(mapper.writeValueAsString(json)) val (wtxJson, signaturesJson) = json.assertHasOnlyFields("wire", "signatures") assertThat(signaturesJson.childrenAs(mapper)).isEqualTo(stx.sigs) - val wtxFields = wtxJson.assertHasOnlyFields("id", "notary", "inputs", "attachments", "outputs", "commands", "timeWindow", "references", "privacySalt") + val wtxFields = wtxJson.assertHasOnlyFields("id", "notary", "inputs", "attachments", "outputs", "commands", "timeWindow", "references", "privacySalt", "networkParametersHash") assertThat(wtxFields[0].valueAs(mapper)).isEqualTo(wtx.id) assertThat(wtxFields[1].valueAs(mapper)).isEqualTo(wtx.notary) assertThat(wtxFields[2].childrenAs(mapper)).isEqualTo(wtx.inputs) diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt index d777bf9df1..ce51e917b1 100644 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt @@ -71,7 +71,8 @@ object TransactionGenerator { TransactionVerificationRequest( wtx3.serialize(), arrayOf(wtx1.serialize(), wtx2.serialize()), - arrayOf(contractAttachment.serialize().bytes)) + arrayOf(contractAttachment.serialize().bytes), + ledgerServices.networkParameters.serialize()) .serialize() .writeTo(output) } @@ -104,7 +105,8 @@ object TransactionGenerator { TransactionVerificationRequest( wtx3.serialize(), arrayOf(wtx1.serialize(), wtx2.serialize()), - arrayOf(contractAttachment.serialize().bytes)) + arrayOf(contractAttachment.serialize().bytes), + ledgerServices.networkParameters.serialize()) .serialize() .writeTo(output) } diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt index 96a886a618..3bd76b7465 100644 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER +import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -14,7 +15,8 @@ import net.corda.core.transactions.WireTransaction @CordaSerializable class TransactionVerificationRequest(val wtxToVerify: SerializedBytes, val dependencies: Array>, - val attachments: Array) { + val attachments: Array, + val networkParameters: SerializedBytes) { fun toLedgerTransaction(): LedgerTransaction { val deps = dependencies.map { it.deserialize() }.associateBy(WireTransaction::id) val attachments = attachments.map { it.deserialize() } @@ -24,10 +26,11 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes() @Suppress("DEPRECATION") return wtxToVerify.deserialize().toLedgerTransaction( - resolveIdentity = { null }, - resolveAttachment = { attachmentMap[it] }, - resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) }, - resolveContractAttachment = { contractAttachmentMap[it.contract]?.id } + resolveIdentity = { null }, + resolveAttachment = { attachmentMap[it] }, + resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) }, + resolveContractAttachment = { contractAttachmentMap[it.contract]?.id }, + resolveParameters = { networkParameters.deserialize() } ) } } diff --git a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt index b6e73f53c6..6492d8154f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt @@ -12,5 +12,6 @@ enum class ComponentGroupEnum { NOTARY_GROUP, // ordinal = 4. TIMEWINDOW_GROUP, // ordinal = 5. SIGNERS_GROUP, // ordinal = 6. - REFERENCES_GROUP // ordinal = 7. + REFERENCES_GROUP, // ordinal = 7. + PARAMETERS_GROUP // ordinal = 8. } diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index aab20f5e78..bad1346dba 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -212,6 +212,11 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio progressTracker.currentStep = RECEIVING // Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures. val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) + // TODO ENT-2666: Have time period for checking the parameters (because of the delay of propagation of new network data). + check(stx.networkParametersHash == serviceHub.networkParametersStorage.currentHash) { + "Received transaction for signing with invalid network parameters hash: ${stx.networkParametersHash}." + + "Node's parameters hash: ${serviceHub.networkParametersStorage.currentHash}" + } // Receive the signing key that the party requesting the signature expects us to sign with. Having this provided // means we only have to check we own that one key, rather than matching all keys in the transaction against all // keys we own. diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt index 18b98ae2d4..aabe51f2e8 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt @@ -33,7 +33,8 @@ class NotaryChangeFlow( val tx = NotaryChangeTransactionBuilder( inputs.map { it.ref }, originalState.state.notary, - modification + modification, + serviceHub.networkParametersStorage.currentHash ).build() val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index c900630fe4..bc5b4d9264 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -2,6 +2,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.DoNotImplement +import net.corda.core.contracts.ComponentGroupEnum import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.SecureHash @@ -12,10 +13,7 @@ import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.notary.generateSignature import net.corda.core.internal.notary.validateSignatures import net.corda.core.internal.pushToLoggingContext -import net.corda.core.transactions.ContractUpgradeWireTransaction -import net.corda.core.transactions.ReferenceStateRef -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction +import net.corda.core.transactions.* import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap @@ -30,7 +28,8 @@ class NotaryFlow { * signatures will be returned – one from each replica that accepted the input state commit. * * @throws NotaryException in case the any of the inputs to the transaction have been consumed - * by another transaction or the time-window is invalid. + * by another transaction or the time-window is invalid or + * the parameters used for this transaction are no longer in force in the network. */ @DoNotImplement @InitiatingFlow @@ -99,7 +98,9 @@ class NotaryFlow { val ctx = stx.coreTransaction val tx = when (ctx) { is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction() - is WireTransaction -> ctx.buildFilteredTransaction(Predicate { it is StateRef || it is ReferenceStateRef || it is TimeWindow || it == notaryParty }) + is WireTransaction -> ctx.buildFilteredTransaction(Predicate { + it is StateRef || it is ReferenceStateRef || it is TimeWindow || it == notaryParty || it is NetworkParametersHash + }) else -> ctx } session.send(NotarisationPayload(tx, signature)) diff --git a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt index 231550b26f..3557674927 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt @@ -21,6 +21,7 @@ object ContractUpgradeUtils { else -> getContractAttachmentId(stateAndRef.state.contract, services) } val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services) + val networkParametersHash = services.networkParametersStorage.currentHash val inputs = listOf(stateAndRef.ref) return ContractUpgradeTransactionBuilder( @@ -29,7 +30,8 @@ object ContractUpgradeUtils { legacyContractAttachmentId, upgradedContractClass.name, upgradedContractAttachmentId, - privacySalt + privacySalt, + networkParametersHash ).build() } diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 29ae9b8c2c..7a0453dd1d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -185,7 +185,7 @@ class FetchAttachmentsFlow(requests: Set, * [FetchDataFlow.DownloadedVsRequestedDataMismatch] being thrown. * If the remote peer doesn't have an entry, it results in a [FetchDataFlow.HashNotFound] exception. * If the remote peer is not authorized to request this transaction, it results in a [FetchDataFlow.IllegalTransactionRequest] exception. - * Authorisation is accorded only on valid ancestors of the root transation. + * Authorisation is accorded only on valid ancestors of the root transaction. * Note that returned transactions are not inserted into the database, because it's up to the caller to actually verify the transactions are valid. */ class FetchTransactionsFlow(requests: Set, otherSide: FlowSession) : diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 8721860184..4bb5fa726c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -17,9 +17,10 @@ import kotlin.reflect.KClass /** Constructs a [NotaryChangeWireTransaction]. */ class NotaryChangeTransactionBuilder(val inputs: List, val notary: Party, - val newNotary: Party) { + val newNotary: Party, + val networkParametersHash: SecureHash) { fun build(): NotaryChangeWireTransaction { - val components = listOf(inputs, notary, newNotary).map { it.serialize() } + val components = listOf(inputs, notary, newNotary, networkParametersHash).map { it.serialize() } return NotaryChangeWireTransaction(components) } } @@ -31,9 +32,10 @@ class ContractUpgradeTransactionBuilder( val legacyContractAttachmentId: SecureHash, val upgradedContractClassName: ContractClassName, val upgradedContractAttachmentId: SecureHash, - val privacySalt: PrivacySalt = PrivacySalt()) { + val privacySalt: PrivacySalt = PrivacySalt(), + val networkParametersHash: SecureHash) { fun build(): ContractUpgradeWireTransaction { - val components = listOf(inputs, notary, legacyContractAttachmentId, upgradedContractClassName, upgradedContractAttachmentId).map { it.serialize() } + val components = listOf(inputs, notary, legacyContractAttachmentId, upgradedContractClassName, upgradedContractAttachmentId, networkParametersHash).map { it.serialize() } return ContractUpgradeWireTransaction(components, privacySalt) } } @@ -128,7 +130,8 @@ fun createComponentGroups(inputs: List, attachments: List, notary: Party?, timeWindow: TimeWindow?, - references: List): List { + references: List, + networkParametersHash: SecureHash?): List { val serialize = { value: Any, _: Int -> value.serialize() } val componentGroupMap: MutableList = mutableListOf() if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUTS_GROUP.ordinal, inputs.lazyMapped(serialize))) @@ -142,6 +145,7 @@ fun createComponentGroups(inputs: List, // Adding signers to their own group. This is required for command visibility purposes: a party receiving // a FilteredTransaction can now verify it sees all the commands it should sign. if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize))) + if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize()))) return componentGroupMap } diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt index 7fdfe4a4ee..fa22de7125 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt @@ -78,7 +78,6 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: requestPayload.requestSignature, tx.timeWindow, tx.references) - } catch (e: NotaryInternalException) { logError(e.error) // Any exception that's not a NotaryInternalException is assumed to be an unexpected internal error @@ -95,6 +94,7 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: val transaction = extractParts(requestPayload) transactionId = transaction.id checkNotary(transaction.notary) + checkParametersHash(transaction.networkParametersHash) checkInputs(transaction.inputs + transaction.references) return transaction } catch (e: Exception) { @@ -122,6 +122,21 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: } } + /** + * Check that network parameters hash on this transaction is the current hash for the network. + */ + // TODO ENT-2666 Implement network parameters fuzzy checking. By design in Corda network we have propagation time delay. + // We will never end up in perfect synchronization with all the nodes. However, network parameters update process + // lets us predict what is the reasonable time window for changing parameters on most of the nodes. + @Suspendable + protected fun checkParametersHash(networkParametersHash: SecureHash?) { + if (networkParametersHash == null && serviceHub.networkParameters.minimumPlatformVersion < 4) return + val notaryParametersHash = serviceHub.networkParametersStorage.currentHash + require (notaryParametersHash == networkParametersHash) { + "Transaction for notarisation was tagged with parameters with hash: $networkParametersHash, but current network parameters are: $notaryParametersHash" + } + } + /** Verifies that the correct notarisation request was signed by the counterparty. */ private fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) { val requestingParty = otherSideSession.counterparty @@ -150,7 +165,8 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: val inputs: List, val timeWindow: TimeWindow?, val notary: Party?, - val references: List = emptyList() + val references: List = emptyList(), + val networkParametersHash: SecureHash? ) private fun logError(error: NotaryError) { diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 23b403e2a4..00be1d5967 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -5,10 +5,7 @@ import net.corda.core.DoNotImplement import net.corda.core.contracts.* import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SignableData -import net.corda.core.crypto.SignatureMetadata -import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.* import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken @@ -41,6 +38,9 @@ interface ServicesForResolution { /** Provides access to anything relating to cordapps including contract attachment resolution and app context */ val cordappProvider: CordappProvider + /** Provides access to storage of historical network parameters that are used in transaction resolution */ + val networkParametersStorage: NetworkParametersStorage + /** Returns the network parameters the node is operating under. */ val networkParameters: NetworkParameters diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkParametersStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkParametersStorage.kt new file mode 100644 index 0000000000..d0a11ca76a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkParametersStorage.kt @@ -0,0 +1,30 @@ +package net.corda.core.node.services + +import net.corda.core.CordaInternal +import net.corda.core.DoNotImplement +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.SignedDataWithCert +import net.corda.core.node.NetworkParameters + +/** + * Interface for handling network parameters storage used for resolving transactions according to parameters that were + * historically in force in the network. + */ +@DoNotImplement +interface NetworkParametersStorage { + /** + * Hash of the current parameters for the network. + */ + val currentHash: SecureHash + + /** + * For backwards compatibility, this parameters hash will be used for resolving historical transactions in the chain. + */ + val defaultHash: SecureHash + + /** + * Return network parameters for the given hash. Null if there are no parameters for this hash in the storage and we are unable to + * get them from network map. + */ + fun lookup(hash: SecureHash): NetworkParameters? +} diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt index 87fbd93d93..5a7df2cd16 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt @@ -3,6 +3,8 @@ package net.corda.core.transactions import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable /** @@ -16,13 +18,24 @@ abstract class CoreTransaction : BaseTransaction() { abstract override val inputs: List /** The reference inputs of this transaction, containing the state references only. **/ abstract override val references: List + /** + * Hash of the network parameters that were in force when the transaction was notarised. Null means, that the transaction + * was created on older version of Corda (before 4), resolution will default to initial parameters. + */ + abstract val networkParametersHash: SecureHash? } /** A transaction with fully resolved components, such as input states. */ abstract class FullTransaction : BaseTransaction() { abstract override val inputs: List> abstract override val references: List> - + // TODO NetworkParameters field is nullable only because of the API stability and the fact that our ledger transactions exposed constructors + // (they were data classes). This should be revisited. + /** + * Network parameters that were in force when this transaction was created. Resolved from the hash of network parameters on the corresponding + * wire transaction. + */ + abstract val networkParameters: NetworkParameters? override fun checkBaseInvariants() { super.checkBaseInvariants() checkInputsAndReferencesHaveSameNotary() diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 31eb89ad5f..11bf5cf621 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -69,6 +69,11 @@ data class ContractUpgradeWireTransaction( val legacyContractAttachmentId: SecureHash by lazy { serializedComponents[LEGACY_ATTACHMENT.ordinal].deserialize() } val upgradedContractClassName: ContractClassName by lazy { serializedComponents[UPGRADED_CONTRACT.ordinal].deserialize() } val upgradedContractAttachmentId: SecureHash by lazy { serializedComponents[UPGRADED_ATTACHMENT.ordinal].deserialize() } + override val networkParametersHash: SecureHash? by lazy { + if (serializedComponents.size >= PARAMETERS_HASH.ordinal + 1) { + serializedComponents[PARAMETERS_HASH.ordinal].deserialize() + } else null + } init { check(inputs.isNotEmpty()) { "A contract upgrade transaction must have inputs" } @@ -106,6 +111,8 @@ data class ContractUpgradeWireTransaction( ?: throw AttachmentResolutionException(legacyContractAttachmentId) val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) ?: throw AttachmentResolutionException(upgradedContractAttachmentId) + val hashToResolve = networkParametersHash ?: services.networkParametersStorage.defaultHash + val resolvedNetworkParameters = services.networkParametersStorage.lookup(hashToResolve) ?: throw TransactionResolutionException(id) return ContractUpgradeLedgerTransaction( resolvedInputs, notary, @@ -115,7 +122,7 @@ data class ContractUpgradeWireTransaction( id, privacySalt, sigs, - services.networkParameters + resolvedNetworkParameters ) } @@ -145,12 +152,13 @@ data class ContractUpgradeWireTransaction( } } - /** Constructs a filtered transaction: the inputs and the notary party are always visible, while the rest are hidden. */ + /** Constructs a filtered transaction: the inputs, the notary party and network parameters hash are always visible, while the rest are hidden. */ fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction { val totalComponents = (0 until serializedComponents.size).toSet() val visibleComponents = mapOf( INPUTS.ordinal to FilteredComponent(serializedComponents[INPUTS.ordinal], nonces[INPUTS.ordinal]), - NOTARY.ordinal to FilteredComponent(serializedComponents[NOTARY.ordinal], nonces[NOTARY.ordinal]) + NOTARY.ordinal to FilteredComponent(serializedComponents[NOTARY.ordinal], nonces[NOTARY.ordinal]), + PARAMETERS_HASH.ordinal to FilteredComponent(serializedComponents[PARAMETERS_HASH.ordinal], nonces[PARAMETERS_HASH.ordinal]) ) val hiddenComponents = (totalComponents - visibleComponents.keys).map { index -> val hash = componentHash(nonces[index], serializedComponents[index]) @@ -161,13 +169,13 @@ data class ContractUpgradeWireTransaction( } enum class Component { - INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT + INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT, PARAMETERS_HASH } } /** * A filtered version of the [ContractUpgradeWireTransaction]. In comparison with a regular [FilteredTransaction], there - * is no flexibility on what parts of the transaction to reveal – the inputs and notary field are always visible and the + * is no flexibility on what parts of the transaction to reveal – the inputs, notary and network parameters hash fields are always visible and the * rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary. */ @KeepForDJVM @@ -189,6 +197,9 @@ data class ContractUpgradeFilteredTransaction( visibleComponents[NOTARY.ordinal]?.component?.deserialize() ?: throw IllegalArgumentException("Notary not specified") } + override val networkParametersHash: SecureHash? by lazy { + visibleComponents[PARAMETERS_HASH.ordinal]?.component?.deserialize() + } override val id: SecureHash by lazy { val totalComponents = visibleComponents.size + hiddenComponents.size val hashList = (0 until totalComponents).map { i -> @@ -230,9 +241,9 @@ data class ContractUpgradeLedgerTransaction( override val id: SecureHash, val privacySalt: PrivacySalt, override val sigs: List, - private val networkParameters: NetworkParameters + override val networkParameters: NetworkParameters ) : FullTransaction(), TransactionWithSignatures { - /** ContractUpgradeLEdgerTransactions do not contain reference input states. */ + /** ContractUpgradeLedgerTransactions do not contain reference input states. */ override val references: List> = emptyList() /** The legacy contract class name is determined by the first input state. */ private val legacyContractClassName = inputs.first().state.contract 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 c347df08c0..7047cd40bf 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -52,7 +52,10 @@ private constructor( override val notary: Party?, val timeWindow: TimeWindow?, val privacySalt: PrivacySalt, - val networkParameters: NetworkParameters?, + /** Network parameters that were in force when the transaction was notarised. */ + // TODO Network parameters should never be null on LedgerTransaction, this is left only because of deprecated constructors. We should decide to + // get rid of them. + override val networkParameters: NetworkParameters?, override val references: List> //DOCEND 1 ) : FullTransaction() { @@ -81,7 +84,7 @@ private constructor( notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt, - networkParameters: NetworkParameters?, + networkParameters: NetworkParameters, references: List>, componentGroups: List? = null, serializedInputs: List? = null, @@ -121,6 +124,11 @@ private constructor( */ @Throws(TransactionVerificationException::class) fun verify() { + if (networkParameters == null) { + // For backwards compatibility only. + logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " + + "Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.") + } val contractAttachmentsByContract: Map = getUniqueContractAttachmentsByContract() AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader -> @@ -169,14 +177,14 @@ private constructor( /** * Verify that for each contract the network wide package owner is respected. * - * TODO - revisit once transaction contains network parameters. + * TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that + * LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/ */ private fun validatePackageOwnership(contractAttachmentsByContract: Map) { // This should never happen once we have network parameters in the transaction. if (networkParameters == null) { return } - val contractsAndOwners = allStates.mapNotNull { transactionState -> val contractClassName = transactionState.contract networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it } @@ -243,7 +251,6 @@ private constructor( if (state.constraint is SignatureAttachmentConstraint) checkMinimumPlatformVersion(networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints") - if (!state.constraint.isSatisfiedBy(constraintAttachment)) { throw TransactionVerificationException.ContractConstraintRejection(id, state.contract) } @@ -806,11 +813,8 @@ private constructor( |)""".trimMargin() } - - // // Stuff that we can't remove and so is deprecated instead // - @Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.") constructor( inputs: List>, @@ -834,7 +838,7 @@ private constructor( notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt, - networkParameters: NetworkParameters? + networkParameters: NetworkParameters ) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList()) @Deprecated("LedgerTransactions should not be created directly, use WireTransaction.toLedgerTransaction instead.") diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 746abfe8af..b3ea830914 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -48,6 +48,12 @@ abstract class TraversableTransaction(open val componentGroups: List> get() { val result = mutableListOf(inputs, outputs, commands, attachments, references) notary?.let { result += listOf(it) } timeWindow?.let { result += listOf(it) } + networkParametersHash?.let { result += listOf(it) } return result } } @@ -149,7 +157,9 @@ class FilteredTransaction internal constructor( if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0) // Note that because [inputs] and [references] share the same type [StateRef], we use a wrapper for references [ReferenceStateRef], // when filtering. Thus, to filter-in all [references] based on type, one should use the wrapper type [ReferenceStateRef] and not [StateRef]. + // Similar situation is for network parameters hash and attachments, one should use wrapper [NetworkParametersHash] and not [SecureHash]. wtx.references.forEachIndexed { internalIndex, it -> filter(ReferenceStateRef(it), REFERENCES_GROUP.ordinal, internalIndex) } + wtx.networkParametersHash?.let { filter(NetworkParametersHash(it), PARAMETERS_GROUP.ordinal, 0) } // It is highlighted that because there is no a signers property in TraversableTransaction, // one cannot specifically filter them in or out. // The above is very important to ensure someone won't filter out the signers component group if at least one @@ -351,3 +361,8 @@ class FilteredTransactionVerificationException(val id: SecureHash, val reason: S @KeepForDJVM @CordaSerializable data class ReferenceStateRef(val stateRef: StateRef) + +/** Wrapper over [SecureHash] to be used when filtering network parameters hash. */ +@KeepForDJVM +@CordaSerializable +data class NetworkParametersHash(val hash: SecureHash) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 98c0ae4678..da84e2763c 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.sha256 import net.corda.core.identity.Party +import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable @@ -37,9 +38,16 @@ data class NotaryChangeWireTransaction( override val inputs: List = serializedComponents[INPUTS.ordinal].deserialize() override val references: List = emptyList() override val notary: Party = serializedComponents[NOTARY.ordinal].deserialize() + /** Identity of the notary service to reassign the states to.*/ val newNotary: Party = serializedComponents[NEW_NOTARY.ordinal].deserialize() + override val networkParametersHash: SecureHash? by lazy { + if (serializedComponents.size >= PARAMETERS_HASH.ordinal + 1) { + serializedComponents[PARAMETERS_HASH.ordinal].deserialize() + } else null + } + /** * This transaction does not contain any output states, outputs can be obtained by resolving a * [NotaryChangeLedgerTransaction] and applying the notary modification to inputs. @@ -66,11 +74,14 @@ data class NotaryChangeWireTransaction( } } - /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ + /** Resolves input states and network parameters and builds a [NotaryChangeLedgerTransaction]. */ @DeleteForDJVM fun resolve(services: ServicesForResolution, sigs: List): NotaryChangeLedgerTransaction { val resolvedInputs = services.loadStates(inputs.toSet()).toList() - return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) + val hashToResolve = networkParametersHash ?: services.networkParametersStorage.defaultHash + val resolvedNetworkParameters = services.networkParametersStorage.lookup(hashToResolve) + ?: throw TransactionResolutionException(id) + return NotaryChangeLedgerTransaction.create(resolvedInputs, notary, newNotary, id, sigs, resolvedNetworkParameters) } /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ @@ -92,7 +103,7 @@ data class NotaryChangeWireTransaction( } enum class Component { - INPUTS, NOTARY, NEW_NOTARY + INPUTS, NOTARY, NEW_NOTARY, PARAMETERS_HASH } @Deprecated("Required only for backwards compatibility purposes. This type of transaction should not be constructed outside Corda code.", ReplaceWith("NotaryChangeTransactionBuilder"), DeprecationLevel.WARNING) @@ -105,13 +116,29 @@ data class NotaryChangeWireTransaction( * needed for signature verification. */ @KeepForDJVM -data class NotaryChangeLedgerTransaction( +class NotaryChangeLedgerTransaction +private constructor( override val inputs: List>, override val notary: Party, val newNotary: Party, override val id: SecureHash, - override val sigs: List + override val sigs: List, + // TODO Network parameters should never be null on NotaryChangeLedgerTransaction, this is left only because of deprecated constructors. We should decide to + // get rid of them. + override val networkParameters: NetworkParameters? ) : FullTransaction(), TransactionWithSignatures { + companion object { + @CordaInternal + internal fun create(inputs: List>, + notary: Party, + newNotary: Party, + id: SecureHash, + sigs: List, + networkParameters: NetworkParameters): NotaryChangeLedgerTransaction { + return NotaryChangeLedgerTransaction(inputs, notary, newNotary, id, sigs, networkParameters) + } + } + init { checkEncumbrances() } @@ -157,4 +184,54 @@ data class NotaryChangeLedgerTransaction( } } } + + operator fun component1(): List> = inputs + operator fun component2(): Party = notary + operator fun component3(): Party = newNotary + operator fun component4(): SecureHash = id + operator fun component5(): List = sigs + operator fun component6(): NetworkParameters? = networkParameters + + override fun equals(other: Any?): Boolean = this === other || other is NotaryChangeLedgerTransaction && this.id == other.id + + override fun hashCode(): Int = id.hashCode() + + override fun toString(): String { + return """NotaryChangeLedgerTransaction( + | id=$id + | inputs=$inputs + | notary=$notary + | newNotary=$newNotary + | sigs=$sigs + | networkParameters=$networkParameters + |)""".trimMargin() + } + + // Things that we can't remove after `data class` removal from this class, so it is deprecated instead. + // + @Deprecated("NotaryChangeLedgerTransaction should not be created directly, use NotaryChangeWireTransaction.resolve instead.") + constructor( + inputs: List>, + notary: Party, + newNotary: Party, + id: SecureHash, + sigs: List + ) : this(inputs, notary, newNotary, id, sigs, null) + + @Deprecated("NotaryChangeLedgerTransaction should not be created directly, use NotaryChangeWireTransaction.resolve instead.") + fun copy(inputs: List> = this.inputs, + notary: Party = this.notary, + newNotary: Party = this.newNotary, + id: SecureHash = this.id, + sigs: List = this.sigs + ): NotaryChangeLedgerTransaction { + return NotaryChangeLedgerTransaction( + inputs, + notary, + newNotary, + id, + sigs, + this.networkParameters + ) + } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 1216477c91..52dd80090f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -76,6 +76,8 @@ data class SignedTransaction(val txBits: SerializedBytes, val references: List get() = coreTransaction.references /** Helper to access the notary of the contained transaction. */ val notary: Party? get() = coreTransaction.notary + /** Helper to access the network parameters hash for the contained transaction. */ + val networkParametersHash: SecureHash? get() = coreTransaction.networkParametersHash override val requiredSigningKeys: Set get() = tx.requiredSigningKeys diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 630b0003e8..6975561528 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -130,8 +130,8 @@ open class TransactionBuilder @JvmOverloads constructor( (allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable. notary, window, - referenceStates - ), + referenceStates, + services.networkParametersStorage.currentHash), privacySalt ) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 1719dd07b7..48871c93b6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -66,7 +66,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt = PrivacySalt() - ) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()), privacySalt) + ) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null), privacySalt) init { check(componentGroups.all { it.components.isNotEmpty() }) { "Empty component groups are not allowed" } @@ -106,7 +106,10 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr resolveIdentity = { services.identityService.partyFromKey(it) }, resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) }, - networkParameters = services.networkParameters + resolveParameters = { + val hashToResolve = it ?: services.networkParametersStorage.defaultHash + services.networkParametersStorage.lookup(hashToResolve) + } ) } @@ -119,21 +122,24 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr */ @Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead") @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) + @JvmOverloads fun toLedgerTransaction( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRef: (StateRef) -> TransactionState<*>?, - @Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState) -> AttachmentId? + @Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState) -> AttachmentId?, + resolveParameters: (SecureHash?) -> NetworkParameters? = { null } // TODO This { null } is left here only because of API stability. It doesn't make much sense anymore as it will fail on transaction verification. ): LedgerTransaction { // This reverts to serializing the resolved transaction state. - return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, null) + return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, resolveParameters) } + private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRefAsSerialized: (StateRef) -> SerializedBytes>?, - networkParameters: NetworkParameters? + resolveParameters: (SecureHash?) -> NetworkParameters? ): LedgerTransaction { // Look up public keys to authenticated identities. val authenticatedCommands = commands.lazyMapped { cmd, _ -> @@ -153,6 +159,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) } + val resolvedNetworkParameters = resolveParameters(networkParametersHash) ?: throw TransactionResolutionException(id) + val ltx = LedgerTransaction.create( resolvedInputs, outputs, @@ -162,14 +170,14 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr notary, timeWindow, privacySalt, - networkParameters, + resolvedNetworkParameters, resolvedReferences, componentGroups, serializedResolvedInputs, serializedResolvedReferences ) - checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: DEFAULT_MAX_TX_SIZE, serializedResolvedInputs, serializedResolvedReferences) + checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences) return ltx } @@ -286,8 +294,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } companion object { - private const val DEFAULT_MAX_TX_SIZE = 10485760 - @CordaInternal @Deprecated("Do not use, this is internal API") fun createComponentGroups(inputs: List, @@ -296,7 +302,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr attachments: List, notary: Party?, timeWindow: TimeWindow?): List { - return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()) + return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) } /** @@ -346,6 +352,9 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr val emoji = Emoji.paperclip buf.appendln("${emoji}ATTACHMENT: $attachment") } + if (networkParametersHash != null) { + buf.appendln("PARAMETERS HASH: $networkParametersHash") + } return buf.toString() } diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index a1c4994fca..c60b68aef8 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -162,6 +162,7 @@ class PartialMerkleTreeTest { assertEquals(1, ftx.commands.size) assertNull(ftx.notary) assertNotNull(ftx.timeWindow) + assertNull(ftx.networkParametersHash) ftx.verify() } @@ -186,6 +187,7 @@ class PartialMerkleTreeTest { assertTrue(ftxNothing.outputs.isEmpty()) assertNull(ftxNothing.timeWindow) assertTrue(ftxNothing.availableComponentGroups.flatten().isEmpty()) + assertNull(ftxNothing.networkParametersHash) ftxNothing.verify() // We allow empty ftx transactions (eg from a timestamp authority that blindly signs). } diff --git a/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt b/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt index fe427339e3..8425bca130 100644 --- a/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt @@ -13,6 +13,7 @@ import net.corda.core.identity.Party import net.corda.core.serialization.serialize import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import org.junit.Rule @@ -31,6 +32,7 @@ class TopologicalSortTest { override val outputs: List> = (1..numberOfOutputs).map { TransactionState(DummyState(), "", notary) } + override val networkParametersHash: SecureHash? = testNetworkParameters().serialize().hash } class DummyState : ContractState { diff --git a/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt index 6508cbd3f9..579e23d57b 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.* import net.corda.core.internal.createComponentGroups import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.* @@ -38,6 +39,7 @@ class CompatibleTransactionTests { private val notary = DUMMY_NOTARY private val timeWindow = TimeWindow.fromOnly(Instant.now()) private val privacySalt: PrivacySalt = PrivacySalt() + private val paramsHash = SecureHash.randomSHA256() private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) } private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) } @@ -46,6 +48,7 @@ class CompatibleTransactionTests { private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) } private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) } private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) } + private val networkParamsGroup by lazy { ComponentGroup(PARAMETERS_GROUP.ordinal, listOf(paramsHash.serialize())) } private val newUnknownComponentGroup = ComponentGroup(100, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8)))) private val newUnknownComponentEmptyGroup = ComponentGroup(101, emptyList()) @@ -125,7 +128,7 @@ class CompatibleTransactionTests { @Test fun `WireTransaction constructors and compatibility`() { - val groups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()) + val groups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) val wireTransactionOldConstructor = WireTransaction(groups, privacySalt) assertEquals(wireTransactionA, wireTransactionOldConstructor) @@ -211,6 +214,7 @@ class CompatibleTransactionTests { ftxAll.checkAllComponentsVisible(NOTARY_GROUP) ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP) ftxAll.checkAllComponentsVisible(SIGNERS_GROUP) + ftxAll.checkAllComponentsVisible(PARAMETERS_GROUP) // Filter inputs only. fun filtering(elem: Any): Boolean { @@ -261,6 +265,8 @@ class CompatibleTransactionTests { assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) assertNotNull(ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) + assertNull(wireTransactionCompatibleA.networkParametersHash) + assertNull(ftxCompatible.networkParametersHash) // Now, let's allow everything, including the new component type that we cannot process. val ftxCompatibleAll = wireTransactionCompatibleA.buildFilteredTransaction(Predicate { true }) // All filtered, including the unknown component. @@ -547,5 +553,34 @@ class CompatibleTransactionTests { // Also, checkAllComponentsVisible() will not pass (top level Merkle tree cannot be verified against transaction's id). assertFailsWith { ftxAlterSignerB.checkCommandVisibility(DUMMY_KEY_1.public) } } + + @Test + fun `parameters hash visibility`() { + fun paramsFilter(elem: Any): Boolean = elem is NetworkParametersHash && elem.hash == paramsHash + fun attachmentFilter(elem: Any): Boolean = elem is SecureHash && elem == paramsHash + val attachments = ComponentGroup(ATTACHMENTS_GROUP.ordinal, listOf(paramsHash.serialize())) // Same hash as network parameters + val componentGroups = listOf( + inputGroup, + outputGroup, + attachments, + commandGroup, + notaryGroup, + timeWindowGroup, + signersGroup, + networkParamsGroup + ) + val wtx = WireTransaction(componentGroups, privacySalt) + val ftx1 = wtx.buildFilteredTransaction(Predicate(::paramsFilter)) // Filter only network parameters hash. + ftx1.verify() + assertEquals(wtx.id, ftx1.id) + ftx1.checkAllComponentsVisible(PARAMETERS_GROUP) + assertFailsWith { ftx1.checkAllComponentsVisible(ATTACHMENTS_GROUP) } + // Filter only attachment. + val ftx2 = wtx.buildFilteredTransaction(Predicate(::attachmentFilter)) + ftx2.verify() + assertEquals(wtx.id, ftx2.id) + ftx2.checkAllComponentsVisible(ATTACHMENTS_GROUP) + assertFailsWith { ftx2.checkAllComponentsVisible(PARAMETERS_GROUP) } + } } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index d560c1fe56..0e6fab5ab2 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -12,6 +12,8 @@ import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.NetworkParametersStorage +import net.corda.core.serialization.serialize import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState @@ -35,13 +37,17 @@ class TransactionBuilderTest { private val services = rigorousMock() private val contractAttachmentId = SecureHash.randomSHA256() private val attachments = rigorousMock() + private val networkParametersStorage = rigorousMock() @Before fun setup() { val cordappProvider = rigorousMock() + val networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION) + doReturn(networkParametersStorage).whenever(services).networkParametersStorage + doReturn(networkParameters.serialize().hash).whenever(networkParametersStorage).currentHash doReturn(cordappProvider).whenever(services).cordappProvider doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) - doReturn(testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION)).whenever(services).networkParameters + doReturn(networkParameters).whenever(services).networkParameters val attachmentStorage = rigorousMock() doReturn(attachmentStorage).whenever(services).attachments @@ -67,6 +73,7 @@ class TransactionBuilderTest { val wtx = builder.toWireTransaction(services) assertThat(wtx.outputs).containsOnly(outputState) assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey)) + assertThat(wtx.networkParametersHash).isEqualTo(networkParametersStorage.currentHash) } @Test diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 0e524e99b0..c471b69730 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -176,7 +176,7 @@ class TransactionTests { notary, timeWindow, privacySalt, - null, + testNetworkParameters(), emptyList() ) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt index 3ec56d525b..41ff716064 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt @@ -15,7 +15,7 @@ import java.nio.file.Path import java.nio.file.StandardCopyOption import java.util.Collections.singleton -// When scanning of the CorDapp Jar is performed without "corda-core.jar" being the in the classpath, there is no way to appreciate +// When scanning of the CorDapp Jar is performed without "corda-core.jar" being in the classpath, there is no way to appreciate // relationships between those interfaces, therefore they have to be listed explicitly. val coreContractClasses = setOf(Contract::class, UpgradedContractWithLegacyConstraint::class, UpgradedContract::class) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 594c67bff6..913daf0182 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -4,13 +4,12 @@ import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.AbstractAttachment import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.NetworkParametersStorage import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction @@ -67,19 +66,18 @@ class AttachmentsClassLoaderStaticContractTests { } } - private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) { - override val id: SecureHash get() = throw UnsupportedOperationException() + private val networkParameters = testNetworkParameters() + + private val networkParametersStorage get() = rigorousMock().also { + doReturn(networkParameters.serialize().hash).whenever(it).currentHash } - private val attachments = rigorousMock().also { - doReturn(unsignedAttachment).whenever(it).openAttachment(any()) - } - - private val serviceHub = rigorousMock().also { + private val serviceHub get() = rigorousMock().also { val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage()) cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations) doReturn(cordappProviderImpl).whenever(it).cordappProvider - doReturn(testNetworkParameters()).whenever(it).networkParameters + doReturn(networkParametersStorage).whenever(it).networkParametersStorage + doReturn(networkParameters).whenever(it).networkParameters val attachmentStorage = rigorousMock() doReturn(attachmentStorage).whenever(it).attachments val attachment = rigorousMock() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 5f77275446..59c9340476 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -1,5 +1,6 @@ package net.corda.node.services +import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.CordaRuntimeException @@ -13,7 +14,9 @@ import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.NetworkParametersStorage import net.corda.core.serialization.SerializationFactory +import net.corda.core.serialization.serialize import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo @@ -63,6 +66,7 @@ class AttachmentLoadingTests { } private val services = object : ServicesForResolution { + private val testNetworkParameters = testNetworkParameters() override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError() override fun loadStates(stateRefs: Set): Set> = throw NotImplementedError() override val identityService = rigorousMock().apply { @@ -70,7 +74,11 @@ class AttachmentLoadingTests { } override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider - override val networkParameters: NetworkParameters = testNetworkParameters() + override val networkParameters: NetworkParameters = testNetworkParameters + override val networkParametersStorage: NetworkParametersStorage get() = rigorousMock().apply { + doReturn(testNetworkParameters.serialize().hash).whenever(this).currentHash + doReturn(testNetworkParameters).whenever(this).lookup(any()) + } } @Test diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index e0b57a1a0f..73089cd696 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -168,16 +168,18 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize() val cryptoService = configuration.makeCryptoService() + val networkParametersStorage = DBNetworkParametersStorage(cacheFactory, database, networkMapClient).tokenize() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() - val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also { + val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { attachments.servicesForResolution = it } @Suppress("LeakingThis") val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize() val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) + // TODO Cancelling parameters updates - if we do that, how we ensure that no one uses cancelled parameters in the transactions? val networkMapUpdater = NetworkMapUpdater( networkMapCache, NodeInfoWatcher( @@ -188,7 +190,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ), networkMapClient, configuration.baseDirectory, - configuration.extraNetworkMapKeys + configuration.extraNetworkMapKeys, + networkParametersStorage ).closeOnStop() @Suppress("LeakingThis") val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize() @@ -326,7 +329,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) { "Node's platform version is lower than network's required minimumPlatformVersion" } - servicesForResolution.start(netParams) networkMapCache.start(netParams.notaries) startDatabase() @@ -354,6 +356,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // Do all of this in a database transaction so anything that might need a connection has one. return database.transaction { + networkParametersStorage.start(signedNetParams, trustRoot) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) attachments.start() cordappProvider.start(netParams.whitelistedContractImplementations) @@ -984,6 +987,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val configuration: NodeConfiguration get() = this@AbstractNode.configuration override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater override val cacheFactory: NamedCacheFactory get() = this@AbstractNode.cacheFactory + override val networkParametersStorage: NetworkParametersStorage get() = this@AbstractNode.networkParametersStorage private lateinit var _myInfo: NodeInfo override val myInfo: NodeInfo get() = _myInfo diff --git a/node/src/main/kotlin/net/corda/node/internal/DBNetworkParametersStorage.kt b/node/src/main/kotlin/net/corda/node/internal/DBNetworkParametersStorage.kt new file mode 100644 index 0000000000..30d42d678d --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/DBNetworkParametersStorage.kt @@ -0,0 +1,159 @@ +package net.corda.node.internal + +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.DigitalSignatureWithCert +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.SignedDataWithCert +import net.corda.core.node.NetworkParameters +import net.corda.core.node.services.NetworkParametersStorage +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.MAX_HASH_HEX_SIZE +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.trace +import net.corda.node.services.network.NetworkMapClient +import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.network.SignedNetworkParameters +import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import org.apache.commons.lang.ArrayUtils +import java.security.cert.X509Certificate +import javax.persistence.* + +interface NetworkParametersStorageInternal : NetworkParametersStorage { + /** + * Return parameters epoch for the given parameters hash. Null if there are no parameters for this hash in the storage and we are unable to + * get them from network map. + */ + fun getEpochFromHash(hash: SecureHash): Int? + + /** + * Save signed network parameters data. Internally network parameters bytes should be stored with the signature. + * It's because of ability of older nodes to function in network where parameters were extended with new fields. + * Hash should always be calculated over the serialized bytes. + */ + fun saveParameters(signedNetworkParameters: SignedDataWithCert) +} + +class DBNetworkParametersStorage( + cacheFactory: NamedCacheFactory, + private val database: CordaPersistence, + // TODO It's very inefficient solution (at least at the beginning when node joins without historical data) + // We could have historic parameters endpoint or always add parameters as an attachment to the transaction. + private val networkMapClient: NetworkMapClient? +) : NetworkParametersStorageInternal, SingletonSerializeAsToken() { + private lateinit var trustRoot: X509Certificate + + companion object { + private val log = contextLogger() + + fun createParametersMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap, PersistentNetworkParameters, String> { + return AppendOnlyPersistentMap( + cacheFactory = cacheFactory, + name = "NodeParametersStorage_networkParametersByHash", + toPersistentEntityKey = { it.toString() }, + fromPersistentEntity = { + Pair( + SecureHash.parse(it.hash), + it.signedNetworkParameters + ) + }, + toPersistentEntity = { key: SecureHash, value: SignedDataWithCert -> + PersistentNetworkParameters(key.toString(), value.verified().epoch, value.raw.bytes, value.sig.bytes, value.sig.by.encoded, + X509Utilities.buildCertPath(value.sig.parentCertsChain).encoded) + }, + persistentEntityClass = PersistentNetworkParameters::class.java + ) + } + } + + fun start(currentSignedParameters: SignedDataWithCert, trustRoot: X509Certificate) { + this.trustRoot = trustRoot + saveParameters(currentSignedParameters) + _currentHash = currentSignedParameters.raw.hash + } + + private lateinit var _currentHash: SecureHash + override val currentHash: SecureHash get() = _currentHash + // TODO Have network map serve special "starting" parameters as parameters for resolution for older transactions? + override val defaultHash: SecureHash get() = currentHash + + private val hashToParameters = createParametersMap(cacheFactory) + + override fun lookup(hash: SecureHash): NetworkParameters? { + return database.transaction { hashToParameters[hash]?.raw?.deserialize() } ?: tryDownloadUnknownParameters(hash) + } + + override fun getEpochFromHash(hash: SecureHash): Int? = lookup(hash)?.epoch + + override fun saveParameters(signedNetworkParameters: SignedNetworkParameters) { + log.trace { "Saving new network parameters to network parameters storage." } + val networkParameters = signedNetworkParameters.verified() + val hash = signedNetworkParameters.raw.hash + log.trace { "Parameters to save $networkParameters with hash $hash" } + database.transaction { + hashToParameters.addWithDuplicatesAllowed(hash, signedNetworkParameters) + } + } + + // TODO For the future we could get them also as signed (by network operator) attachments on transactions. + private fun tryDownloadUnknownParameters(parametersHash: SecureHash): NetworkParameters? { + return if (networkMapClient != null) { + try { + val signedParams = networkMapClient.getNetworkParameters(parametersHash) + val networkParameters = signedParams.verifiedNetworkParametersCert(trustRoot) + saveParameters(signedParams) + networkParameters + } catch (e: Exception) { + log.warn("Failed to download historical network parameters with hash $parametersHash", e) + null + } + } else { + log.warn("Tried to download historical network parameters with hash $parametersHash, but network map url isn't configured") + null + } + } + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}network_parameters") + class PersistentNetworkParameters( + @Id + @Column(name = "hash", length = MAX_HASH_HEX_SIZE, nullable = false) + val hash: String = "", + + @Column(name = "epoch", nullable = false) + val epoch: Int = 0, + + // Stored as serialized bytes because network parameters structure evolves over time. + @Lob + @Column(name = "parameters_bytes", nullable = false) + val networkParametersBytes: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY, + + @Lob + @Column(name = "signature_bytes", nullable = false) + private val signature: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY, + + // First certificate in the certificate chain. + @Lob + @Column(name = "cert", nullable = false) + private val certificate: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY, + + // Parent certificate path (the first one is stored separately), so node is agnostic to certificate hierarchy. + @Lob + @Column(name = "parent_cert_path", nullable = false) + private val certPath: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY + ) { + val networkParameters: NetworkParameters get() = networkParametersBytes.deserialize() + val signedNetworkParameters: SignedDataWithCert + get() { + val certChain = X509CertificateFactory().delegate.generateCertPath(certPath.inputStream()) + .certificates.map { it as X509Certificate } + val signWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), certChain, signature) + return SignedDataWithCert(SerializedBytes(networkParametersBytes), signWithCert) + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt index 5acfb7e237..f35985c0a1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt @@ -6,7 +6,10 @@ import net.corda.core.node.NetworkParameters import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.node.services.network.NetworkMapClient -import net.corda.nodeapi.internal.network.* +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME +import net.corda.nodeapi.internal.network.SignedNetworkParameters +import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert import java.nio.file.Path import java.nio.file.StandardCopyOption import java.security.cert.X509Certificate @@ -23,7 +26,7 @@ class NetworkParametersReader(private val trustRoot: X509Certificate, class NetworkMapNotConfigured : Error("Node hasn't been configured to connect to a network map from which to get the network parameters.") class OldParamsAndUpdate : Error( "Both network parameters and network parameters update files don't match" + - "parameters advertised by network map. Please update node to use correct network parameters file." + "parameters advertised by network map. Please update node to use correct network parameters file." ) class OldParams(previousParametersHash: SecureHash, advertisedParametersHash: SecureHash) : Error( "Node uses parameters with hash: $previousParametersHash but network map is advertising: " + diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt index f2cb962759..01626f60cb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt @@ -5,6 +5,7 @@ import net.corda.core.cordapp.CordappProvider import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.NetworkParametersStorage import net.corda.core.node.services.IdentityService import net.corda.core.node.services.TransactionStorage @@ -12,14 +13,11 @@ data class ServicesForResolutionImpl( override val identityService: IdentityService, override val attachments: AttachmentStorage, override val cordappProvider: CordappProvider, + override val networkParametersStorage: NetworkParametersStorage, private val validatedTransactions: TransactionStorage ) : ServicesForResolution { - private lateinit var _networkParameters: NetworkParameters - override val networkParameters: NetworkParameters get() = _networkParameters - - fun start(networkParameters: NetworkParameters) { - _networkParameters = networkParameters - } + override val networkParameters: NetworkParameters get() = networkParametersStorage.lookup(networkParametersStorage.currentHash) ?: + throw IllegalArgumentException("No current parameters in network parameters storage") @Throws(TransactionResolutionException::class) override fun loadState(stateRef: StateRef): TransactionState<*> { diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 0c80be1589..72cdabcd7e 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -12,6 +12,7 @@ import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes +import net.corda.node.internal.NetworkParametersStorageInternal import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.node.utilities.NamedThreadFactory @@ -33,7 +34,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val nodeInfoWatcher: NodeInfoWatcher, private val networkMapClient: NetworkMapClient?, private val baseDirectory: Path, - private val extraNetworkMapKeys: List + private val extraNetworkMapKeys: List, + private val networkParametersStorage: NetworkParametersStorageInternal ) : AutoCloseable { companion object { private val logger = contextLogger() @@ -209,6 +211,7 @@ The node will shutdown now.""") } val newSignedNetParams = networkMapClient.getNetworkParameters(update.newParametersHash) val newNetParams = newSignedNetParams.verifiedNetworkParametersCert(trustRoot) + networkParametersStorage.saveParameters(newSignedNetParams) logger.info("Downloaded new network parameters: $newNetParams from the update: $update") newNetworkParameters = Pair(update, newSignedNetParams) val updateInfo = ParametersUpdateInfo( diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index c4e27a4ff8..75472779a8 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.LinearState import net.corda.core.schemas.* import net.corda.core.schemas.MappedSchemaValidator.crossReferencesToOtherMappedSchema import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.internal.DBNetworkParametersStorage import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService.SchemaOptions @@ -43,6 +44,7 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentIdentityService.PersistentIdentity::class.java, PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java, + DBNetworkParametersStorage.PersistentNetworkParameters::class.java, PersistentKeyManagementService.PublicKeyHashToExternalId::class.java )) { override val migrationResource = "node-core.changelog-master" diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index d95593af0a..f19136e8bb 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -28,11 +28,12 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP) checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP) checkAllComponentsVisible(ComponentGroupEnum.REFERENCES_GROUP) + if(serviceHub.networkParameters.minimumPlatformVersion >= 4) checkAllComponentsVisible(ComponentGroupEnum.PARAMETERS_GROUP) } - TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references) + TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references, networkParametersHash = tx.networkParametersHash) } is ContractUpgradeFilteredTransaction, - is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) + is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary, networkParametersHash = tx.networkParametersHash) else -> { throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," + "expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}") diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 7fd7f2278d..4cb289b7dd 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -24,7 +24,7 @@ open class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePa override fun extractParts(requestPayload: NotarisationPayload): TransactionParts { val stx = requestPayload.signedTransaction val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null - return TransactionParts(stx.id, stx.inputs, timeWindow, stx.notary, stx.references) + return TransactionParts(stx.id, stx.inputs, timeWindow, stx.notary, stx.references, stx.networkParametersHash) } /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index 11c01ccfd1..92464ca157 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -57,6 +57,7 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi name == "BFTNonValidatingNotaryService_transactions" -> caffeine.maximumSize(defaultCacheSize) name == "RaftUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize) name == "BasicHSMKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize) + name == "NodeParametersStorage_networkParametersByHash" -> caffeine.maximumSize(defaultCacheSize) else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") } } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 30e4a518ba..a6cc0b1bb8 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -1,8 +1,6 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.whenever import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.* @@ -34,7 +32,6 @@ import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.TwoPartyTradeFlow.Buyer import net.corda.finance.flows.TwoPartyTradeFlow.Seller -import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.checkpoints @@ -45,9 +42,7 @@ import net.corda.testing.dsl.TestLedgerDSLInterpreter import net.corda.testing.dsl.TestTransactionDSLInterpreter import net.corda.testing.internal.LogHelper import net.corda.testing.internal.TEST_TX_TIME -import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.VaultFiller -import net.corda.testing.node.MockServices import net.corda.testing.node.internal.* import net.corda.testing.node.ledger import org.assertj.core.api.Assertions.assertThat @@ -71,6 +66,7 @@ import kotlin.test.assertTrue * * We assume that Alice and Bob already found each other via some market, and have agreed the details already. */ +// TODO These tests need serious cleanup. @RunWith(Parameterized::class) class TwoPartyTradeFlowTests(private val anonymous: Boolean) { companion object { @@ -80,7 +76,6 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun data(): Collection = listOf(true, false) private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) - private val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party private val DUMMY_NOTARY get() = dummyNotary.party } @@ -105,17 +100,15 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(cordappPackages), threadPerNode = true) - val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { - val notaryNode = mockNet.defaultNotaryNode + val notaryNode = mockNet.defaultNotaryNode + val notary = mockNet.defaultNotaryIdentity + notaryNode.services.ledger(notary) { val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) val bankNode = mockNet.createPartyNode(BOC_NAME) val alice = aliceNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() - doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val bob = bobNode.info.singleIdentity() - val notary = mockNet.defaultNotaryIdentity val cashIssuer = bank.ref(1) val cpIssuer = bank.ref(1, 2, 3) @@ -157,15 +150,13 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(cordappPackages), threadPerNode = true) - val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { - val notaryNode = mockNet.defaultNotaryNode + val notaryNode = mockNet.defaultNotaryNode + notaryNode.services.ledger(notaryNode.info.singleIdentity()) { val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) val bankNode = mockNet.createPartyNode(BOC_NAME) val alice = aliceNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() - doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val bob = bobNode.info.singleIdentity() val issuer = bank.ref(1) val notary = mockNet.defaultNotaryIdentity @@ -215,9 +206,9 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test fun `shutdown and restore`() { mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(cordappPackages)) - val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { - val notaryNode = mockNet.defaultNotaryNode + val notaryNode = mockNet.defaultNotaryNode + val notary = mockNet.defaultNotaryIdentity + notaryNode.services.ledger(notary) { val aliceNode = mockNet.createPartyNode(ALICE_NAME) var bobNode = mockNet.createPartyNode(BOB_NAME) val bankNode = mockNet.createPartyNode(BOC_NAME) @@ -227,10 +218,8 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val bobAddr = bobNode.network.myAddress mockNet.runNetwork() // Clear network map registration messages - val notary = mockNet.defaultNotaryIdentity val alice = aliceNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() - doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val bob = bobNode.info.singleIdentity() val issuer = bank.ref(1, 2, 3) @@ -336,7 +325,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val bob = bobNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() val issuer = bank.ref(1, 2, 3) - aliceNode.services.ledger(DUMMY_NOTARY) { + aliceNode.services.ledger(notary) { // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() JarOutputStream(stream).use { @@ -440,7 +429,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val bank: Party = bankNode.info.singleIdentity() val bob = bobNode.info.singleIdentity() val issuer = bank.ref(1, 2, 3) - aliceNode.services.ledger(DUMMY_NOTARY) { + aliceNode.services.ledger(notary) { // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() JarOutputStream(stream).use { @@ -508,18 +497,16 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test fun `dependency with error on buyer side`() { mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(cordappPackages)) - val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { - runWithError(ledgerIdentityService, true, false, "at least one cash input") + mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) { + runWithError(true, false, "at least one cash input") } } @Test fun `dependency with error on seller side`() { mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(cordappPackages)) - val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { - runWithError(ledgerIdentityService, false, true, "Issuances have a time-window") + mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) { + runWithError(false, true, "Issuances have a time-window") } } @@ -581,7 +568,6 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { data class TestTx(val notaryIdentity: Party, val price: Amount, val anonymous: Boolean) private fun LedgerDSL.runWithError( - ledgerIdentityService: IdentityServiceInternal, bobError: Boolean, aliceError: Boolean, expectedMessageSubstring: String @@ -595,7 +581,6 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val alice = aliceNode.info.singleIdentity() val bob = bobNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() - doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val issuer = bank.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { @@ -624,7 +609,6 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { } } - private fun insertFakeTransactions( wtxToSign: List, node: TestStartedNode, @@ -719,7 +703,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { notary: Party): Pair, List> { val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", notary = notary, - contractState = CommercialPaper.State(issuer, owner, amount, TEST_TX_TIME + 7.days)) + contractState = CommercialPaper.State(issuer, owner, amount, TEST_TX_TIME + 7.days)) command(issuer.party.owningKey, CommercialPaper.Commands.Issue()) if (!withError) timeWindow(time = TEST_TX_TIME) @@ -736,7 +720,6 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return Pair(vault, listOf(ap)) } - class RecordingTransactionStorage( private val database: CordaPersistence, private val delegate: WritableTransactionStorage @@ -777,5 +760,4 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { data class Add(val transaction: SignedTransaction) : TxRecord data class Get(val id: SecureHash) : TxRecord } - } diff --git a/node/src/test/kotlin/net/corda/node/services/network/DBNetworkParametersStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/network/DBNetworkParametersStorageTest.kt new file mode 100644 index 0000000000..48d99faca1 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/DBNetworkParametersStorageTest.kt @@ -0,0 +1,132 @@ +package net.corda.node.services.network + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.SignedDataWithCert +import net.corda.core.node.NetworkParameters +import net.corda.core.node.services.NetworkParametersStorage +import net.corda.node.internal.DBNetworkParametersStorage +import net.corda.nodeapi.internal.createDevNetworkMapCa +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.PrintStream +import kotlin.streams.toList + +class DBNetworkParametersStorageTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(true) + + private lateinit var networkMapClient: NetworkMapClient + private lateinit var nodeParametersStorage: NetworkParametersStorage + private lateinit var database: CordaPersistence + + private val certKeyPair: CertificateAndKeyPair = createDevNetworkMapCa() + private lateinit var netParams1: SignedDataWithCert + private lateinit var netParams2: SignedDataWithCert + private lateinit var incorrectParams: SignedDataWithCert + private lateinit var hash1: SecureHash + private lateinit var hash2: SecureHash + private lateinit var hash3: SecureHash + + @Before + fun setUp() { + netParams1 = certKeyPair.sign(testNetworkParameters(minimumPlatformVersion = 1)) + netParams2 = certKeyPair.sign(testNetworkParameters(minimumPlatformVersion = 2)) + incorrectParams = createDevNetworkMapCa(DEV_INTERMEDIATE_CA).sign(testNetworkParameters(minimumPlatformVersion = 3)) + hash1 = netParams1.raw.hash + hash2 = netParams2.raw.hash + hash3 = incorrectParams.raw.hash + database = configureDatabase( + MockServices.makeTestDataSourceProperties(), + DatabaseConfig(), + { null }, + { null } + ) + networkMapClient = createMockNetworkMapClient() + nodeParametersStorage = DBNetworkParametersStorage(TestingNamedCacheFactory(), database, networkMapClient).apply { + database.transaction { + start(netParams1, DEV_ROOT_CA.certificate) + } + } + } + + @After + fun tearDown() { + database.close() + } + + @Test + fun `set current parameters`() { + assertThat(nodeParametersStorage.currentHash).isEqualTo(hash1) + assertThat(nodeParametersStorage.lookup(hash1)).isEqualTo(netParams1.verified()) + } + + @Test + fun `get default parameters`() { + // TODO After implementing default endpoint on network map check it is correct, for now we set it to current. + assertThat(nodeParametersStorage.defaultHash).isEqualTo(hash1) + } + + @Test + fun `download parameters from network map server`() { + database.transaction { + val netParams = nodeParametersStorage.lookup(hash2) + assertThat(nodeParametersStorage.lookup(hash2)).isEqualTo(netParams) + verify(networkMapClient, times(1)).getNetworkParameters(hash2) + + } + } + + @Test + fun `try save parameters with incorrect signature`() { + database.transaction { + val consoleOutput = interceptConsoleOutput { + nodeParametersStorage.lookup(hash3) + } + assertThat(consoleOutput).anySatisfy { + it.contains("Caused by: java.security.cert.CertPathValidatorException: subject/issuer name chaining check failed") + } + } + } + + private fun interceptConsoleOutput(block: () -> Unit): List { + val oldOut = System.out + val out = ByteOutputStream() + System.setOut(PrintStream(out)) + block() + System.setOut(oldOut) + return out.bytes.inputStream().bufferedReader().lines().toList() + } + + private fun createMockNetworkMapClient(): NetworkMapClient { + return mock { + on { getNetworkParameters(any()) }.then { + val hash = it.getArguments()[0] + when (hash) { + hash1 -> netParams1 + hash2 -> netParams2 + hash3 -> incorrectParams + else -> null + } + } + } + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index 57c6ce8e57..1e1a5722b1 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -19,6 +19,7 @@ import net.corda.node.VersionInfo import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.core.internal.NODE_INFO_DIRECTORY +import net.corda.node.internal.NetworkParametersStorageInternal import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.X509Utilities @@ -64,6 +65,7 @@ class NetworkMapUpdaterTest { private val networkMapCache = createMockNetworkMapCache() private lateinit var ourKeyPair: KeyPair private lateinit var ourNodeInfo: SignedNodeInfo + private val networkParametersStorage: NetworkParametersStorageInternal = mock() private lateinit var server: NetworkMapServer private lateinit var networkMapClient: NetworkMapClient private lateinit var updater: NetworkMapUpdater @@ -86,7 +88,7 @@ class NetworkMapUpdaterTest { } private fun setUpdater(extraNetworkMapKeys: List = emptyList(), netMapClient: NetworkMapClient? = networkMapClient) { - updater = NetworkMapUpdater(networkMapCache, fileWatcher, netMapClient, baseDir, extraNetworkMapKeys) + updater = NetworkMapUpdater(networkMapCache, fileWatcher, netMapClient, baseDir, extraNetworkMapKeys, networkParametersStorage) } private fun startUpdater(ourNodeInfo: SignedNodeInfo = this.ourNodeInfo, @@ -236,6 +238,7 @@ class NetworkMapUpdaterTest { val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME assert(!updateFile.exists()) { "network parameters should not be auto accepted" } updater.acceptNewNetworkParameters(newHash) { it.serialize().sign(ourKeyPair) } + verify(networkParametersStorage, times(1)).saveParameters(any()) val signedNetworkParams = updateFile.readObject() val paramsFromFile = signedNetworkParams.verifiedNetworkParametersCert(DEV_ROOT_CA.certificate) assertEquals(newParameters, paramsFromFile) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt index 563dd63bcf..7b42e089f6 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt @@ -4,21 +4,18 @@ import co.paralleluniverse.fibers.Suspendable import com.esotericsoftware.kryo.KryoException import net.corda.core.contracts.UniqueIdentifier import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowLogic.Companion.sleep import net.corda.core.identity.CordaX500Name import net.corda.core.schemas.MappedSchema -import net.corda.core.serialization.CordaSerializable import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService -import org.junit.BeforeClass -import org.junit.ClassRule +import org.junit.After +import org.junit.Before import org.junit.Test import java.io.Serializable -import java.time.Duration import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -26,7 +23,6 @@ import javax.persistence.Table import kotlin.test.assertEquals import kotlin.test.assertFailsWith - class ExposeJpaToFlowsTests { object FooSchema @@ -39,15 +35,28 @@ class ExposeJpaToFlowsTests { val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) val cordapps = listOf("net.corda.node.services.persistence") - val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( - cordappPackages = cordapps, - identityService = makeTestIdentityService(myself.identity), - initialIdentity = myself, - networkParameters = testNetworkParameters(minimumPlatformVersion = 4) - ) + lateinit var mockNet: MockNetwork + lateinit var services: MockServices + lateinit var database: CordaPersistence - val services: MockServices = databaseAndServices.second - val database: CordaPersistence = databaseAndServices.first + @Before + fun setUp() { + mockNet = MockNetwork(cordapps) + val (db, mockServices) = MockServices.makeTestDatabaseAndMockServices( + cordappPackages = cordapps, + identityService = makeTestIdentityService(myself.identity), + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + + services = mockServices + database = db + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } @Test fun `can persist and query custom entities`() { @@ -71,7 +80,6 @@ class ExposeJpaToFlowsTests { @Test fun `can't perform suspendable operations inside withEntityManager`() { - val mockNet = MockNetwork(cordapps) val mockNode = mockNet.createNode() assertFailsWith(KryoException::class) { mockNode.startFlow(object : FlowLogic() { @@ -84,6 +92,5 @@ class ExposeJpaToFlowsTests { } }) } - mockNet.stopNodes() } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 887faca322..32ec4f118b 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -4,8 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.MetricRegistry import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -20,7 +18,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.JarSignatureTestUtils.createJar import net.corda.testing.core.JarSignatureTestUtils.generateKey @@ -33,7 +30,10 @@ import net.corda.testing.node.MockServices.Companion.makeTestDataSourcePropertie import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.assertThatIllegalArgumentException -import org.junit.* +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test import java.io.ByteArrayOutputStream import java.io.Closeable import java.io.OutputStream @@ -52,7 +52,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNull - class NodeAttachmentServiceTest { // Use an in memory file system for testing attachment storage. @@ -69,8 +68,6 @@ class NodeAttachmentServiceTest { database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null }) fs = Jimfs.newFileSystem(Configuration.unix()) - doReturn(testNetworkParameters()).whenever(services).networkParameters - storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also { database.transaction { it.start() @@ -427,5 +424,4 @@ class NodeAttachmentServiceTest { return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name) } } - } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt index 19d8693539..1f495e95c0 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt @@ -40,6 +40,7 @@ class MaxTransactionSizeTests { mockNet = MockNetwork(listOf("net.corda.testing.contracts"), networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) + bobNode.registerInitiatedFlow(ReceiveLargeTransactionFlow::class.java) notaryNode = mockNet.defaultNotaryNode notary = mockNet.defaultNotaryIdentity alice = aliceNode.info.singleIdentity() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 40efe942d0..6ab13f5f45 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -3,18 +3,23 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.StateRef import net.corda.core.crypto.* +import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party import net.corda.core.internal.NotaryChangeTransactionBuilder import net.corda.core.node.ServiceHub +import net.corda.core.serialization.serialize +import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.singleIdentity import net.corda.testing.node.MockNetworkNotarySpec import net.corda.testing.node.internal.* +import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test @@ -31,7 +36,8 @@ class NotaryServiceTests { fun setup() { mockNet = InternalMockNetwork( cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), - notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating = false)) + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating = false)), + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) ) aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that @@ -49,6 +55,28 @@ class NotaryServiceTests { notariseWithTooManyInputs(aliceNode, alice, notary, mockNet) } + @Test + fun `should reject when network parameters component is not visible`() { + val stx = generateTransaction(aliceNode, alice, notary, null, 13) + val future = aliceNode.services.startFlow(DummyClientFlow(stx, notary)).resultFuture + mockNet.runNetwork() + val ex = assertFailsWith { future.getOrThrow() } + val notaryError = ex.error as NotaryError.TransactionInvalid + assertThat(notaryError.cause).hasMessageContaining("Transaction for notarisation was tagged with parameters with hash: null") + } + + @Test + fun `should reject when parameters not current`() { + val hash = SecureHash.randomSHA256() + val stx = generateTransaction(aliceNode, alice, notary, hash, 13) + val future = aliceNode.services.startFlow(DummyClientFlow(stx, notary)).resultFuture + mockNet.runNetwork() + val ex = assertFailsWith { future.getOrThrow() } + val notaryError = ex.error as NotaryError.TransactionInvalid + assertThat(notaryError.cause).hasMessageContaining("Transaction for notarisation was tagged with parameters with hash: $hash, " + + "but current network parameters are: ${notaryServices.networkParametersStorage.currentHash}") + } + internal companion object { /** This is used by both [NotaryServiceTests] and [ValidatingNotaryServiceTests]. */ fun notariseWithTooManyInputs(node: TestStartedNode, party: Party, notary: Party, network: InternalMockNetwork) { @@ -59,10 +87,17 @@ class NotaryServiceTests { assertFailsWith { future.getOrThrow() } } - private fun generateTransaction(node: TestStartedNode, party: Party, notary: Party): SignedTransaction { + private fun generateTransaction(node: TestStartedNode, + party: Party, notary: Party, + paramsHash: SecureHash? = node.services.networkParametersStorage.currentHash, + numberOfInputs: Int = 10_005): SignedTransaction { val txHash = SecureHash.randomSHA256() - val inputs = (1..10_005).map { StateRef(txHash, it) } - val tx = NotaryChangeTransactionBuilder(inputs, notary, party).build() + val inputs = (1..numberOfInputs).map { StateRef(txHash, it) } + val tx = if (paramsHash != null) { + NotaryChangeTransactionBuilder(inputs, notary, party, paramsHash).build() + } else { + NotaryChangeWireTransaction(listOf(inputs, notary, party).map { it.serialize() }) + } return node.services.run { val myKey = myInfo.legalIdentities.first().owningKey diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt index 847d6daccd..20b1710cef 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt @@ -11,6 +11,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService +import org.junit.Before import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals @@ -25,14 +26,7 @@ class ResolveStatePointersTest { private val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) private val notary = TestIdentity(DUMMY_NOTARY_NAME, 20) private val cordapps = listOf("net.corda.testing.contracts") - private val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( - cordappPackages = cordapps, - identityService = makeTestIdentityService(notary.identity, myself.identity), - initialIdentity = myself, - networkParameters = testNetworkParameters(minimumPlatformVersion = 4) - ) - - private val services = databaseAndServices.second + private lateinit var services: MockServices private data class Bar( override val participants: List = listOf(), @@ -58,6 +52,17 @@ class ResolveStatePointersTest { } } + @Before + fun setUp() { + val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( + cordappPackages = cordapps, + identityService = makeTestIdentityService(notary.identity, myself.identity), + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + services = databaseAndServices.second + } + @Test fun `resolve state pointers and check reference state is added to transaction`() { val stateAndRef = createPointedToState(barOne) @@ -154,5 +159,4 @@ class ResolveStatePointersTest { val foo = ltx.outputs.single().data as Foo assertEquals(stateAndRef, foo.baz.resolve(ltx)) } - } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 4fa58bbd3a..743850aa46 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -2,6 +2,7 @@ package net.corda.node.services.transactions import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Command +import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.crypto.* @@ -14,16 +15,19 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.services.issueInvalidState import net.corda.node.services.messaging.Message import net.corda.node.services.statemachine.InitialSessionMessage +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.dummyCommand import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.createWireTransaction import net.corda.testing.node.TestClock import net.corda.testing.node.internal.* import org.assertj.core.api.Assertions.assertThat @@ -46,7 +50,8 @@ class ValidatingNotaryServiceTests { @Before fun setup() { - mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), + networkParameters = testNetworkParameters(minimumPlatformVersion = 4)) aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) notaryNode = mockNet.defaultNotaryNode notary = mockNet.defaultNotaryIdentity @@ -96,6 +101,27 @@ class ValidatingNotaryServiceTests { assertEquals(setOf(expectedMissingKey), missingKeys) } + @Test + fun `should reject transaction without network parameters`() { + val inputState = issueState(aliceNode.services, alice).ref + val wtx = createWireTransaction(inputs = listOf(inputState), + attachments = emptyList(), + outputs = emptyList(), + commands = listOf(dummyCommand(alice.owningKey)), + notary = notary, + timeWindow = null) + assertThat(wtx.networkParametersHash).isNull() + val sig = aliceNode.services.keyManagementService.sign( + SignableData(wtx.id, SignatureMetadata(1, Crypto.findSignatureScheme(alice.owningKey).schemeNumberID)), alice.owningKey + ) + val stx = SignedTransaction(wtx, listOf(sig)) + assertThat(stx.networkParametersHash).isNull() + val future = runNotaryClient(stx) + val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() } + val notaryError = ex.error as NotaryError.TransactionInvalid + assertThat(notaryError.cause).hasMessageContaining("Transaction for notarisation was tagged with parameters with hash: null") + } + @Test fun `should sign a unique transaction with a valid time-window`() { val stx = run { @@ -173,7 +199,7 @@ class ValidatingNotaryServiceTests { fun `notarise issue tx with time-window`() { val stx = run { val tx = DummyContract.generateInitial(Random().nextInt(), notary, alice.ref(0)) - .setTimeWindow(Instant.now(), 30.seconds) + .setTimeWindow(Instant.now(), 30.seconds) aliceNode.services.signInitialTransaction(tx) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt index 8e5b966e47..0c62b3d3a5 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt @@ -19,6 +19,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices +import org.junit.Before import org.junit.Rule import org.junit.Test import java.util.* @@ -37,19 +38,25 @@ class ExternalIdMappingTest { private val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) private val notary = TestIdentity(CordaX500Name("NotaryService", "London", "GB"), 1337L) - private val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( - cordappPackages = cordapps, - identityService = rigorousMock().also { - doReturn(notary.party).whenever(it).partyFromKey(notary.publicKey) - doReturn(notary.party).whenever(it).wellKnownPartyFromAnonymous(notary.party) - doReturn(notary.party).whenever(it).wellKnownPartyFromX500Name(notary.name) - }, - initialIdentity = myself, - networkParameters = testNetworkParameters(minimumPlatformVersion = 4) - ) - private val services: MockServices = databaseAndServices.second - private val database: CordaPersistence = databaseAndServices.first + lateinit var services: MockServices + lateinit var database: CordaPersistence + + @Before + fun setUp() { + val (db, mockServices) = MockServices.makeTestDatabaseAndMockServices( + cordappPackages = cordapps, + identityService = rigorousMock().also { + doReturn(notary.party).whenever(it).partyFromKey(notary.publicKey) + doReturn(notary.party).whenever(it).wellKnownPartyFromAnonymous(notary.party) + doReturn(notary.party).whenever(it).wellKnownPartyFromX500Name(notary.name) + }, + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + services = mockServices + database = db + } private fun freshKeyForExternalId(externalId: UUID): AnonymousParty { val anonymousParty = freshKey() diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index edf277fcdf..0a3aa343c7 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -29,6 +29,7 @@ import net.corda.finance.utils.sumCash import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.* @@ -90,17 +91,19 @@ class NodeVaultServiceTest { @Before fun setUp() { LogHelper.setLevel(NodeVaultService::class) + val parameters = testNetworkParameters() val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( cordappPackages, makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), - megaCorp) + megaCorp, + parameters) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) // This is safe because MockServices only ever have a single identity identity = services.myInfo.singleIdentityAndCert() - issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock()) - bocServices = MockServices(cordappPackages, bankOfCorda, rigorousMock()) + issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock(), parameters) + bocServices = MockServices(cordappPackages, bankOfCorda, rigorousMock(), parameters) services.identityService.verifyAndRegisterIdentity(DUMMY_CASH_ISSUER_IDENTITY) services.identityService.verifyAndRegisterIdentity(BOC_IDENTITY) } @@ -571,7 +574,6 @@ class NodeVaultServiceTest { } // TODO: Unit test linear state relevancy checks - @Test fun `correct updates are generated for general transactions`() { val notary = identity.party @@ -647,7 +649,7 @@ class NodeVaultServiceTest { // Change notary services.identityService.verifyAndRegisterIdentity(DUMMY_NOTARY_IDENTITY) val newNotary = DUMMY_NOTARY - val changeNotaryTx = NotaryChangeTransactionBuilder(listOf(initialCashState.ref), issueStx.notary!!, newNotary).build() + val changeNotaryTx = NotaryChangeTransactionBuilder(listOf(initialCashState.ref), issueStx.notary!!, newNotary, services.networkParametersStorage.currentHash).build() val cashStateWithNewNotary = StateAndRef(initialCashState.state.copy(notary = newNotary), StateRef(changeNotaryTx.id, 0)) database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt index 137de148c0..7615755807 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt @@ -16,6 +16,10 @@ import org.junit.rules.ExpectedException class VaultQueryExceptionsTests : VaultQueryParties by rule { companion object { + @ClassRule + @JvmField + val testSerialization = SerializationEnvironmentRule() + @ClassRule @JvmField val rule = object : VaultQueryTestRule() { @@ -26,10 +30,6 @@ class VaultQueryExceptionsTests : VaultQueryParties by rule { } } - @Rule - @JvmField - val testSerialization = SerializationEnvironmentRule() - @Rule @JvmField val expectedEx: ExpectedException = ExpectedException.none() diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 70dbfe79fe..86b0d120fd 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -99,7 +99,6 @@ interface VaultQueryParties { } open class VaultQueryTestRule : ExternalResource(), VaultQueryParties { - override val alice = TestIdentity(ALICE_NAME, 70) override val bankOfCorda = TestIdentity(BOC_NAME) override val bigCorp = TestIdentity(CordaX500Name("BigCorporation", "New York", "US")) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index fe2c054673..1c1e6a5e52 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -9,6 +9,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.SignedDataWithCert import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -16,10 +17,12 @@ import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader +import net.corda.node.internal.NetworkParametersStorageInternal import net.corda.node.internal.ServicesForResolutionImpl import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.* @@ -40,9 +43,11 @@ import net.corda.testing.services.MockAttachmentStorage import java.security.KeyPair import java.sql.Connection import java.time.Clock +import java.time.Instant import java.util.* import java.util.function.Consumer import javax.persistence.EntityManager +import kotlin.collections.HashMap /** * Returns a simple [InMemoryIdentityService] containing the supplied [identities]. @@ -104,7 +109,7 @@ open class MockServices private constructor( fun makeTestDatabaseAndMockServices(cordappPackages: List, identityService: IdentityService, initialIdentity: TestIdentity, - networkParameters: NetworkParameters = testNetworkParameters(), + networkParameters: NetworkParameters = testNetworkParameters(modifiedTime = Instant.MIN), vararg moreKeys: KeyPair): Pair { val cordappLoader = cordappLoaderForPackages(cordappPackages) @@ -114,7 +119,7 @@ open class MockServices private constructor( val mockService = database.transaction { object : MockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys) { override val vaultService: VaultService = makeVaultService(schemaService, database) - + override val networkParametersStorage: NetworkParametersStorage get() = MockNetworkParametersStorage(networkParameters) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { ServiceHubInternal.recordTransactions(statesToRecord, txs, validatedTransactions as WritableTransactionStorage, @@ -164,7 +169,7 @@ open class MockServices private constructor( initialIdentity: TestIdentity, identityService: IdentityService = makeTestIdentityService(), vararg moreKeys: KeyPair) : - this(cordappLoaderForPackages(cordappPackages), identityService, testNetworkParameters(), initialIdentity, moreKeys) + this(cordappLoaderForPackages(cordappPackages), identityService, testNetworkParameters(modifiedTime = Instant.MIN), initialIdentity, moreKeys) constructor(cordappPackages: Iterable, initialIdentity: TestIdentity, @@ -252,15 +257,14 @@ open class MockServices private constructor( } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also { - it.start( networkParameters.whitelistedContractImplementations) + it.start(networkParameters.whitelistedContractImplementations) } override val cordappProvider: CordappProvider get() = mockCordappProvider + override val networkParametersStorage: NetworkParametersStorage get() = MockNetworkParametersStorage(networkParameters) protected val servicesForResolution: ServicesForResolution get() { - return ServicesForResolutionImpl(identityService, attachments, cordappProvider, validatedTransactions).also { - it.start(networkParameters) - } + return ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, validatedTransactions) } internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence): VaultServiceInternal { @@ -318,4 +322,4 @@ fun createMockCordaService(serviceHub: MockServices, serv } } return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance -} \ No newline at end of file +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNetworkParametersStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNetworkParametersStorage.kt new file mode 100644 index 0000000000..02f397b146 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNetworkParametersStorage.kt @@ -0,0 +1,27 @@ +package net.corda.testing.node.internal + +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.SignedDataWithCert +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.serialize +import net.corda.node.internal.NetworkParametersStorageInternal +import net.corda.testing.common.internal.testNetworkParameters +import java.time.Instant + +class MockNetworkParametersStorage(val currentParameters: NetworkParameters = testNetworkParameters(modifiedTime = Instant.MIN)) : NetworkParametersStorageInternal { + private val hashToParametersMap: HashMap = HashMap() + + init { + hashToParametersMap[currentHash] = currentParameters + } + + override val currentHash: SecureHash get() = currentParameters.serialize().hash + override val defaultHash: SecureHash get() = currentHash + override fun getEpochFromHash(hash: SecureHash): Int? = lookup(hash)?.epoch + override fun lookup(hash: SecureHash): NetworkParameters? = hashToParametersMap[hash] + override fun saveParameters(signedNetworkParameters: SignedDataWithCert) { + val networkParameters = signedNetworkParameters.verified() + val hash = signedNetworkParameters.raw.hash + hashToParametersMap[hash] = networkParameters + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index e5a524d67c..cafa1bf687 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -155,7 +155,7 @@ fun createWireTransaction(inputs: List, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt = PrivacySalt()): WireTransaction { - val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()) + val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) return WireTransaction(componentGroups, privacySalt) }