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 51a37bbc7a..cb7844b734 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -56,7 +56,9 @@ import java.security.cert.TrustAnchor import java.security.cert.X509Certificate import java.time.Duration import java.time.temporal.Temporal -import java.util.* +import java.util.Collections +import java.util.PrimitiveIterator +import java.util.Spliterator import java.util.Spliterator.DISTINCT import java.util.Spliterator.IMMUTABLE import java.util.Spliterator.NONNULL @@ -64,6 +66,7 @@ import java.util.Spliterator.ORDERED import java.util.Spliterator.SIZED import java.util.Spliterator.SORTED import java.util.Spliterator.SUBSIZED +import java.util.Spliterators import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit import java.util.stream.Collectors 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 9dd32fb54b..7bdfec76be 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -114,14 +114,14 @@ fun deserialiseCommands( componentGroups: List, forceDeserialize: Boolean = false, factory: SerializationFactory = SerializationFactory.defaultFactory, - @Suppress("UNUSED_PARAMETER") context: SerializationContext = factory.defaultContext, + context: SerializationContext = factory.defaultContext, digestService: DigestService = DigestService.sha2_256 ): List> { // TODO: we could avoid deserialising unrelated signers. // However, current approach ensures the transaction is not malformed // and it will throw if any of the signers objects is not List of public keys). - val signersList: List> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize)) - val commandDataList: List = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize) + val signersList: List> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize, factory, context)) + val commandDataList: List = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize, factory, context) val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal } return if (group is FilteredComponentGroup) { check(commandDataList.size <= signersList.size) { @@ -154,8 +154,9 @@ fun createComponentGroups(inputs: List, timeWindow: TimeWindow?, references: List, networkParametersHash: SecureHash?): List { - val serializationContext = SerializationFactory.defaultFactory.defaultContext - val serialize = { value: Any, _: Int -> value.serialize(context = serializationContext) } + val serializationFactory = SerializationFactory.defaultFactory + val serializationContext = serializationFactory.defaultContext + val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) } 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))) @@ -178,7 +179,11 @@ fun createComponentGroups(inputs: List, */ @KeepForDJVM data class SerializedStateAndRef(val serializedState: SerializedBytes>, val ref: StateRef) { - fun toStateAndRef(): StateAndRef = StateAndRef(serializedState.deserialize(), ref) + fun toStateAndRef(factory: SerializationFactory, context: SerializationContext) = StateAndRef(serializedState.deserialize(factory, context), ref) + fun toStateAndRef(): StateAndRef { + val factory = SerializationFactory.defaultFactory + return toStateAndRef(factory, factory.defaultContext) + } } /** Check that network parameters hash on this transaction is the current hash for the network. */ 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 194a7020e7..5ff7cae23e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -15,6 +15,7 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization +import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.serialize @@ -187,19 +188,26 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ): LedgerTransaction { // Look up public keys to authenticated identities. val authenticatedCommands = commands.lazyMapped { cmd, _ -> - val parties = cmd.signers.mapNotNull { pk -> resolveIdentity(pk) } + val parties = cmd.signers.mapNotNull(resolveIdentity) CommandWithParties(cmd.signers, parties, cmd.value) } + // Ensure that the lazy mappings will use the correct SerializationContext. + val serializationFactory = SerializationFactory.defaultFactory + val serializationContext = serializationFactory.defaultContext + val toStateAndRef = { ssar: SerializedStateAndRef, _: Int -> + ssar.toStateAndRef(serializationFactory, serializationContext) + } + val serializedResolvedInputs = inputs.map { ref -> SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) } - val resolvedInputs = serializedResolvedInputs.lazyMapped { star, _ -> star.toStateAndRef() } + val resolvedInputs = serializedResolvedInputs.lazyMapped(toStateAndRef) val serializedResolvedReferences = references.map { ref -> SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) } - val resolvedReferences = serializedResolvedReferences.lazyMapped { star, _ -> star.toStateAndRef() } + val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef) val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt index 7866d51e08..b594954ef5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt @@ -30,7 +30,7 @@ import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.CertificateFactory import java.security.cert.X509Certificate -import java.util.* +import java.util.Collections import javax.annotation.concurrent.ThreadSafe import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty @@ -509,6 +509,7 @@ class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer @ThreadSafe @SuppressWarnings("ALL") object LazyMappedListSerializer : Serializer>() { - override fun write(kryo: Kryo, output: Output, obj: List<*>) = kryo.writeClassAndObject(output, obj.toList()) - override fun read(kryo: Kryo, input: Input, type: Class>) = kryo.readClassAndObject(input) as List<*> + // Using a MutableList so that Kryo will always write an instance of java.util.ArrayList. + override fun write(kryo: Kryo, output: Output, obj: List<*>) = kryo.writeClassAndObject(output, obj.toMutableList()) + override fun read(kryo: Kryo, input: Input, type: Class>) = kryo.readClassAndObject(input) as? List<*> } diff --git a/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt b/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt new file mode 100644 index 0000000000..399e1d9c2c --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt @@ -0,0 +1,68 @@ +package net.corda.node + +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.node.services.config.NodeConfiguration +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.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.internal.findCordapp +import org.junit.Test +import org.junit.jupiter.api.assertDoesNotThrow + +/** + * Execute a flow with sub-flows, including the finality flow. + * This operation should checkpoint, and have its checkpoint restored. + */ +@Suppress("FunctionName") +class CashIssueAndPaymentTest { + companion object { + private val logger = loggerFor() + + private val configOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true) + private val CASH_AMOUNT = 500.DOLLARS + + fun parametersFor(): DriverParameters { + return DriverParameters( + systemProperties = mapOf("co.paralleluniverse.fibers.verifyInstrumentation" to "false"), + portAllocation = incrementalPortAllocation(), + startNodesInProcess = false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = false, validating = true)), + notaryCustomOverrides = configOverrides, + cordappsForAllNodes = listOf( + findCordapp("net.corda.finance.contracts"), + findCordapp("net.corda.finance.workflows") + ) + ) + } + } + + @Test(timeout = 300_000) + fun `test can issue cash`() { + driver(parametersFor()) { + val alice = startNode(providedName = ALICE_NAME, customOverrides = configOverrides).getOrThrow() + val aliceParty = alice.nodeInfo.singleIdentity() + val notaryParty = notaryHandles.single().identity + val result = assertDoesNotThrow { + alice.rpc.startFlow(::CashIssueAndPaymentFlow, + CASH_AMOUNT, + OpaqueBytes.of(0x01), + aliceParty, + false, + notaryParty + ).use { flowHandle -> + flowHandle.returnValue.getOrThrow() + } + } + logger.info("TXN={}, recipient={}", result.stx, result.recipient) + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt index 0de1960375..4997cac5e3 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt @@ -7,6 +7,7 @@ import net.corda.core.utilities.loggerFor import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.node.DeterministicSourcesRule +import net.corda.node.services.config.NodeConfiguration import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.singleIdentity @@ -22,20 +23,21 @@ import org.junit.jupiter.api.assertDoesNotThrow @Suppress("FunctionName") class DeterministicCashIssueAndPaymentTest { companion object { - val logger = loggerFor() + private val logger = loggerFor() + + private val configOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true) + private val CASH_AMOUNT = 500.DOLLARS @ClassRule @JvmField val djvmSources = DeterministicSourcesRule() - @JvmField - val CASH_AMOUNT = 500.DOLLARS - fun parametersFor(djvmSources: DeterministicSourcesRule): DriverParameters { return DriverParameters( portAllocation = incrementalPortAllocation(), startNodesInProcess = false, notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + notaryCustomOverrides = configOverrides, cordappsForAllNodes = listOf( findCordapp("net.corda.finance.contracts"), findCordapp("net.corda.finance.workflows") @@ -50,7 +52,7 @@ class DeterministicCashIssueAndPaymentTest { fun `test DJVM can issue cash`() { val reference = OpaqueBytes.of(0x01) driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val alice = startNode(providedName = ALICE_NAME, customOverrides = configOverrides).getOrThrow() val aliceParty = alice.nodeInfo.singleIdentity() val notaryParty = notaryHandles.single().identity val txId = assertDoesNotThrow { @@ -60,7 +62,9 @@ class DeterministicCashIssueAndPaymentTest { aliceParty, false, notaryParty - ).returnValue.getOrThrow() + ).use { flowHandle -> + flowHandle.returnValue.getOrThrow() + } } logger.info("TX-ID: {}", txId) }