diff --git a/build.gradle b/build.gradle index 3e6975d757..98d37bf05d 100644 --- a/build.gradle +++ b/build.gradle @@ -488,6 +488,7 @@ bintrayConfig { 'corda-finance-contracts', 'corda-node', 'corda-node-api', + 'corda-node-djvm', 'corda-test-common', 'corda-test-utils', 'corda-test-db', diff --git a/core-tests/src/test/kotlin/net/corda/coretests/crypto/PartialMerkleTreeTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/crypto/PartialMerkleTreeTest.kt index f029be1f25..ac86cf8f6a 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/crypto/PartialMerkleTreeTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/crypto/PartialMerkleTreeTest.kt @@ -8,7 +8,6 @@ import net.corda.core.crypto.* import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.accessLeafIndex import net.corda.core.node.NotaryInfo import net.corda.core.node.services.IdentityService import net.corda.core.serialization.deserialize @@ -307,37 +306,37 @@ class PartialMerkleTreeTest { val pmt = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("1"), SecureHash.sha256("5"), SecureHash.sha256("0"), SecureHash.sha256("19"))) // First leaf. - assertEquals(0, pmt.accessLeafIndex(SecureHash.sha256("0"))) + assertEquals(0, pmt.leafIndex(SecureHash.sha256("0"))) // Second leaf. - assertEquals(1, pmt.accessLeafIndex(SecureHash.sha256("1"))) + assertEquals(1, pmt.leafIndex(SecureHash.sha256("1"))) // A random leaf. - assertEquals(5, pmt.accessLeafIndex(SecureHash.sha256("5"))) + assertEquals(5, pmt.leafIndex(SecureHash.sha256("5"))) // The last leaf. - assertEquals(19, pmt.accessLeafIndex(SecureHash.sha256("19"))) + assertEquals(19, pmt.leafIndex(SecureHash.sha256("19"))) // The provided hash is not in the tree. - assertFailsWith { pmt.accessLeafIndex(SecureHash.sha256("10")) } + assertFailsWith { pmt.leafIndex(SecureHash.sha256("10")) } // The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree). - assertFailsWith { pmt.accessLeafIndex(SecureHash.sha256("30")) } + assertFailsWith { pmt.leafIndex(SecureHash.sha256("30")) } val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("0"))) - assertEquals(0, pmtFirstElementOnly.accessLeafIndex(SecureHash.sha256("0"))) + assertEquals(0, pmtFirstElementOnly.leafIndex(SecureHash.sha256("0"))) // The provided hash is not in the tree. - assertFailsWith { pmtFirstElementOnly.accessLeafIndex(SecureHash.sha256("10")) } + assertFailsWith { pmtFirstElementOnly.leafIndex(SecureHash.sha256("10")) } val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("19"))) - assertEquals(19, pmtLastElementOnly.accessLeafIndex(SecureHash.sha256("19"))) + assertEquals(19, pmtLastElementOnly.leafIndex(SecureHash.sha256("19"))) // The provided hash is not in the tree. - assertFailsWith { pmtLastElementOnly.accessLeafIndex(SecureHash.sha256("10")) } + assertFailsWith { pmtLastElementOnly.leafIndex(SecureHash.sha256("10")) } val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf(SecureHash.sha256("5"))) - assertEquals(5, pmtOneElement.accessLeafIndex(SecureHash.sha256("5"))) + assertEquals(5, pmtOneElement.leafIndex(SecureHash.sha256("5"))) // The provided hash is not in the tree. - assertFailsWith { pmtOneElement.accessLeafIndex(SecureHash.sha256("10")) } + assertFailsWith { pmtOneElement.leafIndex(SecureHash.sha256("10")) } val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves) - for (i in 0..19) assertEquals(i, pmtAllIncluded.accessLeafIndex(SecureHash.sha256(i.toString()))) + for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(SecureHash.sha256(i.toString()))) // The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree). - assertFailsWith { pmtAllIncluded.accessLeafIndex(SecureHash.sha256("30")) } + assertFailsWith { pmtAllIncluded.leafIndex(SecureHash.sha256("30")) } } @Test diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index 1ef11c341b..f57d02d800 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -1,6 +1,7 @@ package net.corda.core.crypto import net.corda.core.CordaException +import net.corda.core.CordaInternal import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.serialization.CordaSerializable @@ -169,8 +170,9 @@ class PartialMerkleTree(val root: PartialTree) { * @return leaf-index of this component (starting from zero). * @throws MerkleTreeException if the provided hash is not in the tree. */ + @CordaInternal @Throws(MerkleTreeException::class) - internal fun leafIndex(leaf: SecureHash): Int { + fun leafIndex(leaf: SecureHash): Int { // Special handling if the tree consists of one node only. if (root is PartialTree.IncludedLeaf && root.hash == leaf) return 0 val flagPath = mutableListOf() diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index 7060c110c3..f26e172d73 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -1,6 +1,8 @@ +@file:KeepForDJVM package net.corda.core.node.services import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION 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 380fafbbf7..11622f3e10 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -78,6 +78,7 @@ private constructor( checkNotaryWhitelisted() } + @KeepForDJVM companion object { private val logger = contextLogger() @@ -126,6 +127,41 @@ private constructor( verifierFactory = ::BasicVerifier ) } + + /** + * This factory function will create an instance of [LedgerTransaction] + * that will be used inside the DJVM sandbox. + */ + @CordaInternal + fun createForSandbox( + inputs: List>, + outputs: List>, + commands: List>, + attachments: List, + id: SecureHash, + notary: Party?, + timeWindow: TimeWindow?, + privacySalt: PrivacySalt, + networkParameters: NetworkParameters, + references: List>): LedgerTransaction { + return LedgerTransaction( + inputs = inputs, + outputs = outputs, + commands = commands, + attachments = attachments, + id = id, + notary = notary, + timeWindow = timeWindow, + privacySalt = privacySalt, + networkParameters = networkParameters, + references = references, + componentGroups = null, + serializedInputs = null, + serializedReferences = null, + isAttachmentTrusted = { true }, + verifierFactory = ::BasicVerifier + ) + } } val inputStates: List get() = inputs.map { it.state.data } @@ -179,8 +215,8 @@ private constructor( * Pass all of this [LedgerTransaction] object's serialized state to a [transformer] function. */ @CordaInternal - fun transform(transformer: (List?, List?, List?) -> T): T { - return transformer(componentGroups, serializedInputs, serializedReferences) + fun transform(transformer: (List, List, List) -> T): T { + return transformer(componentGroups ?: emptyList(), serializedInputs ?: emptyList(), serializedReferences ?: emptyList()) } /** diff --git a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt b/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt index e0545423c5..148aaff7bf 100644 --- a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt +++ b/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt @@ -1,7 +1,6 @@ package net.corda.core.internal import net.corda.core.contracts.* -import net.corda.core.crypto.PartialMerkleTree import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.node.NetworkParameters @@ -37,5 +36,3 @@ fun createLedgerTransaction( fun createContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) = TransactionVerificationException.ContractCreationError(txId, contractClass, cause) fun createContractRejection(txId: SecureHash, contract: Contract, cause: Throwable) = TransactionVerificationException.ContractRejection(txId, contract, cause) - -fun PartialMerkleTree.accessLeafIndex(id: SecureHash) = this.leafIndex(id) \ No newline at end of file diff --git a/node/build.gradle b/node/build.gradle index 6f06735a78..eeb9a68118 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -181,12 +181,18 @@ dependencies { // Sandbox for deterministic contract verification compile "net.corda:corda-djvm:$djvm_version" compile "net.corda:corda-djvm-serialization:$djvm_version" + compile(project(':node:djvm')) { + transitive = false + } jdkRt "net.corda:deterministic-rt:latest.integration" deterministic project(path: ':core-deterministic', configuration: 'deterministicArtifacts') deterministic project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') deterministic("net.corda:corda-djvm-deserializers:$djvm_version") { transitive = false } + deterministic(project(':node:djvm')) { + transitive = false + } deterministic "org.slf4j:slf4j-nop:$slf4j_version" testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" diff --git a/node/djvm/build.gradle b/node/djvm/build.gradle new file mode 100644 index 0000000000..d17bc330d8 --- /dev/null +++ b/node/djvm/build.gradle @@ -0,0 +1,20 @@ +apply from: '../../deterministic.gradle' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'java-library' + +description 'Internal Corda Node modules for deterministic contract verification.' + +dependencies { + api 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + api project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + api project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') +} + +jar { + archiveBaseName = 'corda-node-djvm' +} + +publish { + name jar.archiveBaseName.get() +} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt new file mode 100644 index 0000000000..e91d50a759 --- /dev/null +++ b/node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt @@ -0,0 +1,55 @@ +package net.corda.node.djvm + +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import java.io.InputStream +import java.security.PublicKey +import java.util.Collections.unmodifiableList +import java.util.function.Function + +class AttachmentBuilder : Function?, List?> { + private val attachments = mutableListOf() + + private fun unmodifiable(list: List): List { + return if(list.isEmpty()) { + emptyList() + } else { + unmodifiableList(list) + } + } + + override fun apply(inputs: Array?): List? { + return if (inputs == null) { + unmodifiable(attachments) + } else { + @Suppress("unchecked_cast") + attachments.add(SandboxAttachment( + signerKeys = inputs[0] as List, + size = inputs[1] as Int, + id = inputs[2] as SecureHash, + attachment = inputs[3], + streamer = inputs[4] as Function + )) + null + } + } +} + +/** + * This represents an [Attachment] from within the sandbox. + */ +class SandboxAttachment( + override val signerKeys: List, + override val size: Int, + override val id: SecureHash, + private val attachment: Any, + private val streamer: Function +) : Attachment { + @Suppress("OverridingDeprecatedMember") + override val signers: List = emptyList() + + override fun open(): InputStream { + return streamer.apply(attachment) + } +} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt new file mode 100644 index 0000000000..af10d294b9 --- /dev/null +++ b/node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt @@ -0,0 +1,37 @@ +package net.corda.node.djvm + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.CommandWithParties +import net.corda.core.internal.lazyMapped +import java.security.PublicKey +import java.util.function.Function + +class CommandBuilder : Function, List>> { + @Suppress("unchecked_cast") + override fun apply(inputs: Array): List> { + val signers = inputs[0] as? List> ?: emptyList() + val commandsData = inputs[1] as? List ?: emptyList() + val partialMerkleLeafIndices = inputs[2] as? IntArray + + return if (partialMerkleLeafIndices != null) { + check(commandsData.size <= signers.size) { + "Invalid Transaction. Fewer Signers (${signers.size}) than CommandData (${commandsData.size}) objects" + } + if (partialMerkleLeafIndices.isNotEmpty()) { + check(partialMerkleLeafIndices.max()!! < signers.size) { + "Invalid Transaction. A command with no corresponding signer detected" + } + } + commandsData.lazyMapped { commandData, index -> + CommandWithParties(signers[partialMerkleLeafIndices[index]], emptyList(), commandData) + } + } else { + check(commandsData.size == signers.size) { + "Invalid Transaction. Sizes of CommandData (${commandsData.size}) and Signers (${signers.size}) do not match" + } + commandsData.lazyMapped { commandData, index -> + CommandWithParties(signers[index], emptyList(), commandData) + } + } + } +} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt new file mode 100644 index 0000000000..d5c73b15ea --- /dev/null +++ b/node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt @@ -0,0 +1,24 @@ +package net.corda.node.djvm + +import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.internal.TransactionDeserialisationException +import net.corda.core.internal.lazyMapped +import net.corda.core.utilities.OpaqueBytes +import java.util.function.Function + +class ComponentBuilder : Function, List<*>> { + @Suppress("unchecked_cast") + override fun apply(inputs: Array): List<*> { + val deserializer = inputs[0] as Function + val groupType = inputs[1] as ComponentGroupEnum + val components = (inputs[2] as Array).map { OpaqueBytes(it) } + + return components.lazyMapped { component, index -> + try { + deserializer.apply(component.bytes) + } catch (e: Exception) { + throw TransactionDeserialisationException(groupType, index, e) + } + } + } +} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxFactory.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxFactory.kt new file mode 100644 index 0000000000..643380be97 --- /dev/null +++ b/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxFactory.kt @@ -0,0 +1,43 @@ +@file:JvmName("LtxConstants") +package net.corda.node.djvm + +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.node.NetworkParameters +import net.corda.core.transactions.LedgerTransaction +import java.util.function.Function + +private const val TX_INPUTS = 0 +private const val TX_OUTPUTS = 1 +private const val TX_COMMANDS = 2 +private const val TX_ATTACHMENTS = 3 +private const val TX_ID = 4 +private const val TX_NOTARY = 5 +private const val TX_TIME_WINDOW = 6 +private const val TX_PRIVACY_SALT = 7 +private const val TX_NETWORK_PARAMETERS = 8 +private const val TX_REFERENCES = 9 + +class LtxFactory : Function, LedgerTransaction> { + + @Suppress("unchecked_cast") + override fun apply(txArgs: Array): LedgerTransaction { + return LedgerTransaction.createForSandbox( + inputs = (txArgs[TX_INPUTS] as Array>).map { it.toStateAndRef() }, + outputs = (txArgs[TX_OUTPUTS] as? List>) ?: emptyList(), + commands = (txArgs[TX_COMMANDS] as? List>) ?: emptyList(), + attachments = (txArgs[TX_ATTACHMENTS] as? List) ?: emptyList(), + id = txArgs[TX_ID] as SecureHash, + notary = txArgs[TX_NOTARY] as? Party, + timeWindow = txArgs[TX_TIME_WINDOW] as? TimeWindow, + privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt, + networkParameters = txArgs[TX_NETWORK_PARAMETERS] as NetworkParameters, + references = (txArgs[TX_REFERENCES] as Array>).map { it.toStateAndRef() } + ) + } + + private fun Array<*>.toStateAndRef(): StateAndRef { + return StateAndRef(this[0] as TransactionState<*>, this[1] as StateRef) + } +} 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 4569422623..475a158420 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -33,14 +33,11 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.days import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.minutes -import net.corda.djvm.analysis.AnalysisConfiguration -import net.corda.djvm.analysis.Whitelist import net.corda.djvm.source.* import net.corda.node.CordaClock import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.* -import net.corda.node.internal.djvm.DeterministicVerifier import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy import net.corda.node.services.ContractUpgradeHandler @@ -68,6 +65,7 @@ import net.corda.node.services.persistence.* import net.corda.node.services.rpc.CheckpointDumper import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.* +import net.corda.node.services.transactions.DeterministicVerifierFactoryService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl @@ -98,8 +96,6 @@ import rx.Observable import rx.Scheduler import java.io.IOException import java.lang.reflect.InvocationTargetException -import java.net.URL -import java.net.URLClassLoader import java.nio.file.Path import java.security.KeyPair import java.security.KeyStoreException @@ -221,6 +217,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ).closeOnStop() @Suppress("LeakingThis") val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize() + val deterministicVerifierFactoryService = DeterministicVerifierFactoryService(djvmBootstrapSource, djvmCordaSource).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @Suppress("LeakingThis") @@ -1081,28 +1078,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val ledgerTransaction = servicesForResolution.specialise(ltx) // Do nothing unless we have Corda's deterministic libraries. - val cordaSource = djvmCordaSource ?: return ledgerTransaction + djvmCordaSource ?: return ledgerTransaction // Specialise the LedgerTransaction here so that // contracts are verified inside the DJVM! - return ledgerTransaction.specialise { tx, cl -> - (cl as? URLClassLoader)?.run { DeterministicVerifier(tx, cl, createSandbox(cordaSource, cl.urLs)) } ?: BasicVerifier(tx, cl) - } - } - - private fun createSandbox(cordaSource: UserSource, userSource: Array): AnalysisConfiguration { - return AnalysisConfiguration.createRoot( - userSource = cordaSource, - whitelist = Whitelist.MINIMAL, - bootstrapSource = djvmBootstrapSource - ).createChild( - userSource = UserPathSource(userSource), - newMinimumSeverityLevel = null, - visibleAnnotations = setOf( - CordaSerializable::class.java, - ConstructorForDeserialization::class.java - ) - ) + return ledgerTransaction.specialise(deterministicVerifierFactoryService::specialise) } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt new file mode 100644 index 0000000000..a16447dced --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt @@ -0,0 +1,32 @@ +package net.corda.node.internal.djvm + +import net.corda.core.contracts.Attachment +import net.corda.core.serialization.serialize +import net.corda.djvm.rewiring.SandboxClassLoader +import net.corda.node.djvm.AttachmentBuilder +import java.util.function.Function + +class AttachmentFactory( + private val classLoader: SandboxClassLoader, + private val taskFactory: Function>, + private val sandboxBasicInput: Function, + private val serializer: Serializer +) { + private val sandboxOpenAttachment: Function = classLoader.createForImport( + Function(Attachment::open).andThen(sandboxBasicInput) + ) + + fun toSandbox(attachments: List): Any? { + val builder = classLoader.createTaskFor(taskFactory, AttachmentBuilder::class.java) + for (attachment in attachments) { + builder.apply(arrayOf( + serializer.deserialize(attachment.signerKeys.serialize()), + sandboxBasicInput.apply(attachment.size), + serializer.deserialize(attachment.id.serialize()), + attachment, + sandboxOpenAttachment + )) + } + return builder.apply(null) + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt new file mode 100644 index 0000000000..ccef9690ac --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt @@ -0,0 +1,19 @@ +package net.corda.node.internal.djvm + +import net.corda.djvm.rewiring.SandboxClassLoader +import net.corda.node.djvm.CommandBuilder +import java.util.function.Function + +class CommandFactory( + private val classLoader: SandboxClassLoader, + private val taskFactory: Function> +) { + fun toSandbox(signers: Any?, commands: Any?, partialMerkleLeafIndices: IntArray?): Any? { + val builder = classLoader.createTaskFor(taskFactory, CommandBuilder::class.java) + return builder.apply(arrayOf( + signers, + commands, + partialMerkleLeafIndices + )) + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt new file mode 100644 index 0000000000..9645522baf --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt @@ -0,0 +1,43 @@ +@file:JvmName("ComponentUtils") +package net.corda.node.internal.djvm + +import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.crypto.componentHash +import net.corda.core.transactions.ComponentGroup +import net.corda.core.transactions.FilteredComponentGroup +import net.corda.djvm.rewiring.SandboxClassLoader +import net.corda.node.djvm.ComponentBuilder +import java.util.function.Function + +class ComponentFactory( + private val classLoader: SandboxClassLoader, + private val taskFactory: Function>, + private val sandboxBasicInput: Function, + private val serializer: Serializer, + private val componentGroups: List +) { + fun toSandbox( + groupType: ComponentGroupEnum, + clazz: Class<*> + ): Any? { + val components = (componentGroups.firstOrNull(groupType::isSameType) ?: return null).components + val componentBytes = Array(components.size) { idx -> components[idx].bytes } + return classLoader.createTaskFor(taskFactory, ComponentBuilder::class.java).apply(arrayOf( + classLoader.createForImport(serializer.deserializerFor(clazz)), + sandboxBasicInput.apply(groupType), + componentBytes + )) + } + + fun calculateLeafIndicesFor(groupType: ComponentGroupEnum): IntArray? { + val componentGroup = componentGroups.firstOrNull(groupType::isSameType) as? FilteredComponentGroup ?: return null + val componentHashes = componentGroup.components.mapIndexed { index, component -> + componentHash(componentGroup.nonces[index], component) + } + return componentHashes.map { componentGroup.partialMerkleTree.leafIndex(it) }.toIntArray() + } +} + +private fun ComponentGroupEnum.isSameType(group: ComponentGroup): Boolean { + return group.groupIndex == ordinal +} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt index 7c2490c823..99d3d1d0d3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt @@ -1,15 +1,19 @@ package net.corda.node.internal.djvm +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ComponentGroupEnum.* +import net.corda.core.contracts.TransactionState import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.SecureHash -import net.corda.core.internal.ContractVerifier -import net.corda.core.internal.Verifier +import net.corda.core.internal.* +import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.djvm.SandboxConfiguration import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.execution.* import net.corda.djvm.messages.Message import net.corda.djvm.source.ClassSource +import net.corda.node.djvm.LtxFactory class DeterministicVerifier( ltx: LedgerTransaction, @@ -23,24 +27,67 @@ class DeterministicVerifier( profile = ExecutionProfile.DEFAULT, enableTracing = false ) - val verifierClass = ClassSource.fromClassName(ContractVerifier::class.java.name) - val result = IsolatedTask(verifierClass.qualifiedClassName, configuration).run { - val executor = Executor(classLoader) + val result = IsolatedTask(ltx.id.toString(), configuration).run { + val taskFactory = classLoader.createRawTaskFactory() + val sandboxBasicInput = classLoader.createBasicInput() + + /** + * Deserialise the [LedgerTransaction] again into something + * that we can execute inside the DJVM's sandbox. + */ val sandboxTx = ltx.transform { componentGroups, serializedInputs, serializedReferences -> + val serializer = Serializer(classLoader) + val componentFactory = ComponentFactory( + classLoader, + taskFactory, + sandboxBasicInput, + serializer, + componentGroups + ) + val attachmentFactory = AttachmentFactory( + classLoader, + taskFactory, + sandboxBasicInput, + serializer + ) + + val idData = ltx.id.serialize() + val notaryData = ltx.notary?.serialize() + val timeWindowData = ltx.timeWindow?.serialize() + val privacySaltData = ltx.privacySalt.serialize() + val networkingParametersData = ltx.networkParameters?.serialize() + + val createSandboxTx = classLoader.createTaskFor(taskFactory, LtxFactory::class.java) + createSandboxTx.apply(arrayOf( + serializer.deserialize(serializedInputs), + componentFactory.toSandbox(OUTPUTS_GROUP, TransactionState::class.java), + CommandFactory(classLoader, taskFactory).toSandbox( + componentFactory.toSandbox(SIGNERS_GROUP, List::class.java), + componentFactory.toSandbox(COMMANDS_GROUP, CommandData::class.java), + componentFactory.calculateLeafIndicesFor(COMMANDS_GROUP) + ), + attachmentFactory.toSandbox(ltx.attachments), + serializer.deserialize(idData), + serializer.deserialize(notaryData), + serializer.deserialize(timeWindowData), + serializer.deserialize(privacySaltData), + serializer.deserialize(networkingParametersData), + serializer.deserialize(serializedReferences) + )) } - val verifier = classLoader.loadClassForSandbox(verifierClass).newInstance() + val verifier = classLoader.createTaskFor(taskFactory, ContractVerifier::class.java) // Now execute the contract verifier task within the sandbox... - executor.execute(verifier, sandboxTx) + verifier.apply(sandboxTx) } result.exception?.run { val sandboxEx = SandboxException( Message.getMessageFromException(this), result.identifier, - verifierClass, + ClassSource.fromClassName(ContractVerifier::class.java.name), ExecutionSummary(result.costs), this ) @@ -51,7 +98,7 @@ class DeterministicVerifier( @Throws(Exception::class) override fun close() { - analysisConfiguration.closeAll() + // analysisConfiguration.closeAll() } } diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/Executor.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/Executor.kt deleted file mode 100644 index 3f71c3320b..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/Executor.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.node.internal.djvm - -import java.lang.reflect.Constructor -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method - -class Executor(classLoader: ClassLoader) { - private val constructor: Constructor - private val executeMethod: Method - - init { - val taskClass = classLoader.loadClass("sandbox.RawTask") - constructor = taskClass.getDeclaredConstructor(classLoader.loadClass("sandbox.java.util.function.Function")) - executeMethod = taskClass.getMethod("apply", Any::class.java) - } - - fun execute(task: Any, input: Any?): Any? { - return try { - executeMethod.invoke(constructor.newInstance(task), input) - } catch (ex: InvocationTargetException) { - throw ex.targetException - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt new file mode 100644 index 0000000000..95db8f49c5 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt @@ -0,0 +1,59 @@ +package net.corda.node.internal.djvm + +import net.corda.core.internal.SerializedStateAndRef +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationFactory +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.serialize +import net.corda.core.utilities.ByteSequence +import net.corda.djvm.rewiring.SandboxClassLoader +import net.corda.djvm.serialization.createSandboxSerializationEnv +import net.corda.djvm.serialization.deserializeTo +import net.corda.node.djvm.ComponentBuilder +import java.util.function.Function + +class Serializer(private val classLoader: SandboxClassLoader) { + private val factory: SerializationFactory + private val context: SerializationContext + + init { + val env = createSandboxSerializationEnv(classLoader) + factory = env.serializationFactory + context = env.p2pContext + } + + /** + * Convert a list of [SerializedStateAndRef] objects into arrays + * of deserialized sandbox objects. We will pass this array into + * [net.corda.node.djvm.LtxFactory] to be transformed finally to + * a list of [net.corda.core.contracts.StateAndRef] objects, + */ + fun deserialize(stateRefs: List): Array> { + return stateRefs.map { + arrayOf(deserialize(it.serializedState), deserialize(it.ref.serialize())) + }.toTypedArray() + } + + /** + * Generate a [Function] that deserializes a [ByteArray] into an instance + * of the given sandbox class. We import this [Function] into the sandbox + * so that [ComponentBuilder] can deserialize objects lazily. + */ + fun deserializerFor(clazz: Class<*>): Function { + val sandboxClass = classLoader.toSandboxClass(clazz) + return Function { bytes -> + bytes?.run { + ByteSequence.of(this).deserializeTo(sandboxClass, classLoader, factory, context) + } + } + } + + fun deserializeTo(clazz: Class<*>, bytes: ByteSequence): Any { + val sandboxClass = classLoader.toSandboxClass(clazz) + return bytes.deserializeTo(sandboxClass, classLoader, factory, context) + } + + inline fun deserialize(bytes: SerializedBytes?): Any? { + return deserializeTo(T::class.java, bytes ?: return null) + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt new file mode 100644 index 0000000000..eb1471160f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt @@ -0,0 +1,52 @@ +package net.corda.node.services.transactions + +import net.corda.core.internal.BasicVerifier +import net.corda.core.internal.Verifier +import net.corda.core.serialization.ConstructorForDeserialization +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.DeprecatedConstructorForDeserialization +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.transactions.LedgerTransaction +import net.corda.djvm.analysis.AnalysisConfiguration +import net.corda.djvm.analysis.Whitelist +import net.corda.djvm.source.ApiSource +import net.corda.djvm.source.UserPathSource +import net.corda.djvm.source.UserSource +import net.corda.node.internal.djvm.DeterministicVerifier +import java.net.URL +import java.net.URLClassLoader + +class DeterministicVerifierFactoryService( + private val bootstrapSource: ApiSource, + private val cordaSource: UserSource? +) : SingletonSerializeAsToken(), AutoCloseable { + + fun specialise(ltx: LedgerTransaction, classLoader: ClassLoader): Verifier { + return (classLoader as? URLClassLoader)?.run { + DeterministicVerifier(ltx, classLoader, createSandbox(classLoader.urLs)) + } ?: BasicVerifier(ltx, classLoader) + } + + private fun createSandbox(userSource: Array): AnalysisConfiguration { + return AnalysisConfiguration.createRoot( + userSource = cordaSource!!, + whitelist = Whitelist.MINIMAL, + visibleAnnotations = setOf( + CordaSerializable::class.java, + ConstructorForDeserialization::class.java, + DeprecatedConstructorForDeserialization::class.java + ), + bootstrapSource = bootstrapSource + ).createChild( + userSource = UserPathSource(userSource), + newMinimumSeverityLevel = null, + visibleAnnotations = emptySet() + ) + } + + override fun close() { + bootstrapSource.use { + cordaSource?.close() + } + } +} diff --git a/settings.gradle b/settings.gradle index d9f6c9f20f..7e8eb3c7c6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,7 @@ include 'docs' include 'node-api' include 'node' include 'node:capsule' +include 'node:djvm' include 'client:jackson' include 'client:jfx' include 'client:mock'