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) }