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 388b9c3205..ae68b568a0 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 @@ -27,6 +27,7 @@ import net.corda.core.crypto.* import net.corda.core.crypto.PartialMerkleTree.PartialTree import net.corda.core.identity.* import net.corda.core.internal.DigitalSignatureWithCert +import net.corda.core.internal.createComponentGroups import net.corda.core.internal.kotlinObjectInstance import net.corda.core.node.NodeInfo import net.corda.core.serialization.SerializedBytes @@ -187,6 +188,7 @@ private class WireTransactionSerializer : JsonSerializer() { value.commands, value.timeWindow, value.attachments, + value.references, value.privacySalt )) } @@ -195,13 +197,14 @@ private class WireTransactionSerializer : JsonSerializer() { private class WireTransactionDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction { val wrapper = parser.readValueAs() - val componentGroups = WireTransaction.createComponentGroups( + val componentGroups = createComponentGroups( wrapper.inputs, wrapper.outputs, wrapper.commands, wrapper.attachments, wrapper.notary, - wrapper.timeWindow + wrapper.timeWindow, + wrapper.references ) return WireTransaction(componentGroups, wrapper.privacySalt) } @@ -214,6 +217,7 @@ private class WireTransactionJson(val id: SecureHash, val commands: List>, val timeWindow: TimeWindow?, val attachments: List, + val references: List, val privacySalt: PrivacySalt) private interface TransactionStateMixin { 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 7f30b63dcc..e4aa6ff84f 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 @@ -93,7 +93,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: services = rigorousMock() cordappProvider = rigorousMock() doReturn(cordappProvider).whenever(services).cordappProvider - doReturn(testNetworkParameters()).whenever(services).networkParameters + doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters doReturn(attachments).whenever(services).attachments } @@ -245,6 +245,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: outputs = mutableListOf(createTransactionState()), commands = mutableListOf(Command(DummyCommandData, listOf(BOB_PUBKEY))), window = TimeWindow.fromStartAndDuration(Instant.now(), 1.hours), + references = mutableListOf(StateRef(SecureHash.randomSHA256(), 0)), privacySalt = net.corda.core.contracts.PrivacySalt() ).toWireTransaction(services) val stx = sign(wtx) @@ -253,7 +254,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", "privacySalt") + val wtxFields = wtxJson.assertHasOnlyFields("id", "notary", "inputs", "attachments", "outputs", "commands", "timeWindow", "references", "privacySalt") assertThat(wtxFields[0].valueAs(mapper)).isEqualTo(wtx.id) assertThat(wtxFields[1].valueAs(mapper)).isEqualTo(wtx.notary) assertThat(wtxFields[2].childrenAs(mapper)).isEqualTo(wtx.inputs) @@ -261,7 +262,8 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: assertThat(wtxFields[4].childrenAs>(mapper)).isEqualTo(wtx.outputs) assertThat(wtxFields[5].childrenAs>(mapper)).isEqualTo(wtx.commands) assertThat(wtxFields[6].valueAs(mapper)).isEqualTo(wtx.timeWindow) - assertThat(wtxFields[7].valueAs(mapper)).isEqualTo(wtx.privacySalt) + assertThat(wtxFields[7].childrenAs(mapper)).isEqualTo(wtx.references) + assertThat(wtxFields[8].valueAs(mapper)).isEqualTo(wtx.privacySalt) assertThat(mapper.convertValue(wtxJson)).isEqualTo(wtx) assertThat(mapper.convertValue(json)).isEqualTo(stx) } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index 5c07f82117..6595851d22 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -1,10 +1,6 @@ package net.corda.core.internal import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappConfig import net.corda.core.cordapp.CordappContext @@ -12,14 +8,11 @@ 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.CordaSerializable import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializedBytes 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 net.corda.core.utilities.OpaqueBytes import org.slf4j.MDC // *Internal* Corda-specific utilities @@ -80,11 +73,3 @@ class LazyMappedList(val originalList: List, val transform: (T, Int) -> override fun get(index: Int) = partialResolvedList[index] ?: transform(originalList[index], index).also { computed -> partialResolvedList[index] = computed } } - -/** - * A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef). - * The [serializedState] is the actual component from the original transaction. - */ -@KeepForDJVM -@CordaSerializable -data class SerializedStateAndRef(val serializedState: SerializedBytes>, val ref: StateRef) \ No newline at end of file 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 b8d574803e..8721860184 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -1,18 +1,13 @@ package net.corda.core.internal +import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.componentHash import net.corda.core.crypto.sha256 import net.corda.core.identity.Party -import net.corda.core.serialization.MissingAttachmentsException -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.serialize -import net.corda.core.transactions.ComponentGroup -import net.corda.core.transactions.ContractUpgradeWireTransaction -import net.corda.core.transactions.FilteredComponentGroup -import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.serialization.* +import net.corda.core.transactions.* import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.lazyMapped import java.io.ByteArrayOutputStream @@ -121,4 +116,40 @@ fun deserialiseCommands(componentGroups: List, } commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[index]) } } -} \ No newline at end of file +} + +/** + * Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required + * for backwards compatibility purposes. + */ +fun createComponentGroups(inputs: List, + outputs: List>, + commands: List>, + attachments: List, + notary: Party?, + timeWindow: TimeWindow?, + references: List): 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))) + if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCES_GROUP.ordinal, references.lazyMapped(serialize))) + if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP.ordinal, outputs.lazyMapped(serialize))) + // Adding commandData only to the commands group. Signers are added in their own group. + if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.COMMANDS_GROUP.ordinal, commands.map { it.value }.lazyMapped(serialize))) + if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, attachments.lazyMapped(serialize))) + if (notary != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize))) + if (timeWindow != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize))) + // 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))) + return componentGroupMap +} + +/** + * A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef). + * The [serializedState] is the actual component from the original wire transaction. + */ +@KeepForDJVM +data class SerializedStateAndRef(val serializedState: SerializedBytes>, val ref: StateRef) { + fun toStateAndRef(): StateAndRef = StateAndRef(serializedState.deserialize(), ref) +} 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 23718cb28f..90e09c86b8 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -8,10 +8,11 @@ import net.corda.core.crypto.isFulfilledBy import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.deserialize +import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.warnOnce import java.util.* import java.util.function.Predicate @@ -28,12 +29,13 @@ import kotlin.collections.HashSet * * All the above refer to inputs using a (txhash, output index) pair. */ -// TODO LedgerTransaction is not supposed to be serialisable as it references attachments, etc. The verification logic -// currently sends this across to out-of-process verifiers. We'll need to change that first. -// DOCSTART 1 @KeepForDJVM @CordaSerializable -data class LedgerTransaction private constructor( +class LedgerTransaction +@ConstructorForDeserialization +// LedgerTransaction is not meant to be created directly from client code, but rather via WireTransaction.toLedgerTransaction +private constructor( + // DOCSTART 1 /** The resolved input states which will be consumed/invalidated by the execution of this transaction. */ override val inputs: List>, override val outputs: List>, @@ -46,39 +48,15 @@ data class LedgerTransaction private constructor( override val notary: Party?, val timeWindow: TimeWindow?, val privacySalt: PrivacySalt, - private val networkParameters: NetworkParameters?, - override val references: List>, - val componentGroups: List?, - val resolvedInputBytes: List?, - val resolvedReferenceBytes: List? + val networkParameters: NetworkParameters?, + override val references: List> + //DOCEND 1 ) : FullTransaction() { + // These are not part of the c'tor above as that defines LedgerTransaction's serialisation format + private var componentGroups: List? = null + private var serializedInputs: List? = null + private var serializedReferences: List? = null - @Deprecated("Client code should not instantiate LedgerTransaction.") - constructor( - inputs: List>, - outputs: List>, - commands: List>, - attachments: List, - id: SecureHash, - notary: Party?, - timeWindow: TimeWindow?, - privacySalt: PrivacySalt - ) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null, emptyList(), null, null, null) - - @Deprecated("Client code should not instantiate LedgerTransaction.") - constructor( - inputs: List>, - outputs: List>, - commands: List>, - attachments: List, - id: SecureHash, - notary: Party?, - timeWindow: TimeWindow?, - privacySalt: PrivacySalt, - networkParameters: NetworkParameters? - ) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList(), null, null, null) - - //DOCEND 1 init { checkBaseInvariants() if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" } @@ -87,10 +65,10 @@ data class LedgerTransaction private constructor( } companion object { - private val logger = loggerFor() + private val logger = contextLogger() @CordaInternal - internal fun makeLedgerTransaction( + internal fun create( inputs: List>, outputs: List>, commands: List>, @@ -101,10 +79,16 @@ data class LedgerTransaction private constructor( privacySalt: PrivacySalt, networkParameters: NetworkParameters?, references: List>, - componentGroups: List, - resolvedInputBytes: List, - resolvedReferenceBytes: List - ) = LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, resolvedInputBytes, resolvedReferenceBytes) + componentGroups: List? = null, + serializedInputs: List? = null, + serializedReferences: List? = null + ): LedgerTransaction { + return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply { + this.componentGroups = componentGroups + this.serializedInputs = serializedInputs + this.serializedReferences = serializedReferences + } + } } val inputStates: List get() = inputs.map { it.state.data } @@ -137,7 +121,7 @@ data class LedgerTransaction private constructor( AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader -> - val internalTx = createInternalLedgerTransaction() + val internalTx = createLtxForVerification() // TODO - verify for version downgrade validatePackageOwnership(contractAttachmentsByContract) @@ -195,7 +179,9 @@ data class LedgerTransaction private constructor( * * Constraints should be one of the valid supported ones. * * Constraints should propagate correctly if not marked otherwise. */ - private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map, transactionClassLoader: ClassLoader) { + private fun verifyConstraintsValidity(internalTx: LedgerTransaction, + contractAttachmentsByContract: Map, + transactionClassLoader: ClassLoader) { // First check that the constraints are valid. for (state in internalTx.allStates) { checkConstraintValidity(state) @@ -277,30 +263,38 @@ data class LedgerTransaction private constructor( throw TransactionVerificationException.ContractCreationError(id, className, e) } - private fun createInternalLedgerTransaction(): LedgerTransaction { - return if (resolvedInputBytes != null && resolvedReferenceBytes != null && componentGroups != null) { + private fun createLtxForVerification(): LedgerTransaction { + val serializedInputs = this.serializedInputs + val serializedReferences = this.serializedReferences + val componentGroups = this.componentGroups + return if (serializedInputs != null && serializedReferences != null && componentGroups != null) { // Deserialize all relevant classes in the transaction classloader. - val resolvedDeserializedInputs = resolvedInputBytes.map { StateAndRef(it.serializedState.deserialize(), it.ref) } - val resolvedDeserializedReferences = resolvedReferenceBytes.map { StateAndRef(it.serializedState.deserialize(), it.ref) } + val deserializedInputs = serializedInputs.map { it.toStateAndRef() } + val deserializedReferences = serializedReferences.map { it.toStateAndRef() } val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true) - val deserializedCommands = deserialiseCommands(this.componentGroups, forceDeserialize = true) - val authenticatedArgs = deserializedCommands.map { cmd -> + val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true) + val authenticatedDeserializedCommands = deserializedCommands.map { cmd -> val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties CommandWithParties(cmd.signers, parties, cmd.value) } - val ledgerTransactionToVerify = this.copy( - inputs = resolvedDeserializedInputs, + LedgerTransaction( + inputs = deserializedInputs, outputs = deserializedOutputs, - commands = authenticatedArgs, - references = resolvedDeserializedReferences) - - ledgerTransactionToVerify + commands = authenticatedDeserializedCommands, + attachments = this.attachments, + id = this.id, + notary = this.notary, + timeWindow = this.timeWindow, + privacySalt = this.privacySalt, + networkParameters = this.networkParameters, + references = deserializedReferences + ) } else { // This branch is only present for backwards compatibility. - // TODO - it should be removed once the constructor of LedgerTransaction is no longer public api. - logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction. The result of the verify method might not be accurate.") + logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction." + + "The result of the verify method might not be accurate.") this } } @@ -766,7 +760,92 @@ data class LedgerTransaction private constructor( */ fun getAttachment(id: SecureHash): Attachment = attachments.first { it.id == id } - @JvmOverloads + operator fun component1(): List> = inputs + operator fun component2(): List> = outputs + operator fun component3(): List> = commands + operator fun component4(): List = attachments + operator fun component5(): SecureHash = id + operator fun component6(): Party? = notary + operator fun component7(): TimeWindow? = timeWindow + operator fun component8(): PrivacySalt = privacySalt + operator fun component9(): NetworkParameters? = networkParameters + operator fun component10(): List> = references + + override fun equals(other: Any?): Boolean = this === other || other is LedgerTransaction && this.id == other.id + + override fun hashCode(): Int = id.hashCode() + + override fun toString(): String { + return """LedgerTransaction( + | id=$id + | inputs=$inputs + | outputs=$outputs + | commands=$commands + | attachments=$attachments + | notary=$notary + | timeWindow=$timeWindow + | references=$references + | networkParameters=$networkParameters + | privacySalt=$privacySalt + |)""".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>, + outputs: List>, + commands: List>, + attachments: List, + id: SecureHash, + notary: Party?, + timeWindow: TimeWindow?, + privacySalt: PrivacySalt + ) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null, emptyList()) + + @Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.") + @DeprecatedConstructorForDeserialization(1) + constructor( + inputs: List>, + outputs: List>, + commands: List>, + attachments: List, + id: SecureHash, + notary: Party?, + timeWindow: TimeWindow?, + privacySalt: PrivacySalt, + networkParameters: NetworkParameters? + ) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList()) + + @Deprecated("LedgerTransactions should not be created directly, use WireTransaction.toLedgerTransaction instead.") + fun copy(inputs: List>, + outputs: List>, + commands: List>, + attachments: List, + id: SecureHash, + notary: Party?, + timeWindow: TimeWindow?, + privacySalt: PrivacySalt + ): LedgerTransaction { + return LedgerTransaction( + inputs = inputs, + outputs = outputs, + commands = commands, + attachments = attachments, + id = id, + notary = notary, + timeWindow = timeWindow, + privacySalt = privacySalt, + networkParameters = networkParameters, + references = references + ) + } + + @Deprecated("LedgerTransactions should not be created directly, use WireTransaction.toLedgerTransaction instead.") fun copy(inputs: List> = this.inputs, outputs: List> = this.outputs, commands: List> = this.commands, @@ -776,16 +855,18 @@ data class LedgerTransaction private constructor( timeWindow: TimeWindow? = this.timeWindow, privacySalt: PrivacySalt = this.privacySalt, networkParameters: NetworkParameters? = this.networkParameters - ) = copy(inputs = inputs, - outputs = outputs, - commands = commands, - attachments = attachments, - id = id, - notary = notary, - timeWindow = timeWindow, - privacySalt = privacySalt, - networkParameters = networkParameters, - references = references - ) + ): LedgerTransaction { + return LedgerTransaction( + inputs = inputs, + outputs = outputs, + commands = commands, + attachments = attachments, + id = id, + notary = notary, + timeWindow = timeWindow, + privacySalt = privacySalt, + networkParameters = networkParameters, + references = references + ) + } } - 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 8b01a95040..630b0003e8 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -6,11 +6,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party -import net.corda.core.internal.AttachmentWithContext -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.StatePointerSearch -import net.corda.core.internal.ensureMinimumPlatformVersion -import net.corda.core.internal.isUploaderTrusted +import net.corda.core.internal.* import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution @@ -127,14 +123,15 @@ open class TransactionBuilder @JvmOverloads constructor( return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { WireTransaction( - WireTransaction.createComponentGroups( + createComponentGroups( inputStates(), resolvedOutputs, commands, (allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable. notary, window, - referenceStates), + referenceStates + ), 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 a7ecc86dce..1719dd07b7 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -4,18 +4,19 @@ import net.corda.core.CordaInternal import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.contracts.* -import net.corda.core.contracts.ComponentGroupEnum.* +import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP import net.corda.core.crypto.* import net.corda.core.identity.Party -import net.corda.core.internal.SerializedStateAndRef import net.corda.core.internal.Emoji +import net.corda.core.internal.SerializedStateAndRef +import net.corda.core.internal.createComponentGroups import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.lazyMapped @@ -53,7 +54,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr @DeleteForDJVM constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) - @Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING) + @Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", + ReplaceWith("WireTransaction(val componentGroups: List, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING) @DeleteForDJVM @JvmOverloads constructor( @@ -64,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), privacySalt) + ) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()), privacySalt) init { check(componentGroups.all { it.components.isNotEmpty() }) { "Empty component groups are not allowed" } @@ -103,7 +105,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr return toLedgerTransactionInternal( resolveIdentity = { services.identityService.partyFromKey(it) }, resolveAttachment = { services.attachments.openAttachment(it) }, - resolveStateRefComponent = { resolveStateRefBinaryComponent(it, services) }, + resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) }, networkParameters = services.networkParameters ) } @@ -130,38 +132,44 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, - resolveStateRefComponent: (StateRef) -> SerializedBytes>?, + resolveStateRefAsSerialized: (StateRef) -> SerializedBytes>?, networkParameters: NetworkParameters? ): LedgerTransaction { // Look up public keys to authenticated identities. - val authenticatedArgs = commands.lazyMapped { cmd, _ -> + val authenticatedCommands = commands.lazyMapped { cmd, _ -> val parties = cmd.signers.mapNotNull { pk -> resolveIdentity(pk) } CommandWithParties(cmd.signers, parties, cmd.value) } - val resolvedInputBytes = inputs.map { ref -> - SerializedStateAndRef(resolveStateRefComponent(ref) - ?: throw TransactionResolutionException(ref.txhash), ref) - } - val resolvedInputs = resolvedInputBytes.lazyMapped { (serialized, ref), _ -> - StateAndRef(serialized.deserialize(), ref) + val serializedResolvedInputs = inputs.map { ref -> + SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) } + val resolvedInputs = serializedResolvedInputs.lazyMapped { star, _ -> star.toStateAndRef() } - val resolvedReferenceBytes = references.map { ref -> - SerializedStateAndRef(resolveStateRefComponent(ref) - ?: throw TransactionResolutionException(ref.txhash), ref) - } - val resolvedReferences = resolvedReferenceBytes.lazyMapped { (serialized, ref), _ -> - StateAndRef(serialized.deserialize(), ref) + val serializedResolvedReferences = references.map { ref -> + SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) } + val resolvedReferences = serializedResolvedReferences.lazyMapped { star, _ -> star.toStateAndRef() } - val attachments = attachments.lazyMapped { att, _ -> - resolveAttachment(att) ?: throw AttachmentResolutionException(att) - } + val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) } - val ltx = LedgerTransaction.makeLedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences, componentGroups, resolvedInputBytes, resolvedReferenceBytes) + val ltx = LedgerTransaction.create( + resolvedInputs, + outputs, + authenticatedCommands, + resolvedAttachments, + id, + notary, + timeWindow, + privacySalt, + networkParameters, + resolvedReferences, + componentGroups, + serializedResolvedInputs, + serializedResolvedReferences + ) - checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: DEFAULT_MAX_TX_SIZE) + checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: DEFAULT_MAX_TX_SIZE, serializedResolvedInputs, serializedResolvedReferences) return ltx } @@ -170,7 +178,10 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * Deterministic function that checks if the transaction is below the maximum allowed size. * It uses the binary representation of transactions. */ - private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) { + private fun checkTransactionSize(ltx: LedgerTransaction, + maxTransactionSize: Int, + resolvedSerializedInputs: List, + resolvedSerializedReferences: List) { var remainingTransactionSize = maxTransactionSize fun minus(size: Int) { @@ -187,8 +198,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id. ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) } - minus(ltx.resolvedInputBytes!!.sumBy { it.serializedState.size }) - minus(ltx.resolvedReferenceBytes!!.sumBy { it.serializedState.size }) + minus(resolvedSerializedInputs.sumBy { it.serializedState.size }) + minus(resolvedSerializedReferences.sumBy { it.serializedState.size }) // For Commands and outputs we can use the component groups as they are already serialized. minus(componentGroupSize(COMMANDS_GROUP)) @@ -277,33 +288,15 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr companion object { private const val DEFAULT_MAX_TX_SIZE = 10485760 - /** - * Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required - * for backwards compatibility purposes. - */ - @JvmOverloads @CordaInternal + @Deprecated("Do not use, this is internal API") fun createComponentGroups(inputs: List, outputs: List>, commands: List>, attachments: List, notary: Party?, - timeWindow: TimeWindow?, - references: List = emptyList()): List { - val serialize = { value: Any, _: Int -> value.serialize() } - val componentGroupMap: MutableList = mutableListOf() - if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.lazyMapped(serialize))) - if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(REFERENCES_GROUP.ordinal, references.lazyMapped(serialize))) - if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.lazyMapped(serialize))) - // Adding commandData only to the commands group. Signers are added in their own group. - if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value }.lazyMapped(serialize))) - if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.lazyMapped(serialize))) - if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize))) - if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize))) - // 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(SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize))) - return componentGroupMap + timeWindow: TimeWindow?): List { + return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()) } /** @@ -312,7 +305,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the correct classloader independent of the node's classpath. */ @CordaInternal - fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes>? { + internal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes>? { return if (services is ServiceHub) { val coreTransaction = services.validatedTransactions.getTransaction(stateRef.txhash)?.coreTransaction ?: throw TransactionResolutionException(stateRef.txhash) @@ -333,11 +326,26 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") - for (reference in references) buf.appendln("${Emoji.rightArrow}REFS: $reference") - for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input") - for ((data) in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $data") - for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command") - for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment") + for (reference in references) { + val emoji = Emoji.rightArrow + buf.appendln("${emoji}REFS: $reference") + } + for (input in inputs) { + val emoji = Emoji.rightArrow + buf.appendln("${emoji}INPUT: $input") + } + for ((data) in outputs) { + val emoji = Emoji.leftArrow + buf.appendln("${emoji}OUTPUT: $data") + } + for (command in commands) { + val emoji = Emoji.diamond + buf.appendln("${emoji}COMMAND: $command") + } + for (attachment in attachments) { + val emoji = Emoji.paperclip + buf.appendln("${emoji}ATTACHMENT: $attachment") + } return buf.toString() } 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 ba89c5a1a5..6508cbd3f9 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt @@ -3,6 +3,7 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.* 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.contracts.DummyContract @@ -124,7 +125,7 @@ class CompatibleTransactionTests { @Test fun `WireTransaction constructors and compatibility`() { - val groups = WireTransaction.createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow) + val groups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()) val wireTransactionOldConstructor = WireTransaction(groups, privacySalt) assertEquals(wireTransactionA, wireTransactionOldConstructor) 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 b37a34fc97..0e524e99b0 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -124,7 +124,7 @@ class TransactionTests { val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null val privacySalt = PrivacySalt() - val transaction = LedgerTransaction( + val transaction = LedgerTransaction.create( inputs, outputs, commands, @@ -133,7 +133,8 @@ class TransactionTests { null, timeWindow, privacySalt, - testNetworkParameters() + testNetworkParameters(), + emptyList() ) transaction.verify() @@ -166,7 +167,7 @@ class TransactionTests { val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null val privacySalt = PrivacySalt() - fun buildTransaction() = LedgerTransaction( + fun buildTransaction() = LedgerTransaction.create( inputs, outputs, commands, @@ -174,7 +175,9 @@ class TransactionTests { id, notary, timeWindow, - privacySalt + privacySalt, + null, + emptyList() ) assertFailsWith { buildTransaction() } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 2a812079e5..70e96fb983 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,16 +7,17 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- -* Marked the `Attachment` interface as `@DoNotImplement` because it is not meant to be extended by CorDapp developers. If you have already done so, -please get in contact on the usual communication channels. +* Marked the ``Attachment`` interface as ``@DoNotImplement`` because it is not meant to be extended by CorDapp developers. If you have already + done so, please get in contact on the usual communication channels. * Added auto-acceptance of network parameters for network updates. This behaviour is available for a subset of the network parameters -and is configurable via the node config. See :doc:`network-map` for more information. + and is configurable via the node config. See :doc:`network-map` for more information. -* Deprecated `SerializationContext.withAttachmentsClassLoader`. This functionality has always been disabled by flags -and there is no reason for a CorDapp developer to use it. It is just an internal implementation detail of Corda. +* Deprecated ``SerializationContext.withAttachmentsClassLoader``. This functionality has always been disabled by flags + and there is no reason for a CorDapp developer to use it. It is just an internal implementation detail of Corda. -* Deprecated the `LedgerTransaction` constructor. No client code should call it directly. LedgerTransactions can be created from WireTransactions if required. +* Deprecated all means to directly create a ``LedgerTransaction`` instance, as client code is only meant to get hold of a ``LedgerTransaction`` + via ``WireTransaction.toLedgerTransaction``. * Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner) to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist. diff --git a/docs/source/serialization-default-evolution.rst b/docs/source/serialization-default-evolution.rst index 1f4923a357..9b801afffe 100644 --- a/docs/source/serialization-default-evolution.rst +++ b/docs/source/serialization-default-evolution.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + Default Class Evolution ======================= 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 5da0c03c48..e5a524d67c 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 @@ -10,6 +10,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.createComponentGroups import net.corda.core.node.NodeInfo import net.corda.core.schemas.MappedSchema import net.corda.core.transactions.WireTransaction @@ -154,7 +155,7 @@ fun createWireTransaction(inputs: List, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt = PrivacySalt()): WireTransaction { - val componentGroups = WireTransaction.createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow) + val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList()) return WireTransaction(componentGroups, privacySalt) }