diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a2703c32b2..363b258082 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4735,8 +4735,6 @@ public interface net.corda.core.node.ServiceHub extends net.corda.core.node.Serv @NotNull public abstract net.corda.core.node.services.TelemetryService getTelemetryService() @NotNull - public abstract net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() - @NotNull public abstract net.corda.core.node.services.TransactionStorage getValidatedTransactions() @NotNull public abstract net.corda.core.node.services.VaultService getVaultService() @@ -5087,11 +5085,6 @@ public interface net.corda.core.node.services.TransactionStorage @NotNull public abstract net.corda.core.concurrent.CordaFuture trackTransaction(net.corda.core.crypto.SecureHash) ## -@DoNotImplement -public interface net.corda.core.node.services.TransactionVerifierService - @NotNull - public abstract net.corda.core.concurrent.CordaFuture verify(net.corda.core.transactions.LedgerTransaction) -## @CordaSerializable public final class net.corda.core.node.services.UnknownAnonymousPartyException extends net.corda.core.CordaException public (String) @@ -7846,8 +7839,6 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob @NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) @NotNull - public final net.corda.core.transactions.LedgerTransaction toLedgerTransactionWithContext(net.corda.core.node.ServicesForResolution, net.corda.core.serialization.SerializationContext) - @NotNull public final net.corda.core.transactions.SignedTransaction toSignedTransaction(net.corda.core.node.services.KeyManagementService, java.security.PublicKey, net.corda.core.crypto.SignatureMetadata, net.corda.core.node.ServicesForResolution) @NotNull public final net.corda.core.transactions.WireTransaction toWireTransaction(net.corda.core.node.ServicesForResolution) @@ -9890,8 +9881,6 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @NotNull public net.corda.core.internal.telemetry.TelemetryServiceImpl getTelemetryService() @NotNull - public net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() - @NotNull public net.corda.core.node.services.TransactionStorage getValidatedTransactions() @NotNull public net.corda.core.node.services.VaultService getVaultService() diff --git a/build.gradle b/build.gradle index 7bdd1871cb..c239bed37a 100644 --- a/build.gradle +++ b/build.gradle @@ -215,7 +215,7 @@ plugins { id 'org.jetbrains.kotlin.jvm' apply false id 'org.jetbrains.kotlin.plugin.allopen' apply false id 'org.jetbrains.kotlin.plugin.jpa' apply false - id 'com.github.johnrengelman.shadow' version '2.0.4' apply false + id 'com.github.johnrengelman.shadow' version '7.1.2' apply false id "org.ajoberstar.grgit" version "4.0.0" id 'corda.root-publish' id "org.jetbrains.dokka" version "1.8.20" diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index eb4cca280e..47b9b51ac3 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -4,7 +4,8 @@ apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'corda.common-publishing' dependencies { - implementation project(':core') + api project(':core') + implementation project(':serialization') // Jackson and its plugins: parsing to/from JSON and other textual formats. @@ -27,6 +28,7 @@ dependencies { testImplementation project(':test-common') testImplementation project(':core-test-utils') testImplementation project(':test-utils') + testImplementation project(":node-driver") testImplementation project(path: ':core', configuration: 'testArtifacts') testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" 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 f753375b5a..fddafc6547 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 @@ -8,27 +8,36 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.convertValue -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import org.mockito.kotlin.spy import net.corda.client.jackson.internal.childrenAs import net.corda.client.jackson.internal.valueAs -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Command +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.PartialMerkleTree import net.corda.core.crypto.PartialMerkleTree.PartialTree -import net.corda.core.identity.* -import net.corda.core.internal.AbstractAttachment +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.secureRandomBytes +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -37,14 +46,27 @@ import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.days +import net.corda.core.utilities.hours +import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.toBase64 +import net.corda.core.utilities.toHexString +import net.corda.coretesting.internal.createNodeInfoAndSigned +import net.corda.coretesting.internal.rigorousMock import net.corda.finance.USD import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.* -import net.corda.coretesting.internal.createNodeInfoAndSigned -import net.corda.coretesting.internal.rigorousMock +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +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.node.MockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before @@ -54,15 +76,22 @@ import org.junit.jupiter.api.TestFactory import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever import java.math.BigInteger import java.nio.charset.StandardCharsets.UTF_8 import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.X509Certificate import java.time.Instant -import java.util.* +import java.util.Currency +import java.util.Date +import java.util.UUID import javax.security.auth.x500.X500Principal -import kotlin.collections.ArrayList +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.component3 +import kotlin.collections.component4 @RunWith(Parameterized::class) class JacksonSupportTest(@Suppress("unused") private val name: String, factory: JsonFactory) { @@ -90,23 +119,12 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: @Before fun setup() { - val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }, "test") { - override val id: SecureHash get() = throw UnsupportedOperationException() - } - - val attachments = rigorousMock().also { - doReturn(unsignedAttachment).whenever(it).openAttachment(any()) - } - services = rigorousMock() + services = MockServices( + listOf("net.corda.testing.contracts"), + MINI_CORP, + testNetworkParameters(minimumPlatformVersion = 4) + ) cordappProvider = rigorousMock() - val networkParameters = testNetworkParameters(minimumPlatformVersion = 4) - val networkParametersService = rigorousMock().also { - doReturn(networkParameters.serialize().hash).whenever(it).currentHash - } - doReturn(networkParametersService).whenever(services).networkParametersService - doReturn(cordappProvider).whenever(services).cordappProvider - doReturn(networkParameters).whenever(services).networkParameters - doReturn(attachments).whenever(services).attachments } @Test(timeout=300_000) @@ -263,17 +281,6 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: @Test(timeout=300_000) fun `SignedTransaction (WireTransaction)`() { val attachmentId = SecureHash.randomSHA256() - doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) - val attachmentStorage = rigorousMock() - doReturn(attachmentStorage).whenever(services).attachments - doReturn(mock()).whenever(services).validatedTransactions - doReturn(mock()).whenever(services).identityService - val attachment = rigorousMock() - doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId) - doReturn(attachmentId).whenever(attachment).id - doReturn(emptyList()).whenever(attachment).signerKeys - doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts - doReturn("app").whenever(attachment).uploader val wtx = TransactionBuilder( notary = DUMMY_NOTARY, diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt index 01e9e81df7..70e0996a9d 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt @@ -9,6 +9,7 @@ import net.corda.core.CordaRuntimeException import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.flows.ContractUpgradeFlow +import net.corda.core.internal.getRequiredTransaction import net.corda.core.messaging.CordaRPCOps import net.corda.core.transactions.ContractUpgradeLedgerTransaction import net.corda.core.transactions.SignedTransaction @@ -120,8 +121,7 @@ class ContractUpgradeFlowRPCTest : WithContracts, WithFinality { isUpgrade()) private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef) = - services.validatedTransactions.getTransaction(state.ref.txhash)!! - .resolveContractUpgradeTransaction(services) + services.getRequiredTransaction(state.ref.txhash).resolveContractUpgradeTransaction(services) private inline fun isUpgrade() = isUpgradeFrom() and isUpgradeTo() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt index 1ea3dceb50..0f4dbb7214 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt @@ -1,32 +1,55 @@ package net.corda.coretests.flows -import com.natpryce.hamkrest.* +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.and +import com.natpryce.hamkrest.anything import com.natpryce.hamkrest.assertion.assertThat -import net.corda.core.contracts.* +import com.natpryce.hamkrest.equalTo +import com.natpryce.hamkrest.has +import com.natpryce.hamkrest.isA +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint +import net.corda.core.contracts.Amount +import net.corda.core.contracts.AttachmentConstraint +import net.corda.core.contracts.BelongsToContract +import net.corda.core.contracts.CommandAndState +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.Issued +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.AbstractParty import net.corda.core.internal.Emoji +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.internal.mapToSet import net.corda.core.transactions.ContractUpgradeLedgerTransaction import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.coretesting.internal.matchers.flow.willReturn +import net.corda.coretesting.internal.matchers.flow.willThrow import net.corda.finance.USD -import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.`issued by` import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.contracts.DummyContractV3 import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity -import net.corda.coretesting.internal.matchers.flow.willReturn -import net.corda.coretesting.internal.matchers.flow.willThrow -import net.corda.testing.node.internal.* +import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.enclosedCordapp +import net.corda.testing.node.internal.startFlow import org.junit.AfterClass import org.junit.Ignore import org.junit.Test -import java.util.* +import java.util.Currency @Ignore("TODO JDK17: class cast exception") class ContractUpgradeFlowTest : WithContracts, WithFinality { @@ -161,7 +184,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { @BelongsToContract(CashV2::class) data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: AbstractParty = owners.first() - override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet() + override val exitKeys = (owners + amount.token.issuer.party).mapToSet { it.owningKey } override val participants = owners override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) @@ -182,8 +205,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { isUpgrade()) private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef) = - services.validatedTransactions.getTransaction(state.ref.txhash)!! - .resolveContractUpgradeTransaction(services) + services.getRequiredTransaction(state.ref.txhash).resolveContractUpgradeTransaction(services) private inline fun isUpgrade() = isUpgradeFrom() and isUpgradeTo() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt index 714e8b85fa..127cc1ccf9 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt @@ -13,6 +13,7 @@ import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachineHandle +import net.corda.core.internal.getRequiredTransaction import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow @@ -26,9 +27,7 @@ interface WithFinality : WithMockNet { return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet(), emptySet())) } - fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction { - return services.validatedTransactions.getTransaction(stx.id)!! - } + fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction = services.getRequiredTransaction(stx.id) fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle { return startFlow(WithFinality::FinalityInvoker, stx, recipients.toSet(), emptySet()).andRunNetwork() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt new file mode 100644 index 0000000000..67dc930fe7 --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt @@ -0,0 +1,137 @@ +package net.corda.coretests.internal.verification + +import net.corda.core.internal.verification.AttachmentFixups +import net.corda.core.node.services.AttachmentId +import net.corda.node.VersionInfo +import net.corda.node.internal.cordapp.JarScanningCordappLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarOutputStream +import java.util.zip.Deflater +import java.util.zip.ZipEntry +import kotlin.io.path.outputStream +import kotlin.test.assertFailsWith + +class AttachmentFixupsTest { + companion object { + @JvmField + val ID1 = AttachmentId.randomSHA256() + @JvmField + val ID2 = AttachmentId.randomSHA256() + @JvmField + val ID3 = AttachmentId.randomSHA256() + @JvmField + val ID4 = AttachmentId.randomSHA256() + } + + @Test(timeout=300_000) + fun `test fixup rule that adds attachment`() { + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules("$ID1 => $ID2, $ID3") + val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { + fixupAttachmentIds(listOf(ID1)) + } + assertThat(fixedIDs).containsExactly(ID2, ID3) + } + + @Test(timeout=300_000) + fun `test fixup rule that deletes attachment`() { + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules("$ID1 =>") + val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { + fixupAttachmentIds(listOf(ID1)) + } + assertThat(fixedIDs).isEmpty() + } + + @Test(timeout=300_000) + fun `test fixup rule with blank LHS`() { + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules(" => $ID2") + val ex = assertFailsWith { + newFixupService(fixupJar.toUri().toURL()) + } + assertThat(ex).hasMessageContaining( + "Forbidden empty list of source attachment IDs in '$fixupJar'" + ) + } + + @Test(timeout=300_000) + fun `test fixup rule without arrows`() { + val rule = " $ID1 " + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules(rule) + val ex = assertFailsWith { + newFixupService(fixupJar.toUri().toURL()) + } + assertThat(ex).hasMessageContaining( + "Invalid fix-up line '${rule.trim()}' in '$fixupJar'" + ) + } + + @Test(timeout=300_000) + fun `test fixup rule with too many arrows`() { + val rule = " $ID1 => $ID2 => $ID3 " + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules(rule) + val ex = assertFailsWith { + newFixupService(fixupJar.toUri().toURL()) + } + assertThat(ex).hasMessageContaining( + "Invalid fix-up line '${rule.trim()}' in '$fixupJar'" + ) + } + + @Test(timeout=300_000) + fun `test fixup file containing multiple rules and comments`() { + val fixupJar = Files.createTempFile("fixup", ".jar").writeFixupRules( + "# Whole line comment", + "\t$ID1,$ID2 => $ID2,, $ID3 # EOl comment", + " # Empty line with comment", + "", + "$ID3 => $ID4" + ) + val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { + fixupAttachmentIds(listOf(ID2, ID1)) + } + assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4) + } + + private fun Path.writeFixupRules(vararg lines: String): Path { + JarOutputStream(outputStream()).use { jar -> + jar.setMethod(ZipEntry.DEFLATED) + jar.setLevel(Deflater.NO_COMPRESSION) + jar.putNextEntry(directoryEntry("META-INF")) + jar.putNextEntry(fileEntry("META-INF/Corda-Fixups")) + for (line in lines) { + jar.write(line.toByteArray()) + jar.write('\r'.code) + jar.write('\n'.code) + } + } + return this + } + + private fun directoryEntry(internalName: String): ZipEntry { + return ZipEntry("$internalName/").apply { + method = ZipEntry.STORED + compressedSize = 0 + size = 0 + crc = 0 + } + } + + private fun fileEntry(internalName: String): ZipEntry { + return ZipEntry(internalName).apply { + method = ZipEntry.DEFLATED + } + } + + private fun newFixupService(vararg urls: URL): AttachmentFixups { + val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN) + return AttachmentFixups().apply { load(loader.appClassLoader) } + } +} diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt index b48199ec47..f1887ed00c 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt @@ -1,7 +1,6 @@ package net.corda.coretests.transactions import net.corda.core.contracts.Command -import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.SignatureAttachmentConstraint @@ -10,45 +9,33 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionState import net.corda.core.contracts.TransactionVerificationException.UnsupportedHashTypeException -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party -import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.HashAgility import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.digestService -import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.serialization.serialize +import net.corda.core.serialization.internal._driverSerializationEnv import net.corda.core.transactions.TransactionBuilder -import net.corda.coretesting.internal.rigorousMock import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME 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.node.MockNetwork +import net.corda.testing.node.MockNetworkParameters +import net.corda.testing.node.MockServices +import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import java.security.PublicKey import java.time.Instant import kotlin.test.assertFailsWith @@ -58,33 +45,12 @@ class TransactionBuilderTest { val testSerialization = SerializationEnvironmentRule() private val notary = TestIdentity(DUMMY_NOTARY_NAME).party - private val services = rigorousMock() - private val contractAttachmentId = SecureHash.randomSHA256() - private val attachments = rigorousMock() - private val networkParametersService = mock() - - @Before - fun setup() { - val cordappProvider = rigorousMock() - val networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION) - doReturn(networkParametersService).whenever(services).networkParametersService - doReturn(networkParameters.serialize().hash).whenever(networkParametersService).currentHash - doReturn(cordappProvider).whenever(services).cordappProvider - doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) - doReturn(networkParameters).whenever(services).networkParameters - doReturn(mock()).whenever(services).identityService - - val attachmentStorage = rigorousMock() - doReturn(attachmentStorage).whenever(services).attachments - val attachment = rigorousMock() - doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId) - doReturn(contractAttachmentId).whenever(attachment).id - doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts - doReturn("app").whenever(attachment).uploader - doReturn(emptyList()).whenever(attachment).signerKeys - doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage) - .getLatestContractAttachments("net.corda.testing.contracts.DummyContract") - } + private val services = MockServices( + listOf("net.corda.testing.contracts"), + TestIdentity(ALICE_NAME), + testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION) + ) + private val contractAttachmentId = services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0] @Test(timeout=300_000) fun `bare minimum issuance tx`() { @@ -100,13 +66,11 @@ class TransactionBuilderTest { val wtx = builder.toWireTransaction(services) assertThat(wtx.outputs).containsOnly(outputState) assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey)) - assertThat(wtx.networkParametersHash).isEqualTo(networkParametersService.currentHash) + assertThat(wtx.networkParametersHash).isEqualTo(services.networkParametersService.currentHash) } @Test(timeout=300_000) fun `automatic hash constraint`() { - doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId) - val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) val builder = TransactionBuilder() .addOutputState(outputState) @@ -117,8 +81,6 @@ class TransactionBuilderTest { @Test(timeout=300_000) fun `reference states`() { - doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId) - val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1) val builder = TransactionBuilder(notary) @@ -126,54 +88,55 @@ class TransactionBuilderTest { .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") + with(testNetworkParameters(minimumPlatformVersion = 3)) { + val services = MockServices(listOf("net.corda.testing.contracts"), TestIdentity(ALICE_NAME), this) + assertThatThrownBy { builder.toWireTransaction(services) } + .isInstanceOf(ZoneVersionTooLowException::class.java) + .hasMessageContaining("Reference states") + } - doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters - doReturn(referenceState).whenever(services).loadState(referenceStateRef) - val wtx = builder.toWireTransaction(services) - assertThat(wtx.references).containsOnly(referenceStateRef) + with(testNetworkParameters(minimumPlatformVersion = 4)) { + val services = MockServices(listOf("net.corda.testing.contracts"), TestIdentity(ALICE_NAME), this) + val wtx = builder.toWireTransaction(services) + assertThat(wtx.references).containsOnly(referenceStateRef) + } } @Test(timeout=300_000) fun `automatic signature constraint`() { - val aliceParty = TestIdentity(ALICE_NAME).party - val bobParty = TestIdentity(BOB_NAME).party - val compositeKey = CompositeKey.Builder().addKeys(aliceParty.owningKey, bobParty.owningKey).build() - val expectedConstraint = SignatureAttachmentConstraint(compositeKey) - val signedAttachment = signedAttachment(aliceParty, bobParty) + // We need to use a MockNetwork so that we can create a signed attachment. However, SerializationEnvironmentRule and MockNetwork + // don't work well together, so we temporarily clear out the driverSerializationEnv for this test. + val driverSerializationEnv = _driverSerializationEnv.get() + _driverSerializationEnv.set(null) + val mockNetwork = MockNetwork( + MockNetworkParameters( + networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION), + cordappsForAllNodes = listOf(cordappWithPackages("net.corda.testing.contracts").signed()) + ) + ) - assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment)) - assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment)) + try { + val services = mockNetwork.notaryNodes[0].services - doReturn(attachments).whenever(services).attachments - doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId) - doReturn(listOf(contractAttachmentId)).whenever(attachments) - .getLatestContractAttachments("net.corda.testing.contracts.DummyContract") + val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]) + val attachmentSigner = attachment!!.signerKeys.single() - 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) + val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner) + assertTrue(expectedConstraint.isSatisfiedBy(attachment)) - assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) + 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 = expectedConstraint)) + } finally { + mockNetwork.stopNodes() + _driverSerializationEnv.set(driverSerializationEnv) + } } - private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }, "test") { - override val id: SecureHash get() = throw UnsupportedOperationException() - - override val signerKeys: List get() = emptyList() - }, DummyContract.PROGRAM_ID) - - private fun signedAttachment(vararg parties: Party) = ContractAttachment.create(object : AbstractAttachment({ byteArrayOf() }, "test") { - override val id: SecureHash get() = contractAttachmentId - - override val signerKeys: List get() = parties.map { it.owningKey } - }, DummyContract.PROGRAM_ID, signerKeys = parties.map { it.owningKey }) - @Test(timeout=300_000) fun `list accessors are mutable copies`() { val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) diff --git a/core/build.gradle b/core/build.gradle index 9916f83039..f2fe5c1ea1 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -40,9 +40,6 @@ dependencies { // Hamkrest, for fluent, composable matchers testImplementation "com.natpryce:hamkrest:$hamkrest_version" - // Thread safety annotations - implementation "com.google.code.findbugs:jsr305:$jsr305_version" - // SLF4J: commons-logging bindings for a SLF4J back end implementation "org.slf4j:jcl-over-slf4j:$slf4j_version" implementation "org.slf4j:slf4j-api:$slf4j_version" @@ -66,10 +63,7 @@ dependencies { // Bouncy castle support needed for X509 certificate manipulation implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" - implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" - - // JPA 2.2 annotations. - implementation "javax.persistence:javax.persistence-api:2.2" + testImplementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" // required to use @Type annotation implementation "org.hibernate:hibernate-core:$hibernate_version" diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 0b56707ee1..24852fad3c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -27,6 +27,7 @@ class ContractAttachment private constructor( companion object { @CordaInternal + @JvmSynthetic fun create(attachment: Attachment, contract: ContractClassName, additionalContracts: Set = emptySet(), diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt index 892506aa76..a2f2396607 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt @@ -1,10 +1,10 @@ package net.corda.core.crypto.internal import net.corda.core.crypto.DigestAlgorithm -import java.lang.reflect.Constructor +import net.corda.core.internal.loadClassOfType import java.security.MessageDigest import java.security.NoSuchAlgorithmException -import java.util.* +import java.util.Collections import java.util.concurrent.ConcurrentHashMap sealed class DigestAlgorithmFactory { @@ -28,9 +28,8 @@ sealed class DigestAlgorithmFactory { } private class CustomAlgorithmFactory(className: String) : DigestAlgorithmFactory() { - val constructor: Constructor = Class.forName(className, false, javaClass.classLoader) - .asSubclass(DigestAlgorithm::class.java) - .getConstructor() + private val constructor = loadClassOfType(className, false, javaClass.classLoader).getConstructor() + override val algorithm: String = constructor.newInstance().algorithm override fun create(): DigestAlgorithm { diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 173107dd5f..91bc460bce 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -11,6 +11,8 @@ import net.corda.core.internal.NetworkParametersStorage import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.RetrieveAnyTransactionPayload import net.corda.core.internal.ServiceHubCoreInternal +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.internal.mapToSet import net.corda.core.internal.readFully import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord @@ -169,13 +171,10 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any is SignedTransaction -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload)) is RetrieveAnyTransactionPayload -> TransactionAuthorisationFilter(acceptAll = true) is List<*> -> TransactionAuthorisationFilter().addAuthorised(payload.flatMap { someObject -> - if (someObject is StateAndRef<*>) { - getInputTransactions(serviceHub.validatedTransactions.getTransaction(someObject.ref.txhash)!!) + someObject.ref.txhash - } - else if (someObject is NamedByHash) { - setOf(someObject.id) - } else { - throw Exception("Unknown payload type: ${someObject!!::class.java} ?") + when (someObject) { + is StateAndRef<*> -> getInputTransactions(serviceHub.getRequiredTransaction(someObject.ref.txhash)) + someObject.ref.txhash + is NamedByHash -> setOf(someObject.id) + else -> throw Exception("Unknown payload type: ${someObject!!::class.java} ?") } }.toSet()) else -> throw Exception("Unknown payload type: ${payload::class.java} ?") @@ -308,7 +307,7 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any @Suspendable private fun getInputTransactions(tx: SignedTransaction): Set { - return tx.inputs.map { it.txhash }.toSet() + tx.references.map { it.txhash }.toSet() + return tx.inputs.mapToSet { it.txhash } + tx.references.mapToSet { it.txhash } } private class TransactionAuthorisationFilter(private val authorisedTransactions: MutableSet = mutableSetOf(), val acceptAll: Boolean = false) { diff --git a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt index 2e80d05eb0..4b1fe0b291 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt @@ -21,8 +21,7 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a fun createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class, classVersionRange: IntRange? = null): Set { return getNamesOfClassesImplementing(classloader, clazz, classVersionRange) - .map { Class.forName(it, false, classloader).asSubclass(clazz) } - .mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() } + .mapToSet { loadClassOfType(clazz, it, false, classloader).kotlin.objectOrNewInstance() } } /** @@ -56,10 +55,23 @@ fun getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Clas } result.getClassesImplementing(clazz.name) .filterNot(ClassInfo::isAbstract) - .mapTo(LinkedHashSet(), ClassInfo::getName) + .mapToSet(ClassInfo::getName) } } +/** + * @throws ClassNotFoundException + * @throws ClassCastException + * @see Class.forName + */ +inline fun loadClassOfType(className: String, initialize: Boolean = true, classLoader: ClassLoader? = null): Class { + return loadClassOfType(T::class.java, className, initialize, classLoader) +} + +fun loadClassOfType(type: Class, className: String, initialize: Boolean = true, classLoader: ClassLoader? = null): Class { + return Class.forName(className, initialize, classLoader).asSubclass(type) +} + fun executeWithThreadContextClassLoader(classloader: ClassLoader, fn: () -> T): T { val threadClassLoader = Thread.currentThread().contextClassLoader try { @@ -68,5 +80,4 @@ fun executeWithThreadContextClassLoader(classloader: ClassLoader, fn: } finally { Thread.currentThread().contextClassLoader = threadClassLoader } - } 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 9f315a778d..9531bd512e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -1,29 +1,22 @@ @file:Suppress("TooManyFunctions") package net.corda.core.internal -import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName -import net.corda.core.cordapp.CordappProvider +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.crypto.SecureHash import net.corda.core.flows.DataVendingFlow import net.corda.core.flows.FlowLogic 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.AttachmentStorage -import net.corda.core.node.services.vault.AttachmentQueryCriteria -import net.corda.core.node.services.vault.AttachmentSort -import net.corda.core.node.services.vault.Builder -import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable 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 import java.security.PublicKey -import java.util.jar.JarInputStream // *Internal* Corda-specific utilities. @@ -68,11 +61,6 @@ fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serial return toWireTransactionWithContext(services, serializationContext) } -/** Provide access to internal method for AttachmentClassLoaderTests. */ -fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { - return toLedgerTransactionWithContext(services, serializationContext) -} - /** Checks if this flow is an idempotent flow. */ fun Class>.isIdempotentFlow(): Boolean { return IdempotentFlow::class.java.isAssignableFrom(this) @@ -125,40 +113,6 @@ fun noPackageOverlap(packages: Collection): Boolean { return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } } } -/** - * @return The set of [AttachmentId]s after the node's fix-up rules have been applied to [attachmentIds]. - */ -fun CordappProvider.internalFixupAttachmentIds(attachmentIds: Collection): Set { - return (this as CordappFixupInternal).fixupAttachmentIds(attachmentIds) -} - -/** - * Scans trusted (installed locally) attachments to find all that contain the [className]. - * This is required as a workaround until explicit cordapp dependencies are implemented. - * DO NOT USE IN CLIENT code. - * - * @return the attachments with the highest version. - * - * TODO: Should throw when the class is found in multiple contract attachments (not different versions). - */ -fun AttachmentStorage.internalFindTrustedAttachmentForClass(className: String): Attachment? { - val allTrusted = queryAttachments( - AttachmentQueryCriteria.AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)), - AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))) - - // TODO - add caching if performance is affected. - for (attId in allTrusted) { - val attch = openAttachment(attId)!! - if (attch.openAsJAR().use { hasFile(it, "$className.class") }) return attch - } - return null -} - -private fun hasFile(jarStream: JarInputStream, className: String): Boolean { - while (true) { - val e = jarStream.nextJarEntry ?: return false - if (e.name == className) { - return true - } - } +fun ServiceHub.getRequiredTransaction(txhash: SecureHash): SignedTransaction { + return validatedTransactions.getTransaction(txhash) ?: throw TransactionResolutionException(txhash) } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt b/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt deleted file mode 100644 index e1ac4e22c6..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.node.services.AttachmentId - -interface CordappFixupInternal { - fun fixupAttachmentIds(attachmentIds: Collection): Set -} 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 7ad05fe75c..9f227123ae 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -23,7 +23,6 @@ import rx.subjects.UnicastSubject import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream -import java.io.OutputStream import java.lang.reflect.Field import java.lang.reflect.Member import java.lang.reflect.Modifier @@ -138,10 +137,34 @@ fun List.randomOrNull(): T? { /** Returns the index of the given item or throws [IllegalArgumentException] if not found. */ fun List.indexOfOrThrow(item: T): Int { val i = indexOf(item) - require(i != -1){"No such element"} + require(i != -1) { "No such element" } return i } +/** + * Similar to [Iterable.map] except it maps to a [Set] which preserves the iteration order. + */ +inline fun Iterable.mapToSet(transform: (T) -> R): Set { + if (this is Collection) { + when (size) { + 0 -> return emptySet() + 1 -> return setOf(transform(first())) + } + } + return mapTo(LinkedHashSet(), transform) +} + +/** + * Similar to [Iterable.flatMap] except it maps to a [Set] which preserves the iteration order. + */ +inline fun Iterable.flatMapToSet(transform: (T) -> Iterable): Set { + return if (this is Collection && isEmpty()) { + emptySet() + } else { + flatMapTo(LinkedHashSet(), transform) + } +} + fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) /** Same as [InputStream.readBytes] but also closes the stream. */ @@ -165,12 +188,6 @@ fun InputStream.hash(): SecureHash { inline fun InputStream.readObject(): T = readFully().deserialize() -object NullOutputStream : OutputStream() { - override fun write(b: Int) = Unit - override fun write(b: ByteArray) = Unit - override fun write(b: ByteArray, off: Int, len: Int) = Unit -} - fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + "…" /** Return the sum of an Iterable of [BigDecimal]s. */ @@ -532,11 +549,6 @@ fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } val PublicKey.hash: SecureHash get() = Crypto.encodePublicKey(this).sha256() -/** - * Extension method for providing a sumBy method that processes and returns a Long - */ -fun Iterable.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum() - fun SerializedBytes.checkPayloadIs(type: Class): UntrustworthyData { val payloadData: T = try { val serializer = SerializationDefaults.SERIALIZATION_FACTORY @@ -563,6 +575,10 @@ fun MutableMap.toSynchronised(): MutableMap = Collections.syn /** @see Collections.synchronizedSet */ fun MutableSet.toSynchronised(): MutableSet = Collections.synchronizedSet(this) +fun Collection<*>.equivalent(other: Collection<*>): Boolean { + return this.size == other.size && this.containsAll(other) && other.containsAll(this) +} + /** * List implementation that applies the expensive [transform] function only when the element is accessed and caches calculated values. * Size is very cheap as it doesn't call [transform]. diff --git a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt index 82c1c35b66..d192557345 100644 --- a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt +++ b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt @@ -9,18 +9,24 @@ import com.github.benmanes.caffeine.cache.LoadingCache * Allow extra functionality to be injected to our caches. */ interface NamedCacheFactory { + companion object { + private val allowedChars = Regex("""^[0-9A-Za-z_.]*$""") + } + /** * Restrict the allowed characters of a cache name - this ensures that each cache has a name, and that * the name can be used to create a file name or a metric name. */ fun checkCacheName(name: String) { - require(!name.isBlank()){"Name must not be empty or only whitespace"} - require(allowedChars.matches(name)){"Invalid characters in cache name"} + require(name.isNotBlank()) { "Name must not be empty or only whitespace" } + require(allowedChars.matches(name)) { "Invalid characters in cache name" } } + fun buildNamed(name: String): Cache = buildNamed(Caffeine.newBuilder(), name) + fun buildNamed(caffeine: Caffeine, name: String): Cache + fun buildNamed(name: String, loader: CacheLoader): LoadingCache = buildNamed(Caffeine.newBuilder(), name, loader) + fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache } - -private val allowedChars = Regex("^[0-9A-Za-z_.]*\$") diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index bd7c1142ac..84ba452deb 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -6,19 +6,15 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.notary.NotaryService -import net.corda.core.node.ServiceHub +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.node.StatesToRecord -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.transactions.SignedTransaction import java.util.concurrent.ExecutorService // TODO: This should really be called ServiceHubInternal but that name is already taken by net.corda.node.services.api.ServiceHubInternal. -interface ServiceHubCoreInternal : ServiceHub { - +interface ServiceHubCoreInternal : VerifyingServiceHub { val externalOperationExecutor: ExecutorService - val attachmentTrustCalculator: AttachmentTrustCalculator - /** * Optional `NotaryService` which will be `null` for all non-Notary nodes. */ @@ -26,8 +22,6 @@ interface ServiceHubCoreInternal : ServiceHub { fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver - val attachmentsClassLoaderCache: AttachmentsClassLoaderCache - /** * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage, * inclusive of flow recovery metadata. 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 41e94a236a..a6aa9c2ac4 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -172,11 +172,13 @@ fun createComponentGroups(inputs: List, return componentGroupMap } +typealias SerializedTransactionState = SerializedBytes> + /** * A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef). * The [serializedState] is the actual component from the original wire transaction. */ -data class SerializedStateAndRef(val serializedState: SerializedBytes>, val ref: StateRef) { +data class SerializedStateAndRef(val serializedState: SerializedTransactionState, val ref: StateRef) { fun toStateAndRef(factory: SerializationFactory, context: SerializationContext) = StateAndRef(serializedState.deserialize(factory, context), ref) fun toStateAndRef(): StateAndRef { val factory = SerializationFactory.defaultFactory diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt new file mode 100644 index 0000000000..5b94f2571a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt @@ -0,0 +1,13 @@ +package net.corda.core.internal.cordapp + +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappProvider +import net.corda.core.flows.FlowLogic +import net.corda.core.node.services.AttachmentId + +interface CordappProviderInternal : CordappProvider { + val appClassLoader: ClassLoader + val cordapps: List + fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? + fun fixupAttachmentIds(attachmentIds: Collection): Set +} diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt b/core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt new file mode 100644 index 0000000000..4e20a46d41 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt @@ -0,0 +1,79 @@ +package net.corda.core.internal.verification + +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.mapNotNull +import net.corda.core.node.services.AttachmentFixup +import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.AttachmentStorage +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.loggerFor +import java.net.JarURLConnection +import java.net.URL + +class AttachmentFixups { + private val fixupRules = ArrayList() + + /** + * Loads the "fixup" rules from all META-INF/Corda-Fixups files. + * These files have the following format: + * ,...=>,,... + * where each is the SHA256 of a CorDapp JAR that [TransactionBuilder] will expect to find inside [AttachmentStorage]. + * + * These rules are for repairing broken CorDapps. A correctly written CorDapp should not require them. + */ + fun load(appClassLoader: ClassLoader) { + for (url in appClassLoader.resources("META-INF/Corda-Fixups")) { + val connection = toValidFixupResource(url) ?: continue + connection.inputStream.bufferedReader().lines().use { lines -> + lines.mapNotNull(::cleanLine).forEach { line -> + val tokens = line.split("=>") + require(tokens.size == 2) { + "Invalid fix-up line '$line' in '${connection.jarFile.name}'" + } + val sourceIds = parseIds(tokens[0]) + require(sourceIds.isNotEmpty()) { + "Forbidden empty list of source attachment IDs in '${connection.jarFile.name}'" + } + val targetIds = parseIds(tokens[1]) + fixupRules += AttachmentFixup(sourceIds, targetIds) + } + } + } + } + + private fun toValidFixupResource(url: URL): JarURLConnection? { + val connection = url.openConnection() as? JarURLConnection ?: return null + val isValid = connection.jarFile.stream().allMatch { it.name.startsWith("META-INF/") } + if (!isValid) { + loggerFor().warn("FixUp '{}' contains files outside META-INF/ - IGNORING!", connection.jarFile.name) + return null + } + return connection + } + + private fun cleanLine(line: String): String? = line.substringBefore('#').trim().takeIf(String::isNotEmpty) + + private fun parseIds(ids: String): Set { + return ids.splitToSequence(",") + .map(String::trim) + .filterNot(String::isEmpty) + .mapTo(LinkedHashSet(), SecureHash.Companion::create) + } + + /** + * Apply this node's attachment fix-up rules to the given attachment IDs. + * + * @param attachmentIds A collection of [AttachmentId]s, e.g. as provided by a transaction. + * @return The [attachmentIds] with the fix-up rules applied. + */ + fun fixupAttachmentIds(attachmentIds: Collection): Set { + val replacementIds = LinkedHashSet(attachmentIds) + for ((sourceIds, targetIds) in fixupRules) { + if (replacementIds.containsAll(sourceIds)) { + replacementIds.removeAll(sourceIds) + replacementIds.addAll(targetIds) + } + } + return replacementIds + } +} diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt new file mode 100644 index 0000000000..5d1ea265bb --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt @@ -0,0 +1,50 @@ +package net.corda.core.internal.verification + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.defaultVerifier +import java.security.PublicKey + +/** + * Represents the operations required to resolve and verify a transaction. + */ +interface VerificationSupport { + val isResolutionLazy: Boolean get() = true + + val appClassLoader: ClassLoader + + val attachmentsClassLoaderCache: AttachmentsClassLoaderCache? get() = null + + // TODO Use SequencedCollection if upgraded to Java 21 + fun getParties(keys: Collection): List + + fun getAttachment(id: SecureHash): Attachment? + + // TODO Use SequencedCollection if upgraded to Java 21 + fun getAttachments(ids: Collection): List = ids.map(::getAttachment) + + fun isAttachmentTrusted(attachment: Attachment): Boolean + + fun getTrustedClassAttachment(className: String): Attachment? + + fun getNetworkParameters(id: SecureHash?): NetworkParameters? + + fun getSerializedState(stateRef: StateRef): SerializedTransactionState + + fun getStateAndRef(stateRef: StateRef): StateAndRef<*> = StateAndRef(getSerializedState(stateRef).deserialize(), stateRef) + + fun fixupAttachmentIds(attachmentIds: Collection): Set + + fun createVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { + return defaultVerifier(ltx, serializationContext) + } +} diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt similarity index 95% rename from core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt rename to core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt index 840163238f..f1e55acc80 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt @@ -1,7 +1,5 @@ -package net.corda.core.internal +package net.corda.core.internal.verification -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.Attachment import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName @@ -30,21 +28,26 @@ import net.corda.core.contracts.TransactionVerificationException.TransactionNota import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash +import net.corda.core.internal.AttachmentWithContext +import net.corda.core.internal.MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT +import net.corda.core.internal.PlatformVersionSwitches +import net.corda.core.internal.canBeTransitionedFrom +import net.corda.core.internal.checkConstraintValidity +import net.corda.core.internal.checkMinimumPlatformVersion +import net.corda.core.internal.checkNotaryWhitelisted +import net.corda.core.internal.checkSupportedHashType +import net.corda.core.internal.contractHasAutomaticConstraintPropagation +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.mapToSet +import net.corda.core.internal.requiredContractClassName import net.corda.core.internal.rules.StateContractValidationEnforcementRule +import net.corda.core.internal.warnContractWithoutConstraintPropagation +import net.corda.core.internal.warnOnce import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor import java.util.function.Function import java.util.function.Supplier -interface TransactionVerifierServiceInternal { - fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> -} - -/** - * Defined here for visibility reasons. - */ -fun LedgerTransaction.prepareVerify(attachments: List) = internalPrepareVerify(attachments) - interface Verifier { /** @@ -142,10 +145,12 @@ private class Validator(private val ltx: LedgerTransaction, private val transact */ @Suppress("ThrowsCount") private fun getUniqueContractAttachmentsByContract(): Map { - val contractClasses = allStates.mapTo(LinkedHashSet(), TransactionState<*>::contract) + val contractClasses = allStates.mapToSet { it.contract } // Check that there are no duplicate attachments added. - if (ltx.attachments.size != ltx.attachments.toSet().size) throw DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first()) + if (ltx.attachments.size != ltx.attachments.toSet().size) { + throw DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first()) + } // For each attachment this finds all the relevant state contracts that it provides. // And then maps them to the attachment. @@ -393,7 +398,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact @Suppress("NestedBlockDepth", "MagicNumber") private fun verifyConstraints(contractAttachmentsByContract: Map) { // For each contract/constraint pair check that the relevant attachment is valid. - allStates.mapTo(LinkedHashSet()) { it.contract to it.constraint }.forEach { (contract, constraint) -> + allStates.mapToSet { it.contract to it.constraint }.forEach { (contract, constraint) -> if (constraint is SignatureAttachmentConstraint) { /** * Support for signature constraints has been added on @@ -440,7 +445,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun // Loads the contract class from the transactionClassLoader. private fun createContractClass(id: SecureHash, contractClassName: ContractClassName): Class { return try { - Class.forName(contractClassName, false, transactionClassLoader).asSubclass(Contract::class.java) + loadClassOfType(contractClassName, false, transactionClassLoader) } catch (e: Exception) { throw ContractCreationError(id, contractClassName, e) } @@ -448,7 +453,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun private fun generateContracts(ltx: LedgerTransaction): List { return (ltx.inputs.map(StateAndRef::state) + ltx.outputs) - .mapTo(LinkedHashSet(), TransactionState<*>::contract) + .mapToSet { it.contract } .map { contractClassName -> createContractClass(ltx.id, contractClassName) }.map { contractClass -> diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt new file mode 100644 index 0000000000..eba81ca2dc --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt @@ -0,0 +1,221 @@ +package net.corda.core.internal.verification + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.AttachmentTrustCalculator +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.TRUSTED_UPLOADERS +import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.node.NetworkParameters +import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.vault.AttachmentQueryCriteria +import net.corda.core.node.services.vault.AttachmentSort +import net.corda.core.node.services.vault.AttachmentSort.AttachmentSortAttribute +import net.corda.core.node.services.vault.Builder +import net.corda.core.node.services.vault.Sort +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder +import net.corda.core.serialization.serialize +import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.ContractUpgradeWireTransaction.Companion.calculateUpgradedState +import net.corda.core.transactions.MissingContractAttachments +import net.corda.core.transactions.NotaryChangeLedgerTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import java.security.PublicKey +import java.util.jar.JarInputStream + +@Suppress("TooManyFunctions", "ThrowsCount") +interface VerifyingServiceHub : ServiceHub, VerificationSupport { + override val cordappProvider: CordappProviderInternal + + val attachmentTrustCalculator: AttachmentTrustCalculator + + override val appClassLoader: ClassLoader get() = cordappProvider.appClassLoader + + override fun loadContractAttachment(stateRef: StateRef): Attachment { + // We may need to recursively chase transactions if there are notary changes. + return loadContractAttachment(stateRef, null) + } + + private fun loadContractAttachment(stateRef: StateRef, forContractClassName: String?): Attachment { + val stx = getRequiredTransaction(stateRef.txhash) + return when (val ctx = stx.coreTransaction) { + is WireTransaction -> { + val contractClassName = forContractClassName ?: ctx.outRef(stateRef.index).state.contract + ctx.attachments + .asSequence() + .mapNotNull { id -> loadAttachmentContainingContract(id, contractClassName) } + .firstOrNull() ?: throw AttachmentResolutionException(stateRef.txhash) + } + is ContractUpgradeWireTransaction -> { + attachments.openAttachment(ctx.upgradedContractAttachmentId) ?: throw AttachmentResolutionException(stateRef.txhash) + } + is NotaryChangeWireTransaction -> { + val transactionState = getSerializedState(stateRef).deserialize() + val input = ctx.inputs.firstOrNull() ?: throw AttachmentResolutionException(stateRef.txhash) + loadContractAttachment(input, transactionState.contract) + } + else -> throw UnsupportedOperationException("Attempting to resolve attachment for index ${stateRef.index} of a " + + "${ctx.javaClass} transaction. This is not supported.") + } + } + + private fun loadAttachmentContainingContract(id: SecureHash, contractClassName: String): Attachment? { + return attachments.openAttachment(id)?.takeIf { it is ContractAttachment && contractClassName in it.allContracts } + } + + override fun loadState(stateRef: StateRef): TransactionState<*> = getSerializedState(stateRef).deserialize() + + override fun loadStates(stateRefs: Set): Set> = loadStatesInternal(stateRefs, LinkedHashSet()) + + fun >> loadStatesInternal(input: Iterable, output: C): C { + return input.mapTo(output, ::toStateAndRef) + } + + // TODO Bulk party lookup? + override fun getParties(keys: Collection): List = keys.map(identityService::partyFromKey) + + override fun getAttachment(id: SecureHash): Attachment? = attachments.openAttachment(id) + + override fun getNetworkParameters(id: SecureHash?): NetworkParameters? { + return networkParametersService.lookup(id ?: networkParametersService.defaultHash) + } + + /** + * This is the main logic that knows how to retrieve the binary representation of [StateRef]s. + * + * For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the + * correct classloader independent of the node's classpath. + */ + override fun getSerializedState(stateRef: StateRef): SerializedTransactionState { + val coreTransaction = getRequiredTransaction(stateRef.txhash).coreTransaction + return when (coreTransaction) { + is WireTransaction -> getRegularOutput(coreTransaction, stateRef.index) + is ContractUpgradeWireTransaction -> getContractUpdateOutput(coreTransaction, stateRef.index) + is NotaryChangeWireTransaction -> getNotaryChangeOutput(coreTransaction, stateRef.index) + else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} " + + "transaction. This is not supported.") + } + } + + private fun getRegularOutput(coreTransaction: WireTransaction, outputIndex: Int): SerializedTransactionState { + @Suppress("UNCHECKED_CAST") + return coreTransaction.componentGroups + .first { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal } + .components[outputIndex] as SerializedTransactionState + } + + /** + * Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction. + */ + private fun getContractUpdateOutput(wtx: ContractUpgradeWireTransaction, outputIndex: Int): SerializedTransactionState { + val binaryInput = getSerializedState(wtx.inputs[outputIndex]) + val legacyContractAttachment = getAttachment(wtx.legacyContractAttachmentId) ?: throw MissingContractAttachments(emptyList()) + val upgradedContractAttachment = getAttachment(wtx.upgradedContractAttachmentId) ?: throw MissingContractAttachments(emptyList()) + val networkParameters = getNetworkParameters(wtx.networkParametersHash) ?: throw TransactionResolutionException(wtx.id) + + return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + listOf(legacyContractAttachment, upgradedContractAttachment), + networkParameters, + wtx.id, + ::isAttachmentTrusted, + attachmentsClassLoaderCache = attachmentsClassLoaderCache + ) { serializationContext -> + val upgradedContract = loadUpgradedContract(wtx.upgradedContractClassName, wtx.id, serializationContext.deserializationClassLoader) + val outputState = calculateUpgradedState(binaryInput.deserialize(), upgradedContract, upgradedContractAttachment) + outputState.serialize() + } + } + + /** + * This should return a serialized virtual output state, that will be used to verify spending transactions. + * The binary output should not depend on the classpath of the node that is verifying the transaction. + * + * Ideally the serialization engine would support partial deserialization so that only the Notary ( and the encumbrance can be replaced + * from the binary input state) + */ + // TODO - currently this uses the main classloader. + private fun getNotaryChangeOutput(wtx: NotaryChangeWireTransaction, outputIndex: Int): SerializedTransactionState { + val input = getStateAndRef(wtx.inputs[outputIndex]) + val output = NotaryChangeLedgerTransaction.computeOutput(input, wtx.newNotary) { wtx.inputs } + return output.serialize() + } + + /** + * Scans trusted (installed locally) attachments to find all that contain the [className]. + * This is required as a workaround until explicit cordapp dependencies are implemented. + * + * @return the attachments with the highest version. + */ + // TODO Should throw when the class is found in multiple contract attachments (not different versions). + override fun getTrustedClassAttachment(className: String): Attachment? { + val allTrusted = attachments.queryAttachments( + AttachmentQueryCriteria.AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)), + AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) + ) + + // TODO - add caching if performance is affected. + for (attId in allTrusted) { + val attch = attachments.openAttachment(attId)!! + if (attch.openAsJAR().use { hasFile(it, "$className.class") }) return attch + } + return null + } + + private fun hasFile(jarStream: JarInputStream, className: String): Boolean { + while (true) { + val e = jarStream.nextJarEntry ?: return false + if (e.name == className) { + return true + } + } + } + + override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachmentTrustCalculator.calculate(attachment) + + override fun fixupAttachmentIds(attachmentIds: Collection): Set { + return cordappProvider.fixupAttachmentIds(attachmentIds) + } + + /** + * Try to verify the given transaction on the external verifier, assuming it is available. It is not required to verify externally even + * if the verifier is available. + * + * The default implementation is to only do internal verification. + * + * @return true if the transaction should (also) be verified internally, regardless of whether it was verified externally. + */ + fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + return true + } +} + +fun ServicesForResolution.toVerifyingServiceHub(): VerifyingServiceHub { + if (this is VerifyingServiceHub) { + return this + } + // All ServicesForResolution instances should also implement VerifyingServiceHub, which is something we can enforce with the + // @DoNotImplement annotation. The only exception however is MockServices, which does not since it's public API and VerifyingServiceHub + // is internal. Instead, MockServices has a private VerifyingServiceHub "view" which we get at via reflection. + var clazz: Class<*> = javaClass + while (true) { + if (clazz.name == "net.corda.testing.node.MockServices") { + return clazz.getDeclaredMethod("getVerifyingView").apply { isAccessible = true }.invoke(this) as VerifyingServiceHub + } + clazz = clazz.superclass ?: throw ClassCastException("${javaClass.name} is not a VerifyingServiceHub") + } +} diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 2f8c097804..2bf4bcdfcd 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -11,6 +11,7 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.internal.PlatformVersionSwitches.TWO_PHASE_FINALITY import net.corda.core.internal.telemetry.TelemetryComponent +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.* import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.serialization.CordaSerializable @@ -170,12 +171,6 @@ interface ServiceHub : ServicesForResolution { */ val telemetryService: TelemetryService - /** - * INTERNAL. DO NOT USE. - * @suppress - */ - val transactionVerifierService: TransactionVerifierService - /** * A [Clock] representing the node's current time. This should be used in preference to directly accessing the * clock so the current time can be controlled during unit testing. @@ -283,8 +278,7 @@ interface ServiceHub : ServicesForResolution { */ @Throws(TransactionResolutionException::class) fun toStateAndRef(stateRef: StateRef): StateAndRef { - val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return stx.resolveBaseTransaction(this).outRef(stateRef.index) + return StateAndRef(uncheckedCast(loadState(stateRef)), stateRef) } private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey @@ -424,8 +418,8 @@ interface ServiceHub : ServicesForResolution { * When used within a flow, this session automatically forms part of the enclosing flow transaction boundary, * and thus queryable data will include everything committed as of the last checkpoint. * - * We want to make sure users have a restricted access to administrative functions, this function will return a [RestrictedConnection] instance. - * The following methods are blocked: + * We want to make sure users have a restricted access to administrative functions, this function will return a [Connection] instance + * with the following methods blocked: * - abort(executor: Executor?) * - clearWarnings() * - close() diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt deleted file mode 100644 index d72eec72f2..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.DoNotImplement -import net.corda.core.concurrent.CordaFuture -import net.corda.core.transactions.LedgerTransaction - -/** - * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. - * @suppress - */ -@DoNotImplement -interface TransactionVerifierService { - /** - * @param transaction The transaction to be verified. - * @return A future that completes successfully if the transaction verified, or sets an exception the verifier threw. - */ - fun verify(transaction: LedgerTransaction): CordaFuture<*> -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 145da6a07c..f26ccd00ad 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -1,28 +1,44 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.HashAttachmentConstraint +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.contracts.UpgradedContract +import net.corda.core.contracts.UpgradedContractWithLegacyConstraint +import net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext -import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.combinedHash +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization -import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder -import net.corda.core.serialization.serialize import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent -import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract -import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.retrieveAppClassLoader import net.corda.core.transactions.ContractUpgradeWireTransaction.Companion.calculateUpgradedState -import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.* -import net.corda.core.transactions.WireTransaction.Companion.resolveStateRefBinaryComponent +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.INPUTS +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.LEGACY_ATTACHMENT +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.NOTARY +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.PARAMETERS_HASH +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_ATTACHMENT +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_CONTRACT import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import java.security.PublicKey @@ -52,7 +68,10 @@ data class ContractUpgradeWireTransaction( * Runs the explicit upgrade logic. */ @CordaInternal - internal fun calculateUpgradedState(state: TransactionState, upgradedContract: UpgradedContract, upgradedContractAttachment: Attachment): TransactionState { + @JvmSynthetic + internal fun calculateUpgradedState(state: TransactionState, + upgradedContract: UpgradedContract, + upgradedContractAttachment: Attachment): TransactionState { // TODO: if there are encumbrance states in the inputs, just copy them across without modifying val upgradedState: S = upgradedContract.upgrade(state.data) val inputConstraint = state.constraint @@ -121,60 +140,12 @@ data class ContractUpgradeWireTransaction( /** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */ fun resolve(services: ServicesForResolution, sigs: List): ContractUpgradeLedgerTransaction { - val resolvedInputs = services.loadStates(inputs.toSet()).toList() - val legacyContractAttachment = services.attachments.openAttachment(legacyContractAttachmentId) - ?: throw AttachmentResolutionException(legacyContractAttachmentId) - val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) - ?: throw AttachmentResolutionException(upgradedContractAttachmentId) - val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash - val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve) ?: throw TransactionResolutionException(id) - return ContractUpgradeLedgerTransaction.create( - resolvedInputs, - notary, - legacyContractAttachment, - upgradedContractAttachment, - id, - privacySalt, - sigs, - resolvedNetworkParameters, - loadUpgradedContract(upgradedContractClassName, retrieveAppClassLoader(services)) - ) - } - - private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract = try { - @Suppress("UNCHECKED_CAST") - Class.forName(className, false, classLoader).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract - } catch (e: Exception) { - throw TransactionVerificationException.ContractCreationError(id, className, e) - } - - /** - * Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction. - */ - @CordaInternal - internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef, params: NetworkParameters): SerializedBytes> { - val binaryInput: SerializedBytes> = resolveStateRefBinaryComponent(inputs[stateRef.index], services)!! - val legacyAttachment = services.attachments.openAttachment(legacyContractAttachmentId) - ?: throw MissingContractAttachments(emptyList()) - val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) - ?: throw MissingContractAttachments(emptyList()) - - return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( - listOf(legacyAttachment, upgradedAttachment), - params, - id, - { (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) }, - attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { serializationContext -> - val resolvedInput = binaryInput.deserialize() - val upgradedContract = upgradedContract(upgradedContractClassName, serializationContext.deserializationClassLoader) - val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment) - outputState.serialize() - } + return ContractUpgradeLedgerTransaction.resolve(services.toVerifyingServiceHub(), this, sigs) } /** Constructs a filtered transaction: the inputs, the notary party and network parameters hash are always visible, while the rest are hidden. */ fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction { - val totalComponents = (0 until serializedComponents.size).toSet() + val totalComponents = serializedComponents.indices.toSet() val visibleComponents = mapOf( INPUTS.ordinal to FilteredComponent(serializedComponents[INPUTS.ordinal], nonces[INPUTS.ordinal]), NOTARY.ordinal to FilteredComponent(serializedComponents[NOTARY.ordinal], nonces[NOTARY.ordinal]), @@ -287,39 +258,47 @@ private constructor( get() = upgradedContract::class.java.name companion object { - @CordaInternal - internal fun create( - inputs: List>, - notary: Party, - legacyContractAttachment: Attachment, - upgradedContractAttachment: Attachment, - id: SecureHash, - privacySalt: PrivacySalt, - sigs: List, - networkParameters: NetworkParameters, - upgradedContract: UpgradedContract - ): ContractUpgradeLedgerTransaction { - return ContractUpgradeLedgerTransaction(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, upgradedContract) + @JvmSynthetic + @Suppress("ThrowsCount") + internal fun resolve(verificationSupport: VerificationSupport, + wtx: ContractUpgradeWireTransaction, + sigs: List): ContractUpgradeLedgerTransaction { + val inputs = wtx.inputs.map(verificationSupport::getStateAndRef) + val (legacyContractAttachment, upgradedContractAttachment) = verificationSupport.getAttachments(listOf( + wtx.legacyContractAttachmentId, + wtx.upgradedContractAttachmentId + )) + val networkParameters = verificationSupport.getNetworkParameters(wtx.networkParametersHash) + ?: throw TransactionResolutionException(wtx.id) + val upgradedContract = loadUpgradedContract(wtx.upgradedContractClassName, wtx.id, verificationSupport.appClassLoader) + return ContractUpgradeLedgerTransaction( + inputs, + wtx.notary, + legacyContractAttachment ?: throw AttachmentResolutionException(wtx.legacyContractAttachmentId), + upgradedContractAttachment ?: throw AttachmentResolutionException(wtx.upgradedContractAttachmentId), + wtx.id, + wtx.privacySalt, + sigs, + networkParameters, + upgradedContract + ) } - // TODO - this has to use a classloader created from the upgraded attachment. + // TODO There is an inconsistency with the class loader used with this method. Transaction resolution uses the app class loader, + // whilst TransactionStorageVerification.getContractUpdateOutput uses an attachments class loder comprised of the the legacy and + // upgraded attachments @CordaInternal - internal fun loadUpgradedContract(upgradedContractClassName: ContractClassName, classLoader: ClassLoader): UpgradedContract { - @Suppress("UNCHECKED_CAST") - return Class.forName(upgradedContractClassName, false, classLoader) - .asSubclass(Contract::class.java) - .getConstructor() - .newInstance() as UpgradedContract - } - - // This is a "hack" to retrieve the CordappsClassloader from the services without having access to all classes. - @CordaInternal - internal fun retrieveAppClassLoader(services: ServicesForResolution): ClassLoader { - val cordappLoader = services.cordappProvider::class.java.getMethod("getCordappLoader").invoke(services.cordappProvider) - - @Suppress("UNCHECKED_CAST") - return cordappLoader::class.java.getMethod("getAppClassLoader").invoke(cordappLoader) as ClassLoader + @JvmSynthetic + @Suppress("TooGenericExceptionCaught") + internal fun loadUpgradedContract(className: ContractClassName, id: SecureHash, classLoader: ClassLoader): UpgradedContract { + return try { + loadClassOfType>(className, false, classLoader) + .getDeclaredConstructor() + .newInstance() + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, className, e) + } } } @@ -366,7 +345,7 @@ private constructor( /** The required signers are the set of all input states' participants. */ override val requiredSigningKeys: Set - get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey + get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey override fun getKeyDescriptions(keys: Set): List { return keys.map { it.toBase58String() } @@ -401,7 +380,7 @@ private constructor( privacySalt: PrivacySalt, sigs: List, networkParameters: NetworkParameters - ) : this(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, loadUpgradedContract(upgradedContractClassName, ContractUpgradeLedgerTransaction::class.java.classLoader)) + ) : this(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, loadUpgradedContract(upgradedContractClassName, id, ContractUpgradeLedgerTransaction::class.java.classLoader)) @Deprecated("ContractUpgradeLedgerTransaction should not be created directly, use ContractUpgradeWireTransaction.resolve instead.") fun copy( 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 846799d0b3..8037668b68 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -16,21 +16,21 @@ import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party -import net.corda.core.internal.AbstractVerifier import net.corda.core.internal.SerializedStateAndRef -import net.corda.core.internal.Verifier import net.corda.core.internal.castIfPossible import net.corda.core.internal.deserialiseCommands import net.corda.core.internal.deserialiseComponentGroup import net.corda.core.internal.eagerDeserialise import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.AbstractVerifier +import net.corda.core.internal.verification.Verifier import net.corda.core.node.NetworkParameters import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.utilities.contextLogger import java.util.Collections.unmodifiableList import java.util.function.Predicate @@ -153,34 +153,35 @@ private constructor( serializedInputs: List? = null, serializedReferences: List? = null, isAttachmentTrusted: (Attachment) -> Boolean, + verifierFactory: (LedgerTransaction, SerializationContext) -> Verifier, attachmentsClassLoaderCache: AttachmentsClassLoaderCache?, digestService: DigestService ): LedgerTransaction { return LedgerTransaction( - inputs = inputs, - outputs = outputs, - commands = commands, - attachments = attachments, - id = id, - notary = notary, - timeWindow = timeWindow, - privacySalt = privacySalt, - networkParameters = networkParameters, - references = references, - componentGroups = protectOrNull(componentGroups), - serializedInputs = protectOrNull(serializedInputs), - serializedReferences = protectOrNull(serializedReferences), - isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = ::BasicVerifier, - attachmentsClassLoaderCache = attachmentsClassLoaderCache, - digestService = digestService + inputs = inputs, + outputs = outputs, + commands = commands, + attachments = attachments, + id = id, + notary = notary, + timeWindow = timeWindow, + privacySalt = privacySalt, + networkParameters = networkParameters, + references = references, + componentGroups = protectOrNull(componentGroups), + serializedInputs = protectOrNull(serializedInputs), + serializedReferences = protectOrNull(serializedReferences), + isAttachmentTrusted = isAttachmentTrusted, + verifierFactory = verifierFactory, + attachmentsClassLoaderCache = attachmentsClassLoaderCache, + digestService = digestService ) } /** * This factory function will create an instance of [LedgerTransaction] * that will be used for contract verification. - * @see BasicVerifier + * @see DefaultVerifier */ @CordaInternal fun createForContractVerify( @@ -243,26 +244,26 @@ private constructor( */ @Throws(TransactionVerificationException::class) fun verify() { - internalPrepareVerify(attachments).verify() + verifyInternal() } /** * This method has to be called in a context where it has access to the database. */ @CordaInternal - internal fun internalPrepareVerify(txAttachments: List): Verifier { + @JvmSynthetic + internal fun verifyInternal(txAttachments: List = this.attachments) { // Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules // like no-overlap, package namespace ownership and (in future) deterministic Java. - return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + val verifier = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( txAttachments, getParamsWithGoo(), id, - isAttachmentTrusted = isAttachmentTrusted, - attachmentsClassLoaderCache = attachmentsClassLoaderCache) { serializationContext -> - + isAttachmentTrusted, + attachmentsClassLoaderCache = attachmentsClassLoaderCache + ) { serializationContext -> // Legacy check - warns if the LedgerTransaction was created incorrectly. checkLtxForVerification() - // Create a copy of the outer LedgerTransaction which deserializes all fields using // the serialization context (or its deserializationClassloader). // Only the copy will be used for verification, and the outer shell will be discarded. @@ -270,6 +271,7 @@ private constructor( // NOTE: The Verifier creates the copies of the LedgerTransaction object now. verifierFactory(this, serializationContext) } + verifier.verify() } /** @@ -463,7 +465,7 @@ private constructor( } inline fun filterInputs(crossinline predicate: (T) -> Boolean): List { - return filterInputs(T::class.java, Predicate { predicate(it) }) + return filterInputs(T::class.java) { predicate(it) } } /** @@ -479,7 +481,7 @@ private constructor( } inline fun filterReferenceInputs(crossinline predicate: (T) -> Boolean): List { - return filterReferenceInputs(T::class.java, Predicate { predicate(it) }) + return filterReferenceInputs(T::class.java) { predicate(it) } } /** @@ -495,7 +497,7 @@ private constructor( } inline fun filterInRefs(crossinline predicate: (T) -> Boolean): List> { - return filterInRefs(T::class.java, Predicate { predicate(it) }) + return filterInRefs(T::class.java) { predicate(it) } } /** @@ -511,7 +513,7 @@ private constructor( } inline fun filterReferenceInputRefs(crossinline predicate: (T) -> Boolean): List> { - return filterReferenceInputRefs(T::class.java, Predicate { predicate(it) }) + return filterReferenceInputRefs(T::class.java) { predicate(it) } } /** @@ -528,7 +530,7 @@ private constructor( } inline fun findInput(crossinline predicate: (T) -> Boolean): T { - return findInput(T::class.java, Predicate { predicate(it) }) + return findInput(T::class.java) { predicate(it) } } /** @@ -541,11 +543,11 @@ private constructor( * @throws IllegalArgumentException if no item, or multiple items are found matching the requirements. */ fun findReference(clazz: Class, predicate: Predicate): T { - return referenceInputsOfType(clazz).single { predicate.test(it) } + return referenceInputsOfType(clazz).single(predicate::test) } inline fun findReference(crossinline predicate: (T) -> Boolean): T { - return findReference(T::class.java, Predicate { predicate(it) }) + return findReference(T::class.java) { predicate(it) } } /** @@ -562,7 +564,7 @@ private constructor( } inline fun findInRef(crossinline predicate: (T) -> Boolean): StateAndRef { - return findInRef(T::class.java, Predicate { predicate(it) }) + return findInRef(T::class.java) { predicate(it) } } /** @@ -579,7 +581,7 @@ private constructor( } inline fun findReferenceInputRef(crossinline predicate: (T) -> Boolean): StateAndRef { - return findReferenceInputRef(T::class.java, Predicate { predicate(it) }) + return findReferenceInputRef(T::class.java) { predicate(it) } } /** @@ -614,7 +616,7 @@ private constructor( } inline fun filterCommands(crossinline predicate: (T) -> Boolean): List> { - return filterCommands(T::class.java, Predicate { predicate(it) }) + return filterCommands(T::class.java) { predicate(it) } } /** @@ -631,7 +633,7 @@ private constructor( } inline fun findCommand(crossinline predicate: (T) -> Boolean): Command { - return findCommand(T::class.java, Predicate { predicate(it) }) + return findCommand(T::class.java) { predicate(it) } } /** @@ -706,7 +708,7 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = Attachment::isUploaderTrusted, - verifierFactory = ::BasicVerifier, + verifierFactory = ::DefaultVerifier, attachmentsClassLoaderCache = null ) @@ -736,7 +738,7 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = Attachment::isUploaderTrusted, - verifierFactory = ::BasicVerifier, + verifierFactory = ::DefaultVerifier, attachmentsClassLoaderCache = null ) @@ -804,14 +806,19 @@ private constructor( } } +@CordaInternal +@JvmSynthetic +fun defaultVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { + return DefaultVerifier(ltx, serializationContext) +} + /** * This is the default [Verifier] that configures Corda * to execute [Contract.verify(LedgerTransaction)]. * * THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE! */ -@CordaInternal -private class BasicVerifier( +private class DefaultVerifier( ltx: LedgerTransaction, private val serializationContext: SerializationContext ) : AbstractVerifier(ltx, serializationContext.deserializationClassLoader) { @@ -874,7 +881,6 @@ private class BasicVerifier( * THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE! */ @Suppress("unused_parameter") -@CordaInternal private class NoOpVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext) : Verifier { // Invoking LedgerTransaction.verify() from Contract.verify(LedgerTransaction) // will execute this function. But why would anyone do that?! diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index ab42260f37..cf5db15911 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -1,20 +1,31 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party +import net.corda.core.internal.indexOfOrThrow +import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization -import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.NotaryChangeWireTransaction.Component.* +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.INPUTS +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NEW_NOTARY +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NOTARY +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.PARAMETERS_HASH import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import java.security.PublicKey @@ -88,32 +99,12 @@ data class NotaryChangeWireTransaction( /** Resolves input states and network parameters and builds a [NotaryChangeLedgerTransaction]. */ fun resolve(services: ServicesForResolution, sigs: List): NotaryChangeLedgerTransaction { - val resolvedInputs = services.loadStates(inputs.toSet()).toList() - val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash - val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve) - ?: throw TransactionResolutionException(id) - return NotaryChangeLedgerTransaction.create(resolvedInputs, notary, newNotary, id, sigs, resolvedNetworkParameters) + return NotaryChangeLedgerTransaction.resolve(services.toVerifyingServiceHub(), this, sigs) } /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ - fun resolve(services: ServiceHub, sigs: List) = resolve(services as ServicesForResolution, sigs) - - /** - * This should return a serialized virtual output state, that will be used to verify spending transactions. - * The binary output should not depend on the classpath of the node that is verifying the transaction. - * - * Ideally the serialization engine would support partial deserialization so that only the Notary ( and the encumbrance can be replaced from the binary input state) - * - * - * TODO - currently this uses the main classloader. - */ - @CordaInternal - internal fun resolveOutputComponent( - services: ServicesForResolution, - stateRef: StateRef, - @Suppress("UNUSED_PARAMETER") params: NetworkParameters - ): SerializedBytes> { - return services.loadState(stateRef).serialize() + fun resolve(services: ServiceHub, sigs: List): NotaryChangeLedgerTransaction { + return resolve(services as ServicesForResolution, sigs) } enum class Component { @@ -140,13 +131,25 @@ private constructor( ) : FullTransaction(), TransactionWithSignatures { companion object { @CordaInternal - internal fun create(inputs: List>, - notary: Party, - newNotary: Party, - id: SecureHash, - sigs: List, - networkParameters: NetworkParameters): NotaryChangeLedgerTransaction { - return NotaryChangeLedgerTransaction(inputs, notary, newNotary, id, sigs, networkParameters) + @JvmSynthetic + internal fun resolve(verificationSupport: VerificationSupport, + wireTx: NotaryChangeWireTransaction, + sigs: List): NotaryChangeLedgerTransaction { + val inputs = wireTx.inputs.map(verificationSupport::getStateAndRef) + val networkParameters = verificationSupport.getNetworkParameters(wireTx.networkParametersHash) + ?: throw TransactionResolutionException(wireTx.id) + return NotaryChangeLedgerTransaction(inputs, wireTx.notary, wireTx.newNotary, wireTx.id, sigs, networkParameters) + } + + @CordaInternal + @JvmSynthetic + internal inline fun computeOutput(input: StateAndRef<*>, newNotary: Party, inputs: () -> List): TransactionState<*> { + val (state, ref) = input + val newEncumbrance = state.encumbrance?.let { + val encumbranceStateRef = ref.copy(index = state.encumbrance) + inputs().indexOfOrThrow(encumbranceStateRef) + } + return state.copy(notary = newNotary, encumbrance = newEncumbrance) } } @@ -174,22 +177,10 @@ private constructor( /** We compute the outputs on demand by applying the notary field modification to the inputs. */ override val outputs: List> - get() = computeOutputs() - - private fun computeOutputs(): List> { - val inputPositionIndex: Map = inputs.mapIndexed { index, stateAndRef -> stateAndRef.ref to index }.toMap() - return inputs.map { (state, ref) -> - if (state.encumbrance != null) { - val encumbranceStateRef = StateRef(ref.txhash, state.encumbrance) - val encumbrancePosition = inputPositionIndex[encumbranceStateRef] - ?: throw IllegalStateException("Unable to generate output states – transaction not constructed correctly.") - state.copy(notary = newNotary, encumbrance = encumbrancePosition) - } else state.copy(notary = newNotary) - } - } + get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef::ref) } } override val requiredSigningKeys: Set - get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey + get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey override fun getKeyDescriptions(keys: Set): List { return keys.map { it.toBase58String() } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 94e2079967..b1d6f91b6b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -1,27 +1,40 @@ package net.corda.core.transactions import net.corda.core.CordaException +import net.corda.core.CordaInternal import net.corda.core.CordaThrowable -import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.NamedByHash +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.sign +import net.corda.core.crypto.toStringShort import net.corda.core.identity.Party import net.corda.core.internal.TransactionDeserialisationException -import net.corda.core.internal.TransactionVerifierServiceInternal import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.equivalent +import net.corda.core.internal.isUploaderTrusted +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.MissingSerializerException import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.getOrThrow import java.io.NotSerializableException import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException -import java.util.* import java.util.function.Predicate /** @@ -142,6 +155,12 @@ data class SignedTransaction(val txBits: SerializedBytes, @JvmOverloads @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction { + // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. + resolveAndCheckNetworkParameters(services) + return toLedgerTransactionInternal(services.toVerifyingServiceHub(), checkSufficientSignatures) + } + + private fun toLedgerTransactionInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction { // TODO: We could probably optimise the below by // a) not throwing if threshold is eventually satisfied, but some of the rest of the signatures are failing. // b) omit verifying signatures when threshold requirement is met. @@ -154,9 +173,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } else { checkSignaturesAreValid() } - // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. - resolveAndCheckNetworkParameters(services) - return tx.toLedgerTransaction(services) + return tx.toLedgerTransactionInternal(verificationSupport) } /** @@ -173,10 +190,19 @@ data class SignedTransaction(val txBits: SerializedBytes, @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { resolveAndCheckNetworkParameters(services) + val verifyingServiceHub = services.toVerifyingServiceHub() + if (verifyingServiceHub.tryExternalVerification(this, checkSufficientSignatures)) { + verifyInternal(verifyingServiceHub, checkSufficientSignatures) + } + } + + @CordaInternal + @JvmSynthetic + fun verifyInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { when (coreTransaction) { - is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures) - is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures) - else -> verifyRegularTransaction(services, checkSufficientSignatures) + is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures) + is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures) + else -> verifyRegularTransaction(verificationSupport, checkSufficientSignatures) } } @@ -197,15 +223,15 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */ - private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { - val ntx = resolveNotaryChangeTransaction(services) + private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { + val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs) if (checkSufficientSignatures) ntx.verifyRequiredSignatures() else checkSignaturesAreValid() } /** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */ - private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) { - val ctx = resolveContractUpgradeTransaction(services) + private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { + val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs) if (checkSufficientSignatures) ctx.verifyRequiredSignatures() else checkSignaturesAreValid() } @@ -213,22 +239,21 @@ data class SignedTransaction(val txBits: SerializedBytes, // TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. - private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { - val ltx = toLedgerTransaction(services, checkSufficientSignatures) + private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { + val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures) try { - // TODO: allow non-blocking verification. - services.transactionVerifierService.verify(ltx).getOrThrow() + ltx.verify() } catch (e: NoClassDefFoundError) { checkReverifyAllowed(e) val missingClass = e.message ?: throw e log.warn("Transaction {} has missing class: {}", ltx.id, missingClass) - reverifyWithFixups(ltx, services, missingClass) + reverifyWithFixups(ltx, verificationSupport, missingClass) } catch (e: NotSerializableException) { checkReverifyAllowed(e) - retryVerification(e, e, ltx, services) + retryVerification(e, e, ltx, verificationSupport) } catch (e: TransactionDeserialisationException) { checkReverifyAllowed(e) - retryVerification(e.cause, e, ltx, services) + retryVerification(e.cause, e, ltx, verificationSupport) } } @@ -243,18 +268,18 @@ data class SignedTransaction(val txBits: SerializedBytes, } @Suppress("ThrowsCount") - private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, services: ServiceHub) { + private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, verificationSupport: VerificationSupport) { when (cause) { is MissingSerializerException -> { log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "", cause.typeNames) - reverifyWithFixups(ltx, services, null) + reverifyWithFixups(ltx, verificationSupport, null) } is NotSerializableException -> { val underlying = cause.cause if (underlying is ClassNotFoundException) { val missingClass = underlying.message?.replace('.', '/') ?: throw ex log.warn("Transaction {} has missing class: {}", ltx.id, missingClass) - reverifyWithFixups(ltx, services, missingClass) + reverifyWithFixups(ltx, verificationSupport, missingClass) } else { throw ex } @@ -266,15 +291,93 @@ data class SignedTransaction(val txBits: SerializedBytes, // Transactions created before Corda 4 can be missing dependencies on other CorDapps. // This code has detected a missing custom serializer - probably located inside a workflow CorDapp. // We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again. - private fun reverifyWithFixups(ltx: LedgerTransaction, services: ServiceHub, missingClass: String?) { + private fun reverifyWithFixups(ltx: LedgerTransaction, verificationSupport: VerificationSupport, missingClass: String?) { log.warn("""Detected that transaction $id does not contain all cordapp dependencies. |This may be the result of a bug in a previous version of Corda. |Attempting to re-verify having applied this node's fix-up rules. |Please check with the originator that this is a valid transaction.""".trimMargin()) - (services.transactionVerifierService as TransactionVerifierServiceInternal) - .reverifyWithFixups(ltx, missingClass) - .getOrThrow() + val replacementAttachments = computeReplacementAttachments(ltx, verificationSupport, missingClass) + log.warn("Reverifying transaction {} with attachments:{}", ltx.id, replacementAttachments) + ltx.verifyInternal(replacementAttachments.toList()) + } + + private fun computeReplacementAttachments(ltx: LedgerTransaction, + verificationSupport: VerificationSupport, + missingClass: String?): Collection { + val replacements = fixupAttachments(verificationSupport, ltx.attachments) + if (!replacements.equivalent(ltx.attachments)) { + return replacements + } + + // We cannot continue unless we have some idea which class is missing from the attachments. + if (missingClass == null) { + throw TransactionVerificationException.BrokenTransactionException( + txId = ltx.id, + message = "No fix-up rules provided for broken attachments: $replacements" + ) + } + + /* + * The Node's fix-up rules have not been able to adjust the transaction's attachments, + * so resort to the original mechanism of trying to find an attachment that contains + * the missing class. + */ + val extraAttachment = requireNotNull(verificationSupport.getTrustedClassAttachment(missingClass)) { + """Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda + |when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass, + |but could not find one. + |If you wish to verify this transaction, please contact the originator of the transaction and install the + |provided missing JAR. + |You can install it using the RPC command: `uploadAttachment` without restarting the node. + |""".trimMargin() + } + + return replacements.toMutableSet().apply { + /* + * Check our transaction doesn't already contain this extra attachment. + * It seems unlikely that we would, but better safe than sorry! + */ + if (!add(extraAttachment)) { + throw TransactionVerificationException.BrokenTransactionException( + txId = ltx.id, + message = "Unlinkable class $missingClass inside broken attachments: $replacements" + ) + } + + log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies. + |This may be the result of a bug in a previous version of Corda. + |Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass. + |Please check with the originator that this is a valid transaction. + |YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN! + |WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED! + |PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE! + |THIS MESSAGE IS **SUPPOSED** TO BE SCARY!! + |""".trimMargin() + ) + } + } + + /** + * Apply this node's attachment fix-up rules to the given attachments. + * + * @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction. + * @return The [attachments] with the node's fix-up rules applied. + */ + private fun fixupAttachments(verificationSupport: VerificationSupport, attachments: Collection): Collection { + val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id) + val replacementIds = verificationSupport.fixupAttachmentIds(attachmentsById.keys) + attachmentsById.keys.retainAll(replacementIds) + val extraIds = replacementIds - attachmentsById.keys + val extraAttachments = verificationSupport.getAttachments(extraIds) + for ((index, extraId) in extraIds.withIndex()) { + val extraAttachment = extraAttachments[index] + if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) { + throw MissingAttachmentsException(listOf(extraId)) + } + attachmentsById[extraId] = extraAttachment + } + return attachmentsById.values } /** @@ -306,7 +409,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** - * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a + * If [coreTransaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ fun resolveNotaryChangeTransaction(services: ServicesForResolution): NotaryChangeLedgerTransaction { @@ -316,10 +419,12 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** - * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a + * If [coreTransaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as ServicesForResolution) + fun resolveNotaryChangeTransaction(services: ServiceHub): NotaryChangeLedgerTransaction { + return resolveNotaryChangeTransaction(services as ServicesForResolution) + } /** * If [coreTransaction] is a [ContractUpgradeWireTransaction], loads the input states and resolves it to a 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 6ff76068bd..32ef6351ca 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -9,6 +9,8 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.verification.VerifyingServiceHub +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution @@ -28,7 +30,6 @@ import java.time.Duration import java.time.Instant import java.util.* import java.util.regex.Pattern -import kotlin.collections.ArrayList import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.reflect.KClass @@ -77,9 +78,6 @@ open class TransactionBuilder( private const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*" private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+") private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches() - private fun Collection<*>.deepEquals(other: Collection<*>): Boolean { - return (size == other.size) && containsAll(other) && other.containsAll(this) - } private fun Collection.toPrettyString(): String = sorted().joinToString( separator = System.lineSeparator(), prefix = System.lineSeparator() @@ -178,24 +176,25 @@ open class TransactionBuilder( } @CordaInternal - fun toWireTransactionWithContext( + @JvmSynthetic + internal fun toWireTransactionWithContext( services: ServicesForResolution, serializationContext: SerializationContext? - ) : WireTransaction = toWireTransactionWithContext(services, serializationContext, 0) + ) : WireTransaction = toWireTransactionWithContext(services.toVerifyingServiceHub(), serializationContext, 0) private tailrec fun toWireTransactionWithContext( - services: ServicesForResolution, - serializationContext: SerializationContext?, - tryCount: Int + serviceHub: VerifyingServiceHub, + serializationContext: SerializationContext?, + tryCount: Int ): WireTransaction { val referenceStates = referenceStates() if (referenceStates.isNotEmpty()) { - services.ensureMinimumPlatformVersion(4, "Reference states") + serviceHub.ensureMinimumPlatformVersion(4, "Reference states") } - resolveNotary(services) + resolveNotary(serviceHub) val (allContractAttachments: Collection, resolvedOutputs: List>) - = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext) + = selectContractAttachmentsAndOutputStateConstraints(serviceHub, serializationContext) // Final sanity check that all states have the correct constraints. for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) { @@ -213,9 +212,9 @@ open class TransactionBuilder( notary, window, referenceStates, - services.networkParametersService.currentHash), + serviceHub.networkParametersService.currentHash), privacySalt, - services.digestService + serviceHub.digestService ) } @@ -223,10 +222,10 @@ open class TransactionBuilder( // This is a workaround as the current version of Corda does not support cordapp dependencies. // It works by running transaction validation and then scan the attachment storage for missing classes. // TODO - remove once proper support for cordapp dependencies is added. - val addedDependency = addMissingDependency(services, wireTx, tryCount) + val addedDependency = addMissingDependency(serviceHub, wireTx, tryCount) return if (addedDependency) - toWireTransactionWithContext(services, serializationContext, tryCount + 1) + toWireTransactionWithContext(serviceHub, serializationContext, tryCount + 1) else wireTx } @@ -241,9 +240,9 @@ open class TransactionBuilder( /** * @return true if a new dependency was successfully added. */ - private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction, tryCount: Int): Boolean { + private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean { return try { - wireTx.toLedgerTransaction(services).verify() + wireTx.toLedgerTransactionInternal(serviceHub).verify() // The transaction verified successfully without adding any extra dependency. false } catch (e: Throwable) { @@ -253,12 +252,12 @@ open class TransactionBuilder( // Handle various exceptions that can be thrown during verification and drill down the wrappings. // Note: this is a best effort to preserve backwards compatibility. rootError is ClassNotFoundException -> { - ((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e)) - || addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), services, e) + ((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e)) + || addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e) } rootError is NoClassDefFoundError -> { - ((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e)) - || addMissingAttachment(rootError.message ?: throw e, services, e) + ((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e)) + || addMissingAttachment(rootError.message ?: throw e, serviceHub, e) } // Ignore these exceptions as they will break unit tests. @@ -281,18 +280,18 @@ open class TransactionBuilder( } private fun fixupAttachments( - txAttachments: List, - services: ServicesForResolution, - originalException: Throwable + txAttachments: List, + serviceHub: VerifyingServiceHub, + originalException: Throwable ): Boolean { - val replacementAttachments = services.cordappProvider.internalFixupAttachmentIds(txAttachments) - if (replacementAttachments.deepEquals(txAttachments)) { + val replacementAttachments = serviceHub.fixupAttachmentIds(txAttachments) + if (replacementAttachments.equivalent(txAttachments)) { return false } val extraAttachments = replacementAttachments - txAttachments extraAttachments.forEach { id -> - val attachment = services.attachments.openAttachment(id) + val attachment = serviceHub.attachments.openAttachment(id) if (attachment == null || !attachment.isUploaderTrusted()) { log.warn("""The node's fix-up rules suggest including attachment {}, which cannot be found either. |Please contact the developer of the CorDapp for further instructions. @@ -315,7 +314,7 @@ open class TransactionBuilder( return true } - private fun addMissingAttachment(missingClass: String, services: ServicesForResolution, originalException: Throwable): Boolean { + private fun addMissingAttachment(missingClass: String, serviceHub: VerifyingServiceHub, originalException: Throwable): Boolean { if (!isValidJavaClass(missingClass)) { log.warn("Could not autodetect a valid attachment for the transaction being built.") throw originalException @@ -324,7 +323,7 @@ open class TransactionBuilder( throw originalException } - val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass) + val attachment = serviceHub.getTrustedClassAttachment(missingClass) if (attachment == null) { log.error("""The transaction currently built is missing an attachment for class: $missingClass. @@ -475,14 +474,14 @@ open class TransactionBuilder( // Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them. val hashAttachments = inputsAndOutputs .filter { it.constraint is HashAttachmentConstraint } - .map { state -> + .mapToSet { state -> val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId) if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) { // This should never happen because these are input states that should have been validated already. throw MissingContractAttachments(listOf(state)) } attachment - }.toSet() + } // Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment. require(hashAttachments.size <= 1) { @@ -490,7 +489,7 @@ open class TransactionBuilder( } if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) { - require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) { + require(explicitContractAttachment == hashAttachments.single().attachment.id) { "An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state." } } @@ -665,10 +664,6 @@ open class TransactionBuilder( @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) - fun toLedgerTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { - return toWireTransactionWithContext(services, serializationContext).toLedgerTransaction(services) - } - @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub) { toLedgerTransaction(services).verify() @@ -692,7 +687,7 @@ open class TransactionBuilder( } // Transaction can combine different identities of the same notary after key rotation. - private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary.name }.toSet().size == 1 + private fun checkReferencesUseSameNotary() = referencesWithTransactionState.mapToSet { it.notary.name }.size == 1 // Automatically correct notary after its key rotation private fun resolveNotary(services: ServicesForResolution) { @@ -719,8 +714,6 @@ open class TransactionBuilder( * If this method is called outside the context of a flow, a [ServiceHub] instance must be passed to this method * for it to be able to resolve [StatePointer]s. Usually for a unit test, this will be an instance of mock services. * - * @param serviceHub a [ServiceHub] instance needed for performing vault queries. - * * @throws IllegalStateException if no [ServiceHub] is provided and no flow context is available. */ private fun resolveStatePointers(transactionState: TransactionState<*>) { 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 a0fa249240..457b33d246 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -1,21 +1,41 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandWithParties +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.contracts.ContractState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.keys import net.corda.core.identity.Party -import net.corda.core.internal.* +import net.corda.core.internal.Emoji +import net.corda.core.internal.SerializedStateAndRef +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.createComponentGroups +import net.corda.core.internal.flatMapToSet +import net.corda.core.internal.isUploaderTrusted +import net.corda.core.internal.lazyMapped +import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub 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.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 import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey @@ -47,6 +67,7 @@ import java.util.function.Predicate *

