diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt new file mode 100644 index 0000000000..35ea7158ef --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -0,0 +1,57 @@ +package net.corda.core.internal + +import net.corda.core.DeleteForDJVM +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappContext +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.ZoneVersionTooLowException +import net.corda.core.serialization.SerializationContext +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.transactions.WireTransaction +import org.slf4j.MDC + +// *Internal* Corda-specific utilities + +fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) { + val currentMinPlatformVersion = networkParameters.minimumPlatformVersion + if (currentMinPlatformVersion < requiredMinPlatformVersion) { + throw ZoneVersionTooLowException( + "$feature requires all nodes on the Corda compatibility zone to be running at least platform version " + + "$requiredMinPlatformVersion. The current zone is only enforcing a minimum platform version of " + + "$currentMinPlatformVersion. Please contact your zone operator." + ) + } +} + +/** Provide access to internal method for AttachmentClassLoaderTests */ +@DeleteForDJVM +fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { + return toWireTransactionWithContext(services, serializationContext) +} + +/** Provide access to internal method for AttachmentClassLoaderTests */ +@DeleteForDJVM +fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { + return toLedgerTransactionWithContext(services, serializationContext) +} + +fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { + return CordappContext(cordapp, attachmentId, classLoader, config) +} + +/** Checks if this flow is an idempotent flow. */ +fun Class>.isIdempotentFlow(): Boolean { + return IdempotentFlow::class.java.isAssignableFrom(this) +} + +/** + * Ensures each log entry from the current thread will contain id of the transaction in the MDC. + */ +internal fun SignedTransaction.pushToLoggingContext() { + MDC.put("tx_id", id.toString()) +} diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 6a53f25755..9626cf88a0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -388,18 +388,6 @@ fun uncheckedCast(obj: T) = obj as U fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } -/** Provide access to internal method for AttachmentClassLoaderTests */ -@DeleteForDJVM -fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { - return toWireTransactionWithContext(services, serializationContext) -} - -/** Provide access to internal method for AttachmentClassLoaderTests */ -@DeleteForDJVM -fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { - return toLedgerTransactionWithContext(services, serializationContext) -} - /** Returns the location of this class. */ val Class<*>.location: URL get() = protectionDomain.codeSource.location @@ -499,29 +487,13 @@ fun SerializedBytes.sign(keyPair: KeyPair): SignedData = SignedD fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } -fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { - return CordappContext(cordapp, attachmentId, classLoader, config) -} - val PublicKey.hash: SecureHash get() = encoded.sha256() -/** Checks if this flow is an idempotent flow. */ -fun Class>.isIdempotentFlow(): Boolean { - return IdempotentFlow::class.java.isAssignableFrom(this) -} - /** * Extension method for providing a sumBy method that processes and returns a Long */ fun Iterable.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum() -/** - * Ensures each log entry from the current thread will contain id of the transaction in the MDC. - */ -internal fun SignedTransaction.pushToLoggingContext() { - MDC.put("tx_id", id.toString()) -} - fun SerializedBytes.checkPayloadIs(type: Class): UntrustworthyData { val payloadData: T = try { val serializer = SerializationDefaults.SERIALIZATION_FACTORY diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 1b8618500d..7127177c71 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -1,5 +1,6 @@ package net.corda.core.node +import net.corda.core.CordaRuntimeException import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentId @@ -105,4 +106,10 @@ data class NetworkParameters( */ @KeepForDJVM @CordaSerializable -data class NotaryInfo(val identity: Party, val validating: Boolean) \ No newline at end of file +data class NotaryInfo(val identity: Party, val validating: Boolean) + +/** + * When a Corda feature cannot be used due to the node's compatibility zone not enforcing a high enough minimum platform + * version. + */ +class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) 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 0331d54dbc..831a6e098e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -1,6 +1,7 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand +import net.corda.core.CordaInternal import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider @@ -9,9 +10,11 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.ensureMinimumPlatformVersion import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution +import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationContext @@ -74,7 +77,7 @@ open class TransactionBuilder @JvmOverloads constructor( for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) - is ReferencedStateAndRef<*> -> @Suppress("DEPRECATION") addReferenceState(t) // Will remove when feature finalised. + is ReferencedStateAndRef<*> -> addReferenceState(t) is SecureHash -> addAttachment(t) is TransactionState<*> -> addOutputState(t) is StateAndContract -> addOutputState(t.state, t.contract) @@ -95,11 +98,18 @@ open class TransactionBuilder @JvmOverloads constructor( * [HashAttachmentConstraint]. * * @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder]. + * + * @throws ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4. */ @Throws(MissingContractAttachments::class) fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services) + @CordaInternal internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction { + val referenceStates = referenceStates() + if (referenceStates.isNotEmpty()) { + services.ensureMinimumPlatformVersion(4, "Reference states") + } // Resolves the AutomaticHashConstraints to HashAttachmentConstraints or WhitelistedByZoneAttachmentConstraint based on a global parameter. // The AutomaticHashConstraint allows for less boiler plate when constructing transactions since for the typical case the named contract @@ -109,14 +119,27 @@ open class TransactionBuilder @JvmOverloads constructor( when { state.constraint !== AutomaticHashConstraint -> state useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint) - else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let { - state.copy(constraint = HashAttachmentConstraint(it)) - } ?: throw MissingContractAttachments(listOf(state)) + else -> { + services.cordappProvider.getContractAttachmentID(state.contract)?.let { + state.copy(constraint = HashAttachmentConstraint(it)) + } ?: throw MissingContractAttachments(listOf(state)) + } } } return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { - WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window, referenceStates()), privacySalt) + WireTransaction( + WireTransaction.createComponentGroups( + inputStates(), + resolvedOutputs, + commands, + attachments + makeContractAttachments(services.cordappProvider), + notary, + window, + referenceStates + ), + privacySalt + ) } } @@ -169,12 +192,9 @@ open class TransactionBuilder @JvmOverloads constructor( /** * Adds a reference input [StateRef] to the transaction. * - * This feature was added in version 4 of Corda, so will throw an exception for any Corda networks with a minimum - * platform version less than 4. - * - * @throws UncheckedVersionException + * Note: Reference states are only supported on Corda networks running a minimum platform version of 4. + * [toWireTransaction] will throw an [IllegalStateException] if called in such an environment. */ - @Deprecated(message = "Feature not yet released. Pending stabilisation.") open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { val stateAndRef = referencedStateAndRef.stateAndRef referencesWithTransactionState.add(stateAndRef.state) @@ -283,10 +303,10 @@ open class TransactionBuilder @JvmOverloads constructor( return this } - /** Returns an immutable list of input [StateRefs]. */ + /** Returns an immutable list of input [StateRef]s. */ fun inputStates(): List = ArrayList(inputs) - /** Returns an immutable list of reference input [StateRefs]. */ + /** Returns an immutable list of reference input [StateRef]s. */ fun referenceStates(): List = ArrayList(references) /** Returns an immutable list of attachment hashes. */ @@ -302,7 +322,10 @@ open class TransactionBuilder @JvmOverloads constructor( * Sign the built transaction and return it. This is an internal function for use by the service hub, please use * [ServiceHub.signInitialTransaction] instead. */ - fun toSignedTransaction(keyManagementService: KeyManagementService, publicKey: PublicKey, signatureMetadata: SignatureMetadata, services: ServicesForResolution): SignedTransaction { + fun toSignedTransaction(keyManagementService: KeyManagementService, + publicKey: PublicKey, + signatureMetadata: SignatureMetadata, + services: ServicesForResolution): SignedTransaction { val wtx = toWireTransaction(services) val signableData = SignableData(wtx.id, signatureMetadata) val sig = keyManagementService.sign(signableData, publicKey) diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt new file mode 100644 index 0000000000..c24a485776 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -0,0 +1,85 @@ +package net.corda.core.transactions + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappProvider +import net.corda.core.crypto.SecureHash +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.ZoneVersionTooLowException +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class TransactionBuilderTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val notary = TestIdentity(DUMMY_NOTARY_NAME).party + private val services = rigorousMock() + private val contractAttachmentId = SecureHash.randomSHA256() + + @Before + fun setup() { + val cordappProvider = rigorousMock() + doReturn(cordappProvider).whenever(services).cordappProvider + doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) + doReturn(testNetworkParameters()).whenever(services).networkParameters + } + + @Test + fun `bare minimum issuance tx`() { + val outputState = TransactionState( + data = DummyState(), + contract = DummyContract.PROGRAM_ID, + notary = notary, + constraint = HashAttachmentConstraint(contractAttachmentId) + ) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, notary.owningKey) + val wtx = builder.toWireTransaction(services) + assertThat(wtx.outputs).containsOnly(outputState) + assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey)) + } + + @Test + fun `automatic hash constraint`() { + val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, notary.owningKey) + val wtx = builder.toWireTransaction(services) + assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = HashAttachmentConstraint(contractAttachmentId))) + } + + @Test + fun `reference states`() { + val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) + val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1) + val builder = TransactionBuilder(notary) + .addReferenceState(StateAndRef(referenceState, referenceStateRef).referenced()) + .addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)) + .addCommand(DummyCommandData, notary.owningKey) + + doReturn(testNetworkParameters(minimumPlatformVersion = 3)).whenever(services).networkParameters + assertThatThrownBy { builder.toWireTransaction(services) } + .isInstanceOf(ZoneVersionTooLowException::class.java) + .hasMessageContaining("Reference states") + + doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters + val wtx = builder.toWireTransaction(services) + assertThat(wtx.references).containsOnly(referenceStateRef) + } +} diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f9e3ffacd8..e3499fb54b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -188,7 +188,8 @@ Unreleased to in a transaction by the contracts of input and output states but whose contract is not executed as part of the transaction verification process and is not consumed when the transaction is committed to the ledger but is checked for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a - normal state when it occurs in an input or output position. + normal state when it occurs in an input or output position. *This feature is only available on Corda networks running + with a minimum platform version of 4.* .. _changelog_v3.1: 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 258755982f..617e3a2455 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -220,7 +220,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private var _started: S? = null private fun T.tokenize(): T { - tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finialised") + tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised") return this } @@ -239,10 +239,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun initKeyStore(): X509Certificate { if (configuration.devMode) { - log.warn("The Corda node is running in developer mode. This is not suitable for production usage.") configuration.configureWithDevSSLCertificate() - } else { - log.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.") } return validateKeyStore() } @@ -317,12 +314,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned + services.start(nodeInfo, netParams) networkMapUpdater.start(trustRoot, signedNetParams.raw.hash, signedNodeInfo.raw.hash) startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) // Do all of this in a database transaction so anything that might need a connection has one. return database.transaction { - services.start(nodeInfo, netParams) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) attachments.start() cordappProvider.start(netParams.whitelistedContractImplementations) @@ -731,7 +728,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected open fun startDatabase() { val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.hikariStart(props) + database.startHikariPool(props) // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) } @@ -994,7 +991,7 @@ fun configureDatabase(hikariProperties: Properties, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, schemaService: SchemaService = NodeSchemaService()): CordaPersistence { val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties) - persistence.hikariStart(hikariProperties) + persistence.startHikariPool(hikariProperties) return persistence } @@ -1013,7 +1010,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters) } -fun CordaPersistence.hikariStart(hikariProperties: Properties) { +fun CordaPersistence.startHikariPool(hikariProperties: Properties) { try { start(DataSourceFactory.createDataSource(hikariProperties)) } catch (ex: Exception) { diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index ac8d29d96b..fddaeb47eb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -27,9 +27,9 @@ import net.corda.node.utilities.registration.UnableToRegisterNodeWithDoormanExce import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore import net.corda.nodeapi.internal.addShutdownHook -import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException +import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.tools.shell.InteractiveShell import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole @@ -319,6 +319,8 @@ open class NodeStartup(val args: Array) { Emoji.renderIfSupported { Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.") } + } else { + logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.") } val nodeInfo = node.start() @@ -332,7 +334,7 @@ open class NodeStartup(val args: Array) { if (conf.shouldStartLocalShell()) { node.startupComplete.then { try { - InteractiveShell.runLocalShell({ node.stop() }) + InteractiveShell.runLocalShell(node::stop) } catch (e: Throwable) { logger.error("Shell failed to start", e) } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 1016e34cde..0fe84c9d3a 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -70,9 +70,7 @@ class NetworkMapCacheImpl( } } -/** - * Extremely simple in-memory cache of the network map. - */ +/** Database-based network map cache. */ @ThreadSafe open class PersistentNetworkMapCache(private val database: CordaPersistence) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { companion object {