mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
ENT-11055: Basic external verification (#7545)
* ENT-11055: Basic external verification Introduction of the external transaction verifier, a separate JVM process for verifying `SignedTransaction`s. The end goal is for this verifier to be built with Kotlin 1.2 so that it creates a compatible verification environment for transactions with 4.11 contracts. For now however the verifier is built against Kotlin 1.8, same as the node. External verification is enabled when the the system property `net.corda.node.verification.external` is set to `true`. When enabled, all verification requests made via `SignedTransaction.verify` are sent to the external verifier, regardless of the transaction content. It will do the vast bulk of the verification and then send the result back, namely if an exception occurred. If it did, then it's re-thrown in the node. The external verifier is a stateless process, with no connection to the node's database. All transaction resolution information needed to create the relevant ledger transaction object are made to the node, which waits in a loop servicing these requests until it receives the result. The verifier Jar is embedded in the Corda node Jar, and is extracted and run when needed for the first time. The node opens up a local port for the verifier to communicate with, which is specified to the verifier in the process command line. This all means there is no extra configuration or deployment required to support external verification. The existing code had some initial attempts and abstractions to support a future external verification feature. However, they were either incorrect or didn't quite fit. One such example was `TransactionVerifierService`. It incorrectly operated on the `LedgerTransaction` level, which doesn't work since the transaction needs to be first serialised. Instead a new abstraction, `VerificationSupport` has been introduced, which represents all the operations needed to resolve and verify a `SignedTransaction`, essentially replacing `ServicesForResolution` (a lot of the changes are due to this). The external verifier implements this with a simple RPC mechanism, whilst the node needed a new (internal) `ServiceHub` abstraction, `VerifyingServiceHub`. `ServicesForResolution` hasn't been deleted since it's public API, however all classes implementing it must also implement `VerifyingServiceHub`. This is possible to do without breaking compatibility since `ServicesForResolution` is annotated with `@DoNotImplement`. Changes to `api-current.txt` were made due to the removal of `TransactionVerifierService`, which was clearly indicated as an internal class, and returning `TransactionBuilder.toLedgerTransactionWithContext` back to an internal method. * Address review comments * One bulk load states method * Merge fix
This commit is contained in:
parent
74ca2c6734
commit
11d0054fcc
@ -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 <init>(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()
|
||||
|
@ -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"
|
||||
|
@ -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}"
|
||||
|
@ -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<AttachmentStorage>().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<NetworkParametersService>().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<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
doReturn(mock<TransactionStorage>()).whenever(services).validatedTransactions
|
||||
doReturn(mock<IdentityService>()).whenever(services).identityService
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||
doReturn(attachmentId).whenever(attachment).id
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signerKeys
|
||||
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
|
||||
val wtx = TransactionBuilder(
|
||||
notary = DUMMY_NOTARY,
|
||||
|
@ -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<FROM, TO>())
|
||||
|
||||
private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
|
||||
services.validatedTransactions.getTransaction(state.ref.txhash)!!
|
||||
.resolveContractUpgradeTransaction(services)
|
||||
services.getRequiredTransaction(state.ref.txhash).resolveContractUpgradeTransaction(services)
|
||||
|
||||
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
|
||||
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
|
||||
|
@ -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<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
|
||||
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<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
|
||||
@ -182,8 +205,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
|
||||
isUpgrade<FROM, TO>())
|
||||
|
||||
private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
|
||||
services.validatedTransactions.getTransaction(state.ref.txhash)!!
|
||||
.resolveContractUpgradeTransaction(services)
|
||||
services.getRequiredTransaction(state.ref.txhash).resolveContractUpgradeTransaction(services)
|
||||
|
||||
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
|
||||
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
|
||||
|
@ -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<SignedTransaction> {
|
||||
return startFlow(WithFinality::FinalityInvoker, stx, recipients.toSet(), emptySet()).andRunNetwork()
|
||||
|
@ -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<IllegalArgumentException> {
|
||||
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<IllegalArgumentException> {
|
||||
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<IllegalArgumentException> {
|
||||
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) }
|
||||
}
|
||||
}
|
@ -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<ServicesForResolution>()
|
||||
private val contractAttachmentId = SecureHash.randomSHA256()
|
||||
private val attachments = rigorousMock<AttachmentStorage>()
|
||||
private val networkParametersService = mock<NetworkParametersService>()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val cordappProvider = rigorousMock<CordappProvider>()
|
||||
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<IdentityService>()).whenever(services).identityService
|
||||
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
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<Party>()).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<PublicKey> 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<PublicKey> 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)
|
||||
|
@ -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"
|
||||
|
@ -27,6 +27,7 @@ class ContractAttachment private constructor(
|
||||
|
||||
companion object {
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
fun create(attachment: Attachment,
|
||||
contract: ContractClassName,
|
||||
additionalContracts: Set<ContractClassName> = emptySet(),
|
||||
|
@ -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<out DigestAlgorithm> = Class.forName(className, false, javaClass.classLoader)
|
||||
.asSubclass(DigestAlgorithm::class.java)
|
||||
.getConstructor()
|
||||
private val constructor = loadClassOfType<DigestAlgorithm>(className, false, javaClass.classLoader).getConstructor()
|
||||
|
||||
override val algorithm: String = constructor.newInstance().algorithm
|
||||
|
||||
override fun create(): DigestAlgorithm {
|
||||
|
@ -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<FlowSession>, 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<FlowSession>, val payload: Any
|
||||
|
||||
@Suspendable
|
||||
private fun getInputTransactions(tx: SignedTransaction): Set<SecureHash> {
|
||||
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<SecureHash> = mutableSetOf(), val acceptAll: Boolean = false) {
|
||||
|
@ -21,8 +21,7 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a
|
||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||
classVersionRange: IntRange? = null): Set<T> {
|
||||
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 <T: Any> 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 <reified T> loadClassOfType(className: String, initialize: Boolean = true, classLoader: ClassLoader? = null): Class<out T> {
|
||||
return loadClassOfType(T::class.java, className, initialize, classLoader)
|
||||
}
|
||||
|
||||
fun <T> loadClassOfType(type: Class<T>, className: String, initialize: Boolean = true, classLoader: ClassLoader? = null): Class<out T> {
|
||||
return Class.forName(className, initialize, classLoader).asSubclass(type)
|
||||
}
|
||||
|
||||
fun <T: Any?> executeWithThreadContextClassLoader(classloader: ClassLoader, fn: () -> T): T {
|
||||
val threadClassLoader = Thread.currentThread().contextClassLoader
|
||||
try {
|
||||
@ -68,5 +80,4 @@ fun <T: Any?> executeWithThreadContextClassLoader(classloader: ClassLoader, fn:
|
||||
} finally {
|
||||
Thread.currentThread().contextClassLoader = threadClassLoader
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<out FlowLogic<*>>.isIdempotentFlow(): Boolean {
|
||||
return IdempotentFlow::class.java.isAssignableFrom(this)
|
||||
@ -125,40 +113,6 @@ fun noPackageOverlap(packages: Collection<String>): 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<AttachmentId>): Set<AttachmentId> {
|
||||
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)
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
|
||||
interface CordappFixupInternal {
|
||||
fun fixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId>
|
||||
}
|
@ -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 <T> List<T>.randomOrNull(): T? {
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.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 <T, R> Iterable<T>.mapToSet(transform: (T) -> R): Set<R> {
|
||||
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 <T, R> Iterable<T>.flatMapToSet(transform: (T) -> Iterable<R>): Set<R> {
|
||||
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 <reified T : Any> 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 <T> Iterable<T>.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum()
|
||||
|
||||
fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): UntrustworthyData<T> {
|
||||
val payloadData: T = try {
|
||||
val serializer = SerializationDefaults.SERIALIZATION_FACTORY
|
||||
@ -563,6 +575,10 @@ fun <K, V> MutableMap<K, V>.toSynchronised(): MutableMap<K, V> = Collections.syn
|
||||
/** @see Collections.synchronizedSet */
|
||||
fun <E> MutableSet<E>.toSynchronised(): MutableSet<E> = 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].
|
||||
|
@ -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 <K, V> buildNamed(name: String): Cache<K, V> = buildNamed(Caffeine.newBuilder(), name)
|
||||
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
||||
|
||||
fun <K, V> buildNamed(name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> = buildNamed(Caffeine.newBuilder(), name, loader)
|
||||
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V>
|
||||
}
|
||||
|
||||
private val allowedChars = Regex("^[0-9A-Za-z_.]*\$")
|
||||
|
@ -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.
|
||||
|
@ -172,11 +172,13 @@ fun createComponentGroups(inputs: List<StateRef>,
|
||||
return componentGroupMap
|
||||
}
|
||||
|
||||
typealias SerializedTransactionState = SerializedBytes<TransactionState<ContractState>>
|
||||
|
||||
/**
|
||||
* A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef).
|
||||
* The [serializedState] is the actual component from the original wire transaction.
|
||||
*/
|
||||
data class SerializedStateAndRef(val serializedState: SerializedBytes<TransactionState<ContractState>>, 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<ContractState> {
|
||||
val factory = SerializationFactory.defaultFactory
|
||||
|
@ -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<CordappImpl>
|
||||
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
|
||||
fun fixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId>
|
||||
}
|
@ -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<AttachmentFixup>()
|
||||
|
||||
/**
|
||||
* Loads the "fixup" rules from all META-INF/Corda-Fixups files.
|
||||
* These files have the following format:
|
||||
* <AttachmentId>,<AttachmentId>...=><AttachmentId>,<AttachmentId>,...
|
||||
* where each <AttachmentId> 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<AttachmentFixups>().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<AttachmentId> {
|
||||
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<AttachmentId>): Set<AttachmentId> {
|
||||
val replacementIds = LinkedHashSet(attachmentIds)
|
||||
for ((sourceIds, targetIds) in fixupRules) {
|
||||
if (replacementIds.containsAll(sourceIds)) {
|
||||
replacementIds.removeAll(sourceIds)
|
||||
replacementIds.addAll(targetIds)
|
||||
}
|
||||
}
|
||||
return replacementIds
|
||||
}
|
||||
}
|
@ -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<PublicKey>): List<Party?>
|
||||
|
||||
fun getAttachment(id: SecureHash): Attachment?
|
||||
|
||||
// TODO Use SequencedCollection if upgraded to Java 21
|
||||
fun getAttachments(ids: Collection<SecureHash>): List<Attachment?> = 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<SecureHash>): Set<SecureHash>
|
||||
|
||||
fun createVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier {
|
||||
return defaultVerifier(ltx, serializationContext)
|
||||
}
|
||||
}
|
@ -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<Attachment>) = internalPrepareVerify(attachments)
|
||||
|
||||
interface Verifier {
|
||||
|
||||
/**
|
||||
@ -142,10 +145,12 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
|
||||
*/
|
||||
@Suppress("ThrowsCount")
|
||||
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
|
||||
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<ContractClassName, ContractAttachment>) {
|
||||
// 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<out Contract> {
|
||||
return try {
|
||||
Class.forName(contractClassName, false, transactionClassLoader).asSubclass(Contract::class.java)
|
||||
loadClassOfType<Contract>(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<Contract> {
|
||||
return (ltx.inputs.map(StateAndRef<ContractState>::state) + ltx.outputs)
|
||||
.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
||||
.mapToSet { it.contract }
|
||||
.map { contractClassName ->
|
||||
createContractClass(ltx.id, contractClassName)
|
||||
}.map { contractClass ->
|
@ -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<ContractState>(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<StateRef>): Set<StateAndRef<ContractState>> = loadStatesInternal(stateRefs, LinkedHashSet())
|
||||
|
||||
fun <T : ContractState, C : MutableCollection<StateAndRef<T>>> loadStatesInternal(input: Iterable<StateRef>, output: C): C {
|
||||
return input.mapTo(output, ::toStateAndRef)
|
||||
}
|
||||
|
||||
// TODO Bulk party lookup?
|
||||
override fun getParties(keys: Collection<PublicKey>): List<Party?> = 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<SecureHash>): Set<SecureHash> {
|
||||
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")
|
||||
}
|
||||
}
|
@ -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 <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
|
||||
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()
|
||||
|
@ -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<*>
|
||||
}
|
@ -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 <T : ContractState, S : ContractState> calculateUpgradedState(state: TransactionState<T>, upgradedContract: UpgradedContract<T, S>, upgradedContractAttachment: Attachment): TransactionState<S> {
|
||||
@JvmSynthetic
|
||||
internal fun <T : ContractState, S : ContractState> calculateUpgradedState(state: TransactionState<T>,
|
||||
upgradedContract: UpgradedContract<T, S>,
|
||||
upgradedContractAttachment: Attachment): TransactionState<S> {
|
||||
// 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<TransactionSignature>): 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<ContractState, ContractState> = try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
Class.forName(className, false, classLoader).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract<ContractState, ContractState>
|
||||
} 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<TransactionState<ContractState>> {
|
||||
val binaryInput: SerializedBytes<TransactionState<ContractState>> = 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<StateAndRef<ContractState>>,
|
||||
notary: Party,
|
||||
legacyContractAttachment: Attachment,
|
||||
upgradedContractAttachment: Attachment,
|
||||
id: SecureHash,
|
||||
privacySalt: PrivacySalt,
|
||||
sigs: List<TransactionSignature>,
|
||||
networkParameters: NetworkParameters,
|
||||
upgradedContract: UpgradedContract<ContractState, *>
|
||||
): ContractUpgradeLedgerTransaction {
|
||||
return ContractUpgradeLedgerTransaction(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, upgradedContract)
|
||||
@JvmSynthetic
|
||||
@Suppress("ThrowsCount")
|
||||
internal fun resolve(verificationSupport: VerificationSupport,
|
||||
wtx: ContractUpgradeWireTransaction,
|
||||
sigs: List<TransactionSignature>): 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<ContractState, *> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return Class.forName(upgradedContractClassName, false, classLoader)
|
||||
.asSubclass(Contract::class.java)
|
||||
.getConstructor()
|
||||
.newInstance() as UpgradedContract<ContractState, *>
|
||||
}
|
||||
|
||||
// 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<ContractState, *> {
|
||||
return try {
|
||||
loadClassOfType<UpgradedContract<ContractState, *>>(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<PublicKey>
|
||||
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<PublicKey>): List<String> {
|
||||
return keys.map { it.toBase58String() }
|
||||
@ -401,7 +380,7 @@ private constructor(
|
||||
privacySalt: PrivacySalt,
|
||||
sigs: List<TransactionSignature>,
|
||||
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(
|
||||
|
@ -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<SerializedStateAndRef>? = null,
|
||||
serializedReferences: List<SerializedStateAndRef>? = 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<Attachment>): Verifier {
|
||||
@JvmSynthetic
|
||||
internal fun verifyInternal(txAttachments: List<Attachment> = 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 <reified T : ContractState> filterInputs(crossinline predicate: (T) -> Boolean): List<T> {
|
||||
return filterInputs(T::class.java, Predicate { predicate(it) })
|
||||
return filterInputs(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -479,7 +481,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> filterReferenceInputs(crossinline predicate: (T) -> Boolean): List<T> {
|
||||
return filterReferenceInputs(T::class.java, Predicate { predicate(it) })
|
||||
return filterReferenceInputs(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -495,7 +497,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> filterInRefs(crossinline predicate: (T) -> Boolean): List<StateAndRef<T>> {
|
||||
return filterInRefs(T::class.java, Predicate { predicate(it) })
|
||||
return filterInRefs(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -511,7 +513,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> filterReferenceInputRefs(crossinline predicate: (T) -> Boolean): List<StateAndRef<T>> {
|
||||
return filterReferenceInputRefs(T::class.java, Predicate { predicate(it) })
|
||||
return filterReferenceInputRefs(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -528,7 +530,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> 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 <T : ContractState> findReference(clazz: Class<T>, predicate: Predicate<T>): T {
|
||||
return referenceInputsOfType(clazz).single { predicate.test(it) }
|
||||
return referenceInputsOfType(clazz).single(predicate::test)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> 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 <reified T : ContractState> findInRef(crossinline predicate: (T) -> Boolean): StateAndRef<T> {
|
||||
return findInRef(T::class.java, Predicate { predicate(it) })
|
||||
return findInRef(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -579,7 +581,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> findReferenceInputRef(crossinline predicate: (T) -> Boolean): StateAndRef<T> {
|
||||
return findReferenceInputRef(T::class.java, Predicate { predicate(it) })
|
||||
return findReferenceInputRef(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -614,7 +616,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : CommandData> filterCommands(crossinline predicate: (T) -> Boolean): List<Command<T>> {
|
||||
return filterCommands(T::class.java, Predicate { predicate(it) })
|
||||
return filterCommands(T::class.java) { predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -631,7 +633,7 @@ private constructor(
|
||||
}
|
||||
|
||||
inline fun <reified T : CommandData> findCommand(crossinline predicate: (T) -> Boolean): Command<T> {
|
||||
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?!
|
||||
|
@ -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<TransactionSignature>): 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<TransactionSignature>) = 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<TransactionState<ContractState>> {
|
||||
return services.loadState(stateRef).serialize()
|
||||
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>): 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<StateAndRef<ContractState>>,
|
||||
notary: Party,
|
||||
newNotary: Party,
|
||||
id: SecureHash,
|
||||
sigs: List<TransactionSignature>,
|
||||
networkParameters: NetworkParameters): NotaryChangeLedgerTransaction {
|
||||
return NotaryChangeLedgerTransaction(inputs, notary, newNotary, id, sigs, networkParameters)
|
||||
@JvmSynthetic
|
||||
internal fun resolve(verificationSupport: VerificationSupport,
|
||||
wireTx: NotaryChangeWireTransaction,
|
||||
sigs: List<TransactionSignature>): 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<StateRef>): 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<TransactionState<ContractState>>
|
||||
get() = computeOutputs()
|
||||
|
||||
private fun computeOutputs(): List<TransactionState<ContractState>> {
|
||||
val inputPositionIndex: Map<StateRef, Int> = 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<ContractState>::ref) } }
|
||||
|
||||
override val requiredSigningKeys: Set<PublicKey>
|
||||
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<PublicKey>): List<String> {
|
||||
return keys.map { it.toBase58String() }
|
||||
|
@ -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<CoreTransaction>,
|
||||
@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<CoreTransaction>,
|
||||
} 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<CoreTransaction>,
|
||||
@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<CoreTransaction>,
|
||||
}
|
||||
|
||||
/** 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<CoreTransaction>,
|
||||
// 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<CoreTransaction>,
|
||||
}
|
||||
|
||||
@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 ?: "<unknown>", 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<CoreTransaction>,
|
||||
// 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<Attachment> {
|
||||
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<Attachment>): Collection<Attachment> {
|
||||
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<CoreTransaction>,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<CoreTransaction>,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -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<AttachmentId>.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<AttachmentId>, resolvedOutputs: List<TransactionState<ContractState>>)
|
||||
= 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<AttachmentId>,
|
||||
services: ServicesForResolution,
|
||||
originalException: Throwable
|
||||
txAttachments: List<AttachmentId>,
|
||||
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<*>) {
|
||||
|
@ -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
|
||||
* </ul></p>
|
||||
*/
|
||||
@CordaSerializable
|
||||
@Suppress("ThrowsCount")
|
||||
class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) {
|
||||
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt())
|
||||
|
||||
@ -71,7 +92,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, 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<ComponentGroup>, 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<ComponentGroup>, val privacySalt: Pr
|
||||
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> 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<PublicKey>): List<Party?> = 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<SecureHash>) = throw AbstractMethodError()
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "ThrowsCount")
|
||||
private fun toLedgerTransactionInternal(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
|
||||
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<ComponentGroup>, 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<ComponentGroup>, val privacySalt: Pr
|
||||
componentGroups,
|
||||
serializedResolvedInputs,
|
||||
serializedResolvedReferences,
|
||||
isAttachmentTrusted,
|
||||
attachmentsClassLoaderCache,
|
||||
verificationSupport::isAttachmentTrusted,
|
||||
verificationSupport::createVerifier,
|
||||
verificationSupport.attachmentsClassLoaderCache,
|
||||
digestService
|
||||
)
|
||||
|
||||
@ -230,15 +248,15 @@ class WireTransaction(componentGroups: List<ComponentGroup>, 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<ComponentGroup>, 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<ComponentGroup>, val privacySalt: Pr
|
||||
timeWindow: TimeWindow?): List<ComponentGroup> {
|
||||
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<TransactionState<ContractState>>? {
|
||||
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<TransactionState<ContractState>>?
|
||||
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 {
|
||||
|
@ -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<LedgerTransaction>
|
@ -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)
|
||||
|
@ -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<NetworkParametersService>().also {
|
||||
doReturn(networkParameters.serialize().hash).whenever(it).currentHash
|
||||
}
|
||||
|
||||
private val serviceHub get() = rigorousMock<ServicesForResolution>().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<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(it).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
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<Party>()).whenever(attachment).signerKeys
|
||||
val contractAttachmentId = SecureHash.randomSHA256()
|
||||
doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage)
|
||||
.getLatestContractAttachments(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)
|
||||
doReturn(mock<IdentityService>()).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<String>): CordappLoader {
|
||||
return JarScanningCordappLoader.fromJarUrls(listOf(cordappWithPackages(*packages.toTypedArray()).jarFile.toUri().toURL()))
|
||||
}
|
||||
}
|
||||
|
@ -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<Set<MappedSchema>, SessionFactory>(Caffeine.newBuilder(), "HibernateConfiguration_sessionFactories")
|
||||
private val sessionFactories = cacheFactory.buildNamed<Set<MappedSchema>, SessionFactory>("HibernateConfiguration_sessionFactories")
|
||||
|
||||
val sessionFactoryForRegisteredSchemas = schemas.let {
|
||||
logger.info("Init HibernateConfiguration for schemas: $it")
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<SecureHash>() {
|
||||
class WriteTxToLedgerFlow(private val counterparty: Party, val notary: Party) : FlowLogic<SecureHash>() {
|
||||
@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<SignedTransaction>() {
|
||||
@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<Boolean>() {
|
||||
class SendFlow(private val counterparty: Party) : FlowLogic<Boolean>() {
|
||||
@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<WireTransaction>() {
|
||||
class CreateWireTxFlow(private val counterparty: Party) : FlowLogic<WireTransaction>() {
|
||||
@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<Unit>() {
|
||||
@Suspendable
|
||||
@ -301,6 +305,7 @@ class CustomSerializationSchemeDriverTest {
|
||||
kryo.isRegistrationRequired = false
|
||||
kryo.instantiatorStrategy = CustomInstantiatorStrategy()
|
||||
kryo.classLoader = classLoader
|
||||
@Suppress("ReplaceJavaStaticMethodWithKotlinAnalog")
|
||||
kryo.register(Arrays.asList("").javaClass, ArraysAsListSerializer())
|
||||
}
|
||||
|
||||
|
@ -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<Command>().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<AbstractParty> get() = listOf(party)
|
||||
}
|
||||
|
||||
data class Command(val failForParty: Party) : CommandData
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FailExternallyFlow(private val inputState: StateAndRef<State>?,
|
||||
private val failForParty: NodeInfo,
|
||||
private val recipient: NodeInfo) : FlowLogic<StateAndRef<State>>() {
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<State> {
|
||||
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<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@StartableByRPC
|
||||
class IssueAndChangeNotaryFlow(private val oldNotary: Party, private val newNotary: Party) : FlowLogic<SecureHash>() {
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<S>(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<S>(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<S>(val configuration: NodeConfiguration,
|
||||
private val cordappTelemetryComponents = MutableClassToInstanceMap.create<TelemetryComponent>()
|
||||
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<S>(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<S>(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<S>(val configuration: NodeConfiguration,
|
||||
networkMapCache,
|
||||
NodeInfoWatcher(
|
||||
configuration.baseDirectory,
|
||||
@Suppress("LeakingThis")
|
||||
rxIoScheduler,
|
||||
Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)
|
||||
),
|
||||
@ -846,7 +832,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
platformClock,
|
||||
database,
|
||||
flowStarter,
|
||||
servicesForResolution,
|
||||
services,
|
||||
flowLogicRefFactory,
|
||||
nodeProperties,
|
||||
configuration.drainingModePollPeriod,
|
||||
@ -1160,12 +1146,19 @@ abstract class AbstractNode<S>(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<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener {
|
||||
inner class ServiceHubImpl : SingletonSerializeAsToken(), ServiceHubInternal, NetworkParameterUpdateListener {
|
||||
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
||||
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
|
||||
override val identityService: IdentityService get() = this@AbstractNode.identityService
|
||||
@ -1191,7 +1184,6 @@ abstract class AbstractNode<S>(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<S>(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<S>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<T : SerializeAsToken>(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter,
|
||||
internal class AppServiceHubImpl<T : SerializeAsToken>(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 {
|
||||
|
||||
|
@ -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<NetworkHostAndPort> = 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)
|
||||
|
@ -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<StateRef>): Set<StateAndRef<ContractState>> = loadStates(stateRefs, LinkedHashSet())
|
||||
|
||||
fun <T : ContractState, C : MutableCollection<StateAndRef<T>>> loadStates(input: Iterable<StateRef>, output: C): C
|
||||
}
|
@ -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 <T : ContractState, C : MutableCollection<StateAndRef<T>>> loadStates(input: Iterable<StateRef>, output: C): C {
|
||||
val baseTxs = HashMap<SecureHash, BaseTransaction>()
|
||||
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<ContractState>(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)
|
||||
}
|
||||
}
|
@ -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 <reified A : Annotation> 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.")
|
||||
}
|
@ -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<Cordapp, CordappContext>()
|
||||
private val cordappAttachments = HashBiMap.create<SecureHash, URL>()
|
||||
private val attachmentFixups = arrayListOf<AttachmentFixup>()
|
||||
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<SecureHash, URL> =
|
||||
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<SecureHash, URL> {
|
||||
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:
|
||||
* <AttachmentId>,<AttachmentId>...=><AttachmentId>,<AttachmentId>,...
|
||||
* where each <AttachmentId> 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<AttachmentFixup> {
|
||||
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<AttachmentId> {
|
||||
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<AttachmentId>): Set<AttachmentId> {
|
||||
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<Attachment>): Collection<Attachment> {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<CordappImpl>
|
||||
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
|
||||
fun fixupAttachments(attachments: Collection<Attachment>): Collection<Attachment>
|
||||
}
|
@ -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 <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
|
||||
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
|
||||
|
@ -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 <K : Any, V> buildCache(name: String): ShiroCache<K, V> {
|
||||
logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s")
|
||||
return cacheFactory.buildNamed<K, V>(Caffeine.newBuilder(), "RPCSecurityManagerShiroCache_$name").toShiroCache()
|
||||
return cacheFactory.buildNamed<K, V>("RPCSecurityManagerShiroCache_$name").toShiroCache()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -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,
|
||||
|
@ -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<Int>): List<TransactionState<ContractState>> {
|
||||
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<StateRef>): Set<StateAndRef<ContractState>> {
|
||||
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()
|
||||
}
|
||||
}
|
@ -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<ContractState>) {
|
||||
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<ContractState> {
|
||||
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<VaultSchemaV1.VaultStates> {
|
||||
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<Vaul
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private val pageSize = 1000
|
||||
private var pageNumber = 0
|
||||
private var transaction: DatabaseTransaction? = null
|
||||
private var currentPage = getNextPage()
|
||||
|
||||
private fun endTransaction() {
|
||||
try {
|
||||
transaction?.commit()
|
||||
} catch (e: Exception) {
|
||||
transaction?.rollback()
|
||||
logger.error("Failed to commit transaction while iterating vault states: ${e.message}", e)
|
||||
} finally {
|
||||
transaction?.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextPage(): List<VaultSchemaV1.VaultStates> {
|
||||
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<VaultSchemaV1.VaultStates>,
|
||||
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<VaultPageTask> {
|
||||
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)
|
@ -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<Void?>
|
||||
@ -186,11 +186,14 @@ interface ServiceHubInternal : ServiceHubCoreInternal {
|
||||
val configuration: NodeConfiguration
|
||||
val nodeProperties: NodePropertiesStore
|
||||
val networkMapUpdater: NetworkMapUpdater
|
||||
override val cordappProvider: CordappProviderInternal
|
||||
|
||||
fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
|
||||
val cacheFactory: NamedCacheFactory
|
||||
|
||||
override fun createVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier {
|
||||
return NoDbAccessVerifier(defaultVerifier(ltx, serializationContext))
|
||||
}
|
||||
|
||||
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) =
|
||||
recordTransactions(statesToRecord, txs, SIGNATURE_VERIFICATION_DISABLED)
|
||||
|
||||
|
@ -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<PublicKey, Boolean>(
|
||||
Caffeine.newBuilder(),
|
||||
"NodeAttachmentTrustCalculator_trustedKeysCache"
|
||||
)
|
||||
private val trustedKeysCache = cacheFactory.buildNamed<PublicKey, Boolean>("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,
|
||||
|
@ -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<PublicKey, KeyOwningIdentity>(
|
||||
Caffeine.newBuilder(),
|
||||
"PublicKeyToOwningIdentityCache_cache"
|
||||
)
|
||||
private val cache = cacheFactory.buildNamed<PublicKey, KeyOwningIdentity>("PublicKeyToOwningIdentityCache_cache")
|
||||
|
||||
/**
|
||||
* Return the owning identity associated with a given key.
|
||||
|
@ -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<out FlowLogic<*>> {
|
||||
val forName = try {
|
||||
Class.forName(flowClassName, true, classloader)
|
||||
return try {
|
||||
loadClassOfType<FlowLogic<*>>(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<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||
|
@ -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<Attachment>.toPrettyString(): String {
|
||||
return joinToString(separator = SEPARATOR, prefix = SEPARATOR, postfix = System.lineSeparator()) { attachment ->
|
||||
attachment.id.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun verify(transaction: LedgerTransaction): CordaFuture<*> {
|
||||
return openFuture<Unit>().apply {
|
||||
capture {
|
||||
val verifier = transaction.prepareVerify(transaction.attachments)
|
||||
withoutDatabaseAccess {
|
||||
verifier.verify()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeReplacementAttachmentsFor(ltx: LedgerTransaction, missingClass: String?): Collection<Attachment> {
|
||||
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<Unit>().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() {}
|
||||
}
|
@ -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>(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 <T> withValidDeserialization(list: List<T>, txId: SecureHash): Map<Int, T> {
|
||||
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<ContractState>().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<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
|
||||
val lockIdPredicate = criteriaBuilder.or(get<String>(VaultSchemaV1.VaultStates::lockId.name).isNull,
|
||||
criteriaBuilder.equal(get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()))
|
||||
update.set(get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
|
||||
update.set(get<Instant>(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<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
|
||||
val lockIdPredicate = criteriaBuilder.equal(get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
|
||||
update.set<String>(get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
|
||||
update.set(get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
|
||||
update.set(get<String>(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<StateAndRef<T>> = servicesForResolution.loadStates(
|
||||
statesMetadata.mapTo(LinkedHashSet()) { it.ref },
|
||||
ArrayList()
|
||||
)
|
||||
val states: List<StateAndRef<T>> = 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 <T: ContractState> hasBeenSeen(update: Vault.Update<T>, snapshotStatesRefs: Set<StateRef>, snapshotConsumedStatesRefs: Set<StateRef>): 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)
|
||||
}
|
||||
|
@ -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<K : Any, V : Any>(name: String, cacheFactory: Nam
|
||||
}
|
||||
}
|
||||
|
||||
private val backingCache = cacheFactory.buildNamed<K, Wrapper<V>>(Caffeine.newBuilder(), name)
|
||||
private val backingCache = cacheFactory.buildNamed<K, Wrapper<V>>(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<K, Wrapper.Invalidated<V>>()
|
||||
|
@ -15,8 +15,7 @@ class NonInvalidatingCache<K, V> private constructor(
|
||||
|
||||
private companion object {
|
||||
private fun <K, V> buildCache(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V): LoadingCache<K, V> {
|
||||
val builder = Caffeine.newBuilder()
|
||||
return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction))
|
||||
return cacheFactory.buildNamed(name, NonInvalidatingCacheLoader(loadFunction))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Unit> {
|
||||
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<ExternalVerifierOutbound>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<DummySerializationSchemeException>("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<ConfigurationException>("$missingClassName was declared as a custom serialization scheme but could not " +
|
||||
assertFailsWith<CordaException>("$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<ConfigurationException>("$schemeName was declared as a custom serialization scheme but does not " +
|
||||
assertFailsWith<CordaException>("$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<ConfigurationException>("$schemeName was declared as a custom serialization scheme but does not " +
|
||||
assertFailsWith<CordaException>("$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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Currency>, 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<KeyPair>) {
|
||||
cordaDB.transaction {
|
||||
keys.forEach {
|
||||
val persistentKey = BasicHSMKeyManagementService.PersistentKey(it.public, it.private)
|
||||
session.save(persistentKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAllIdentities(identities: List<PartyAndCertificate>) {
|
||||
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<Vault.RelevancyStatus>("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<AbstractParty> = 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<AbstractParty>) {
|
||||
cordaDB.transaction {
|
||||
(1..statesToAdd).map { createLinearStateTransaction("A".repeat(it), parties) }.forEach {
|
||||
storeTransaction(it)
|
||||
createVaultStatesFromTransaction(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCommodityTransaction(amount: Amount<Issued<Commodity>>, 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<StateRef>, 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<TransactionState<ContractState>>) {
|
||||
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 <T> getState(clazz: Class<T>): 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<ContractState>) {
|
||||
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<VaultStateMigrationException> { 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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<StateRef>?) {
|
||||
// Should be called before flow is removed
|
||||
|
@ -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<SingleOwnerState> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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() }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ThrowsCount")
|
||||
fun loadCustomSerializationScheme(className: String, classLoader: ClassLoader): SerializationScheme {
|
||||
val schemeClass = try {
|
||||
loadClassOfType<CustomSerializationScheme>(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())
|
||||
}
|
@ -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<NetworkParameters>
|
||||
|
||||
@CordaSerializable
|
||||
sealed interface ExternalVerifierInbound {
|
||||
data class Initialisation(
|
||||
val customSerializerClassNames: Set<String>,
|
||||
val serializationWhitelistClassNames: Set<String>,
|
||||
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<StateRef, SerializedTransactionState>,
|
||||
val checkSufficientSignatures: Boolean
|
||||
) : ExternalVerifierInbound
|
||||
|
||||
data class PartiesResult(val parties: List<Party?>) : ExternalVerifierInbound
|
||||
data class AttachmentResult(val attachment: AttachmentWithTrust?) : ExternalVerifierInbound
|
||||
data class AttachmentsResult(val attachments: List<AttachmentWithTrust?>) : 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<PublicKey>) : VerifierRequest {
|
||||
override fun toString(): String = "GetParty(keys=${keys.map { it.toStringShort() }}})"
|
||||
}
|
||||
data class GetAttachment(val id: SecureHash) : VerifierRequest
|
||||
data class GetAttachments(val ids: Set<SecureHash>) : VerifierRequest
|
||||
data class GetNetworkParameters(val id: SecureHash) : VerifierRequest
|
||||
data class GetTrustedClassAttachment(val className: String) : VerifierRequest
|
||||
}
|
||||
|
||||
data class VerificationResult(val result: Try<Unit>) : ExternalVerifierOutbound
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeCordaSerializable(payload: Any) {
|
||||
val serialised = payload.serialize()
|
||||
writeInt(serialised.size)
|
||||
serialised.writeTo(this)
|
||||
flush()
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> 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<T>()
|
||||
}
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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<String>, 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<SerializeAsToken> = MutableClassToInstanceMap.create<SerializeAsToken>()
|
||||
internal val cordappServices: MutableClassToInstanceMap<SerializeAsToken> = MutableClassToInstanceMap.create()
|
||||
|
||||
internal val cordappTelemetryComponents: MutableClassToInstanceMap<TelemetryComponent> = MutableClassToInstanceMap.create<TelemetryComponent>()
|
||||
private val cordappTelemetryComponents: MutableClassToInstanceMap<TelemetryComponent> = MutableClassToInstanceMap.create()
|
||||
|
||||
override fun <T : SerializeAsToken> cordaService(type: Class<T>): 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<StateRef>) = servicesForResolution.loadStates(stateRefs)
|
||||
override fun loadState(stateRef: StateRef): TransactionState<ContractState> {
|
||||
return getRequiredTransaction(stateRef.txhash).resolveBaseTransaction(this).outputs[stateRef.index]
|
||||
}
|
||||
|
||||
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = 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 <T : SerializeAsToken> createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T {
|
||||
class MockAppServiceHubImpl<out T : SerializeAsToken>(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<StateRef>): Set<StateAndRef<ContractState>> = mockServices.loadStates(stateRefs)
|
||||
}
|
||||
|
||||
|
||||
@CordaInternal
|
||||
internal class MockAppServiceHubImpl<out T : SerializeAsToken>(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 <T : SerializeAsToken> 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 <T : SerializeAsToken> createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T {
|
||||
return MockServices.MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance
|
||||
}
|
||||
|
@ -352,7 +352,6 @@ open class InternalMockNetwork(cordappPackages: List<String> = 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
|
||||
|
@ -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<ContractState>(stateRef)
|
||||
|
||||
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> {
|
||||
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<ContractState>(stateRef)
|
||||
@Suppress("DEPRECATION") // Will remove when feature finalised.
|
||||
transactionBuilder.addReferenceState(StateAndRef(state, stateRef).referenced())
|
||||
}
|
||||
|
||||
|
35
verifier/build.gradle
Normal file
35
verifier/build.gradle
Normal file
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
@ -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<StateRef, SerializedTransactionState>
|
||||
) : VerificationSupport {
|
||||
override val isResolutionLazy: Boolean get() = false
|
||||
|
||||
override fun getParties(keys: Collection<PublicKey>): List<Party?> = externalVerifier.getParties(keys)
|
||||
|
||||
override fun getAttachment(id: SecureHash): Attachment? = externalVerifier.getAttachment(id)?.attachment
|
||||
|
||||
override fun getAttachments(ids: Collection<SecureHash>): List<Attachment?> {
|
||||
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<SecureHash>): Set<SecureHash> {
|
||||
return externalVerifier.fixupAttachmentIds(attachmentIds)
|
||||
}
|
||||
}
|
237
verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt
Normal file
237
verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt
Normal file
@ -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<PublicKey, Party>
|
||||
private val attachments: OptionalCache<SecureHash, AttachmentWithTrust>
|
||||
private val networkParametersMap: OptionalCache<SecureHash, NetworkParameters>
|
||||
private val trustedClassAttachments: OptionalCache<String, SecureHash>
|
||||
|
||||
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<VerificationRequest>()
|
||||
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<Initialisation>()
|
||||
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<PublicKey>): List<Party?> {
|
||||
return parties.retrieveAll(keys) {
|
||||
request<PartiesResult>(GetParties(it)).parties
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttachment(id: SecureHash): AttachmentWithTrust? {
|
||||
return attachments.retrieve(id) {
|
||||
request<AttachmentResult>(GetAttachment(id)).attachment
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttachments(ids: Collection<SecureHash>): List<AttachmentWithTrust?> {
|
||||
return attachments.retrieveAll(ids) {
|
||||
request<AttachmentsResult>(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<TrustedClassAttachmentResult>(GetTrustedClassAttachment(className)).id
|
||||
}
|
||||
return attachmentId?.let(::getAttachment)?.attachment
|
||||
}
|
||||
|
||||
fun getNetworkParameters(id: SecureHash?): NetworkParameters? {
|
||||
return if (id == null) {
|
||||
currentNetworkParameters
|
||||
} else {
|
||||
networkParametersMap.retrieve(id) {
|
||||
request<NetworkParametersResult>(GetNetworkParameters(id)).networkParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>): Set<SecureHash> = attachmentFixups.fixupAttachmentIds(attachmentIds)
|
||||
|
||||
private inline fun <reified T : Any> request(request: Any): T {
|
||||
log.debug { "Sending request to node: $request" }
|
||||
toNode.writeCordaSerializable(request)
|
||||
val response = fromNode.readCordaSerializable<T>()
|
||||
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<SerializationFactoryCacheKey, SerializerFactory>(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 <reified T> Set<String>?.load(classLoader: ClassLoader?): Set<T> {
|
||||
return this?.mapToSet { loadClassOfType<T>(it, classLoader = classLoader).kotlin.objectOrNewInstance() } ?: emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private typealias OptionalCache<K, V> = Cache<K, Optional<V>>
|
||||
|
||||
private fun <K : Any, V : Any> OptionalCache<K, V>.retrieve(key: K, request: () -> V?): V? {
|
||||
return get(key) { Optional.ofNullable(request()) }!!.orElse(null)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <K : Any, V : Any> OptionalCache<K, V>.retrieveAll(keys: Collection<K>, request: (Set<K>) -> List<V?>): List<V?> {
|
||||
val optionalResults = getAll(keys) {
|
||||
val missingKeys = if (it is Set<*>) it as Set<K> 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) }
|
||||
}
|
@ -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 <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> {
|
||||
checkCacheName(name)
|
||||
return configure(caffeine, name).build()
|
||||
}
|
||||
|
||||
override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> {
|
||||
checkCacheName(name)
|
||||
return configure(caffeine, name).build(loader)
|
||||
}
|
||||
|
||||
private fun<K, V> configure(caffeine: Caffeine<in K, in V>, name: String): Caffeine<in K, in V> {
|
||||
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?")
|
||||
}
|
||||
}
|
||||
}
|
46
verifier/src/main/kotlin/net/corda/verifier/Main.kt
Normal file
46
verifier/src/main/kotlin/net/corda/verifier/Main.kt
Normal file
@ -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<Main>()
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
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()
|
||||
}
|
||||
}
|
44
verifier/src/main/resources/log4j2.xml
Normal file
44
verifier/src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" shutdownHook="disable">
|
||||
|
||||
<Properties>
|
||||
<Property name="log_path">${sys:logPath:-logs}</Property>
|
||||
<Property name="log_name">verifier-${hostName}</Property>
|
||||
<Property name="archive">${log_path}/archive</Property>
|
||||
<Property name="default_log_level">${sys:defaultLogLevel:-info}</Property>
|
||||
</Properties>
|
||||
|
||||
<Appenders>
|
||||
<!-- Will generate up to 500 log files for a given day. Adjust this number according to the available storage.
|
||||
During every rollover it will delete those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingRandomAccessFile name="RollingFile-Appender"
|
||||
fileName="${log_path}/${log_name}.log"
|
||||
filePattern="${archive}/${log_name}.%date{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg%n"/>
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="100MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="500">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log_name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
</IfAny>
|
||||
</IfLastModified>
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
|
||||
</RollingRandomAccessFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="${default_log_level}">
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
Loading…
Reference in New Issue
Block a user