Skip to content

Commit

Permalink
CORDA-1845: Check for min plaform version of 4 when building transact…
Browse files Browse the repository at this point in the history
…ions with reference states (corda#3705)

Also includes some minor cleanup brought up in a previous PR.
  • Loading branch information
shamsasari authored Jul 31, 2018
1 parent d42b9f5 commit 994fe0d
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 56 deletions.
57 changes: 57 additions & 0 deletions core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt
Original file line number Diff line number Diff line change
@@ -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<out FlowLogic<*>>.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())
}
28 changes: 0 additions & 28 deletions core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -388,18 +388,6 @@ fun <T, U : T> uncheckedCast(obj: T) = obj as U

fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = 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

Expand Down Expand Up @@ -499,29 +487,13 @@ fun <T : Any> SerializedBytes<T>.sign(keyPair: KeyPair): SignedData<T> = 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<out FlowLogic<*>>.isIdempotentFlow(): Boolean {
return IdempotentFlow::class.java.isAssignableFrom(this)
}

/**
* Extension method for providing a sumBy method that processes and returns a Long
*/
fun <T> Iterable<T>.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 <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): UntrustworthyData<T> {
val payloadData: T = try {
val serializer = SerializationDefaults.SERIALIZATION_FACTORY
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -105,4 +106,10 @@ data class NetworkParameters(
*/
@KeepForDJVM
@CordaSerializable
data class NotaryInfo(val identity: Party, val validating: Boolean)
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)
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
)
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<StateRef> = ArrayList(inputs)

/** Returns an immutable list of reference input [StateRefs]. */
/** Returns an immutable list of reference input [StateRef]s. */
fun referenceStates(): List<StateRef> = ArrayList(references)

/** Returns an immutable list of attachment hashes. */
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ServicesForResolution>()
private val contractAttachmentId = SecureHash.randomSHA256()

@Before
fun setup() {
val cordappProvider = rigorousMock<CordappProvider>()
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)
}
}
3 changes: 2 additions & 1 deletion docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading

0 comments on commit 994fe0d

Please sign in to comment.