*/ @CordaSerializable +@Suppress("ThrowsCount") class WireTransaction(componentGroups: List, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) { constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) @@ -71,7 +92,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr init { check(componentGroups.all { it.components.isNotEmpty() }) { "Empty component groups are not allowed" } - check(componentGroups.map { it.groupIndex }.toSet().size == componentGroups.size) { "Duplicated component groups detected" } + check(componentGroups.mapToSet { it.groupIndex }.size == componentGroups.size) { "Duplicated component groups detected" } checkBaseInvariants() check(inputs.isNotEmpty() || outputs.isNotEmpty()) { "A transaction must contain at least one input or output state" } check(commands.isNotEmpty()) { "A transaction must contain at least one command" } @@ -102,28 +123,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr */ @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { - return services.specialise( - toLedgerTransactionInternal( - resolveIdentity = { services.identityService.partyFromKey(it) }, - resolveAttachment = { services.attachments.openAttachment(it) }, - resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) }, - resolveParameters = { - val hashToResolve = it ?: services.networkParametersService.defaultHash - services.networkParametersService.lookup(hashToResolve) - }, - // `as?` is used due to [MockServices] not implementing [ServiceHubCoreInternal] - isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true }, - attachmentsClassLoaderCache = (services as? ServiceHubCoreInternal)?.attachmentsClassLoaderCache - ) - ) - } - - // Helper for deprecated toLedgerTransaction - @Suppress("UNUSED") // not sure if this field can be removed safely?? - private val missingAttachment: Attachment by lazy { - object : AbstractAttachment({ byteArrayOf() }, DEPLOYED_CORDAPP_UPLOADER ) { - override val id: SecureHash get() = throw UnsupportedOperationException() - } + return toLedgerTransactionInternal(services.toVerifyingServiceHub()) } /** @@ -143,29 +143,37 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr @Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState) -> AttachmentId? ): LedgerTransaction { // This reverts to serializing the resolved transaction state. - return toLedgerTransactionInternal( - resolveIdentity, - resolveAttachment, - { stateRef -> resolveStateRef(stateRef)?.serialize() }, - { null }, - Attachment::isUploaderTrusted, - null - ) + return toLedgerTransactionInternal(object : VerificationSupport { + override fun getParties(keys: Collection): List = keys.map(resolveIdentity) + override fun getAttachment(id: SecureHash): Attachment? = resolveAttachment(id) + override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = null + override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachment.isUploaderTrusted() + override fun getSerializedState(stateRef: StateRef): SerializedTransactionState { + return resolveStateRef(stateRef)?.serialize() ?: throw TransactionResolutionException(stateRef.txhash) + } + // These are not used + override val appClassLoader: ClassLoader get() = throw AbstractMethodError() + override fun getTrustedClassAttachment(className: String) = throw AbstractMethodError() + override fun fixupAttachmentIds(attachmentIds: Collection) = throw AbstractMethodError() + }) } - @Suppress("LongParameterList", "ThrowsCount") - private fun toLedgerTransactionInternal( - resolveIdentity: (PublicKey) -> Party?, - resolveAttachment: (SecureHash) -> Attachment?, - resolveStateRefAsSerialized: (StateRef) -> SerializedBytes>?, - resolveParameters: (SecureHash?) -> NetworkParameters?, - isAttachmentTrusted: (Attachment) -> Boolean, - attachmentsClassLoaderCache: AttachmentsClassLoaderCache? - ): LedgerTransaction { + @CordaInternal + @JvmSynthetic + internal fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction { // Look up public keys to authenticated identities. - val authenticatedCommands = commands.lazyMapped { cmd, _ -> - val parties = cmd.signers.mapNotNull(resolveIdentity) - CommandWithParties(cmd.signers, parties, cmd.value) + val authenticatedCommands = if (verificationSupport.isResolutionLazy) { + commands.lazyMapped { cmd, _ -> + val parties = verificationSupport.getParties(cmd.signers).filterNotNull() + CommandWithParties(cmd.signers, parties, cmd.value) + } + } else { + val allSigners = commands.flatMapToSet { it.signers } + val allParties = verificationSupport.getParties(allSigners) + commands.map { cmd -> + val parties = cmd.signers.mapNotNull { allParties[allSigners.indexOf(it)] } + CommandWithParties(cmd.signers, parties, cmd.value) + } } // Ensure that the lazy mappings will use the correct SerializationContext. @@ -175,19 +183,28 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ssar.toStateAndRef(serializationFactory, serializationContext) } - val serializedResolvedInputs = inputs.map { ref -> - SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) + val serializedResolvedInputs = inputs.map { + SerializedStateAndRef(verificationSupport.getSerializedState(it), it) } val resolvedInputs = serializedResolvedInputs.lazyMapped(toStateAndRef) - val serializedResolvedReferences = references.map { ref -> - SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) + val serializedResolvedReferences = references.map { + SerializedStateAndRef(verificationSupport.getSerializedState(it), it) } val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef) - val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) } + val resolvedAttachments = if (verificationSupport.isResolutionLazy) { + attachments.lazyMapped { id, _ -> + verificationSupport.getAttachment(id) ?: throw AttachmentResolutionException(id) + } + } else { + verificationSupport.getAttachments(attachments).mapIndexed { index, attachment -> + attachment ?: throw AttachmentResolutionException(attachments[index]) + } + } - val resolvedNetworkParameters = resolveParameters(networkParametersHash) ?: throw TransactionResolutionException.UnknownParametersException(id, networkParametersHash!!) + val resolvedNetworkParameters = verificationSupport.getNetworkParameters(networkParametersHash) + ?: throw TransactionResolutionException.UnknownParametersException(id, networkParametersHash!!) val ltx = LedgerTransaction.create( resolvedInputs, @@ -203,8 +220,9 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr componentGroups, serializedResolvedInputs, serializedResolvedReferences, - isAttachmentTrusted, - attachmentsClassLoaderCache, + verificationSupport::isAttachmentTrusted, + verificationSupport::createVerifier, + verificationSupport.attachmentsClassLoaderCache, digestService ) @@ -230,15 +248,15 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // This calculates a value that is slightly lower than the actual re-serialized version. But it is stable and does not depend on the classloader. fun componentGroupSize(componentGroup: ComponentGroupEnum): Int { - return this.componentGroups.firstOrNull { it.groupIndex == componentGroup.ordinal }?.let { cg -> cg.components.sumBy { it.size } + 4 } ?: 0 + return this.componentGroups.firstOrNull { it.groupIndex == componentGroup.ordinal }?.let { cg -> cg.components.sumOf { it.size } + 4 } ?: 0 } // Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances // 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(resolvedSerializedInputs.sumBy { it.serializedState.size }) - minus(resolvedSerializedReferences.sumBy { it.serializedState.size }) + minus(resolvedSerializedInputs.sumOf { it.serializedState.size }) + minus(resolvedSerializedReferences.sumOf { it.serializedState.size }) // For Commands and outputs we can use the component groups as they are already serialized. minus(componentGroupSize(COMMANDS_GROUP)) @@ -273,7 +291,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // Even if empty and not used, we should at least send oneHashes for each known // or received but unknown (thus, bigger than known ordinal) component groups. val allOnesHash = digestService.allOnesHash - for (i in 0..componentGroups.map { it.groupIndex }.max()!!) { + for (i in 0..componentGroups.maxOf { it.groupIndex }) { val root = groupsMerkleRoots[i] ?: allOnesHash listOfLeaves.add(root) } @@ -340,37 +358,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr timeWindow: TimeWindow?): List { return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) } - - /** - * This is the main logic that knows how to retrieve the binary representation of [StateRef]s. - * - * 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>? { - return if (services is ServiceHub) { - val coreTransaction = services.validatedTransactions.getTransaction(stateRef.txhash)?.coreTransaction - ?: throw TransactionResolutionException(stateRef.txhash) - // Get the network parameters from the tx or whatever the default params are. - val paramsHash = coreTransaction.networkParametersHash ?: services.networkParametersService.defaultHash - val params = services.networkParametersService.lookup(paramsHash) - ?: throw IllegalStateException("Should have been able to fetch parameters by this point: $paramsHash") - @Suppress("UNCHECKED_CAST") - when (coreTransaction) { - is WireTransaction -> coreTransaction.componentGroups - .firstOrNull { it.groupIndex == OUTPUTS_GROUP.ordinal } - ?.components - ?.get(stateRef.index) as SerializedBytes>? - is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params) - is NotaryChangeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params) - else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} transaction. This is not supported.") - } - } else { - // For backwards compatibility revert to using the node classloader. - services.loadState(stateRef).serialize() - } - } } override fun toString(): String { diff --git a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt b/core/src/test/kotlin/net/corda/core/internal/InternalAccessTestHelpers.kt similarity index 68% rename from core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt rename to core/src/test/kotlin/net/corda/core/internal/InternalAccessTestHelpers.kt index 16a6e6bef8..5f9e48bb2e 100644 --- a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalAccessTestHelpers.kt @@ -1,9 +1,19 @@ package net.corda.core.internal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.CommandWithParties +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.internal.verification.AbstractVerifier import net.corda.core.node.NetworkParameters import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.internal.AttachmentsClassLoaderCache @@ -40,15 +50,35 @@ fun createLedgerTransaction( isAttachmentTrusted: (Attachment) -> Boolean, attachmentsClassLoaderCache: AttachmentsClassLoaderCache, digestService: DigestService = DigestService.default -): LedgerTransaction = LedgerTransaction.create( - inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache, digestService -).specialise(::PassthroughVerifier) +): LedgerTransaction { + return LedgerTransaction.create( + inputs, + outputs, + commands, + attachments, + id, + notary, + timeWindow, + privacySalt, + networkParameters, + references, + componentGroups, + serializedInputs, + serializedReferences, + isAttachmentTrusted, + ::PassthroughVerifier, + attachmentsClassLoaderCache, + digestService + ) +} 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) /** * Verify the [LedgerTransaction] we already have. + * + * Note, this is not secure! */ private class PassthroughVerifier(ltx: LedgerTransaction, context: SerializationContext) : AbstractVerifier(ltx, context.deserializationClassLoader) { override val transaction: Supplier diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 67b075dee0..ee1e0584e6 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.getRequiredTransaction import net.corda.core.node.NotaryInfo import net.corda.core.node.services.Vault import net.corda.core.transactions.SignedTransaction @@ -284,8 +285,8 @@ class CommercialPaperTestsGeneric { } // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpCash.states.map { megaCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) - megaCorpServices.recordTransactions(aliceCash.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + aliceServices.recordTransactions(bigCorpCash.states.map { megaCorpServices.getRequiredTransaction(it.ref.txhash) }) + megaCorpServices.recordTransactions(aliceCash.states.map { aliceServices.getRequiredTransaction(it.ref.txhash) }) // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val faceValue = 10000.DOLLARS `issued by` dummyCashIssuer.ref(1) diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt index cbc8211c72..d08375d8aa 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -1,34 +1,24 @@ package net.corda.nodeapitests.internal -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.StateAndContract +import net.corda.core.contracts.TypeOnlyCommandData import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.node.ServicesForResolution -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.nodeapi.internal.cordapp.CordappLoader -import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.nodeapitests.internal.AttachmentsClassLoaderStaticContractTests.AttachmentDummyContract.Companion.ATTACHMENT_PROGRAM_ID -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.testing.internal.MockCordappConfigProvider -import net.corda.coretesting.internal.rigorousMock -import net.corda.testing.node.internal.cordappWithPackages -import net.corda.testing.services.MockAttachmentStorage +import net.corda.testing.node.MockServices import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertEquals import org.junit.Rule @@ -69,31 +59,7 @@ class AttachmentsClassLoaderStaticContractTests { } } - private val networkParameters = testNetworkParameters() - - private val networkParametersService get() = mock().also { - doReturn(networkParameters.serialize().hash).whenever(it).currentHash - } - - private val serviceHub get() = rigorousMock().also { - val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapitests.internal")), MockCordappConfigProvider(), MockAttachmentStorage()) - cordappProviderImpl.start() - doReturn(cordappProviderImpl).whenever(it).cordappProvider - doReturn(networkParametersService).whenever(it).networkParametersService - doReturn(networkParameters).whenever(it).networkParameters - val attachmentStorage = rigorousMock() - doReturn(attachmentStorage).whenever(it).attachments - val attachment = rigorousMock() - doReturn(attachment).whenever(attachmentStorage).openAttachment(any()) - doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id - doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts - doReturn("app").whenever(attachment).uploader - doReturn(emptyList()).whenever(attachment).signerKeys - val contractAttachmentId = SecureHash.randomSHA256() - doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage) - .getLatestContractAttachments(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID) - doReturn(mock()).whenever(it).identityService - } + private val serviceHub = MockServices() @Test(timeout=300_000) fun `test serialization of WireTransaction with statically loaded contract`() { @@ -112,8 +78,4 @@ class AttachmentsClassLoaderStaticContractTests { val contractClass = Class.forName(ATTACHMENT_PROGRAM_ID) assertThat(contractClass.getDeclaredConstructor().newInstance()).isInstanceOf(Contract::class.java) } - - private fun cordappLoaderForPackages(packages: Collection): CordappLoader { - return JarScanningCordappLoader.fromJarUrls(listOf(cordappWithPackages(*packages.toTypedArray()).jarFile.toUri().toURL())) - } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index ee210ca365..598b666c58 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -1,6 +1,5 @@ package net.corda.nodeapi.internal.persistence -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.castIfPossible import net.corda.core.schemas.MappedSchema @@ -54,7 +53,7 @@ class HibernateConfiguration( val sessionFactoryFactory = findSessionFactoryFactory(jdbcUrl, customClassLoader) - private val sessionFactories = cacheFactory.buildNamed, SessionFactory>(Caffeine.newBuilder(), "HibernateConfiguration_sessionFactories") + private val sessionFactories = cacheFactory.buildNamed, SessionFactory>("HibernateConfiguration_sessionFactories") val sessionFactoryForRegisteredSchemas = schemas.let { logger.info("Init HibernateConfiguration for schemas: $it") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt index 2d4f751ddf..9671c2dfa6 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt @@ -4,6 +4,7 @@ import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.testutils.serializationContext +import net.corda.serialization.internal.verifier.CustomSerializationSchemeAdapter import org.junit.Test import org.junit.jupiter.api.Assertions.assertTrue import java.io.NotSerializableException diff --git a/node/build.gradle b/node/build.gradle index e3f184e590..81c5f23fec 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -73,6 +73,10 @@ jib.container { processResources { from file("$rootDir/config/dev/log4j2.xml") from file("$rootDir/config/dev/jolokia-access.xml") + from(tasks.findByPath(":verifier:shadowJar")) { + into("net/corda/node/verification") + rename { "external-verifier.jar" } + } } processTestResources { diff --git a/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt index 239525c576..d6a44ff4e6 100644 --- a/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt +++ b/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt @@ -8,7 +8,7 @@ import net.corda.core.contracts.TransactionState import net.corda.core.contracts.requireSingleCommand import net.corda.core.contracts.requireThat import net.corda.core.identity.AbstractParty -import net.corda.core.internal.Verifier +import net.corda.core.internal.verification.Verifier import net.corda.core.serialization.SerializationContext import net.corda.core.transactions.LedgerTransaction diff --git a/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt index 89677dede5..7a30f4840b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt @@ -58,7 +58,7 @@ import org.objenesis.strategy.StdInstantiatorStrategy import java.io.ByteArrayOutputStream import java.lang.reflect.Modifier import java.security.PublicKey -import java.util.* +import java.util.Arrays import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -94,7 +94,10 @@ class CustomSerializationSchemeDriverTest { @Test(timeout = 300_000) fun `flow can write a wire transaction serialized with custom kryo serializer to the ledger`() { - driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = listOf(enclosedCordapp()))) { + driver(DriverParameters( + cordappsForAllNodes = listOf(enclosedCordapp()), + systemProperties = mapOf("experimental.corda.customSerializationScheme" to KryoScheme::class.java.name) + )) { val (alice, bob) = listOf( startNode(NodeParameters(providedName = ALICE_NAME)), startNode(NodeParameters(providedName = BOB_NAME)) @@ -135,7 +138,7 @@ class CustomSerializationSchemeDriverTest { @StartableByRPC @InitiatingFlow - class WriteTxToLedgerFlow(val counterparty: Party, val notary: Party) : FlowLogic() { + class WriteTxToLedgerFlow(private val counterparty: Party, val notary: Party) : FlowLogic() { @Suspendable override fun call(): SecureHash { val wireTx = createWireTx(serviceHub, notary, counterparty.owningKey, KryoScheme.SCHEME_ID) @@ -146,7 +149,7 @@ class CustomSerializationSchemeDriverTest { return fullySignedTx.id } - fun signWireTx(wireTx: WireTransaction) : SignedTransaction { + private fun signWireTx(wireTx: WireTransaction) : SignedTransaction { val signatureMetadata = SignatureMetadata( serviceHub.myInfo.platformVersion, Crypto.findSignatureScheme(serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey).schemeNumberID @@ -157,18 +160,18 @@ class CustomSerializationSchemeDriverTest { } } + @Suppress("unused") @InitiatedBy(WriteTxToLedgerFlow::class) class SignWireTxFlow(private val session: FlowSession): FlowLogic() { @Suspendable override fun call(): SignedTransaction { - val signTransactionFlow = object : SignTransactionFlow(session) { - override fun checkTransaction(stx: SignedTransaction) { - return - } - } - val txId = subFlow(signTransactionFlow).id + val txId = subFlow(NoCheckSignTransactionFlow(session)).id return subFlow(ReceiveFinalityFlow(session, expectedTxId = txId)) } + + class NoCheckSignTransactionFlow(session: FlowSession) : SignTransactionFlow(session) { + override fun checkTransaction(stx: SignedTransaction) = Unit + } } @StartableByRPC @@ -226,7 +229,7 @@ class CustomSerializationSchemeDriverTest { @StartableByRPC @InitiatingFlow - class SendFlow(val counterparty: Party) : FlowLogic() { + class SendFlow(private val counterparty: Party) : FlowLogic() { @Suspendable override fun call(): Boolean { val wtx = createWireTx(serviceHub, counterparty, counterparty.owningKey, KryoScheme.SCHEME_ID) @@ -237,13 +240,14 @@ class CustomSerializationSchemeDriverTest { } @StartableByRPC - class CreateWireTxFlow(val counterparty: Party) : FlowLogic() { + class CreateWireTxFlow(private val counterparty: Party) : FlowLogic() { @Suspendable override fun call(): WireTransaction { return createWireTx(serviceHub, counterparty, counterparty.owningKey, KryoScheme.SCHEME_ID) } } + @Suppress("unused") @InitiatedBy(SendFlow::class) class ReceiveFlow(private val session: FlowSession): FlowLogic() { @Suspendable @@ -301,6 +305,7 @@ class CustomSerializationSchemeDriverTest { kryo.isRegistrationRequired = false kryo.instantiatorStrategy = CustomInstantiatorStrategy() kryo.classLoader = classLoader + @Suppress("ReplaceJavaStaticMethodWithKotlinAnalog") kryo.register(Arrays.asList("").javaClass, ArraysAsListSerializer()) } diff --git a/node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt b/node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt new file mode 100644 index 0000000000..810b1ada08 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt @@ -0,0 +1,219 @@ +package net.corda.node.verification + +import co.paralleluniverse.fibers.Suspendable +import com.typesafe.config.ConfigFactory +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.NotaryChangeFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.map +import net.corda.core.internal.concurrent.transpose +import net.corda.core.messaging.startFlow +import net.corda.core.node.NodeInfo +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.verification.ExternalVerificationTest.FailExternallyContract.State +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.cordappWithPackages +import net.corda.testing.node.internal.enclosedCordapp +import net.corda.testing.node.internal.internalDriver +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import java.io.File +import java.net.InetAddress +import kotlin.io.path.div +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.readText + +class ExternalVerificationTest { + @Test(timeout=300_000) + fun `regular transactions are verified in external verifier`() { + internalDriver( + systemProperties = mapOf("net.corda.node.verification.external" to "true"), + cordappsForAllNodes = FINANCE_CORDAPPS, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true, startInProcess = false)) + ) { + val (notary, alice, bob) = listOf( + defaultNotaryNode, + startNode(NodeParameters(providedName = ALICE_NAME)), + startNode(NodeParameters(providedName = BOB_NAME)) + ).transpose().getOrThrow() + + val (issuanceTx) = alice.rpc.startFlow( + ::CashIssueFlow, + 10.DOLLARS, + OpaqueBytes.of(0x01), + defaultNotaryIdentity + ).returnValue.getOrThrow() + + val (paymentTx) = alice.rpc.startFlow( + ::CashPaymentFlow, + 10.DOLLARS, + bob.nodeInfo.singleIdentity(), + false, + ).returnValue.getOrThrow() + + notary.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id) + bob.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id) + } + } + + @Test(timeout=300_000) + fun `regular transactions can fail verification in external verifier`() { + internalDriver( + systemProperties = mapOf("net.corda.node.verification.external" to "true"), + cordappsForAllNodes = listOf(cordappWithPackages("net.corda.node.verification", "com.typesafe.config")) + ) { + val (alice, bob, charlie) = listOf( + startNode(NodeParameters(providedName = ALICE_NAME)), + startNode(NodeParameters(providedName = BOB_NAME)), + startNode(NodeParameters(providedName = CHARLIE_NAME)) + ).transpose().getOrThrow() + + // Create a transaction from Alice to Bob, where Charlie is specified as the contract verification trigger + val firstState = alice.rpc.startFlow(::FailExternallyFlow, null, charlie.nodeInfo, bob.nodeInfo).returnValue.getOrThrow() + // When the transaction chain tries to moves onto Charlie, it will trigger the failure + assertThatExceptionOfType(TransactionVerificationException.ContractRejection::class.java) + .isThrownBy { bob.rpc.startFlow(::FailExternallyFlow, firstState, charlie.nodeInfo, charlie.nodeInfo).returnValue.getOrThrow() } + .withMessageContaining("Fail in external verifier: ${firstState.ref.txhash}") + + // Make sure Charlie tried to verify the first transaction externally + assertThat(charlie.externalVerifierLogs()).contains("Fail in external verifier: ${firstState.ref.txhash}") + } + } + + @Test(timeout=300_000) + fun `notary change transactions are verified in external verifier`() { + internalDriver( + systemProperties = mapOf("net.corda.node.verification.external" to "true"), + cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(), + notarySpecs = listOf(DUMMY_NOTARY_NAME, BOC_NAME).map { NotarySpec(it, validating = true, startInProcess = false) } + ) { + val (notary1, notary2) = notaryHandles.map { handle -> handle.nodeHandles.map { it[0] } }.transpose().getOrThrow() + val alice = startNode(NodeParameters(providedName = ALICE_NAME)).getOrThrow() + + val txId = alice.rpc.startFlow( + ::IssueAndChangeNotaryFlow, + notary1.nodeInfo.singleIdentity(), + notary2.nodeInfo.singleIdentity() + ).returnValue.getOrThrow() + + notary1.assertTransactionsWereVerifiedExternally(txId) + alice.assertTransactionsWereVerifiedExternally(txId) + } + } + + private fun NodeHandle.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) { + val verifierLogContent = externalVerifierLogs() + for (txId in txIds) { + assertThat(verifierLogContent).contains("SignedTransaction(id=$txId) verified") + } + } + + private fun NodeHandle.externalVerifierLogs(): String { + val verifierLogs = (baseDirectory / "logs") + .listDirectoryEntries() + .filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" } + assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1) + return verifierLogs[0].readText() + } + + class FailExternallyContract : Contract { + override fun verify(tx: LedgerTransaction) { + val command = tx.commandsOfType().single() + if (insideExternalVerifier()) { + // The current directory for the external verifier is the node's base directory + val localName = CordaX500Name.parse(ConfigFactory.parseFile(File("node.conf")).getString("myLegalName")) + check(localName != command.value.failForParty.name) { "Fail in external verifier: ${tx.id}" } + } + } + + private fun insideExternalVerifier(): Boolean { + return StackWalker.getInstance().walk { frames -> + frames.anyMatch { it.className.startsWith("net.corda.verifier.") } + } + } + + data class State(val party: Party) : ContractState { + override val participants: List get() = listOf(party) + } + + data class Command(val failForParty: Party) : CommandData + } + + @StartableByRPC + @InitiatingFlow + class FailExternallyFlow(private val inputState: StateAndRef?, + private val failForParty: NodeInfo, + private val recipient: NodeInfo) : FlowLogic>() { + @Suspendable + override fun call(): StateAndRef { + val myParty = serviceHub.myInfo.legalIdentities[0] + val txBuilder = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities[0]) + inputState?.let(txBuilder::addInputState) + txBuilder.addOutputState(State(myParty), FailExternallyContract::class.java.name) + txBuilder.addCommand(FailExternallyContract.Command(failForParty.legalIdentities[0]), myParty.owningKey) + val initialTx = serviceHub.signInitialTransaction(txBuilder) + val sessions = arrayListOf(initiateFlow(recipient.legalIdentities[0])) + inputState?.let { sessions += initiateFlow(it.state.data.party) } + val notarisedTx = subFlow(FinalityFlow(initialTx, sessions)) + return notarisedTx.toLedgerTransaction(serviceHub).outRef(0) + } + } + + @Suppress("unused") + @InitiatedBy(FailExternallyFlow::class) + class ReceiverFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(ReceiveFinalityFlow(otherSide)) + } + } + + + @StartableByRPC + class IssueAndChangeNotaryFlow(private val oldNotary: Party, private val newNotary: Party) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + subFlow(CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x01), oldNotary)) + val oldState = serviceHub.vaultService.queryBy(Cash.State::class.java).states.single() + assertThat(oldState.state.notary).isEqualTo(oldNotary) + val newState = subFlow(NotaryChangeFlow(oldState, newNotary)) + assertThat(newState.state.notary).isEqualTo(newNotary) + val notaryChangeTx = serviceHub.validatedTransactions.getTransaction(newState.ref.txhash) + assertThat(notaryChangeTx?.coreTransaction).isInstanceOf(NotaryChangeWireTransaction::class.java) + return notaryChangeTx!!.id + } + } +} 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 72c31fc33c..ece28a229f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -38,6 +38,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.div import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps import net.corda.core.internal.notary.NotaryService @@ -47,6 +48,7 @@ import net.corda.core.internal.telemetry.SimpleLogTelemetryComponent import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps @@ -55,13 +57,11 @@ import net.corda.core.node.AppServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub -import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.ContractUpgradeService import net.corda.core.node.services.CordaService import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.TelemetryService -import net.corda.core.node.services.TransactionVerifierService import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist @@ -70,7 +70,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.toFuture -import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.days import net.corda.core.utilities.millis @@ -82,7 +82,6 @@ import net.corda.node.internal.checkpoints.FlowManagerRPCOpsImpl import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.VirtualCordapp import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy @@ -122,10 +121,10 @@ import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.node.services.persistence.AbstractPartyDescriptor import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter +import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.DBCheckpointPerformanceRecorder import net.corda.node.services.persistence.DBCheckpointStorage -import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.DBTransactionMappingStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.NodeAttachmentService @@ -141,7 +140,6 @@ import net.corda.node.services.statemachine.FlowOperator import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.SingleThreadedStateMachineManager import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor @@ -157,6 +155,7 @@ import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor import net.corda.nodeapi.internal.lifecycle.NodeServicesContext +import net.corda.nodeapi.internal.namedThreadPoolExecutor import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaTransactionSupportImpl import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException @@ -169,7 +168,6 @@ import net.corda.nodeapi.internal.persistence.RestrictedEntityManager import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess -import net.corda.nodeapi.internal.namedThreadPoolExecutor import org.apache.activemq.artemis.utils.ReusableLatch import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServerConfig @@ -181,7 +179,6 @@ import java.sql.Savepoint import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException -import java.util.ArrayList import java.util.Properties import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -299,22 +296,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory) @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() - val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { - attachments.servicesForResolution = it - } - @Suppress("LeakingThis") - val vaultService = makeVaultService(keyManagementService, servicesForResolution, database, cordappLoader).tokenize() val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory) val flowLogicRefFactory = makeFlowLogicRefFactoryImpl() // TODO Cancelling parameters updates - if we do that, how we ensure that no one uses cancelled parameters in the transactions? val networkMapUpdater = makeNetworkMapUpdater() - @Suppress("LeakingThis") - val transactionVerifierService = InMemoryTransactionVerifierService( - numberOfWorkers = transactionVerifierWorkerCount, - cordappProvider = cordappProvider, - attachments = attachments - ).tokenize() private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @@ -326,7 +312,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.warn("MessagingService subscription error", it) }) } - val services = ServiceHubInternalImpl().tokenize() + val services = ServiceHubImpl().tokenize() + @Suppress("LeakingThis") + val vaultService = makeVaultService(keyManagementService, database, cordappLoader).tokenize() val checkpointStorage = DBCheckpointStorage(DBCheckpointPerformanceRecorder(services.monitoringService.metrics), platformClock) @Suppress("LeakingThis") val smm = makeStateMachineManager() @@ -338,7 +326,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private val cordappTelemetryComponents = MutableClassToInstanceMap.create() private val shutdownExecutor = Executors.newSingleThreadExecutor(DefaultThreadFactory("Shutdown")) - protected abstract val transactionVerifierWorkerCount: Int /** * Should be [rx.schedulers.Schedulers.io] for production, * or [rx.internal.schedulers.CachedThreadScheduler] (with shutdown registered with [runOnStop]) for shared-JVM testing. @@ -469,8 +456,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, Node.printBasicNodeInfo("Running database schema migration scripts ...") val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - var pendingAppChanges: Int = 0 - var pendingCoreChanges: Int = 0 + var pendingAppChanges = 0 + var pendingCoreChanges = 0 database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints -> val schemaMigration = SchemaMigration(dataSource, cordappLoader, configuration.networkParametersPath, configuration.myLegalName) if(updateCoreSchemas) { @@ -505,13 +492,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val updatedSchemas = listOfNotNull( ("core").takeIf { updateCoreSchemas }, ("app").takeIf { updateAppSchemas } - ).joinToString(separator = " and "); + ).joinToString(separator = " and ") val pendingChanges = listOfNotNull( ("no outstanding").takeIf { pendingAppChanges == 0 && pendingCoreChanges == 0 }, ("$pendingCoreChanges outstanding core").takeIf { !updateCoreSchemas && pendingCoreChanges > 0 }, ("$pendingAppChanges outstanding app").takeIf { !updateAppSchemas && pendingAppChanges > 0 } - ).joinToString(prefix = "There are ", postfix = " database changes."); + ).joinToString(prefix = "There are ", postfix = " database changes.") Node.printBasicNodeInfo("Database migration scripts for $updatedSchemas schemas complete. $pendingChanges") } @@ -832,7 +819,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkMapCache, NodeInfoWatcher( configuration.baseDirectory, - @Suppress("LeakingThis") rxIoScheduler, Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec) ), @@ -846,7 +832,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, platformClock, database, flowStarter, - servicesForResolution, + services, flowLogicRefFactory, nodeProperties, configuration.drainingModePollPeriod, @@ -1160,12 +1146,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkParameters: NetworkParameters) protected open fun makeVaultService(keyManagementService: KeyManagementService, - services: NodeServicesForResolution, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService(platformClock, keyManagementService, services, database, schemaService, cordappLoader.appClassLoader) } + /** + * Dy default only internal verification is done. + * @see VerifyingServiceHub.tryExternalVerification + */ + protected open fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + return true + } + // JDK 11: switch to directly instantiating jolokia server (rather than indirectly via dynamically self attaching Java Agents, // which is no longer supported from JDK 9 onwards (https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8180425). // No longer need to use https://github.com/electronicarts/ea-agent-loader either (which is also deprecated) @@ -1178,7 +1171,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener { + inner class ServiceHubImpl : SingletonSerializeAsToken(), ServiceHubInternal, NetworkParameterUpdateListener { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val identityService: IdentityService get() = this@AbstractNode.identityService @@ -1191,7 +1184,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val nodeProperties: NodePropertiesStore get() = this@AbstractNode.nodeProperties override val database: CordaPersistence get() = this@AbstractNode.database override val monitoringService: MonitoringService get() = this@AbstractNode.monitoringService - override val transactionVerifierService: TransactionVerifierService get() = this@AbstractNode.transactionVerifierService override val contractUpgradeService: ContractUpgradeService get() = this@AbstractNode.contractUpgradeService override val auditService: AuditService get() = this@AbstractNode.auditService override val attachments: AttachmentStorageInternal get() = this@AbstractNode.attachments @@ -1216,6 +1208,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private lateinit var _networkParameters: NetworkParameters override val networkParameters: NetworkParameters get() = _networkParameters + init { + this@AbstractNode.attachments.servicesForResolution = this + } + fun start(myInfo: NodeInfo, networkParameters: NetworkParameters) { this._myInfo = myInfo this._networkParameters = networkParameters @@ -1300,13 +1296,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, this@AbstractNode.runOnStop += runOnStop } - override fun specialise(ltx: LedgerTransaction): LedgerTransaction { - return servicesForResolution.specialise(ltx) - } - override fun onNewNetworkParameters(networkParameters: NetworkParameters) { this._networkParameters = networkParameters } + + override fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + return this@AbstractNode.tryExternalVerification(stx, checkSufficientSignatures) + } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt b/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt index 5acf706f91..041a1ec756 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt @@ -4,13 +4,13 @@ import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByService import net.corda.core.internal.FlowStateMachineHandle +import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowHandleImpl import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.node.AppServiceHub -import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceLifecycleEvent import net.corda.core.node.services.ServiceLifecycleObserver import net.corda.core.node.services.vault.CordaTransactionSupport @@ -24,15 +24,16 @@ import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver.Companion.reportSuccess import rx.Observable -import java.util.* +import java.util.Objects /** * This customizes the ServiceHub for each [net.corda.core.node.services.CordaService] that is initiating flows. */ -internal class AppServiceHubImpl(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter, +internal class AppServiceHubImpl(private val serviceHub: ServiceHubCoreInternal, + private val flowStarter: FlowStarter, override val database: CordaTransactionSupport, private val nodeLifecycleEventsDistributor: NodeLifecycleEventsDistributor) - : AppServiceHub, ServiceHub by serviceHub { + : AppServiceHub, ServiceHubCoreInternal by serviceHub { companion object { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 975aa9ea41..ffebec67df 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -7,8 +7,8 @@ import com.github.benmanes.caffeine.cache.Caffeine import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import io.netty.util.NettyRuntime -import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme import net.corda.cliutils.ShellConstants +import net.corda.common.logging.errorReporting.NodeDatabaseErrors import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name @@ -26,6 +26,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.node.CordaClock @@ -36,9 +37,6 @@ import net.corda.node.internal.artemis.BrokerAddresses import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser -import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme -import net.corda.nodeapi.internal.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.nodeapi.internal.serialization.kryo.KryoCheckpointSerializer import net.corda.node.services.Permissions import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal @@ -64,9 +62,8 @@ import net.corda.node.utilities.BindableNamedCacheFactory import net.corda.node.utilities.DefaultNamedCacheFactory import net.corda.node.utilities.DemoClock import net.corda.node.utilities.errorAndTerminate +import net.corda.node.verification.ExternalVerifierHandle import net.corda.nodeapi.internal.ArtemisMessagingClient -import net.corda.common.logging.errorReporting.NodeDatabaseErrors -import net.corda.node.internal.classloading.scanForCustomSerializationScheme import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.bridging.BridgeControlListener @@ -74,6 +71,10 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig +import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme +import net.corda.nodeapi.internal.serialization.kryo.KRYO_CHECKPOINT_CONTEXT +import net.corda.nodeapi.internal.serialization.kryo.KryoCheckpointSerializer import net.corda.serialization.internal.AMQP_P2P_CONTEXT import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.serialization.internal.AMQP_RPC_SERVER_CONTEXT @@ -81,6 +82,7 @@ import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT import net.corda.serialization.internal.SerializationFactoryImpl import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey import net.corda.serialization.internal.amqp.SerializerFactory +import net.corda.serialization.internal.verifier.loadCustomSerializationScheme import org.apache.commons.lang3.JavaVersion import org.apache.commons.lang3.SystemUtils import org.h2.jdbc.JdbcSQLNonTransientConnectionException @@ -92,7 +94,6 @@ import java.lang.Long.max import java.lang.Long.min import java.net.BindException import java.net.InetAddress -import java.nio.file.Path import java.nio.file.Paths import java.time.Clock import java.util.concurrent.TimeUnit @@ -194,13 +195,14 @@ open class Node(configuration: NodeConfiguration, } override val log: Logger get() = staticLog - override val transactionVerifierWorkerCount: Int get() = 4 private var internalRpcMessagingClient: InternalRPCMessagingClient? = null private var rpcBroker: ArtemisBroker? = null protected open val journalBufferTimeout : Int? = null + private val externalVerifierHandle = ExternalVerifierHandle(services).also { runOnStop += it::close } + private var shutdownHook: ShutdownHook? = null // DISCUSSION @@ -297,7 +299,7 @@ open class Node(configuration: NodeConfiguration, printBasicNodeInfo("Advertised P2P messaging addresses", nodeInfo.addresses.joinToString()) val rpcServerConfiguration = RPCServerConfiguration.DEFAULT - rpcServerAddresses?.let { + rpcServerAddresses.let { internalRpcMessagingClient = InternalRPCMessagingClient(configuration.p2pSslOptions, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.p2pSslOptions.keyStore.get()[X509Utilities.CORDA_CLIENT_TLS].subjectX500Principal), rpcServerConfiguration) printBasicNodeInfo("RPC connection address", it.primary.toString()) printBasicNodeInfo("RPC admin connection address", it.admin.toString()) @@ -353,22 +355,18 @@ open class Node(configuration: NodeConfiguration, ) } - private fun startLocalRpcBroker(securityManager: RPCSecurityManager): BrokerAddresses? { - return with(configuration) { - rpcOptions.address.let { - val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" - with(rpcOptions) { - rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, - journalBufferTimeout, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) - } else { - ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, - journalBufferTimeout, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) - } - } - rpcBroker!!.addresses + private fun startLocalRpcBroker(securityManager: RPCSecurityManager): BrokerAddresses { + val rpcBrokerDirectory = configuration.baseDirectory / "brokers" / "rpc" + with(configuration.rpcOptions) { + rpcBroker = if (useSsl) { + ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, + journalBufferTimeout, configuration.jmxMonitoringHttpPort != null, rpcBrokerDirectory, configuration.shouldStartLocalShell()) + } else { + ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, + journalBufferTimeout, configuration.jmxMonitoringHttpPort != null, rpcBrokerDirectory, configuration.shouldStartLocalShell()) } } + return rpcBroker!!.addresses } override fun myAddresses(): List = listOf(getAdvertisedAddress()) + configuration.additionalP2PAddresses @@ -392,7 +390,7 @@ open class Node(configuration: NodeConfiguration, * machine's public IP address to be used instead by looking through the network interfaces. */ private fun tryDetectIfNotPublicHost(host: String): String? { - return if (host.toLowerCase() == "localhost") { + return if (host.lowercase() == "localhost") { log.warn("p2pAddress specified as localhost. Trying to autodetect a suitable public address to advertise in network map." + "To disable autodetect set detectPublicIp = false in the node.conf, or consider using messagingServerAddress and messagingServerExternal") val foundPublicIP = AddressUtils.tryDetectPublicIP() @@ -572,7 +570,7 @@ open class Node(configuration: NodeConfiguration, if (!initialiseSerialization) return val classloader = cordappLoader.appClassLoader val customScheme = System.getProperty("experimental.corda.customSerializationScheme")?.let { - scanForCustomSerializationScheme(it, classloader) + loadCustomSerializationScheme(it, classloader) } nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { @@ -590,6 +588,17 @@ open class Node(configuration: NodeConfiguration, ) } + override fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + // TODO Determine from transaction whether it should be verified externally + // TODO If both old and new attachments are present then return true so that internal verification is also done. + return if (java.lang.Boolean.getBoolean("net.corda.node.verification.external")) { + externalVerifierHandle.verifyTransaction(stx, checkSufficientSignatures) + false + } else { + true + } + } + /** Starts a blocking event loop for message dispatch. */ fun run() { internalRpcMessagingClient?.start(rpcBroker!!.serverControl) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt deleted file mode 100644 index 5baa528297..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.node.internal - -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.node.ServicesForResolution -import java.util.LinkedHashSet - -interface NodeServicesForResolution : ServicesForResolution { - @Throws(TransactionResolutionException::class) - override fun loadStates(stateRefs: Set): Set> = loadStates(stateRefs, LinkedHashSet()) - - fun >> loadStates(input: Iterable, output: C): C -} diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt deleted file mode 100644 index ffb21894c1..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ /dev/null @@ -1,85 +0,0 @@ -package net.corda.node.internal - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.AttachmentResolutionException -import net.corda.core.contracts.ContractAttachment -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.contracts.TransactionState -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.SerializedStateAndRef -import net.corda.core.internal.uncheckedCast -import net.corda.core.node.NetworkParameters -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.node.services.TransactionStorage -import net.corda.core.transactions.BaseTransaction -import net.corda.core.transactions.ContractUpgradeWireTransaction -import net.corda.core.transactions.NotaryChangeWireTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction -import net.corda.core.transactions.WireTransaction.Companion.resolveStateRefBinaryComponent - -data class ServicesForResolutionImpl( - override val identityService: IdentityService, - override val attachments: AttachmentStorage, - override val cordappProvider: CordappProvider, - override val networkParametersService: NetworkParametersService, - private val validatedTransactions: TransactionStorage -) : NodeServicesForResolution { - override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?: - throw IllegalArgumentException("No current parameters in network parameters storage") - - @Throws(TransactionResolutionException::class) - override fun loadState(stateRef: StateRef): TransactionState<*> { - return toBaseTransaction(stateRef.txhash).outputs[stateRef.index] - } - - override fun >> loadStates(input: Iterable, output: C): C { - val baseTxs = HashMap() - return input.mapTo(output) { stateRef -> - val baseTx = baseTxs.computeIfAbsent(stateRef.txhash, ::toBaseTransaction) - StateAndRef(uncheckedCast(baseTx.outputs[stateRef.index]), stateRef) - } - } - - @Throws(TransactionResolutionException::class, AttachmentResolutionException::class) - override fun loadContractAttachment(stateRef: StateRef): Attachment { - // We may need to recursively chase transactions if there are notary changes. - fun inner(stateRef: StateRef, forContractClassName: String?): Attachment { - val ctx = getSignedTransaction(stateRef.txhash).coreTransaction - when (ctx) { - is WireTransaction -> { - val transactionState = ctx.outRef(stateRef.index).state - for (attachmentId in ctx.attachments) { - val attachment = attachments.openAttachment(attachmentId) - if (attachment is ContractAttachment && (forContractClassName ?: transactionState.contract) in attachment.allContracts) { - return attachment - } - } - throw AttachmentResolutionException(stateRef.txhash) - } - is ContractUpgradeWireTransaction -> { - return attachments.openAttachment(ctx.upgradedContractAttachmentId) ?: throw AttachmentResolutionException(stateRef.txhash) - } - is NotaryChangeWireTransaction -> { - val transactionState = SerializedStateAndRef(resolveStateRefBinaryComponent(stateRef, this)!!, stateRef).toStateAndRef().state - // TODO: check only one (or until one is resolved successfully), max recursive invocations check? - return ctx.inputs.map { inner(it, transactionState.contract) }.firstOrNull() ?: throw AttachmentResolutionException(stateRef.txhash) - } - else -> throw UnsupportedOperationException("Attempting to resolve attachment for index ${stateRef.index} of a ${ctx.javaClass} transaction. This is not supported.") - } - } - return inner(stateRef, null) - } - - private fun toBaseTransaction(txhash: SecureHash): BaseTransaction = getSignedTransaction(txhash).resolveBaseTransaction(this) - - private fun getSignedTransaction(txhash: SecureHash): SignedTransaction { - return validatedTransactions.getTransaction(txhash) ?: throw TransactionResolutionException(txhash) - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt b/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt index bb49eeb179..958e879981 100644 --- a/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt +++ b/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt @@ -2,31 +2,6 @@ package net.corda.node.internal.classloading -import net.corda.core.serialization.CustomSerializationScheme -import net.corda.node.internal.ConfigurationException -import net.corda.nodeapi.internal.serialization.CustomSerializationSchemeAdapter -import net.corda.serialization.internal.SerializationScheme -import java.lang.reflect.Constructor - inline fun Class<*>.requireAnnotation(): A { return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" } } - -fun scanForCustomSerializationScheme(className: String, classLoader: ClassLoader) : SerializationScheme { - val schemaClass = try { - Class.forName(className, false, classLoader) - } catch (exception: ClassNotFoundException) { - throw ConfigurationException("$className was declared as a custom serialization scheme but could not be found.") - } - val constructor = validateScheme(schemaClass, className) - return CustomSerializationSchemeAdapter(constructor.newInstance() as CustomSerializationScheme) -} - -private fun validateScheme(clazz: Class<*>, className: String): Constructor<*> { - if (!clazz.interfaces.contains(CustomSerializationScheme::class.java)) { - throw ConfigurationException("$className was declared as a custom serialization scheme but does not implement" + - " ${CustomSerializationScheme::class.java.canonicalName}") - } - return clazz.constructors.singleOrNull { it.parameters.isEmpty() } ?: throw ConfigurationException("$className was declared as a " + - "custom serialization scheme but does not have a no argument constructor.") -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index 79a657b0ad..3153db4853 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -1,7 +1,6 @@ package net.corda.node.internal.cordapp import com.google.common.collect.HashBiMap -import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext @@ -9,19 +8,16 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.internal.isUploaderTrusted -import net.corda.core.node.services.AttachmentFixup +import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.verification.AttachmentFixups import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage -import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.contextLogger import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.nodeapi.internal.cordapp.CordappLoader -import java.net.JarURLConnection import java.net.URL +import java.nio.file.FileAlreadyExistsException import java.util.concurrent.ConcurrentHashMap -import java.util.jar.JarFile /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. @@ -29,14 +25,11 @@ import java.util.jar.JarFile open class CordappProviderImpl(val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, private val attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { - companion object { - const val COMMENT_MARKER = '#' - private val log = contextLogger() - } - private val contextCache = ConcurrentHashMap() private val cordappAttachments = HashBiMap.create() - private val attachmentFixups = arrayListOf() + private val attachmentFixups = AttachmentFixups() + + override val appClassLoader: ClassLoader get() = cordappLoader.appClassLoader /** * Current known CorDapps loaded on this node @@ -47,7 +40,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, cordappAttachments.putAll(loadContractsIntoAttachmentStore()) verifyInstalledCordapps() // Load the fix-ups after uploading any new contracts into attachment storage. - attachmentFixups.addAll(loadAttachmentFixups()) + attachmentFixups.load(cordappLoader.appClassLoader) } private fun verifyInstalledCordapps() { @@ -79,116 +72,35 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, */ fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse()[cordapp.jarPath] - private fun loadContractsIntoAttachmentStore(): Map = - cordapps.filter { it.contractClassNames.isNotEmpty() }.map { cordapp -> - cordapp.jarPath.openStream().use { stream -> - try { - // This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage] - // [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because - // doing so results in internal functions being exposed in the public API. - if (attachmentStorage is AttachmentStorageInternal) { - attachmentStorage.privilegedImportAttachment( + private fun loadContractsIntoAttachmentStore(): Map { + return cordapps.filter { it.contractClassNames.isNotEmpty() }.associate { cordapp -> + cordapp.jarPath.openStream().use { stream -> + try { + // This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage] + // [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because + // doing so results in internal functions being exposed in the public API. + if (attachmentStorage is AttachmentStorageInternal) { + attachmentStorage.privilegedImportAttachment( stream, DEPLOYED_CORDAPP_UPLOADER, cordapp.info.shortName - ) - } else { - attachmentStorage.importAttachment( + ) + } else { + attachmentStorage.importAttachment( stream, DEPLOYED_CORDAPP_UPLOADER, cordapp.info.shortName - ) - } - } catch (faee: java.nio.file.FileAlreadyExistsException) { - AttachmentId.create(faee.message!!) + ) } - } to cordapp.jarPath - }.toMap() - - /** - * Loads the "fixup" rules from all META-INF/Corda-Fixups files. - * These files have the following format: - * ,...=>,,... - * where each is the SHA256 of a CorDapp JAR that - * [net.corda.core.transactions.TransactionBuilder] will expect to find - * inside [AttachmentStorage]. - * - * These rules are for repairing broken CorDapps. A correctly written - * CorDapp should not require them. - */ - private fun loadAttachmentFixups(): List { - return cordappLoader.appClassLoader.getResources("META-INF/Corda-Fixups").asSequence() - .mapNotNull { fixup -> - fixup.openConnection() as? JarURLConnection - }.filter { fixupConnection -> - isValidFixup(fixupConnection.jarFile) - }.flatMapTo(ArrayList()) { fixupConnection -> - fixupConnection.inputStream.bufferedReader().useLines { lines -> - lines.map { it.substringBefore(COMMENT_MARKER) }.map(String::trim).filterNot(String::isEmpty).map { line -> - val tokens = line.split("=>") - require(tokens.size == 2) { - "Invalid fix-up line '$line' in '${fixupConnection.jarFile.name}'" - } - val source = parseIds(tokens[0]) - require(source.isNotEmpty()) { - "Forbidden empty list of source attachment IDs in '${fixupConnection.jarFile.name}'" - } - val target = parseIds(tokens[1]) - Pair(source, target) - }.toList().asSequence() + } catch (faee: FileAlreadyExistsException) { + AttachmentId.create(faee.message!!) } - } - } - - private fun isValidFixup(jarFile: JarFile): Boolean { - return jarFile.entries().asSequence().all { it.name.startsWith("META-INF/") }.also { isValid -> - if (!isValid) { - log.warn("FixUp '{}' contains files outside META-INF/ - IGNORING!", jarFile.name) - } + } to cordapp.jarPath } } - private fun parseIds(ids: String): Set { - return ids.split(",").map(String::trim) - .filterNot(String::isEmpty) - .mapTo(LinkedHashSet(), SecureHash.Companion::create) - } - - /** - * Apply this node's attachment fix-up rules to the given attachment IDs. - * - * @param attachmentIds A collection of [AttachmentId]s, e.g. as provided by a transaction. - * @return The [attachmentIds] with the fix-up rules applied. - */ override fun fixupAttachmentIds(attachmentIds: Collection): Set { - val replacementIds = LinkedHashSet(attachmentIds) - attachmentFixups.forEach { (source, target) -> - if (replacementIds.containsAll(source)) { - replacementIds.removeAll(source) - replacementIds.addAll(target) - } - } - return replacementIds - } - - /** - * Apply this node's attachment fix-up rules to the given attachments. - * - * @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction. - * @return The [attachments] with the node's fix-up rules applied. - */ - override fun fixupAttachments(attachments: Collection): Collection { - val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id) - val replacementIds = fixupAttachmentIds(attachmentsById.keys) - attachmentsById.keys.retainAll(replacementIds) - (replacementIds - attachmentsById.keys).forEach { extraId -> - val extraAttachment = attachmentStorage.openAttachment(extraId) - if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) { - throw MissingAttachmentsException(listOf(extraId)) - } - attachmentsById[extraId] = extraAttachment - } - return attachmentsById.values + return attachmentFixups.fixupAttachmentIds(attachmentIds) } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt deleted file mode 100644 index ed8f410bfa..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.node.internal.cordapp - -import net.corda.core.contracts.Attachment -import net.corda.core.cordapp.Cordapp -import net.corda.core.cordapp.CordappProvider -import net.corda.core.flows.FlowLogic -import net.corda.core.internal.CordappFixupInternal -import net.corda.core.internal.cordapp.CordappImpl - -interface CordappProviderInternal : CordappProvider, CordappFixupInternal { - val cordapps: List - fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? - fun fixupAttachments(attachments: Collection): Collection -} diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 8fb8e8c671..75f0a759a5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -9,15 +9,33 @@ import net.corda.core.CordaRuntimeException import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.flows.* -import net.corda.core.internal.* +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.SchedulableFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.StartableByService +import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION +import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION +import net.corda.core.internal.JarSignatureCollector +import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO import net.corda.core.internal.cordapp.get +import net.corda.core.internal.exists +import net.corda.core.internal.hash +import net.corda.core.internal.isAbstractClass +import net.corda.core.internal.list +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.location import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.SinglePartyNotaryService -import net.corda.core.node.services.CordaService +import net.corda.core.internal.objectOrNewInstance +import net.corda.core.internal.pooledScan +import net.corda.core.internal.readFully import net.corda.core.internal.telemetry.TelemetryComponent +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.warnContractWithoutConstraintPropagation +import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.CheckpointCustomSerializer import net.corda.core.serialization.SerializationCustomSerializer @@ -33,12 +51,12 @@ import java.math.BigInteger import java.net.URL import java.net.URLClassLoader import java.nio.file.Path -import java.util.* +import java.util.Random +import java.util.ServiceLoader import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarInputStream import java.util.jar.Manifest import java.util.zip.ZipInputStream -import kotlin.collections.LinkedHashSet import kotlin.reflect.KClass /** @@ -363,7 +381,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun loadClass(className: String, type: KClass): Class? { return try { - Class.forName(className, false, appClassLoader).asSubclass(type.java) + loadClassOfType(type.java, className, false, appClassLoader) } catch (e: ClassCastException) { logger.warn("As $className must be a sub-type of ${type.java.name}") null diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt index 605bf0bd71..f246c02330 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -1,8 +1,6 @@ package net.corda.node.internal.security - import com.github.benmanes.caffeine.cache.Cache -import com.github.benmanes.caffeine.cache.Caffeine import com.google.common.primitives.Ints import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.uncheckedCast @@ -241,7 +239,7 @@ private class CaffeineCacheManager(val maxSize: Long, private fun buildCache(name: String): ShiroCache { logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s") - return cacheFactory.buildNamed(Caffeine.newBuilder(), "RPCSecurityManagerShiroCache_$name").toShiroCache() + return cacheFactory.buildNamed("RPCSecurityManagerShiroCache_$name").toShiroCache() } companion object { diff --git a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt index 4c28ab7304..717b94a5d1 100644 --- a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt @@ -7,17 +7,13 @@ import liquibase.database.jvm.JdbcConnection import liquibase.exception.ValidationErrors import liquibase.resource.ResourceAccessor import net.corda.core.schemas.MappedSchema -import net.corda.node.SimpleClock import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter -import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.PublicKeyToTextConverter import net.corda.nodeapi.internal.persistence.CordaPersistence import java.io.PrintWriter import java.sql.Connection import java.sql.SQLFeatureNotSupportedException -import java.time.Clock import java.util.logging.Logger import javax.sql.DataSource @@ -39,11 +35,6 @@ abstract class CordaMigration : CustomTaskChange { private lateinit var _cordaDB: CordaPersistence - val servicesForResolution: MigrationServicesForResolution - get() = _servicesForResolution - - private lateinit var _servicesForResolution: MigrationServicesForResolution - /** * Initialise a subset of node services so that data from these can be used to perform migrations. * @@ -60,12 +51,6 @@ abstract class CordaMigration : CustomTaskChange { _cordaDB = createDatabase(url, cacheFactory, identityService, schema) cordaDB.start(dataSource) identityService.database = cordaDB - - cordaDB.transaction { - val dbTransactions = DBTransactionStorage(cordaDB, cacheFactory, SimpleClock(Clock.systemUTC())) - val attachmentsService = NodeAttachmentService(metricRegistry, cacheFactory, cordaDB) - _servicesForResolution = MigrationServicesForResolution(identityService, attachmentsService, dbTransactions, cordaDB, cacheFactory) - } } private fun createDatabase(jdbcUrl: String, diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt deleted file mode 100644 index 0186b9659c..0000000000 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt +++ /dev/null @@ -1,175 +0,0 @@ -package net.corda.node.migration - -import net.corda.core.contracts.* -import net.corda.core.cordapp.CordappContext -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.deserialiseComponentGroup -import net.corda.core.internal.div -import net.corda.core.internal.readObject -import net.corda.core.node.NetworkParameters -import net.corda.core.node.ServicesForResolution -import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.node.services.TransactionStorage -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache -import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl -import net.corda.core.transactions.ContractUpgradeLedgerTransaction -import net.corda.core.transactions.NotaryChangeLedgerTransaction -import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.contextLogger -import net.corda.node.internal.DBNetworkParametersStorage -import net.corda.node.services.attachments.NodeAttachmentTrustCalculator -import net.corda.node.services.persistence.AttachmentStorageInternal -import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME -import net.corda.nodeapi.internal.network.SignedNetworkParameters -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.SchemaMigration -import java.nio.file.Paths -import java.time.Clock -import java.time.Duration -import java.util.Comparator.comparingInt - -class MigrationServicesForResolution( - override val identityService: IdentityService, - override val attachments: AttachmentStorageInternal, - private val transactions: TransactionStorage, - private val cordaDB: CordaPersistence, - cacheFactory: MigrationNamedCacheFactory -): ServicesForResolution { - - companion object { - val logger = contextLogger() - } - override val cordappProvider: CordappProvider - get() = object : CordappProvider { - - val cordappLoader = SchemaMigration.loader.get() - - override fun getAppContext(): CordappContext { - TODO("not implemented") - } - - override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { - TODO("not implemented") - } - } - private val cordappLoader = SchemaMigration.loader.get() - - private val attachmentTrustCalculator = NodeAttachmentTrustCalculator( - attachments, - cacheFactory - ) - - private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory) - - private fun defaultNetworkParameters(): NetworkParameters { - logger.warn("Using a dummy set of network parameters for migration.") - val clock = Clock.systemUTC() - return NetworkParameters( - 1, - listOf(), - 1, - 1, - clock.instant(), - 1, - mapOf(), - Duration.ZERO, - mapOf() - ) - } - - private fun getNetworkParametersFromFile(): SignedNetworkParameters? { - return try { - val dir = System.getProperty(SchemaMigration.NODE_BASE_DIR_KEY) - val path = Paths.get(dir) / NETWORK_PARAMS_FILE_NAME - path.readObject() - } catch (e: Exception) { - logger.info("Couldn't find network parameters file: ${e.message}. This is expected if the node is starting for the first time.") - null - } - } - - override val networkParametersService: NetworkParametersService = object : NetworkParametersService { - - private val storage = DBNetworkParametersStorage.createParametersMap(cacheFactory) - - private val filedParams = getNetworkParametersFromFile() - - override val defaultHash: SecureHash = filedParams?.raw?.hash ?: SecureHash.getZeroHash() - override val currentHash: SecureHash = cordaDB.transaction { - storage.allPersisted.use { - it.max(comparingInt { it.second.verified().epoch }).map { it.first }.orElse(defaultHash) - } - } - - override fun lookup(hash: SecureHash): NetworkParameters? { - // Note that the parameters in any file shouldn't be put into the database - this will be done by the node on startup. - return if (hash == filedParams?.raw?.hash) { - filedParams.raw.deserialize() - } else { - cordaDB.transaction { storage[hash]?.verified() } - } - } - } - - override val networkParameters: NetworkParameters = networkParametersService.lookup(networkParametersService.currentHash) - ?: getNetworkParametersFromFile()?.raw?.deserialize() - ?: defaultNetworkParameters() - - private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection): List> { - return try { - val txAttachments = tx.attachments.mapNotNull { attachments.openAttachment(it)} - val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( - txAttachments, - networkParameters, - tx.id, - attachmentTrustCalculator::calculate, - cordappLoader.appClassLoader, - attachmentsClassLoaderCache) { - deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true) - } - states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList() - } catch (e: Exception) { - // If there is no attachment that allows the state class to be deserialised correctly, then carpent a state class anyway. It - // might still be possible to access the participants depending on how the state class was serialised. - logger.debug("Could not use attachments to deserialise transaction output states for transaction ${tx.id}") - tx.outputs.filterIndexed { index, _ -> stateIndices.contains(index)} - } - } - - override fun loadState(stateRef: StateRef): TransactionState<*> { - val stx = transactions.getTransaction(stateRef.txhash) - ?: throw MigrationException("Could not get transaction with hash ${stateRef.txhash} out of vault") - val baseTx = stx.resolveBaseTransaction(this) - return when (baseTx) { - is NotaryChangeLedgerTransaction -> baseTx.outputs[stateRef.index] - is ContractUpgradeLedgerTransaction -> baseTx.outputs[stateRef.index] - is WireTransaction -> extractStateFromTx(baseTx, listOf(stateRef.index)).first() - else -> throw MigrationException("Unknown transaction type ${baseTx::class.qualifiedName} found when loading a state") - } - } - - override fun loadStates(stateRefs: Set): Set> { - return stateRefs.groupBy { it.txhash }.flatMap { - val stx = transactions.getTransaction(it.key) - ?: throw MigrationException("Could not get transaction with hash ${it.key} out of vault") - val baseTx = stx.resolveBaseTransaction(this) - val stateList = when (baseTx) { - is NotaryChangeLedgerTransaction -> it.value.map { stateRef -> StateAndRef(baseTx.outputs[stateRef.index], stateRef) } - is ContractUpgradeLedgerTransaction -> it.value.map { stateRef -> StateAndRef(baseTx.outputs[stateRef.index], stateRef) } - is WireTransaction -> extractStateFromTx(baseTx, it.value.map { stateRef -> stateRef.index }) - .mapIndexed {index, state -> StateAndRef(state, StateRef(baseTx.id, index)) } - else -> throw MigrationException("Unknown transaction type ${baseTx::class.qualifiedName} found when loading a state") - } - stateList - }.toSet() - } - - override fun loadContractAttachment(stateRef: StateRef): Attachment { - throw NotImplementedError() - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt index 28d8dc3a89..f47b80c374 100644 --- a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt @@ -1,13 +1,9 @@ package net.corda.node.migration import liquibase.database.Database -import net.corda.core.contracts.* -import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.internal.* import net.corda.core.utilities.contextLogger import net.corda.node.internal.DBNetworkParametersStorage import net.corda.node.internal.schemas.NodeInfoSchemaV1 @@ -16,103 +12,21 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.node.services.vault.toStateRef import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseTransaction -import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.nodeapi.internal.persistence.currentDBSession -import net.corda.serialization.internal.AMQP_P2P_CONTEXT -import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT -import net.corda.serialization.internal.CordaSerializationMagic -import net.corda.serialization.internal.SerializationFactoryImpl -import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme -import net.corda.serialization.internal.amqp.amqpMagic -import org.hibernate.Session import org.hibernate.query.Query -import java.util.concurrent.ForkJoinPool -import java.util.concurrent.ForkJoinTask -import java.util.concurrent.RecursiveAction import javax.persistence.criteria.Root import javax.persistence.criteria.Selection class VaultStateMigration : CordaMigration() { - companion object { - private val logger = contextLogger() - } - - private fun addStateParties(session: Session, stateAndRef: StateAndRef) { - val state = stateAndRef.state.data - val persistentStateRef = PersistentStateRef(stateAndRef.ref) - try { - state.participants.groupBy { it.owningKey }.forEach { participants -> - val persistentParty = VaultSchemaV1.PersistentParty(persistentStateRef, participants.value.first()) - session.persist(persistentParty) - } - } catch (e: AbstractMethodError) { - // This should only happen if there was no attachment that could be used to deserialise the output states, and the state was - // serialised such that the participants list cannot be accessed (participants is calculated and not marked as a - // SerializableCalculatedProperty. - throw VaultStateMigrationException("Cannot add state parties for state ${stateAndRef.ref} as state class is not on the " + - "classpath and participants cannot be synthesised") - } - } - - private fun getStateAndRef(persistentState: VaultSchemaV1.VaultStates): StateAndRef { - val persistentStateRef = persistentState.stateRef ?: - throw VaultStateMigrationException("Persistent state ref missing from state") - val stateRef = persistentStateRef.toStateRef() - val state = try { - servicesForResolution.loadState(stateRef) - } catch (e: Exception) { - throw VaultStateMigrationException("Could not load state for stateRef $stateRef : ${e.message}", e) - } - return StateAndRef(state, stateRef) - } - - override fun execute(database: Database?) { - logger.info("Migrating vault state data to V4 tables") - if (database == null) { - logger.error("Cannot migrate vault states: Liquibase failed to provide a suitable database connection") - throw VaultStateMigrationException("Cannot migrate vault states as liquibase failed to provide a suitable database connection") - } + override fun execute(database: Database) { initialiseNodeServices(database, setOf(VaultMigrationSchemaV1, VaultSchemaV1, NodeInfoSchemaV1)) - var statesSkipped = 0 val persistentStates = VaultStateIterator(cordaDB) if (persistentStates.numStates > 0) { - logger.warn("Found ${persistentStates.numStates} states to update from a previous version. This may take a while for large " - + "volumes of data.") + throw VaultStateMigrationException("Found ${persistentStates.numStates} states that need to be updated to V4. Please upgrade " + + "to an older version of Corda first to perform this migration.") } - val ourName = CordaX500Name.parse(System.getProperty(SchemaMigration.NODE_X500_NAME)) - VaultStateIterator.withSerializationEnv { - persistentStates.forEach { - val session = currentDBSession() - try { - val stateAndRef = getStateAndRef(it) - - addStateParties(session, stateAndRef) - - // Can get away without checking for AbstractMethodErrors here as these will have already occurred when trying to add - // state parties. - val myKeys = stateAndRef.state.data.participants.map { participant -> participant.owningKey} - .filter { key -> identityService.certificateFromKey(key)?.name == ourName }.toSet() - if (!NodeVaultService.isRelevant(stateAndRef.state.data, myKeys)) { - it.relevancyStatus = Vault.RelevancyStatus.NOT_RELEVANT - } - } catch (e: VaultStateMigrationException) { - logger.warn("An error occurred while migrating a vault state: ${e.message}. Skipping. This will cause the " + - "migration to fail.", e) - statesSkipped++ - } - } - } - if (statesSkipped > 0) { - logger.error("$statesSkipped states could not be migrated as there was no class available for them.") - throw VaultStateMigrationException("Failed to migrate $statesSkipped states in the vault. Check the logs for details of the " + - "error for each state.") - } - logger.info("Finished performing vault state data migration for ${persistentStates.numStates - statesSkipped} states") } } @@ -147,49 +61,9 @@ object VaultMigrationSchemaV1 : MappedSchema(schemaFamily = VaultMigrationSchema * Currently, this class filters out those persistent states that have entries in the state party table. This behaviour is required for the * vault state migration, as entries in this table should not be duplicated. Unconsumed states are also filtered out for performance. */ -class VaultStateIterator(private val database: CordaPersistence) : Iterator { +class VaultStateIterator(private val database: CordaPersistence) { companion object { val logger = contextLogger() - - private object AMQPInspectorSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { - return magic == amqpMagic - } - - override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() - override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() - } - - private fun initialiseSerialization() { - // Deserialise with the lenient carpenter as we only care for the AMQP field getters - _inheritableContextSerializationEnv.set(SerializationEnvironment.with( - SerializationFactoryImpl().apply { - registerScheme(AMQPInspectorSerializationScheme) - }, - p2pContext = AMQP_P2P_CONTEXT.withLenientCarpenter(), - storageContext = AMQP_STORAGE_CONTEXT.withLenientCarpenter() - )) - } - - private fun disableSerialization() { - _inheritableContextSerializationEnv.set(null) - } - - fun withSerializationEnv(block: () -> Unit) { - val newEnv = if (_allEnabledSerializationEnvs.isEmpty()) { - initialiseSerialization() - true - } else { - false - } - effectiveSerializationEnv.serializationFactory.withCurrentContext(effectiveSerializationEnv.storageContext.withLenientCarpenter()) { - block() - } - - if (newEnv) { - disableSerialization() - } - } } private val criteriaBuilder = database.entityManagerFactory.criteriaBuilder val numStates = getTotalStates() @@ -224,111 +98,6 @@ class VaultStateIterator(private val database: CordaPersistence) : Iterator { - endTransaction() - transaction = database.newTransaction() - val query = createVaultStatesQuery(VaultSchemaV1.VaultStates::class.java) { it } - // The above query excludes states that have entries in the state party table. As the iteration proceeds, each state has entries - // added to this table. The result is that when the next page is retrieved, any results that were in the previous page are not in - // the query at all! As such, the next set of states that need processing start at the first result. - query.firstResult = 0 - query.maxResults = pageSize - pageNumber++ - val result = query.resultList - logger.debug("Loaded page $pageNumber of ${(numStates - 1 / pageNumber.toLong()) + 1}. Current page has ${result.size} vault states") - return result - } - - private var currentIndex = 0 - - override fun hasNext(): Boolean { - val nextElementPresent = currentIndex + ((pageNumber - 1) * pageSize) < numStates - if (!nextElementPresent) { - endTransaction() - } - return nextElementPresent - } - - override fun next(): VaultSchemaV1.VaultStates { - if (currentIndex == pageSize) { - currentPage = getNextPage() - currentIndex = 0 - } - val stateToReturn = currentPage[currentIndex] - currentIndex++ - return stateToReturn - } - - // The rest of this class is an attempt at multithreading that was ultimately scuppered by liquibase not providing a connection pool. - // This may be useful as a starting point for improving performance of the migration, so is left here. To start using it, remove the - // serialization environment changes in the execute function in the migration, and change forEach -> parallelForEach. - private val pool = ForkJoinPool.commonPool() - - private class VaultPageTask(val database: CordaPersistence, - val page: List, - val block: (VaultSchemaV1.VaultStates) -> Unit): RecursiveAction() { - - private val pageSize = page.size - private val tolerance = 10 - - override fun compute() { - withSerializationEnv { - if (pageSize > tolerance) { - ForkJoinTask.invokeAll(createSubtasks()) - } else { - applyBlock() - } - } - } - - private fun createSubtasks(): List { - return listOf(VaultPageTask(database, page.subList(0, pageSize / 2), block), VaultPageTask(database, page.subList(pageSize / 2, pageSize), block)) - } - - private fun applyBlock() { - effectiveSerializationEnv.serializationFactory.withCurrentContext(effectiveSerializationEnv.storageContext.withLenientCarpenter()) { - database.transaction { - page.forEach { block(it) } - } - } - } - } - - private fun hasNextPage(): Boolean { - val nextPagePresent = pageNumber * pageSize < numStates - if (!nextPagePresent) { - endTransaction() - } - return nextPagePresent - } - - /** - * Iterate through all states in the vault, parallelizing the work on each page of vault states. - */ - fun parallelForEach(block: (VaultSchemaV1.VaultStates) -> Unit) { - pool.invoke(VaultPageTask(database, currentPage, block)) - while (hasNextPage()) { - currentPage = getNextPage() - pool.invoke(VaultPageTask(database, currentPage, block)) - } - } } class VaultStateMigrationException(msg: String, cause: Exception? = null) : Exception(msg, cause) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 0ee732b210..bea46121fc 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -5,8 +5,8 @@ import net.corda.core.context.InvocationContext import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic -import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.FlowStateMachineHandle import net.corda.core.internal.NamedCacheFactory @@ -17,6 +17,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.dependencies import net.corda.core.internal.requireSupportedHashType +import net.corda.core.internal.verification.Verifier import net.corda.core.internal.warnOnce import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping @@ -25,11 +26,13 @@ import net.corda.core.node.StatesToRecord import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCacheBase import net.corda.core.node.services.TransactionStorage +import net.corda.core.serialization.SerializationContext +import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction +import net.corda.core.transactions.defaultVerifier import net.corda.core.utilities.contextLogger import net.corda.node.internal.InitiatedFlowFactory -import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.DbTransactionsResolver import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService @@ -37,14 +40,11 @@ import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl +import net.corda.node.verification.NoDbAccessVerifier import net.corda.nodeapi.internal.persistence.CordaPersistence -import java.lang.IllegalStateException +import java.security.PublicKey import java.security.SignatureException -import java.util.ArrayList import java.util.Collections -import java.util.HashMap -import java.util.HashSet -import java.util.LinkedHashSet interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { override val nodeReady: OpenFuture @@ -186,11 +186,14 @@ interface ServiceHubInternal : ServiceHubCoreInternal { val configuration: NodeConfiguration val nodeProperties: NodePropertiesStore val networkMapUpdater: NetworkMapUpdater - override val cordappProvider: CordappProviderInternal fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? val cacheFactory: NamedCacheFactory + override fun createVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { + return NoDbAccessVerifier(defaultVerifier(ltx, serializationContext)) + } + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) = recordTransactions(statesToRecord, txs, SIGNATURE_VERIFICATION_DISABLED) diff --git a/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt b/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt index 2305203338..285f7fca5a 100644 --- a/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt +++ b/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt @@ -1,10 +1,16 @@ package net.corda.node.services.attachments -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash -import net.corda.core.internal.* +import net.corda.core.internal.AbstractAttachment +import net.corda.core.internal.AttachmentTrustCalculator +import net.corda.core.internal.AttachmentTrustInfo +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.TRUSTED_UPLOADERS +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.hash +import net.corda.core.internal.isUploaderTrusted import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.Builder @@ -35,10 +41,7 @@ class NodeAttachmentTrustCalculator( ) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys) // A cache for caching whether a signing key is trusted - private val trustedKeysCache = cacheFactory.buildNamed( - Caffeine.newBuilder(), - "NodeAttachmentTrustCalculator_trustedKeysCache" - ) + private val trustedKeysCache = cacheFactory.buildNamed("NodeAttachmentTrustCalculator_trustedKeysCache") override fun calculate(attachment: Attachment): Boolean { @@ -92,9 +95,7 @@ class NodeAttachmentTrustCalculator( val trustRoot = if (attachment.isSignedByBlacklistedKey()) { null } else { - attachment.signerKeys - .mapNotNull { publicKeyToTrustRootMap[it] } - .firstOrNull() + attachment.signerKeys.firstNotNullOfOrNull { publicKeyToTrustRootMap[it] } } attachmentTrustInfos += AttachmentTrustInfo( attachmentId = attachment.id, diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt index fd3d01431d..c40b9cad89 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt @@ -1,6 +1,5 @@ package net.corda.node.services.persistence -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.crypto.toStringShort import net.corda.core.internal.NamedCacheFactory import net.corda.core.utilities.contextLogger @@ -19,10 +18,7 @@ class PublicKeyToOwningIdentityCacheImpl(private val database: CordaPersistence, val log = contextLogger() } - private val cache = cacheFactory.buildNamed( - Caffeine.newBuilder(), - "PublicKeyToOwningIdentityCache_cache" - ) + private val cache = cacheFactory.buildNamed("PublicKeyToOwningIdentityCache_cache") /** * Return the owning identity associated with a given key. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index dfd2b1a8db..178182a9d9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -3,10 +3,12 @@ package net.corda.node.services.statemachine import com.google.common.primitives.Primitives import net.corda.core.flows.* import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.loadClassOfType import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import org.slf4j.Logger +import java.lang.ClassCastException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.TypeVariable @@ -57,13 +59,13 @@ open class FlowLogicRefFactoryImpl(private val classloader: ClassLoader) : Singl } private fun validatedFlowClassFromName(flowClassName: String): Class> { - val forName = try { - Class.forName(flowClassName, true, classloader) + return try { + loadClassOfType>(flowClassName, true, classloader) } catch (e: ClassNotFoundException) { throw IllegalFlowLogicException(flowClassName, "Flow not found: $flowClassName") - } - return forName.asSubclass(FlowLogic::class.java) ?: + } catch (e: ClassCastException) { throw IllegalFlowLogicException(flowClassName, "The class $flowClassName is not a subclass of FlowLogic.") + } } override fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt deleted file mode 100644 index e70712e9e0..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt +++ /dev/null @@ -1,120 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.TransactionVerificationException.BrokenTransactionException -import net.corda.core.internal.TransactionVerifierServiceInternal -import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.internalFindTrustedAttachmentForClass -import net.corda.core.internal.prepareVerify -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.contextLogger -import net.corda.node.internal.cordapp.CordappProviderInternal -import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess - -class InMemoryTransactionVerifierService( - @Suppress("UNUSED_PARAMETER") numberOfWorkers: Int, - private val cordappProvider: CordappProviderInternal, - private val attachments: AttachmentStorage -) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable { - companion object { - private val SEPARATOR = System.lineSeparator() + "-> " - private val log = contextLogger() - - fun Collection<*>.deepEquals(other: Collection<*>): Boolean { - return size == other.size && containsAll(other) && other.containsAll(this) - } - - fun Collection.toPrettyString(): String { - return joinToString(separator = SEPARATOR, prefix = SEPARATOR, postfix = System.lineSeparator()) { attachment -> - attachment.id.toString() - } - } - } - - override fun verify(transaction: LedgerTransaction): CordaFuture<*> { - return openFuture().apply { - capture { - val verifier = transaction.prepareVerify(transaction.attachments) - withoutDatabaseAccess { - verifier.verify() - } - } - } - } - - private fun computeReplacementAttachmentsFor(ltx: LedgerTransaction, missingClass: String?): Collection { - val replacements = cordappProvider.fixupAttachments(ltx.attachments) - return if (replacements.deepEquals(ltx.attachments)) { - /* - * We cannot continue unless we have some idea which - * class is missing from the attachments. - */ - if (missingClass == null) { - throw BrokenTransactionException( - txId = ltx.id, - message = "No fix-up rules provided for broken attachments:${replacements.toPrettyString()}" - ) - } - - /* - * The Node's fix-up rules have not been able to adjust the transaction's attachments, - * so resort to the original mechanism of trying to find an attachment that contains - * the missing class. (Do you feel lucky, Punk?) - */ - val extraAttachment = requireNotNull(attachments.internalFindTrustedAttachmentForClass(missingClass)) { - """Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda - |when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass, - |but could not find one. - |If you wish to verify this transaction, please contact the originator of the transaction and install the - |provided missing JAR. - |You can install it using the RPC command: `uploadAttachment` without restarting the node. - |""".trimMargin() - } - - replacements.toMutableSet().apply { - /* - * Check our transaction doesn't already contain this extra attachment. - * It seems unlikely that we would, but better safe than sorry! - */ - if (!add(extraAttachment)) { - throw BrokenTransactionException( - txId = ltx.id, - message = "Unlinkable class $missingClass inside broken attachments:${replacements.toPrettyString()}" - ) - } - - log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies. - |This may be the result of a bug in a previous version of Corda. - |Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass. - |Please check with the originator that this is a valid transaction. - |YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN! - |WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED! - |PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE! - |THIS MESSAGE IS **SUPPOSED** TO BE SCARY!! - |""".trimMargin() - ) - } - } else { - replacements - } - } - - override fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> { - return openFuture().apply { - capture { - val replacementAttachments = computeReplacementAttachmentsFor(transaction, missingClass) - log.warn("Reverifying transaction {} with attachments:{}", transaction.id, replacementAttachments.toPrettyString()) - val verifier = transaction.prepareVerify(replacementAttachments.toList()) - withoutDatabaseAccess { - verifier.verify() - } - } - } - } - - override fun close() {} -} diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 098c0d2155..6a20f2c4e5 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -19,8 +19,10 @@ import net.corda.core.internal.ThreadBox import net.corda.core.internal.TransactionDeserialisationException import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.mapToSet import net.corda.core.internal.tee import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.internal.warnOnce import net.corda.core.messaging.DataFeed import net.corda.core.node.StatesToRecord @@ -50,7 +52,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.trace -import net.corda.node.internal.NodeServicesForResolution import net.corda.node.services.api.SchemaService import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.schema.PersistentStateService @@ -80,8 +81,6 @@ import javax.persistence.criteria.CriteriaQuery import javax.persistence.criteria.CriteriaUpdate import javax.persistence.criteria.Predicate import javax.persistence.criteria.Root -import kotlin.collections.ArrayList -import kotlin.collections.LinkedHashSet import kotlin.collections.component1 import kotlin.collections.component2 @@ -98,7 +97,7 @@ import kotlin.collections.component2 class NodeVaultService( private val clock: Clock, private val keyManagementService: KeyManagementService, - private val servicesForResolution: NodeServicesForResolution, + private val serviceHub: VerifyingServiceHub, private val database: CordaPersistence, schemaService: SchemaService, private val appClassloader: ClassLoader @@ -231,7 +230,7 @@ class NodeVaultService( // Persist the consumed inputs. consumedStateRefs.forEach { stateRef -> - val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) + val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) state?.run { // Only update the state if it has not previously been consumed (this could have happened if the transaction is being // re-recorded. @@ -312,7 +311,7 @@ class NodeVaultService( fun withValidDeserialization(list: List, txId: SecureHash): Map { var error: TransactionDeserialisationException? = null - val map = (0 until list.size).mapNotNull { idx -> + val map = list.indices.mapNotNull { idx -> try { idx to list[idx] } catch (e: TransactionDeserialisationException) { @@ -354,7 +353,7 @@ class NodeVaultService( val outputRefs = tx.outRefsOfType().map { it.ref } val seenRefs = loadStates(outputRefs).map { it.ref } val unseenRefs = outputRefs - seenRefs - val unseenOutputIdxs = unseenRefs.map { it.index }.toSet() + val unseenOutputIdxs = unseenRefs.mapToSet { it.index } outputs.filter { it.key in unseenOutputIdxs } } else { outputs @@ -383,7 +382,7 @@ class NodeVaultService( StatesToRecord.ALL_VISIBLE, StatesToRecord.ONLY_RELEVANT -> { val notSeenReferences = tx.references - loadStates(tx.references).map { it.ref } // TODO: This is expensive - is there another way? - tx.toLedgerTransaction(servicesForResolution).deserializableRefStates() + tx.toLedgerTransaction(serviceHub).deserializableRefStates() .filter { (_, stateAndRef) -> stateAndRef.ref in notSeenReferences } .values } @@ -398,8 +397,8 @@ class NodeVaultService( // We also can't do filtering beforehand, since for notary change transactions output encumbrance pointers // get recalculated based on input positions. val ltx: FullTransaction = when (tx) { - is NotaryChangeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) - is ContractUpgradeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) + is NotaryChangeWireTransaction -> tx.resolve(serviceHub, emptyList()) + is ContractUpgradeWireTransaction -> tx.resolve(serviceHub, emptyList()) else -> throw IllegalArgumentException("Unsupported transaction type: ${tx.javaClass.name}") } val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) } @@ -542,8 +541,8 @@ class NodeVaultService( val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) val lockIdPredicate = criteriaBuilder.or(get(VaultSchemaV1.VaultStates::lockId.name).isNull, criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) - update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) update.where(stateStatusPredication, lockIdPredicate, *commonPredicates) } if (updatedRows > 0 && updatedRows == stateRefs.size) { @@ -596,8 +595,8 @@ class NodeVaultService( criteriaBuilder.executeUpdate(session, stateRefs) { update, persistentStateRefs -> val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) configure(update, arrayOf(stateStatusPredication, lockIdPredicate), persistentStateRefs) } @@ -748,16 +747,13 @@ class NodeVaultService( if (result0 is VaultSchemaV1.VaultStates) { statesMetadata.add(result0.toStateMetadata()) } else { - log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + log.debug { "OtherResults: ${result.toArray().contentToString()}" } otherResults.addAll(result.toArray().asList()) } } } - val states: List> = servicesForResolution.loadStates( - statesMetadata.mapTo(LinkedHashSet()) { it.ref }, - ArrayList() - ) + val states: List> = serviceHub.loadStatesInternal(statesMetadata.mapToSet { it.ref }, ArrayList()) val totalStatesAvailable = when { paging.isDefault -> -1L @@ -844,9 +840,8 @@ class NodeVaultService( log.warn("trackBy is called with an already existing, open DB transaction. As a result, there might be states missing from both the snapshot and observable, included in the returned data feed, because of race conditions.") } val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - val snapshotStatesRefs = snapshotResults.statesMetadata.map { it.ref }.toSet() - val snapshotConsumedStatesRefs = snapshotResults.statesMetadata.filter { it.consumedTime != null } - .map { it.ref }.toSet() + val snapshotStatesRefs = snapshotResults.statesMetadata.mapToSet { it.ref } + val snapshotConsumedStatesRefs = snapshotResults.statesMetadata.filter { it.consumedTime != null }.mapToSet { it.ref } val filteredUpdates = updates.filter { it.containsType(contractStateType, snapshotResults.stateTypes) } .map { filterContractStates(it, contractStateType) } .filter { !hasBeenSeen(it, snapshotStatesRefs, snapshotConsumedStatesRefs) } @@ -881,8 +876,8 @@ class NodeVaultService( * the snapshot or in the observable). */ private fun hasBeenSeen(update: Vault.Update, snapshotStatesRefs: Set, snapshotConsumedStatesRefs: Set): Boolean { - val updateProducedStatesRefs = update.produced.map { it.ref }.toSet() - val updateConsumedStatesRefs = update.consumed.map { it.ref }.toSet() + val updateProducedStatesRefs = update.produced.mapToSet { it.ref } + val updateConsumedStatesRefs = update.consumed.mapToSet { it.ref } return snapshotStatesRefs.containsAll(updateProducedStatesRefs) && snapshotConsumedStatesRefs.containsAll(updateConsumedStatesRefs) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt index 4121a92cb9..4902ada180 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt @@ -1,6 +1,5 @@ package net.corda.node.utilities -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.VisibleForTesting import net.corda.nodeapi.internal.persistence.contextTransactionOrNull @@ -101,7 +100,7 @@ class InfrequentlyMutatedCache(name: String, cacheFactory: Nam } } - private val backingCache = cacheFactory.buildNamed>(Caffeine.newBuilder(), name) + private val backingCache = cacheFactory.buildNamed>(name) // This protects against the cache purging something that is marked as invalid and thus we "forget" it shouldn't be cached. private val currentlyInvalid = ConcurrentHashMap>() diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt index 25688233c2..42b7738a1d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -15,8 +15,7 @@ class NonInvalidatingCache private constructor( private companion object { private fun buildCache(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V): LoadingCache { - val builder = Caffeine.newBuilder() - return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction)) + return cacheFactory.buildNamed(name, NonInvalidatingCacheLoader(loadFunction)) } } diff --git a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt new file mode 100644 index 0000000000..4b82ab3cf1 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt @@ -0,0 +1,229 @@ +package net.corda.node.verification + +import net.corda.core.contracts.Attachment +import net.corda.core.internal.AbstractAttachment +import net.corda.core.internal.copyTo +import net.corda.core.internal.div +import net.corda.core.internal.mapToSet +import net.corda.core.internal.readFully +import net.corda.core.serialization.serialize +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.services.api.ServiceHubInternal +import net.corda.serialization.internal.GeneratedAttachment +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.customSerializers +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.serializationWhitelists +import net.corda.serialization.internal.verifier.AttachmentWithTrust +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentsResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachment +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment +import net.corda.serialization.internal.verifier.readCordaSerializable +import net.corda.serialization.internal.verifier.writeCordaSerializable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException +import java.lang.ProcessBuilder.Redirect +import java.net.ServerSocket +import java.net.Socket +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import kotlin.io.path.createDirectories + +/** + * Handle to the node's external verifier. The verifier process is started lazily on the first verification request. + */ +@Suppress("TooGenericExceptionCaught") +class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoCloseable { + companion object { + private val log = contextLogger() + + private const val MAX_ATTEMPTS = 5 + + private val verifierJar: Path = Files.createTempFile("corda-external-verifier", ".jar") + init { + // Extract the embedded verifier jar + Companion::class.java.getResourceAsStream("external-verifier.jar")!!.use { + it.copyTo(verifierJar, REPLACE_EXISTING) + } + verifierJar.toFile().deleteOnExit() + } + } + + private lateinit var server: ServerSocket + @Volatile + private var connection: Connection? = null + + fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) { + log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures") + // By definition input states are unique, and so it makes sense to eagerly send them across with the transaction. + // Reference states are not, but for now we'll send them anyway and assume they aren't used often. If this assumption is not + // correct, and there's a benefit, then we can send them lazily. + val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(serviceHub::getSerializedState) + val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures) + + // To keep things simple the verifier only supports one verification request at a time. + synchronized(this) { + startServer() + var attempt = 1 + while (true) { + val result = try { + tryVerification(request) + } catch (e: Exception) { + processError(attempt, e) + attempt += 1 + continue + } + when (result) { + is Try.Success -> return + is Try.Failure -> throw result.exception + } + } + } + } + + private fun startServer() { + if (::server.isInitialized) return + server = ServerSocket(0) + // Just in case... + Runtime.getRuntime().addShutdownHook(Thread(::close)) + } + + private fun processError(attempt: Int, e: Exception) { + if (attempt == MAX_ATTEMPTS) { + throw IOException("Unable to verify with external verifier", e) + } else { + log.warn("Unable to verify with external verifier, trying again...", e) + } + try { + connection?.close() + } catch (e: Exception) { + log.debug("Problem closing external verifier connection", e) + } + connection = null + } + + private fun tryVerification(request: VerificationRequest): Try { + val connection = getConnection() + connection.toVerifier.writeCordaSerializable(request) + // Send the verification request and then wait for any requests from verifier for more information. The last message will either + // be a verification success or failure message. + while (true) { + val message = connection.fromVerifier.readCordaSerializable() + log.debug { "Received from external verifier: $message" } + when (message) { + // Process the information the verifier needs and then loop back and wait for more messages + is VerifierRequest -> processVerifierRequest(message, connection) + is VerificationResult -> return message.result + } + } + } + + private fun getConnection(): Connection { + return connection ?: Connection().also { connection = it } + } + + private fun processVerifierRequest(request: VerifierRequest, connection: Connection) { + val result = when (request) { + is GetParties -> PartiesResult(serviceHub.getParties(request.keys)) + is GetAttachment -> AttachmentResult(prepare(serviceHub.attachments.openAttachment(request.id))) + is GetAttachments -> AttachmentsResult(serviceHub.getAttachments(request.ids).map(::prepare)) + is GetNetworkParameters -> NetworkParametersResult(serviceHub.getNetworkParameters(request.id)) + is GetTrustedClassAttachment -> TrustedClassAttachmentResult(serviceHub.getTrustedClassAttachment(request.className)?.id) + } + log.debug { "Sending response to external verifier: $result" } + connection.toVerifier.writeCordaSerializable(result) + } + + private fun prepare(attachment: Attachment?): AttachmentWithTrust? { + if (attachment == null) return null + val isTrusted = serviceHub.isAttachmentTrusted(attachment) + val attachmentForSer = when (attachment) { + // The Attachment retrieved from the database is not serialisable, so we have to convert it into one + is AbstractAttachment -> GeneratedAttachment(attachment.open().readFully(), attachment.uploader) + // For everything else we keep as is, in particular preserving ContractAttachment + else -> attachment + } + return AttachmentWithTrust(attachmentForSer, isTrusted) + } + + override fun close() { + connection?.let { + connection = null + try { + it.close() + } finally { + server.close() + } + } + } + + private inner class Connection : AutoCloseable { + private val verifierProcess: Process + private val socket: Socket + val toVerifier: DataOutputStream + val fromVerifier: DataInputStream + + init { + val logsDirectory = (serviceHub.configuration.baseDirectory / "logs").createDirectories() + val command = listOf( + "${System.getProperty("java.home") / "bin" / "java"}", + "-jar", + "$verifierJar", + "${server.localPort}", + System.getProperty("log4j2.level")?.lowercase() ?: "info" // TODO + ) + log.debug { "Verifier command: $command" } + verifierProcess = ProcessBuilder(command) + .redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile())) + .redirectError(Redirect.appendTo((logsDirectory / "verifier-stderr.log").toFile())) + .directory(serviceHub.configuration.baseDirectory.toFile()) + .start() + log.info("External verifier process started; PID ${verifierProcess.pid()}") + + verifierProcess.onExit().whenComplete { _, _ -> + if (connection != null) { + log.error("The external verifier has unexpectedly terminated with error code ${verifierProcess.exitValue()}. " + + "Please check verifier logs for more details.") + } + // Allow a new process to be started on the next verification request + connection = null + } + + socket = server.accept() + toVerifier = DataOutputStream(socket.outputStream) + fromVerifier = DataInputStream(socket.inputStream) + + val cordapps = serviceHub.cordappProvider.cordapps + val initialisation = Initialisation( + customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name }, + serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name }, + System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization + serializedCurrentNetworkParameters = serviceHub.networkParameters.serialize() + ) + toVerifier.writeCordaSerializable(initialisation) + } + + override fun close() { + try { + socket.close() + } finally { + verifierProcess.destroyForcibly() + } + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt b/node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt new file mode 100644 index 0000000000..992031cb80 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt @@ -0,0 +1,12 @@ +package net.corda.node.verification + +import net.corda.core.internal.verification.Verifier +import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess + +class NoDbAccessVerifier(private val delegate: Verifier) : Verifier { + override fun verify() { + withoutDatabaseAccess { + delegate.verify() + } + } +} diff --git a/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt b/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt index 1837a8fb49..e920a197f0 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt @@ -1,10 +1,11 @@ package net.corda.node.internal +import net.corda.core.CordaException import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.utilities.ByteSequence -import net.corda.node.internal.classloading.scanForCustomSerializationScheme +import net.corda.serialization.internal.verifier.loadCustomSerializationScheme import org.junit.Test import org.mockito.Mockito import kotlin.test.assertFailsWith @@ -33,7 +34,7 @@ class CustomSerializationSchemeScanningTest { @Test(timeout = 300_000) fun `Can scan for custom serialization scheme and build a serialization scheme`() { - val scheme = scanForCustomSerializationScheme(DummySerializationScheme::class.java.name, this::class.java.classLoader) + val scheme = loadCustomSerializationScheme(DummySerializationScheme::class.java.name, this::class.java.classLoader) val mockContext = Mockito.mock(SerializationContext::class.java) assertFailsWith("Tried to serialize with DummySerializationScheme") { scheme.serialize(Any::class.java, mockContext) @@ -43,27 +44,27 @@ class CustomSerializationSchemeScanningTest { @Test(timeout = 300_000) fun `verification fails with a helpful error if the class is not found in the classloader`() { val missingClassName = "org.testing.DoesNotExist" - assertFailsWith("$missingClassName was declared as a custom serialization scheme but could not " + + assertFailsWith("$missingClassName was declared as a custom serialization scheme but could not " + "be found.") { - scanForCustomSerializationScheme(missingClassName, this::class.java.classLoader) + loadCustomSerializationScheme(missingClassName, this::class.java.classLoader) } } @Test(timeout = 300_000) fun `verification fails with a helpful error if the class is not a custom serialization scheme`() { val schemeName = NonSerializationScheme::class.java.name - assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + + assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + "implement CustomSerializationScheme.") { - scanForCustomSerializationScheme(schemeName, this::class.java.classLoader) + loadCustomSerializationScheme(schemeName, this::class.java.classLoader) } } @Test(timeout = 300_000) fun `verification fails with a helpful error if the class does not have a no arg constructor`() { val schemeName = DummySerializationSchemeWithoutNoArgConstructor::class.java.name - assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + + assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + "have a no argument constructor.") { - scanForCustomSerializationScheme(schemeName, this::class.java.classLoader) + loadCustomSerializationScheme(schemeName, this::class.java.classLoader) } } } diff --git a/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt b/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt deleted file mode 100644 index b0a5a0e778..0000000000 --- a/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt +++ /dev/null @@ -1,637 +0,0 @@ -package net.corda.node.migration - -import liquibase.database.Database -import liquibase.database.jvm.JdbcConnection -import net.corda.core.contracts.Amount -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.Issued -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignableData -import net.corda.core.crypto.SignatureMetadata -import net.corda.core.crypto.toStringShort -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.NotaryChangeTransactionBuilder -import net.corda.core.internal.packageName -import net.corda.core.internal.signWithCert -import net.corda.core.node.NetworkParameters -import net.corda.core.node.NotaryInfo -import net.corda.core.node.services.Vault -import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.serialize -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.contextLogger -import net.corda.finance.DOLLARS -import net.corda.finance.contracts.Commodity -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.Obligation -import net.corda.finance.contracts.asset.OnLedgerAsset -import net.corda.finance.schemas.CashSchemaV1 -import net.corda.node.internal.DBNetworkParametersStorage -import net.corda.node.internal.schemas.NodeInfoSchemaV1 -import net.corda.node.services.identity.PersistentIdentityService -import net.corda.node.services.keys.BasicHSMKeyManagementService -import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.contextTransactionOrNull -import net.corda.nodeapi.internal.persistence.currentDBSession -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.BOC_NAME -import net.corda.testing.core.CHARLIE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.core.TestIdentity -import net.corda.testing.core.dummyCommand -import net.corda.testing.internal.configureDatabase -import net.corda.testing.internal.vault.CommodityState -import net.corda.testing.internal.vault.DUMMY_LINEAR_CONTRACT_PROGRAM_ID -import net.corda.testing.internal.vault.DummyLinearContract -import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 -import net.corda.testing.node.MockServices -import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.TestClock -import net.corda.testing.node.makeTestIdentityService -import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.After -import org.junit.Before -import org.junit.ClassRule -import org.junit.Ignore -import org.junit.Test -import org.mockito.Mockito -import java.security.KeyPair -import java.time.Clock -import java.time.Duration -import java.time.Instant -import java.util.Currency -import java.util.Properties -import kotlin.collections.List -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.first -import kotlin.collections.forEach -import kotlin.collections.forEachIndexed -import kotlin.collections.groupBy -import kotlin.collections.listOf -import kotlin.collections.map -import kotlin.collections.mapOf -import kotlin.collections.plus -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse - -/** - * These tests aim to verify that migrating vault states from V3 to later versions works correctly. While these unit tests verify the - * migrating behaviour is correct (tables populated, columns updated for the right states), it comes with a caveat: they do not test that - * deserialising states with the attachment classloader works correctly. - * - * The reason for this is that it is impossible to do so. There is no real way of writing a unit or integration test to upgrade from one - * version to another (at the time of writing). These tests simulate a small part of the upgrade process by directly using hibernate to - * populate a database as a V3 node would, then running the migration class. However, it is impossible to do this for attachments as there - * is no contract state jar to serialise. - */ -class VaultStateMigrationTest { - companion object { - val alice = TestIdentity(ALICE_NAME, 70) - val bankOfCorda = TestIdentity(BOC_NAME) - val bob = TestIdentity(BOB_NAME, 80) - private val charlie = TestIdentity(CHARLIE_NAME, 90) - val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) - val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) - val ALICE get() = alice.party - val ALICE_IDENTITY get() = alice.identity - val BOB get() = bob.party - val BOB_IDENTITY get() = bob.identity - val BOC_IDENTITY get() = bankOfCorda.identity - val BOC_KEY get() = bankOfCorda.keyPair - val CHARLIE get() = charlie.party - val DUMMY_NOTARY get() = dummyNotary.party - val bob2 = TestIdentity(BOB_NAME, 40) - val BOB2 = bob2.party - val BOB2_IDENTITY = bob2.identity - - val clock: TestClock = TestClock(Clock.systemUTC()) - - @ClassRule - @JvmField - val testSerialization = SerializationEnvironmentRule() - - val logger = contextLogger() - } - - val cordappPackages = listOf( - "net.corda.finance.contracts", - CashSchemaV1::class.packageName, - DummyLinearStateSchemaV1::class.packageName) - - lateinit var liquibaseDB: Database - lateinit var cordaDB: CordaPersistence - lateinit var notaryServices: MockServices - - @Before - fun setUp() { - val identityService = makeTestIdentityService(dummyNotary.identity, BOB_IDENTITY, ALICE_IDENTITY) - notaryServices = MockServices(cordappPackages, dummyNotary, identityService, dummyCashIssuer.keyPair, BOC_KEY) - cordaDB = configureDatabase( - makeTestDataSourceProperties(), - DatabaseConfig(), - notaryServices.identityService::wellKnownPartyFromX500Name, - notaryServices.identityService::wellKnownPartyFromAnonymous, - ourName = BOB_IDENTITY.name) - val liquibaseConnection = Mockito.mock(JdbcConnection::class.java) - Mockito.`when`(liquibaseConnection.url).thenReturn(cordaDB.jdbcUrl) - Mockito.`when`(liquibaseConnection.wrappedConnection).thenReturn(cordaDB.dataSource.connection) - liquibaseDB = Mockito.mock(Database::class.java) - Mockito.`when`(liquibaseDB.connection).thenReturn(liquibaseConnection) - - saveOurKeys(listOf(bob.keyPair, bob2.keyPair)) - saveAllIdentities(listOf(BOB_IDENTITY, ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity, BOB2_IDENTITY)) - addNetworkParameters() - } - - @After - fun close() { - contextTransactionOrNull?.close() - cordaDB.close() - } - - private fun addNetworkParameters() { - cordaDB.transaction { - val clock = Clock.systemUTC() - val params = NetworkParameters( - 1, - listOf(NotaryInfo(DUMMY_NOTARY, false), NotaryInfo(CHARLIE, false)), - 1, - 1, - clock.instant(), - 1, - mapOf(), - Duration.ZERO, - mapOf() - ) - val signedParams = params.signWithCert(bob.keyPair.private, BOB_IDENTITY.certificate) - val persistentParams = DBNetworkParametersStorage.PersistentNetworkParameters( - SecureHash.allOnesHash.toString(), - params.epoch, - signedParams.raw.bytes, - signedParams.sig.bytes, - signedParams.sig.by.encoded, - X509Utilities.buildCertPath(signedParams.sig.parentCertsChain).encoded - ) - session.save(persistentParams) - } - } - - private fun createCashTransaction(cash: Cash, value: Amount, owner: AbstractParty): SignedTransaction { - val tx = TransactionBuilder(DUMMY_NOTARY) - cash.generateIssue(tx, Amount(value.quantity, Issued(bankOfCorda.ref(1), value.token)), owner, DUMMY_NOTARY) - return notaryServices.signInitialTransaction(tx, bankOfCorda.party.owningKey) - } - - private fun createVaultStatesFromTransaction(tx: SignedTransaction, stateStatus: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) { - cordaDB.transaction { - tx.coreTransaction.outputs.forEachIndexed { index, state -> - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val persistentState = VaultSchemaV1.VaultStates( - notary = state.notary, - contractStateClassName = state.data.javaClass.name, - stateStatus = stateStatus, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.RELEVANT, //Always persist as relevant to mimic V3 - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - persistentState.stateRef = PersistentStateRef(tx.id.toString(), index) - session.save(persistentState) - } - } - } - - private fun saveOurKeys(keys: List) { - cordaDB.transaction { - keys.forEach { - val persistentKey = BasicHSMKeyManagementService.PersistentKey(it.public, it.private) - session.save(persistentKey) - } - } - } - - private fun saveAllIdentities(identities: List) { - cordaDB.transaction { - identities.groupBy { it.name }.forEach { (_, certs) -> - val persistentIDs = certs.map { PersistentIdentityService.PersistentPublicKeyHashToCertificate(it.owningKey.toStringShort(), it.certPath.encoded) } - persistentIDs.forEach { session.save(it) } - val networkIdentity = NodeInfoSchemaV1.DBPartyAndCertificate(certs.first(), true) - val persistentNodeInfo = NodeInfoSchemaV1.PersistentNodeInfo(0, "", listOf(), listOf(networkIdentity), 0, 0) - session.save(persistentNodeInfo) - } - } - } - - private fun storeTransaction(tx: SignedTransaction) { - cordaDB.transaction { - val persistentTx = DBTransactionStorage.DBTransaction( - txId = tx.id.toString(), - stateMachineRunId = null, - transaction = tx.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes, - status = DBTransactionStorage.TransactionStatus.VERIFIED, - timestamp = Instant.now(), - signatures = null - ) - session.save(persistentTx) - } - } - - private fun getVaultStateCount(relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL): Long { - return cordaDB.transaction { - val criteriaBuilder = cordaDB.entityManagerFactory.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) - val queryRootStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - criteriaQuery.select(criteriaBuilder.count(queryRootStates)) - if (relevancyStatus != Vault.RelevancyStatus.ALL) { - criteriaQuery.where(criteriaBuilder.equal(queryRootStates.get("relevancyStatus"), relevancyStatus)) - } - val query = session.createQuery(criteriaQuery) - query.singleResult - } - } - - private fun getStatePartyCount(): Long { - return cordaDB.transaction { - val criteriaBuilder = cordaDB.entityManagerFactory.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) - val queryRootStates = criteriaQuery.from(VaultSchemaV1.PersistentParty::class.java) - criteriaQuery.select(criteriaBuilder.count(queryRootStates)) - val query = session.createQuery(criteriaQuery) - query.singleResult - } - } - - private fun addCashStates(statesToAdd: Int, owner: AbstractParty, stateStatus: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) { - val cash = Cash() - cordaDB.transaction { - (1..statesToAdd).map { createCashTransaction(cash, it.DOLLARS, owner) }.forEach { - storeTransaction(it) - createVaultStatesFromTransaction(it, stateStatus) - } - } - } - - private fun createLinearStateTransaction(idString: String, - parties: List = listOf(), - linearString: String = "foo", - linearNumber: Long = 0L, - linearBoolean: Boolean = false): SignedTransaction { - val tx = TransactionBuilder(notary = dummyNotary.party).apply { - addOutputState(DummyLinearContract.State( - linearId = UniqueIdentifier(idString), - participants = parties, - linearString = linearString, - linearNumber = linearNumber, - linearBoolean = linearBoolean, - linearTimestamp = clock.instant()), DUMMY_LINEAR_CONTRACT_PROGRAM_ID - ) - addCommand(dummyCommand()) - } - return notaryServices.signInitialTransaction(tx) - } - - private fun addLinearStates(statesToAdd: Int, parties: List) { - cordaDB.transaction { - (1..statesToAdd).map { createLinearStateTransaction("A".repeat(it), parties) }.forEach { - storeTransaction(it) - createVaultStatesFromTransaction(it) - } - } - } - - private fun createCommodityTransaction(amount: Amount>, owner: AbstractParty): SignedTransaction { - val txBuilder = TransactionBuilder(notary = dummyNotary.party) - OnLedgerAsset.generateIssue(txBuilder, TransactionState(CommodityState(amount, owner), Obligation.PROGRAM_ID, dummyNotary.party), Obligation.Commands.Issue()) - return notaryServices.signInitialTransaction(txBuilder) - } - - private fun addCommodityStates(statesToAdd: Int, owner: AbstractParty) { - cordaDB.transaction { - (1..statesToAdd).map { - createCommodityTransaction(Amount(it.toLong(), Issued(bankOfCorda.ref(2), Commodity.getInstance("FCOJ")!!)), owner) - }.forEach { - storeTransaction(it) - createVaultStatesFromTransaction(it) - } - } - } - - private fun createNotaryChangeTransaction(inputs: List, paramsHash: SecureHash): SignedTransaction { - val notaryTx = NotaryChangeTransactionBuilder(inputs, DUMMY_NOTARY, CHARLIE, paramsHash).build() - val notaryKey = DUMMY_NOTARY.owningKey - val signableData = SignableData(notaryTx.id, SignatureMetadata(3, Crypto.findSignatureScheme(notaryKey).schemeNumberID)) - val notarySignature = notaryServices.keyManagementService.sign(signableData, notaryKey) - return SignedTransaction(notaryTx, listOf(notarySignature)) - } - - private fun createVaultStatesFromNotaryChangeTransaction(tx: SignedTransaction, inputs: List>) { - cordaDB.transaction { - inputs.forEachIndexed { index, state -> - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val persistentState = VaultSchemaV1.VaultStates( - notary = tx.notary!!, - contractStateClassName = state.data.javaClass.name, - stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.RELEVANT, //Always persist as relevant to mimic V3 - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - persistentState.stateRef = PersistentStateRef(tx.id.toString(), index) - session.save(persistentState) - } - } - } - - private fun getState(clazz: Class): T { - return cordaDB.transaction { - val criteriaBuilder = cordaDB.entityManagerFactory.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(clazz) - val queryRootStates = criteriaQuery.from(clazz) - criteriaQuery.select(queryRootStates) - val query = session.createQuery(criteriaQuery) - query.singleResult - } - } - - private fun checkStatesEqual(expected: VaultSchemaV1.VaultStates, actual: VaultSchemaV1.VaultStates) { - assertEquals(expected.notary, actual.notary) - assertEquals(expected.stateStatus, actual.stateStatus) - assertEquals(expected.relevancyStatus, actual.relevancyStatus) - } - - private fun addToStatePartyTable(stateAndRef: StateAndRef) { - cordaDB.transaction { - val persistentStateRef = PersistentStateRef(stateAndRef.ref.txhash.toString(), stateAndRef.ref.index) - val session = currentDBSession() - stateAndRef.state.data.participants.forEach { - val persistentParty = VaultSchemaV1.PersistentParty( - persistentStateRef, - it - ) - session.save(persistentParty) - } - } - } - - @Test(timeout=300_000) - fun `Check a simple migration works`() { - addCashStates(10, BOB) - addCashStates(10, ALICE) - assertEquals(20, getVaultStateCount()) - assertEquals(0, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(20, getVaultStateCount()) - assertEquals(20, getStatePartyCount()) - assertEquals(10, getVaultStateCount(Vault.RelevancyStatus.RELEVANT)) - } - - @Test(timeout=300_000) - fun `Check state paging works`() { - addCashStates(1010, BOB) - - assertEquals(0, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(1010, getStatePartyCount()) - assertEquals(1010, getVaultStateCount()) - assertEquals(0, getVaultStateCount(Vault.RelevancyStatus.NOT_RELEVANT)) - } - - @Test(timeout=300_000) - fun `Check state fields are correct`() { - val tx = createCashTransaction(Cash(), 100.DOLLARS, ALICE) - storeTransaction(tx) - createVaultStatesFromTransaction(tx) - val expectedPersistentParty = VaultSchemaV1.PersistentParty( - PersistentStateRef(tx.id.toString(), 0), - ALICE - ) - val state = tx.coreTransaction.outputs.first() - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val expectedPersistentState = VaultSchemaV1.VaultStates( - notary = state.notary, - contractStateClassName = state.data.javaClass.name, - stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.NOT_RELEVANT, - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - val persistentStateParty = getState(VaultSchemaV1.PersistentParty::class.java) - val persistentState = getState(VaultSchemaV1.VaultStates::class.java) - checkStatesEqual(expectedPersistentState, persistentState) - assertEquals(expectedPersistentParty.x500Name, persistentStateParty.x500Name) - assertEquals(expectedPersistentParty.compositeKey, persistentStateParty.compositeKey) - } - - @Test(timeout=300_000) - fun `Check the connection is open post migration`() { - // Liquibase automatically closes the database connection when doing an actual migration. This test ensures the custom migration - // leaves it open. - addCashStates(12, ALICE) - - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertFalse(cordaDB.dataSource.connection.isClosed) - } - - @Test(timeout=300_000) - fun `All parties added to state party table`() { - val stx = createLinearStateTransaction("test", parties = listOf(ALICE, BOB, CHARLIE)) - storeTransaction(stx) - createVaultStatesFromTransaction(stx) - - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(3, getStatePartyCount()) - assertEquals(1, getVaultStateCount()) - assertEquals(0, getVaultStateCount(Vault.RelevancyStatus.NOT_RELEVANT)) - } - - @Test(timeout=300_000) - fun `State with corresponding transaction missing fails migration`() { - val cash = Cash() - val unknownTx = createCashTransaction(cash, 100.DOLLARS, BOB) - createVaultStatesFromTransaction(unknownTx) - - addCashStates(10, BOB) - val migration = VaultStateMigration() - assertFailsWith { migration.execute(liquibaseDB) } - assertEquals(10, getStatePartyCount()) - - // Now add the missing transaction and ensure that the migration succeeds - storeTransaction(unknownTx) - migration.execute(liquibaseDB) - assertEquals(11, getStatePartyCount()) - } - - @Test(timeout=300_000) - fun `State with unknown ID is handled correctly`() { - addCashStates(1, CHARLIE) - addCashStates(10, BOB) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(11, getStatePartyCount()) - assertEquals(1, getVaultStateCount(Vault.RelevancyStatus.NOT_RELEVANT)) - assertEquals(10, getVaultStateCount(Vault.RelevancyStatus.RELEVANT)) - } - - @Test(timeout = 300_000) - fun `Null database causes migration to fail`() { - val migration = VaultStateMigration() - // Just check this does not throw an exception - assertThatExceptionOfType(VaultStateMigrationException::class.java).isThrownBy { - migration.execute(null) - } - } - - @Test(timeout=300_000) - fun `State with non-owning key for our name marked as relevant`() { - val tx = createCashTransaction(Cash(), 100.DOLLARS, BOB2) - storeTransaction(tx) - createVaultStatesFromTransaction(tx) - val state = tx.coreTransaction.outputs.first() - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val expectedPersistentState = VaultSchemaV1.VaultStates( - notary = state.notary, - contractStateClassName = state.data.javaClass.name, - stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.RELEVANT, - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - val persistentState = getState(VaultSchemaV1.VaultStates::class.java) - checkStatesEqual(expectedPersistentState, persistentState) - } - - @Test(timeout=300_000) - fun `State already in state party table is excluded`() { - val tx = createCashTransaction(Cash(), 100.DOLLARS, BOB) - storeTransaction(tx) - createVaultStatesFromTransaction(tx) - addToStatePartyTable(tx.coreTransaction.outRef(0)) - addCashStates(5, BOB) - assertEquals(1, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(6, getStatePartyCount()) - } - - @Test(timeout=300_000) - fun `Consumed states are not migrated`() { - addCashStates(1010, BOB, Vault.StateStatus.CONSUMED) - assertEquals(0, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(0, getStatePartyCount()) - } - - @Test(timeout=300_000) - fun `State created with notary change transaction can be migrated`() { - // This test is a little bit of a hack - it checks that these states are migrated correctly by looking at params in the database, - // but these will not be there for V3 nodes. Handling for this must be tested manually. - val cashTx = createCashTransaction(Cash(), 5.DOLLARS, BOB) - val cashTx2 = createCashTransaction(Cash(), 10.DOLLARS, BOB) - val notaryTx = createNotaryChangeTransaction(listOf(StateRef(cashTx.id, 0), StateRef(cashTx2.id, 0)), SecureHash.allOnesHash) - createVaultStatesFromTransaction(cashTx, stateStatus = Vault.StateStatus.CONSUMED) - createVaultStatesFromTransaction(cashTx2, stateStatus = Vault.StateStatus.CONSUMED) - createVaultStatesFromNotaryChangeTransaction(notaryTx, cashTx.coreTransaction.outputs + cashTx2.coreTransaction.outputs) - storeTransaction(cashTx) - storeTransaction(cashTx2) - storeTransaction(notaryTx) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(2, getStatePartyCount()) - } - - // Used to test migration performance - @Test(timeout=300_000) -@Ignore - fun `Migrate large database`() { - val statesAtOnce = 500L - val stateMultiplier = 300L - logger.info("Start adding states to vault") - (1..stateMultiplier).forEach { - addCashStates(statesAtOnce.toInt(), BOB) - } - logger.info("Finish adding states to vault") - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals((statesAtOnce * stateMultiplier), getStatePartyCount()) - } - - private fun makePersistentDataSourceProperties(): Properties { - val props = Properties() - props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") - props.setProperty("dataSource.url", "jdbc:h2:~/test/persistence;DB_CLOSE_ON_EXIT=TRUE") - props.setProperty("dataSource.user", "sa") - props.setProperty("dataSource.password", "") - return props - } - - // Used to generate a persistent database for further testing. - @Test(timeout=300_000) -@Ignore - fun `Create persistent DB`() { - val cashStatesToAdd = 1000 - val linearStatesToAdd = 0 - val commodityStatesToAdd = 0 - val stateMultiplier = 10 - - cordaDB = configureDatabase(makePersistentDataSourceProperties(), DatabaseConfig(), notaryServices.identityService::wellKnownPartyFromX500Name, notaryServices.identityService::wellKnownPartyFromAnonymous) - - // Starting the database this way runs the migration under test. This is fine for the unit tests (as the changelog table is ignored), - // but when starting an actual node using these databases the migration will be skipped, as it has an entry in the changelog table. - // This must therefore be removed. - cordaDB.dataSource.connection.createStatement().use { - it.execute("DELETE FROM DATABASECHANGELOG WHERE FILENAME IN ('migration/vault-schema.changelog-v9.xml')") - } - - for (i in 1..stateMultiplier) { - addCashStates(cashStatesToAdd, BOB) - addLinearStates(linearStatesToAdd, listOf(BOB, ALICE)) - addCommodityStates(commodityStatesToAdd, BOB) - } - saveOurKeys(listOf(bob.keyPair)) - saveAllIdentities(listOf(BOB_IDENTITY, ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity)) - cordaDB.close() - } - - @Test(timeout=300_000) -@Ignore - fun `Run on persistent DB`() { - cordaDB = configureDatabase(makePersistentDataSourceProperties(), DatabaseConfig(), notaryServices.identityService::wellKnownPartyFromX500Name, notaryServices.identityService::wellKnownPartyFromAnonymous) - val connection = (liquibaseDB.connection as JdbcConnection) - Mockito.`when`(connection.url).thenReturn(cordaDB.jdbcUrl) - Mockito.`when`(connection.wrappedConnection).thenReturn(cordaDB.dataSource.connection) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - cordaDB.close() - } -} - diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 44b44bcf40..2da031f5b2 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -9,6 +9,7 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.getRequiredTransaction import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord import net.corda.core.transactions.TransactionBuilder @@ -116,7 +117,7 @@ class NotaryChangeTests { val newState = future.getOrThrow() assertEquals(newState.state.notary, newNotary) - val recordedTx = clientNodeA.services.validatedTransactions.getTransaction(newState.ref.txhash)!! + val recordedTx = clientNodeA.services.getRequiredTransaction(newState.ref.txhash) val notaryChangeTx = recordedTx.resolveNotaryChangeTransaction(clientNodeA.services) // Check that all encumbrances have been propagated to the outputs @@ -140,7 +141,7 @@ class NotaryChangeTests { // We don't to tx resolution when moving state to another node, so need to add the issue transaction manually // to node B. The resolution process is tested later during notarisation. - clientNodeB.services.recordTransactions(clientNodeA.services.validatedTransactions.getTransaction(issued.ref.txhash)!!) + clientNodeB.services.recordTransactions(clientNodeA.services.getRequiredTransaction(issued.ref.txhash)) val changedNotary = changeNotary(moved, clientNodeB, newNotaryParty) val movedBack = moveState(changedNotary, clientNodeB, clientNodeA) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index b67921cf8c..f1c45cf30d 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -1,6 +1,5 @@ package net.corda.node.services.persistence -import org.mockito.kotlin.* import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef @@ -10,6 +9,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.StatesToRecord import net.corda.core.node.services.IdentityService import net.corda.core.node.services.Vault @@ -28,7 +28,6 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.test.SampleCashSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 -import net.corda.node.internal.NodeServicesForResolution import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.schema.ContractStateAndRef import net.corda.node.services.schema.NodeSchemaService @@ -41,7 +40,14 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.HibernateSchemaChangeException -import net.corda.testing.core.* +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.singleIdentity import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.vault.DummyDealStateSchemaV1 import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 @@ -49,15 +55,25 @@ import net.corda.testing.internal.vault.DummyLinearStateSchemaV2 import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.hibernate.SessionFactory -import org.junit.* +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import java.math.BigDecimal import java.time.Clock import java.time.Instant -import java.util.* +import java.util.Currency +import java.util.Random +import java.util.UUID import javax.persistence.EntityManager import javax.persistence.Tuple import javax.persistence.criteria.CriteriaBuilder @@ -85,7 +101,7 @@ class HibernateConfigurationTest { val vault: VaultService get() = services.vaultService // Hibernate configuration objects - lateinit var hibernateConfig: HibernateConfiguration + private lateinit var hibernateConfig: HibernateConfiguration private lateinit var hibernatePersister: PersistentStateService private lateinit var sessionFactory: SessionFactory private lateinit var entityManager: EntityManager @@ -126,7 +142,7 @@ class HibernateConfigurationTest { override val vaultService = NodeVaultService( Clock.systemUTC(), keyManagementService, - servicesForResolution as NodeServicesForResolution, + toVerifyingServiceHub(), database, schemaService, cordappClassloader @@ -236,7 +252,7 @@ class HibernateConfigurationTest { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList - Assertions.assertThat(queryResults.size).isEqualTo(3) + assertThat(queryResults.size).isEqualTo(3) } @Test(timeout=300_000) @@ -327,7 +343,7 @@ class HibernateConfigurationTest { // execute query val queryResults = query.resultList - Assertions.assertThat(queryResults.size).isEqualTo(15) + assertThat(queryResults.size).isEqualTo(15) // try towards end query.firstResult = 100 @@ -335,7 +351,7 @@ class HibernateConfigurationTest { val lastQueryResults = query.resultList - Assertions.assertThat(lastQueryResults.size).isEqualTo(10) + assertThat(lastQueryResults.size).isEqualTo(10) } /** diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 7bd3690925..424d6810f2 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -28,7 +28,6 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.core.singleIdentity import net.corda.testing.flows.registerCoreFlowFactory import net.corda.coretesting.internal.rigorousMock -import net.corda.node.internal.NodeServicesForResolution import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.startFlow @@ -86,11 +85,10 @@ class VaultSoftLockManagerTest { private val mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(enclosedCordapp()), defaultFactory = { args -> object : InternalMockNetwork.MockNode(args) { override fun makeVaultService(keyManagementService: KeyManagementService, - services: NodeServicesForResolution, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { val node = this - val realVault = super.makeVaultService(keyManagementService, services, database, cordappLoader) + val realVault = super.makeVaultService(keyManagementService, database, cordappLoader) return object : SingletonSerializeAsToken(), VaultServiceInternal by realVault { override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { // Should be called before flow is removed diff --git a/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt index cde76833d9..9c69d86b17 100644 --- a/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt @@ -1,19 +1,17 @@ package net.corda.notary.experimental.raft import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map -import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyContract.SingleOwnerState import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.core.dummyCommand import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess @@ -22,7 +20,7 @@ import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP import org.junit.Test -import java.util.* +import java.util.Random import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -39,20 +37,13 @@ class RaftNotaryServiceTests { val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow() val inputState = issueState(bankA, defaultNotaryIdentity) - val firstTxBuilder = TransactionBuilder(defaultNotaryIdentity) - .addInputState(inputState) - .addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey)) + val firstTxBuilder = DummyContract.move(inputState, bankA.services.myInfo.singleIdentity()) val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder) val firstSpend = bankA.startFlow(NotaryFlow.Client(firstSpendTx)) firstSpend.getOrThrow() - val secondSpendBuilder = TransactionBuilder(defaultNotaryIdentity).withItems(inputState).run { - val dummyState = DummyContract.SingleOwnerState(0, bankA.services.myInfo.singleIdentity()) - addOutputState(dummyState, DummyContract.PROGRAM_ID) - addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey)) - this - } + val secondSpendBuilder = DummyContract.move(inputState, bankA.services.myInfo.singleIdentity()) val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder) val secondSpend = bankA.startFlow(NotaryFlow.Client(secondSpendTx)) @@ -78,10 +69,10 @@ class RaftNotaryServiceTests { } } - private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef<*> { + private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef { val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0)) val stx = nodeHandle.services.signInitialTransaction(builder) nodeHandle.services.recordTransactions(stx) - return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0)) + return stx.coreTransaction.outRef(0) } } diff --git a/serialization/build.gradle b/serialization/build.gradle index 6f3cffa35a..7393bae2e7 100644 --- a/serialization/build.gradle +++ b/serialization/build.gradle @@ -28,8 +28,6 @@ dependencies { // For caches rather than guava implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version" - testImplementation project(":serialization") - testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" testImplementation "junit:junit:$junit_version" diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt index e7b97cf45b..df90dbc534 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt @@ -32,7 +32,7 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } // Anything else, such as arrays, will be taken care of by the above } } catch (e: ClassCarpenterException) { - throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}") + throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}").apply { initCause(e) } } return try { @@ -40,7 +40,7 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } catch (e: ClassNotFoundException) { // This might happen if we've been asked to carpent up a parameterised type, and it's the rawtype itself // rather than any of its type parameters that were missing. - throw NotSerializableException("Could not carpent ${typeInformation.typeIdentifier.prettyPrint(false)}") + throw NotSerializableException("Could not carpent ${typeInformation.typeIdentifier.prettyPrint(false)}").apply { initCause(e) } } } @@ -87,6 +87,6 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } private fun RemoteTypeInformation.AnEnum.carpentEnum() { - carpenter.build(EnumSchema(name = typeIdentifier.name, fields = members.associate { it to EnumField() })) + carpenter.build(EnumSchema(name = typeIdentifier.name, fields = members.associateWith { EnumField() })) } -} \ No newline at end of file +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/CustomSerializationSchemeAdapter.kt similarity index 66% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapter.kt rename to serialization/src/main/kotlin/net/corda/serialization/internal/verifier/CustomSerializationSchemeAdapter.kt index f656f81502..3bbdb170a9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/CustomSerializationSchemeAdapter.kt @@ -1,8 +1,10 @@ -package net.corda.nodeapi.internal.serialization +package net.corda.serialization.internal.verifier -import net.corda.core.serialization.SerializationSchemeContext +import net.corda.core.CordaException +import net.corda.core.internal.loadClassOfType import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId import net.corda.core.utilities.ByteSequence @@ -12,8 +14,7 @@ import java.io.ByteArrayOutputStream import java.io.NotSerializableException class CustomSerializationSchemeAdapter(private val customScheme: CustomSerializationScheme): SerializationScheme { - - val serializationSchemeMagic = getCustomSerializationMagicFromSchemeId(customScheme.getSchemeId()) + private val serializationSchemeMagic = getCustomSerializationMagicFromSchemeId(customScheme.getSchemeId()) override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { return magic == serializationSchemeMagic @@ -44,4 +45,21 @@ class CustomSerializationSchemeAdapter(private val customScheme: CustomSerializa override val whitelist = context.whitelist override val properties = context.properties } -} \ No newline at end of file +} + +@Suppress("ThrowsCount") +fun loadCustomSerializationScheme(className: String, classLoader: ClassLoader): SerializationScheme { + val schemeClass = try { + loadClassOfType(className, false, classLoader) + } catch (e: ClassNotFoundException) { + throw CordaException("$className was declared as a custom serialization scheme but could not be found.") + } catch (e: ClassCastException) { + throw CordaException("$className was declared as a custom serialization scheme but does not implement CustomSerializationScheme") + } + val constructor = try { + schemeClass.getDeclaredConstructor() + } catch (e: NoSuchMethodException) { + throw CordaException("$className was declared as a custom serialization scheme but does not have a no argument constructor.") + } + return CustomSerializationSchemeAdapter(constructor.newInstance()) +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt new file mode 100644 index 0000000000..61f00b2b98 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt @@ -0,0 +1,87 @@ +package net.corda.serialization.internal.verifier + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.toStringShort +import net.corda.core.identity.Party +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.node.NetworkParameters +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.transactions.SignedTransaction +import net.corda.core.utilities.Try +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.EOFException +import java.security.PublicKey + +typealias SerializedNetworkParameters = SerializedBytes + +@CordaSerializable +sealed interface ExternalVerifierInbound { + data class Initialisation( + val customSerializerClassNames: Set, + val serializationWhitelistClassNames: Set, + val customSerializationSchemeClassName: String?, + val serializedCurrentNetworkParameters: SerializedNetworkParameters + ) : ExternalVerifierInbound { + val currentNetworkParameters: NetworkParameters by lazy(serializedCurrentNetworkParameters::deserialize) + + override fun toString(): String { + return "Initialisation(" + + "customSerializerClassNames=$customSerializerClassNames, " + + "serializationWhitelistClassNames=$serializationWhitelistClassNames, " + + "customSerializationSchemeClassName=$customSerializationSchemeClassName, " + + "currentNetworkParameters=$currentNetworkParameters)" + } + } + + data class VerificationRequest( + val stx: SignedTransaction, + val stxInputsAndReferences: Map, + val checkSufficientSignatures: Boolean + ) : ExternalVerifierInbound + + data class PartiesResult(val parties: List) : ExternalVerifierInbound + data class AttachmentResult(val attachment: AttachmentWithTrust?) : ExternalVerifierInbound + data class AttachmentsResult(val attachments: List) : ExternalVerifierInbound + data class NetworkParametersResult(val networkParameters: NetworkParameters?) : ExternalVerifierInbound + data class TrustedClassAttachmentResult(val id: SecureHash?) : ExternalVerifierInbound +} + +@CordaSerializable +data class AttachmentWithTrust(val attachment: Attachment, val isTrusted: Boolean) + +@CordaSerializable +sealed interface ExternalVerifierOutbound { + sealed interface VerifierRequest : ExternalVerifierOutbound { + data class GetParties(val keys: Set) : VerifierRequest { + override fun toString(): String = "GetParty(keys=${keys.map { it.toStringShort() }}})" + } + data class GetAttachment(val id: SecureHash) : VerifierRequest + data class GetAttachments(val ids: Set) : VerifierRequest + data class GetNetworkParameters(val id: SecureHash) : VerifierRequest + data class GetTrustedClassAttachment(val className: String) : VerifierRequest + } + + data class VerificationResult(val result: Try) : ExternalVerifierOutbound +} + +fun DataOutputStream.writeCordaSerializable(payload: Any) { + val serialised = payload.serialize() + writeInt(serialised.size) + serialised.writeTo(this) + flush() +} + +inline fun DataInputStream.readCordaSerializable(): T { + val length = readInt() + val bytes = readNBytes(length) + if (bytes.size != length) { + throw EOFException("Incomplete read of ${T::class.java.name}") + } + return bytes.deserialize() +} diff --git a/settings.gradle b/settings.gradle index 69c3ea22d3..8f2543aebb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -48,6 +48,7 @@ include 'node-api' include 'node-api-tests' include 'node' include 'node:capsule' +include 'verifier' include 'client:jackson' include 'client:jfx' include 'client:mock' diff --git a/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt b/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt index 6345dd7549..28cf91fc2b 100644 --- a/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt +++ b/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt @@ -7,7 +7,7 @@ import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.nodeapi.internal.serialization.CustomSerializationSchemeAdapter +import net.corda.serialization.internal.verifier.CustomSerializationSchemeAdapter import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.KRYO_CHECKPOINT_CONTEXT import net.corda.nodeapi.internal.serialization.kryo.KryoCheckpointSerializer diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 7c19eb0265..ecf26a5211 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,9 +1,13 @@ package net.corda.testing.node import com.google.common.collect.MutableClassToInstanceMap +import net.corda.core.CordaInternal import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -13,9 +17,13 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.internal.mapToSet import net.corda.core.internal.requireSupportedHashType import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryServiceImpl +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -34,23 +42,22 @@ import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkParametersService import net.corda.core.node.services.ServiceLifecycleObserver import net.corda.core.node.services.TransactionStorage -import net.corda.core.node.services.TransactionVerifierService import net.corda.core.node.services.VaultService import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.node.services.vault.CordaTransactionSupport import net.corda.core.serialization.SerializeAsToken +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.coretesting.internal.DEV_ROOT_CA import net.corda.node.VersionInfo -import net.corda.node.internal.ServicesForResolutionImpl -import net.corda.node.internal.NodeServicesForResolution import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.SchemaService import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.WritableTransactionStorage +import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.diagnostics.NodeDiagnosticsService import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.PersistentIdentityService @@ -58,7 +65,6 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -69,6 +75,7 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase +import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.node.internal.DriverDSLImpl import net.corda.testing.node.internal.MockCryptoService import net.corda.testing.node.internal.MockKeyManagementService @@ -116,7 +123,6 @@ open class MockServices private constructor( *arrayOf(initialIdentity.keyPair) + moreKeys ) ) : ServiceHub { - companion object { private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo) @@ -485,24 +491,20 @@ open class MockServices private constructor( private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also { it.start() } - override val transactionVerifierService: TransactionVerifierService - get() = InMemoryTransactionVerifierService( - numberOfWorkers = 2, - cordappProvider = mockCordappProvider, - attachments = attachments - ) override val cordappProvider: CordappProvider get() = mockCordappProvider override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() - protected val servicesForResolution: ServicesForResolution - get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) + // This is kept here for backwards compatibility, otherwise this has no extra utility. + protected val servicesForResolution: ServicesForResolution get() = verifyingView + + private val verifyingView: VerifyingServiceHub by lazy { VerifyingView(this) } internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService( clock, keyManagementService, - servicesForResolution as NodeServicesForResolution, + verifyingView, database, schemaService, cordappLoader.appClassLoader @@ -511,9 +513,9 @@ open class MockServices private constructor( // This needs to be internal as MutableClassToInstanceMap is a guava type and shouldn't be part of our public API /** A map of available [CordaService] implementations */ - internal val cordappServices: MutableClassToInstanceMap = MutableClassToInstanceMap.create() + internal val cordappServices: MutableClassToInstanceMap = MutableClassToInstanceMap.create() - internal val cordappTelemetryComponents: MutableClassToInstanceMap = MutableClassToInstanceMap.create() + private val cordappTelemetryComponents: MutableClassToInstanceMap = MutableClassToInstanceMap.create() override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } @@ -543,19 +545,43 @@ open class MockServices private constructor( mockCordappProvider.addMockCordapp(contractClassName, attachments) } - override fun loadState(stateRef: StateRef) = servicesForResolution.loadState(stateRef) - override fun loadStates(stateRefs: Set) = servicesForResolution.loadStates(stateRefs) + override fun loadState(stateRef: StateRef): TransactionState { + return getRequiredTransaction(stateRef.txhash).resolveBaseTransaction(this).outputs[stateRef.index] + } + + override fun loadStates(stateRefs: Set): Set> = stateRefs.mapToSet(::toStateAndRef) /** Returns a dummy Attachment, in context of signature constrains non-downgrade rule this default to contract class version `1`. */ override fun loadContractAttachment(stateRef: StateRef) = dummyAttachment -} -/** - * Function which can be used to create a mock [CordaService] for use within testing, such as an Oracle. - */ -fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { - class MockAppServiceHubImpl(val serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : AppServiceHub, ServiceHub by serviceHub { - val serviceInstance: T = serviceConstructor(this) + + /** + * All [ServiceHub]s must also implement [VerifyingServiceHub]. However, since [MockServices] is part of the public API, making it + * extend [VerifyingServiceHub] would leak internal APIs. Instead we have this private view class and have the `toVerifyingServiceHub` + * extension method return it. + */ + private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices { + override val attachmentTrustCalculator = NodeAttachmentTrustCalculator( + attachmentStorage = InternalMockAttachmentStorage(mockServices.attachments), + cacheFactory = TestingNamedCacheFactory() + ) + + override val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) + + override val cordappProvider: CordappProviderInternal get() = mockServices.mockCordappProvider + + override fun loadContractAttachment(stateRef: StateRef): Attachment = mockServices.loadContractAttachment(stateRef) + + override fun loadState(stateRef: StateRef): TransactionState<*> = mockServices.loadState(stateRef) + + override fun loadStates(stateRefs: Set): Set> = mockServices.loadStates(stateRefs) + } + + + @CordaInternal + internal class MockAppServiceHubImpl(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : + AppServiceHub, VerifyingServiceHub by serviceHub.verifyingView { + internal val serviceInstance: T = serviceConstructor(this) init { serviceHub.cordappServices.putInstance(serviceInstance.javaClass, serviceInstance) @@ -576,5 +602,11 @@ fun createMockCordaService(serviceHub: MockServices, serv throw UnsupportedOperationException() } } - return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance +} + +/** + * Function which can be used to create a mock [CordaService] for use within testing, such as an Oracle. + */ +fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { + return MockServices.MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 50a0a2c017..c370129c15 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -352,7 +352,6 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), private val entropyCounter = AtomicReference(args.entropyRoot) override val log get() = staticLog - override val transactionVerifierWorkerCount: Int get() = 1 private var _rxIoScheduler: Scheduler? = null override val rxIoScheduler: Scheduler diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index b460d02f30..eb52d9d614 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -3,7 +3,6 @@ package net.corda.testing.dsl import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.DoNotImplement import net.corda.core.contracts.* -import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.NullKeys.NULL_SIGNATURE import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature @@ -12,6 +11,7 @@ import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution @@ -95,7 +95,6 @@ data class TestTransactionDSLInterpreter private constructor( // Implementing [ServiceHubCoreInternal] allows better use in internal Corda tests val services: ServicesForResolution = object : ServiceHubCoreInternal, ServiceHub by ledgerInterpreter.services { - // [validatedTransactions.getTransaction] needs overriding as there are no calls to // [ServiceHub.recordTransactions] in the test dsl override val validatedTransactions: TransactionStorage = @@ -129,17 +128,17 @@ data class TestTransactionDSLInterpreter private constructor( override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) - override fun loadStates(stateRefs: Set): Set> { - return stateRefs.map { StateAndRef(loadState(it), it) }.toSet() - } - - override val cordappProvider: CordappProvider = - ledgerInterpreter.services.cordappProvider + override val cordappProvider: CordappProviderInternal + get() = ledgerInterpreter.services.cordappProvider as CordappProviderInternal override val notaryService: NotaryService? = null override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) + override fun loadContractAttachment(stateRef: StateRef): Attachment { + return ledgerInterpreter.services.loadContractAttachment(stateRef) + } + override fun recordUnnotarisedTransaction(txn: SignedTransaction) {} override fun removeUnnotarisedTransaction(id: SecureHash) {} @@ -169,7 +168,6 @@ data class TestTransactionDSLInterpreter private constructor( override fun reference(stateRef: StateRef) { val state = ledgerInterpreter.resolveStateRef(stateRef) - @Suppress("DEPRECATION") // Will remove when feature finalised. transactionBuilder.addReferenceState(StateAndRef(state, stateRef).referenced()) } diff --git a/verifier/build.gradle b/verifier/build.gradle new file mode 100644 index 0000000000..e794026070 --- /dev/null +++ b/verifier/build.gradle @@ -0,0 +1,35 @@ +plugins { + id "org.jetbrains.kotlin.jvm" + id "application" + id "com.github.johnrengelman.shadow" +} + +application { + mainClass.set("net.corda.verifier.Main") +} + +dependencies { + implementation project(":core") + implementation project(":serialization") + implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version" + implementation "org.slf4j:jul-to-slf4j:$slf4j_version" + + runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" +} + +jar { + manifest { + attributes("Add-Opens": + "java.base/java.lang " + + "java.base/java.lang.reflect " + + "java.base/java.lang.invoke " + + "java.base/java.util " + + "java.base/java.time " + + "java.base/java.io " + + "java.base/java.net " + + "java.base/javax.net.ssl " + + "java.base/java.security.cert " + + "java.base/java.nio" + ) + } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt new file mode 100644 index 0000000000..3db0294a2f --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt @@ -0,0 +1,42 @@ +package net.corda.verifier + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import java.security.PublicKey + +class ExternalVerificationContext( + override val appClassLoader: ClassLoader, + override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache, + private val externalVerifier: ExternalVerifier, + private val transactionInputsAndReferences: Map +) : VerificationSupport { + override val isResolutionLazy: Boolean get() = false + + override fun getParties(keys: Collection): List = externalVerifier.getParties(keys) + + override fun getAttachment(id: SecureHash): Attachment? = externalVerifier.getAttachment(id)?.attachment + + override fun getAttachments(ids: Collection): List { + return externalVerifier.getAttachments(ids).map { it?.attachment } + } + + override fun isAttachmentTrusted(attachment: Attachment): Boolean = externalVerifier.getAttachment(attachment.id)!!.isTrusted + + override fun getTrustedClassAttachment(className: String): Attachment? { + return externalVerifier.getTrustedClassAttachment(className) + } + + override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = externalVerifier.getNetworkParameters(id) + + override fun getSerializedState(stateRef: StateRef): SerializedTransactionState = transactionInputsAndReferences.getValue(stateRef) + + override fun fixupAttachmentIds(attachmentIds: Collection): Set { + return externalVerifier.fixupAttachmentIds(attachmentIds) + } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt new file mode 100644 index 0000000000..fc1b5ec121 --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt @@ -0,0 +1,237 @@ +package net.corda.verifier + +import com.github.benmanes.caffeine.cache.Cache +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.mapToSet +import net.corda.core.internal.objectOrNewInstance +import net.corda.core.internal.toSynchronised +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.verification.AttachmentFixups +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl +import net.corda.core.serialization.internal.SerializationEnvironment +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.serialization.internal.AMQP_P2P_CONTEXT +import net.corda.serialization.internal.CordaSerializationMagic +import net.corda.serialization.internal.SerializationFactoryImpl +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap +import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey +import net.corda.serialization.internal.amqp.SerializerFactory +import net.corda.serialization.internal.amqp.amqpMagic +import net.corda.serialization.internal.verifier.AttachmentWithTrust +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentsResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachment +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment +import net.corda.serialization.internal.verifier.loadCustomSerializationScheme +import net.corda.serialization.internal.verifier.readCordaSerializable +import net.corda.serialization.internal.verifier.writeCordaSerializable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.URLClassLoader +import java.nio.file.Path +import java.security.PublicKey +import java.util.Optional +import kotlin.io.path.div +import kotlin.io.path.listDirectoryEntries + +@Suppress("TooGenericExceptionCaught", "MagicNumber") +class ExternalVerifier( + private val baseDirectory: Path, + private val fromNode: DataInputStream, + private val toNode: DataOutputStream +) { + companion object { + private val log = contextLogger() + } + + private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache + private val attachmentFixups = AttachmentFixups() + private val parties: OptionalCache + private val attachments: OptionalCache + private val networkParametersMap: OptionalCache + private val trustedClassAttachments: OptionalCache + + private lateinit var appClassLoader: ClassLoader + private lateinit var currentNetworkParameters: NetworkParameters + + init { + val cacheFactory = ExternalVerifierNamedCacheFactory() + attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory) + parties = cacheFactory.buildNamed("ExternalVerifier_parties") + attachments = cacheFactory.buildNamed("ExternalVerifier_attachments") + networkParametersMap = cacheFactory.buildNamed("ExternalVerifier_networkParameters") + trustedClassAttachments = cacheFactory.buildNamed("ExternalVerifier_trustedClassAttachments") + } + + fun run() { + initialise() + while (true) { + val request = fromNode.readCordaSerializable() + log.debug { "Received $request" } + verifyTransaction(request) + } + } + + private fun initialise() { + // Use a preliminary serialization context to receive the initialisation message + _contextSerializationEnv.set(SerializationEnvironment.with( + verifierSerializationFactory(), + p2pContext = AMQP_P2P_CONTEXT + )) + + log.info("Waiting for initialisation message from node...") + val initialisation = fromNode.readCordaSerializable() + log.info("Received $initialisation") + + appClassLoader = createAppClassLoader() + + // Then use the initialisation message to create the correct serialization context + _contextSerializationEnv.set(null) + _contextSerializationEnv.set(SerializationEnvironment.with( + verifierSerializationFactory(initialisation, appClassLoader).apply { + initialisation.customSerializationSchemeClassName?.let { + registerScheme(loadCustomSerializationScheme(it, appClassLoader)) + } + }, + p2pContext = AMQP_P2P_CONTEXT.withClassLoader(appClassLoader) + )) + + attachmentFixups.load(appClassLoader) + + currentNetworkParameters = initialisation.currentNetworkParameters + networkParametersMap.put(initialisation.serializedCurrentNetworkParameters.hash, Optional.of(currentNetworkParameters)) + + log.info("External verifier initialised") + } + + private fun createAppClassLoader(): ClassLoader { + val cordappJarUrls = (baseDirectory / "cordapps").listDirectoryEntries() + .stream() + .filter { it.toString().endsWith(".jar") } + .map { it.toUri().toURL() } + .toTypedArray() + log.debug { "CorDapps: ${cordappJarUrls?.joinToString()}" } + return URLClassLoader(cordappJarUrls, javaClass.classLoader) + } + + private fun verifyTransaction(request: VerificationRequest) { + val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences) + val result = try { + request.stx.verifyInternal(verificationContext, request.checkSufficientSignatures) + log.info("${request.stx} verified") + Try.Success(Unit) + } catch (t: Throwable) { + log.info("${request.stx} failed to verify", t) + Try.Failure(t) + } + toNode.writeCordaSerializable(VerificationResult(result)) + } + + fun getParties(keys: Collection): List { + return parties.retrieveAll(keys) { + request(GetParties(it)).parties + } + } + + fun getAttachment(id: SecureHash): AttachmentWithTrust? { + return attachments.retrieve(id) { + request(GetAttachment(id)).attachment + } + } + + fun getAttachments(ids: Collection): List { + return attachments.retrieveAll(ids) { + request(GetAttachments(it)).attachments + } + } + + fun getTrustedClassAttachment(className: String): Attachment? { + val attachmentId = trustedClassAttachments.retrieve(className) { + // GetTrustedClassAttachment returns back the attachment ID, not the whole attachment. This lets us avoid downloading the whole + // attachment again if we already have it. + request(GetTrustedClassAttachment(className)).id + } + return attachmentId?.let(::getAttachment)?.attachment + } + + fun getNetworkParameters(id: SecureHash?): NetworkParameters? { + return if (id == null) { + currentNetworkParameters + } else { + networkParametersMap.retrieve(id) { + request(GetNetworkParameters(id)).networkParameters + } + } + } + + fun fixupAttachmentIds(attachmentIds: Collection): Set = attachmentFixups.fixupAttachmentIds(attachmentIds) + + private inline fun request(request: Any): T { + log.debug { "Sending request to node: $request" } + toNode.writeCordaSerializable(request) + val response = fromNode.readCordaSerializable() + log.debug { "Received response from node: $response" } + return response + } + + private fun verifierSerializationFactory(initialisation: Initialisation? = null, classLoader: ClassLoader? = null): SerializationFactoryImpl { + val serializationFactory = SerializationFactoryImpl() + serializationFactory.registerScheme(AMQPVerifierSerializationScheme(initialisation, classLoader)) + return serializationFactory + } + + + private class AMQPVerifierSerializationScheme(initialisation: Initialisation?, classLoader: ClassLoader?) : AbstractAMQPSerializationScheme( + initialisation?.customSerializerClassNames.load(classLoader), + initialisation?.serializationWhitelistClassNames.load(classLoader), + AccessOrderLinkedHashMap(128).toSynchronised() + ) { + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return magic == amqpMagic && target == SerializationContext.UseCase.P2P + } + + override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + + companion object { + inline fun Set?.load(classLoader: ClassLoader?): Set { + return this?.mapToSet { loadClassOfType(it, classLoader = classLoader).kotlin.objectOrNewInstance() } ?: emptySet() + } + } + } +} + +private typealias OptionalCache = Cache> + +private fun OptionalCache.retrieve(key: K, request: () -> V?): V? { + return get(key) { Optional.ofNullable(request()) }!!.orElse(null) +} + +@Suppress("UNCHECKED_CAST") +private fun OptionalCache.retrieveAll(keys: Collection, request: (Set) -> List): List { + val optionalResults = getAll(keys) { + val missingKeys = if (it is Set<*>) it as Set else it.toSet() + val response = request(missingKeys) + missingKeys.zip(response) { key, value -> key to Optional.ofNullable(value) }.toMap() + } + return keys.map { optionalResults.getValue(it).orElse(null) } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt new file mode 100644 index 0000000000..cc3887eab8 --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt @@ -0,0 +1,35 @@ +package net.corda.verifier + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.CacheLoader +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache +import net.corda.core.internal.NamedCacheFactory + +@Suppress("MagicNumber") +class ExternalVerifierNamedCacheFactory : NamedCacheFactory { + companion object { + private const val DEFAULT_CACHE_SIZE = 1024L + } + + override fun buildNamed(caffeine: Caffeine, name: String): Cache { + checkCacheName(name) + return configure(caffeine, name).build() + } + + override fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache { + checkCacheName(name) + return configure(caffeine, name).build(loader) + } + + private fun configure(caffeine: Caffeine, name: String): Caffeine { + return when (name) { + "AttachmentsClassLoader_cache" -> caffeine.maximumSize(32) + "ExternalVerifier_parties" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + "ExternalVerifier_attachments" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + "ExternalVerifier_networkParameters" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + "ExternalVerifier_trustedClassAttachments" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") + } + } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/Main.kt b/verifier/src/main/kotlin/net/corda/verifier/Main.kt new file mode 100644 index 0000000000..ba1269f63d --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/Main.kt @@ -0,0 +1,46 @@ +package net.corda.verifier + +import net.corda.core.utilities.loggerFor +import org.slf4j.bridge.SLF4JBridgeHandler +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.Socket +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.system.exitProcess + +@Suppress("TooGenericExceptionCaught") +object Main { + private val log = loggerFor
() + + @JvmStatic + fun main(args: Array) { + val port = args[0].toInt() + val loggingLevel = args[0] + val baseDirectory = Path.of("").toAbsolutePath() + + initLogging(baseDirectory, loggingLevel) + + log.info("External verifier started; PID ${ProcessHandle.current().pid()}") + log.info("Node base directory: $baseDirectory") + + try { + val socket = Socket("localhost", port) + log.info("Connected to node on port $port") + val fromNode = DataInputStream(socket.getInputStream()) + val toNode = DataOutputStream(socket.getOutputStream()) + ExternalVerifier(baseDirectory, fromNode, toNode).run() + } catch (t: Throwable) { + log.error("Unexpected error which has terminated the verifier", t) + exitProcess(1) + } + } + + private fun initLogging(baseDirectory: Path, loggingLevel: String) { + System.setProperty("logPath", (baseDirectory / "logs").toString()) + System.setProperty("defaultLogLevel", loggingLevel) + + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() + } +} diff --git a/verifier/src/main/resources/log4j2.xml b/verifier/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..684820a3df --- /dev/null +++ b/verifier/src/main/resources/log4j2.xml @@ -0,0 +1,44 @@ + + + + + ${sys:logPath:-logs} + verifier-${hostName} + ${log_path}/archive + ${sys:defaultLogLevel:-info} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +