mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
ENT-11355: Backwards compatibility with older nodes via new attachments component group
This commit is contained in:
parent
c2742ba6a5
commit
200333b198
@ -7924,11 +7924,6 @@ public final class net.corda.core.transactions.WireTransaction extends net.corda
|
|||||||
public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution)
|
public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution)
|
||||||
@NotNull
|
@NotNull
|
||||||
public String toString()
|
public String toString()
|
||||||
@NotNull
|
|
||||||
public static final net.corda.core.transactions.WireTransaction$Companion Companion
|
|
||||||
##
|
|
||||||
public static final class net.corda.core.transactions.WireTransaction$Companion extends java.lang.Object
|
|
||||||
public <init>(kotlin.jvm.internal.DefaultConstructorMarker)
|
|
||||||
##
|
##
|
||||||
public final class net.corda.core.utilities.ByteArrays extends java.lang.Object
|
public final class net.corda.core.utilities.ByteArrays extends java.lang.Object
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -95,7 +95,8 @@ import java.math.BigDecimal
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.Currency
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class CordaModule : SimpleModule("corda-core") {
|
class CordaModule : SimpleModule("corda-core") {
|
||||||
override fun setupModule(context: SetupContext) {
|
override fun setupModule(context: SetupContext) {
|
||||||
@ -256,6 +257,7 @@ private data class StxJson(
|
|||||||
private interface WireTransactionMixin
|
private interface WireTransactionMixin
|
||||||
|
|
||||||
private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
||||||
|
@Suppress("INVISIBLE_MEMBER")
|
||||||
override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
|
override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||||
gen.writeObject(WireTransactionJson(
|
gen.writeObject(WireTransactionJson(
|
||||||
value.digestService,
|
value.digestService,
|
||||||
@ -265,7 +267,7 @@ private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
|||||||
value.outputs,
|
value.outputs,
|
||||||
value.commands,
|
value.commands,
|
||||||
value.timeWindow,
|
value.timeWindow,
|
||||||
value.attachments,
|
value.legacyAttachments.map { "$it-legacy" } + value.nonLegacyAttachments.map { it.toString() },
|
||||||
value.references,
|
value.references,
|
||||||
value.privacySalt,
|
value.privacySalt,
|
||||||
value.networkParametersHash
|
value.networkParametersHash
|
||||||
@ -276,15 +278,18 @@ private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
|||||||
private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>() {
|
private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>() {
|
||||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction {
|
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction {
|
||||||
val wrapper = parser.readValueAs<WireTransactionJson>()
|
val wrapper = parser.readValueAs<WireTransactionJson>()
|
||||||
|
// We're not concerned with backwards compatibility for any JSON string that was created with 4.11 and being materialised in 4.12.
|
||||||
|
val (legacyAttachments, newerAttachments) = wrapper.attachments.partition { it.endsWith("-legacy") }
|
||||||
val componentGroups = createComponentGroups(
|
val componentGroups = createComponentGroups(
|
||||||
wrapper.inputs,
|
wrapper.inputs,
|
||||||
wrapper.outputs,
|
wrapper.outputs,
|
||||||
wrapper.commands,
|
wrapper.commands,
|
||||||
wrapper.attachments,
|
newerAttachments.map(SecureHash::parse),
|
||||||
wrapper.notary,
|
wrapper.notary,
|
||||||
wrapper.timeWindow,
|
wrapper.timeWindow,
|
||||||
wrapper.references,
|
wrapper.references,
|
||||||
wrapper.networkParametersHash
|
wrapper.networkParametersHash,
|
||||||
|
legacyAttachments.map { SecureHash.parse(it.removeSuffix("-legacy")) }
|
||||||
)
|
)
|
||||||
return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256)
|
return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256)
|
||||||
}
|
}
|
||||||
@ -297,10 +302,11 @@ private class WireTransactionJson(@get:JsonInclude(Include.NON_NULL) val digestS
|
|||||||
val outputs: List<TransactionState<*>>,
|
val outputs: List<TransactionState<*>>,
|
||||||
val commands: List<Command<*>>,
|
val commands: List<Command<*>>,
|
||||||
val timeWindow: TimeWindow?,
|
val timeWindow: TimeWindow?,
|
||||||
val attachments: List<SecureHash>,
|
val attachments: List<String>,
|
||||||
val references: List<StateRef>,
|
val references: List<StateRef>,
|
||||||
val privacySalt: PrivacySalt,
|
val privacySalt: PrivacySalt,
|
||||||
val networkParametersHash: SecureHash?)
|
val networkParametersHash: SecureHash?
|
||||||
|
)
|
||||||
|
|
||||||
private interface TransactionStateMixin {
|
private interface TransactionStateMixin {
|
||||||
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||||
|
@ -57,6 +57,10 @@ processSmokeTestResources {
|
|||||||
from(configurations.corda4_11)
|
from(configurations.corda4_11)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processTestResources {
|
||||||
|
from(configurations.corda4_11)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||||
testImplementation "junit:junit:$junit_version"
|
testImplementation "junit:junit:$junit_version"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.coretests.verification
|
package net.corda.coretests.verification
|
||||||
|
|
||||||
import co.paralleluniverse.strands.concurrent.CountDownLatch
|
|
||||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||||
import net.corda.client.rpc.notUsed
|
import net.corda.client.rpc.notUsed
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
@ -13,12 +12,18 @@ import net.corda.core.internal.toPath
|
|||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.coretests.verification.VerificationType.BOTH
|
||||||
|
import net.corda.coretests.verification.VerificationType.EXTERNAL
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.USD
|
||||||
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.finance.flows.AbstractCashFlow
|
import net.corda.finance.flows.AbstractCashFlow
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
import net.corda.finance.flows.CashPaymentFlow
|
import net.corda.finance.flows.CashPaymentFlow
|
||||||
|
import net.corda.finance.workflows.getCashBalance
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.smoketesting.NodeParams
|
import net.corda.smoketesting.NodeParams
|
||||||
import net.corda.smoketesting.NodeProcess
|
import net.corda.smoketesting.NodeProcess
|
||||||
@ -31,15 +36,16 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
|||||||
import org.junit.AfterClass
|
import org.junit.AfterClass
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.copyTo
|
import kotlin.io.path.copyTo
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
import kotlin.io.path.listDirectoryEntries
|
import kotlin.io.path.listDirectoryEntries
|
||||||
import kotlin.io.path.name
|
|
||||||
import kotlin.io.path.readText
|
import kotlin.io.path.readText
|
||||||
|
|
||||||
class ExternalVerificationSignedCordappsTest {
|
class ExternalVerificationSignedCordappsTest {
|
||||||
@ -48,27 +54,30 @@ class ExternalVerificationSignedCordappsTest {
|
|||||||
|
|
||||||
private lateinit var notaries: List<NodeProcess>
|
private lateinit var notaries: List<NodeProcess>
|
||||||
private lateinit var oldNode: NodeProcess
|
private lateinit var oldNode: NodeProcess
|
||||||
private lateinit var newNode: NodeProcess
|
private lateinit var currentNode: NodeProcess
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun startNodes() {
|
fun startNodes() {
|
||||||
// The 4.11 finance CorDapp jars
|
val (legacyContractsCordapp, legacyWorkflowsCordapp) = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") }
|
||||||
val oldCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") }
|
|
||||||
// The current version finance CorDapp jars
|
// The current version finance CorDapp jars
|
||||||
val newCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it.jar") }
|
val currentCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it.jar") }
|
||||||
|
|
||||||
notaries = factory.createNotaries(
|
notaries = factory.createNotaries(
|
||||||
nodeParams(DUMMY_NOTARY_NAME, oldCordapps),
|
nodeParams(DUMMY_NOTARY_NAME, cordappJars = currentCordapps, legacyContractJars = listOf(legacyContractsCordapp)),
|
||||||
nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), newCordapps)
|
nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), currentCordapps)
|
||||||
)
|
)
|
||||||
oldNode = factory.createNode(nodeParams(
|
oldNode = factory.createNode(nodeParams(
|
||||||
CordaX500Name("Old", "Delhi", "IN"),
|
CordaX500Name("Old", "Delhi", "IN"),
|
||||||
oldCordapps + listOf(smokeTestResource("4.11-workflows-cordapp.jar")),
|
listOf(legacyContractsCordapp, legacyWorkflowsCordapp, smokeTestResource("4.11-workflows-cordapp.jar")),
|
||||||
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
|
clientRpcConfig = CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
|
||||||
version = "4.11"
|
version = "4.11"
|
||||||
))
|
))
|
||||||
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
|
currentNode = factory.createNode(nodeParams(
|
||||||
|
CordaX500Name("New", "York", "US"),
|
||||||
|
currentCordapps,
|
||||||
|
listOf(legacyContractsCordapp)
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
@ -79,8 +88,17 @@ class ExternalVerificationSignedCordappsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `transaction containing 4_11 contract sent to new node`() {
|
fun `transaction containing 4_11 contract attachment only sent to current node`() {
|
||||||
assertCashIssuanceAndPayment(issuer = oldNode, recipient = newNode)
|
val (issuanceTx, paymentTx) = cashIssuanceAndPayment(issuer = oldNode, recipient = currentNode)
|
||||||
|
notaries[0].assertTransactionsWereVerified(EXTERNAL, paymentTx.id)
|
||||||
|
currentNode.assertTransactionsWereVerified(EXTERNAL, issuanceTx.id, paymentTx.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `transaction containing 4_11 and 4_12 contract attachments sent to old node`() {
|
||||||
|
val (issuanceTx, paymentTx) = cashIssuanceAndPayment(issuer = currentNode, recipient = oldNode)
|
||||||
|
notaries[0].assertTransactionsWereVerified(BOTH, paymentTx.id)
|
||||||
|
currentNode.assertTransactionsWereVerified(BOTH, issuanceTx.id, paymentTx.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
@ -94,12 +112,14 @@ class ExternalVerificationSignedCordappsTest {
|
|||||||
oldRpc.startFlow(::IssueAndChangeNotaryFlow, notaryIdentities[0], notaryIdentities[1]).returnValue.getOrThrow()
|
oldRpc.startFlow(::IssueAndChangeNotaryFlow, notaryIdentities[0], notaryIdentities[1]).returnValue.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertCashIssuanceAndPayment(issuer: NodeProcess, recipient: NodeProcess) {
|
private fun cashIssuanceAndPayment(issuer: NodeProcess, recipient: NodeProcess): Pair<SignedTransaction, SignedTransaction> {
|
||||||
val issuerRpc = issuer.connect(superUser).proxy
|
val issuerRpc = issuer.connect(superUser).proxy
|
||||||
val recipientRpc = recipient.connect(superUser).proxy
|
val recipientRpc = recipient.connect(superUser).proxy
|
||||||
val recipientNodeInfo = recipientRpc.nodeInfo()
|
val recipientNodeInfo = recipientRpc.nodeInfo()
|
||||||
val notaryIdentity = issuerRpc.notaryIdentities()[0]
|
val notaryIdentity = issuerRpc.notaryIdentities()[0]
|
||||||
|
|
||||||
|
val beforeAmount = recipientRpc.getCashBalance(USD)
|
||||||
|
|
||||||
val (issuanceTx) = issuerRpc.startFlow(
|
val (issuanceTx) = issuerRpc.startFlow(
|
||||||
::CashIssueFlow,
|
::CashIssueFlow,
|
||||||
10.DOLLARS,
|
10.DOLLARS,
|
||||||
@ -110,6 +130,9 @@ class ExternalVerificationSignedCordappsTest {
|
|||||||
issuerRpc.waitForVisibility(recipientNodeInfo)
|
issuerRpc.waitForVisibility(recipientNodeInfo)
|
||||||
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
|
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
|
||||||
|
|
||||||
|
val (_, update) = recipientRpc.vaultTrack(Cash.State::class.java)
|
||||||
|
val cashArrived = update.waitForFirst { true }
|
||||||
|
|
||||||
val (paymentTx) = issuerRpc.startFlow(
|
val (paymentTx) = issuerRpc.startFlow(
|
||||||
::CashPaymentFlow,
|
::CashPaymentFlow,
|
||||||
10.DOLLARS,
|
10.DOLLARS,
|
||||||
@ -117,8 +140,10 @@ class ExternalVerificationSignedCordappsTest {
|
|||||||
false,
|
false,
|
||||||
).returnValue.getOrThrow()
|
).returnValue.getOrThrow()
|
||||||
|
|
||||||
notaries[0].assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
|
cashArrived.getOrThrow()
|
||||||
recipient.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
|
assertThat(recipientRpc.getCashBalance(USD) - beforeAmount).isEqualTo(10.DOLLARS)
|
||||||
|
|
||||||
|
return Pair(issuanceTx, paymentTx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,18 +159,18 @@ class ExternalVerificationUnsignedCordappsTest {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun startNodes() {
|
fun startNodes() {
|
||||||
// The 4.11 finance CorDapp jars
|
// The 4.11 finance CorDapp jars
|
||||||
val oldCordapps = listOf(unsignedResourceJar("corda-finance-contracts-4.11.jar"), smokeTestResource("corda-finance-workflows-4.11.jar"))
|
val legacyCordapps = listOf(unsignedResourceJar("corda-finance-contracts-4.11.jar"), smokeTestResource("corda-finance-workflows-4.11.jar"))
|
||||||
// The current version finance CorDapp jars
|
// The current version finance CorDapp jars
|
||||||
val newCordapps = listOf(unsignedResourceJar("corda-finance-contracts.jar"), smokeTestResource("corda-finance-workflows.jar"))
|
val currentCordapps = listOf(unsignedResourceJar("corda-finance-contracts.jar"), smokeTestResource("corda-finance-workflows.jar"))
|
||||||
|
|
||||||
notary = factory.createNotaries(nodeParams(DUMMY_NOTARY_NAME, oldCordapps))[0]
|
notary = factory.createNotaries(nodeParams(DUMMY_NOTARY_NAME, currentCordapps))[0]
|
||||||
oldNode = factory.createNode(nodeParams(
|
oldNode = factory.createNode(nodeParams(
|
||||||
CordaX500Name("Old", "Delhi", "IN"),
|
CordaX500Name("Old", "Delhi", "IN"),
|
||||||
oldCordapps,
|
legacyCordapps,
|
||||||
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
|
clientRpcConfig = CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
|
||||||
version = "4.11"
|
version = "4.11"
|
||||||
))
|
))
|
||||||
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
|
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), currentCordapps))
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
@ -200,6 +225,7 @@ private fun smokeTestResource(name: String): Path = ExternalVerificationSignedCo
|
|||||||
private fun nodeParams(
|
private fun nodeParams(
|
||||||
legalName: CordaX500Name,
|
legalName: CordaX500Name,
|
||||||
cordappJars: List<Path> = emptyList(),
|
cordappJars: List<Path> = emptyList(),
|
||||||
|
legacyContractJars: List<Path> = emptyList(),
|
||||||
clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||||
version: String? = null
|
version: String? = null
|
||||||
): NodeParams {
|
): NodeParams {
|
||||||
@ -210,6 +236,7 @@ private fun nodeParams(
|
|||||||
rpcAdminPort = portCounter.andIncrement,
|
rpcAdminPort = portCounter.andIncrement,
|
||||||
users = listOf(superUser),
|
users = listOf(superUser),
|
||||||
cordappJars = cordappJars,
|
cordappJars = cordappJars,
|
||||||
|
legacyContractJars = legacyContractJars,
|
||||||
clientRpcConfig = clientRpcConfig,
|
clientRpcConfig = clientRpcConfig,
|
||||||
version = version
|
version = version
|
||||||
)
|
)
|
||||||
@ -220,28 +247,41 @@ private fun CordaRPCOps.waitForVisibility(other: NodeInfo) {
|
|||||||
if (other in snapshot) {
|
if (other in snapshot) {
|
||||||
updates.notUsed()
|
updates.notUsed()
|
||||||
} else {
|
} else {
|
||||||
val found = CountDownLatch(1)
|
updates.waitForFirst { it.node == other }.getOrThrow()
|
||||||
val subscription = updates.subscribe {
|
|
||||||
if (it.node == other) {
|
|
||||||
found.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
found.await()
|
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NodeProcess.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) {
|
private fun <T> Observable<T>.waitForFirst(predicate: (T) -> Boolean): CompletableFuture<Unit> {
|
||||||
val verifierLogContent = externalVerifierLogs()
|
val found = CompletableFuture<Unit>()
|
||||||
|
val subscription = subscribe {
|
||||||
|
if (predicate(it)) {
|
||||||
|
found.complete(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found.whenComplete { _, _ -> subscription.unsubscribe() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NodeProcess.assertTransactionsWereVerified(verificationType: VerificationType, vararg txIds: SecureHash) {
|
||||||
|
val nodeLogs = logs("node")!!
|
||||||
|
val externalVerifierLogs = externalVerifierLogs()
|
||||||
for (txId in txIds) {
|
for (txId in txIds) {
|
||||||
assertThat(verifierLogContent).contains("SignedTransaction(id=$txId) verified")
|
assertThat(nodeLogs).contains("Transaction $txId has verification type $verificationType")
|
||||||
|
if (verificationType != VerificationType.IN_PROCESS) {
|
||||||
|
assertThat(externalVerifierLogs).describedAs("External verifier was not started").isNotNull()
|
||||||
|
assertThat(externalVerifierLogs).contains("SignedTransaction(id=$txId) verified")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NodeProcess.externalVerifierLogs(): String {
|
private fun NodeProcess.externalVerifierLogs(): String? = logs("verifier")
|
||||||
val verifierLogs = (nodeDir / "logs")
|
|
||||||
.listDirectoryEntries()
|
private fun NodeProcess.logs(name: String): String? {
|
||||||
.filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" }
|
return (nodeDir / "logs")
|
||||||
assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1)
|
.listDirectoryEntries("$name-${InetAddress.getLocalHost().hostName}.log")
|
||||||
return verifierLogs[0].readText()
|
.singleOrNull()
|
||||||
|
?.readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class VerificationType {
|
||||||
|
IN_PROCESS, EXTERNAL, BOTH
|
||||||
}
|
}
|
@ -1,23 +1,56 @@
|
|||||||
package net.corda.coretests.transactions
|
package net.corda.coretests.transactions
|
||||||
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
import net.corda.core.contracts.ComponentGroupEnum
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.contracts.ComponentGroupEnum.ATTACHMENTS_V2_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.INPUTS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.NOTARY_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.PARAMETERS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.TIMEWINDOW_GROUP
|
||||||
|
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.crypto.DigestService
|
||||||
|
import net.corda.core.crypto.MerkleTree
|
||||||
|
import net.corda.core.crypto.PartialMerkleTree
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
|
import net.corda.core.crypto.secureRandomBytes
|
||||||
import net.corda.core.internal.accessAvailableComponentHashes
|
import net.corda.core.internal.accessAvailableComponentHashes
|
||||||
import net.corda.core.internal.accessGroupHashes
|
import net.corda.core.internal.accessGroupHashes
|
||||||
import net.corda.core.internal.accessGroupMerkleRoots
|
import net.corda.core.internal.accessGroupMerkleRoots
|
||||||
import net.corda.core.internal.createComponentGroups
|
import net.corda.core.internal.createComponentGroups
|
||||||
|
import net.corda.core.internal.getRequiredGroup
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.*
|
import net.corda.core.transactions.ComponentGroup
|
||||||
|
import net.corda.core.transactions.ComponentVisibilityException
|
||||||
|
import net.corda.core.transactions.FilteredComponentGroup
|
||||||
|
import net.corda.core.transactions.FilteredTransaction
|
||||||
|
import net.corda.core.transactions.FilteredTransactionVerificationException
|
||||||
|
import net.corda.core.transactions.NetworkParametersHash
|
||||||
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.BOB_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 org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
import kotlin.test.*
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
class CompatibleTransactionTests {
|
class CompatibleTransactionTests {
|
||||||
private companion object {
|
private companion object {
|
||||||
@ -47,7 +80,7 @@ class CompatibleTransactionTests {
|
|||||||
private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) }
|
private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) }
|
||||||
private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) }
|
private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) }
|
||||||
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) }
|
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) }
|
||||||
private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty.
|
private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty.
|
||||||
private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) }
|
private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) }
|
||||||
private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) }
|
private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) }
|
||||||
private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) }
|
private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) }
|
||||||
@ -96,7 +129,7 @@ class CompatibleTransactionTests {
|
|||||||
|
|
||||||
// Ordering inside a component group matters.
|
// Ordering inside a component group matters.
|
||||||
val inputsShuffled = listOf(stateRef2, stateRef1, stateRef3)
|
val inputsShuffled = listOf(stateRef2, stateRef1, stateRef3)
|
||||||
val inputShuffledGroup = ComponentGroup(INPUTS_GROUP.ordinal, inputsShuffled.map { it -> it.serialize() })
|
val inputShuffledGroup = ComponentGroup(INPUTS_GROUP.ordinal, inputsShuffled.map { it.serialize() })
|
||||||
val componentGroupsB = listOf(
|
val componentGroupsB = listOf(
|
||||||
inputShuffledGroup,
|
inputShuffledGroup,
|
||||||
outputGroup,
|
outputGroup,
|
||||||
@ -114,8 +147,8 @@ class CompatibleTransactionTests {
|
|||||||
// But outputs group Merkle leaf (and the rest) remained the same.
|
// But outputs group Merkle leaf (and the rest) remained the same.
|
||||||
assertEquals(wireTransactionA.accessGroupMerkleRoots()[OUTPUTS_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[OUTPUTS_GROUP.ordinal])
|
assertEquals(wireTransactionA.accessGroupMerkleRoots()[OUTPUTS_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[OUTPUTS_GROUP.ordinal])
|
||||||
assertEquals(wireTransactionA.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal])
|
assertEquals(wireTransactionA.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal])
|
||||||
assertNull(wireTransactionA.accessGroupMerkleRoots()[ATTACHMENTS_GROUP.ordinal])
|
assertNull(wireTransactionA.accessGroupMerkleRoots()[ATTACHMENTS_V2_GROUP.ordinal])
|
||||||
assertNull(wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[ATTACHMENTS_GROUP.ordinal])
|
assertNull(wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[ATTACHMENTS_V2_GROUP.ordinal])
|
||||||
|
|
||||||
// Group leaves (components) ordering does not affect the id. In this case, we added outputs group before inputs.
|
// Group leaves (components) ordering does not affect the id. In this case, we added outputs group before inputs.
|
||||||
val shuffledComponentGroupsA = listOf(
|
val shuffledComponentGroupsA = listOf(
|
||||||
@ -140,7 +173,7 @@ class CompatibleTransactionTests {
|
|||||||
inputGroup,
|
inputGroup,
|
||||||
outputGroup,
|
outputGroup,
|
||||||
commandGroup,
|
commandGroup,
|
||||||
ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components),
|
ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, inputGroup.components),
|
||||||
notaryGroup,
|
notaryGroup,
|
||||||
timeWindowGroup,
|
timeWindowGroup,
|
||||||
signersGroup
|
signersGroup
|
||||||
@ -201,23 +234,16 @@ class CompatibleTransactionTests {
|
|||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `FilteredTransaction constructors and compatibility`() {
|
fun `FilteredTransaction constructors and compatibility`() {
|
||||||
// Filter out all of the components.
|
// Filter out all of the components.
|
||||||
val ftxNothing = wireTransactionA.buildFilteredTransaction(Predicate { false }) // Nothing filtered.
|
val ftxNothing = wireTransactionA.buildFilteredTransaction { false } // Nothing filtered.
|
||||||
// Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
|
// Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
|
||||||
// Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes.
|
// Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes.
|
||||||
assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size)
|
assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size)
|
||||||
ftxNothing.verify()
|
ftxNothing.verify()
|
||||||
|
|
||||||
// Include all of the components.
|
// Include all of the components.
|
||||||
val ftxAll = wireTransactionA.buildFilteredTransaction(Predicate { true }) // All filtered.
|
val ftxAll = wireTransactionA.buildFilteredTransaction { true } // All filtered.
|
||||||
ftxAll.verify()
|
ftxAll.verify()
|
||||||
ftxAll.checkAllComponentsVisible(INPUTS_GROUP)
|
ComponentGroupEnum.entries.forEach(ftxAll::checkAllComponentsVisible)
|
||||||
ftxAll.checkAllComponentsVisible(OUTPUTS_GROUP)
|
|
||||||
ftxAll.checkAllComponentsVisible(COMMANDS_GROUP)
|
|
||||||
ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP)
|
|
||||||
ftxAll.checkAllComponentsVisible(NOTARY_GROUP)
|
|
||||||
ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP)
|
|
||||||
ftxAll.checkAllComponentsVisible(SIGNERS_GROUP)
|
|
||||||
ftxAll.checkAllComponentsVisible(PARAMETERS_GROUP)
|
|
||||||
|
|
||||||
// Filter inputs only.
|
// Filter inputs only.
|
||||||
fun filtering(elem: Any): Boolean {
|
fun filtering(elem: Any): Boolean {
|
||||||
@ -232,9 +258,9 @@ class CompatibleTransactionTests {
|
|||||||
ftxInputs.checkAllComponentsVisible(INPUTS_GROUP)
|
ftxInputs.checkAllComponentsVisible(INPUTS_GROUP)
|
||||||
|
|
||||||
assertEquals(1, ftxInputs.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only.
|
assertEquals(1, ftxInputs.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only.
|
||||||
assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // All 3 inputs are present.
|
assertEquals(3, ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) // All 3 inputs are present.
|
||||||
assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And their corresponding nonces.
|
assertEquals(3, ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) // And their corresponding nonces.
|
||||||
assertNotNull(ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
|
assertNotNull(ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) // And the Merkle tree.
|
||||||
|
|
||||||
// Filter one input only.
|
// Filter one input only.
|
||||||
fun filteringOneInput(elem: Any) = elem == inputs[0]
|
fun filteringOneInput(elem: Any) = elem == inputs[0]
|
||||||
@ -244,9 +270,9 @@ class CompatibleTransactionTests {
|
|||||||
assertFailsWith<ComponentVisibilityException> { ftxOneInput.checkAllComponentsVisible(INPUTS_GROUP) }
|
assertFailsWith<ComponentVisibilityException> { ftxOneInput.checkAllComponentsVisible(INPUTS_GROUP) }
|
||||||
|
|
||||||
assertEquals(1, ftxOneInput.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only.
|
assertEquals(1, ftxOneInput.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only.
|
||||||
assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // 1 input is present.
|
assertEquals(1, ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) // 1 input is present.
|
||||||
assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And its corresponding nonce.
|
assertEquals(1, ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) // And its corresponding nonce.
|
||||||
assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
|
assertNotNull(ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) // And the Merkle tree.
|
||||||
|
|
||||||
// The old client (receiving more component types than expected) is still compatible.
|
// The old client (receiving more component types than expected) is still compatible.
|
||||||
val componentGroupsCompatibleA = listOf(
|
val componentGroupsCompatibleA = listOf(
|
||||||
@ -265,14 +291,14 @@ class CompatibleTransactionTests {
|
|||||||
assertEquals(wireTransactionCompatibleA.id, ftxCompatible.id)
|
assertEquals(wireTransactionCompatibleA.id, ftxCompatible.id)
|
||||||
|
|
||||||
assertEquals(1, ftxCompatible.filteredComponentGroups.size)
|
assertEquals(1, ftxCompatible.filteredComponentGroups.size)
|
||||||
assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size)
|
assertEquals(3, ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size)
|
||||||
assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size)
|
assertEquals(3, ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size)
|
||||||
assertNotNull(ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree)
|
assertNotNull(ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree)
|
||||||
assertNull(wireTransactionCompatibleA.networkParametersHash)
|
assertNull(wireTransactionCompatibleA.networkParametersHash)
|
||||||
assertNull(ftxCompatible.networkParametersHash)
|
assertNull(ftxCompatible.networkParametersHash)
|
||||||
|
|
||||||
// Now, let's allow everything, including the new component type that we cannot process.
|
// Now, let's allow everything, including the new component type that we cannot process.
|
||||||
val ftxCompatibleAll = wireTransactionCompatibleA.buildFilteredTransaction(Predicate { true }) // All filtered, including the unknown component.
|
val ftxCompatibleAll = wireTransactionCompatibleA.buildFilteredTransaction { true } // All filtered, including the unknown component.
|
||||||
ftxCompatibleAll.verify()
|
ftxCompatibleAll.verify()
|
||||||
assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id)
|
assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id)
|
||||||
|
|
||||||
@ -292,7 +318,7 @@ class CompatibleTransactionTests {
|
|||||||
ftxCompatibleNoInputs.verify()
|
ftxCompatibleNoInputs.verify()
|
||||||
assertFailsWith<ComponentVisibilityException> { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) }
|
assertFailsWith<ComponentVisibilityException> { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) }
|
||||||
assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size)
|
assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size)
|
||||||
assertEquals(wireTransactionCompatibleA.componentGroups.map { it.groupIndex }.max(), ftxCompatibleNoInputs.groupHashes.size - 1)
|
assertEquals(wireTransactionCompatibleA.componentGroups.maxOfOrNull { it.groupIndex }, ftxCompatibleNoInputs.groupHashes.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
@ -451,7 +477,7 @@ class CompatibleTransactionTests {
|
|||||||
val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands))
|
val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands))
|
||||||
|
|
||||||
// val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components
|
// val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components
|
||||||
val commandDataHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!!
|
val commandDataHashes = wtx.accessAvailableComponentHashes()[COMMANDS_GROUP.ordinal]!!
|
||||||
val noLastCommandDataPMT = PartialMerkleTree.build(
|
val noLastCommandDataPMT = PartialMerkleTree.build(
|
||||||
MerkleTree.getMerkleTree(commandDataHashes, wtx.digestService),
|
MerkleTree.getMerkleTree(commandDataHashes, wtx.digestService),
|
||||||
commandDataHashes.subList(0, 1)
|
commandDataHashes.subList(0, 1)
|
||||||
@ -466,7 +492,7 @@ class CompatibleTransactionTests {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components
|
val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components
|
||||||
val signerHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!
|
val signerHashes = wtx.accessAvailableComponentHashes()[SIGNERS_GROUP.ordinal]!!
|
||||||
val noLastSignerPMT = PartialMerkleTree.build(
|
val noLastSignerPMT = PartialMerkleTree.build(
|
||||||
MerkleTree.getMerkleTree(signerHashes, wtx.digestService),
|
MerkleTree.getMerkleTree(signerHashes, wtx.digestService),
|
||||||
signerHashes.subList(0, 2)
|
signerHashes.subList(0, 2)
|
||||||
@ -527,7 +553,7 @@ class CompatibleTransactionTests {
|
|||||||
// Modify last signer (we have a pointer from commandData).
|
// Modify last signer (we have a pointer from commandData).
|
||||||
// Update partial Merkle tree for signers.
|
// Update partial Merkle tree for signers.
|
||||||
val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice.
|
val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice.
|
||||||
val alterSignersHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + wtx.digestService.componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
|
val alterSignersHashes = wtx.accessAvailableComponentHashes()[SIGNERS_GROUP.ordinal]!!.subList(0, 2) + wtx.digestService.componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
|
||||||
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes, wtx.digestService)
|
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes, wtx.digestService)
|
||||||
val alterSignerPMTK = PartialMerkleTree.build(
|
val alterSignerPMTK = PartialMerkleTree.build(
|
||||||
alterMTree,
|
alterMTree,
|
||||||
@ -561,7 +587,7 @@ class CompatibleTransactionTests {
|
|||||||
fun `parameters hash visibility`() {
|
fun `parameters hash visibility`() {
|
||||||
fun paramsFilter(elem: Any): Boolean = elem is NetworkParametersHash && elem.hash == paramsHash
|
fun paramsFilter(elem: Any): Boolean = elem is NetworkParametersHash && elem.hash == paramsHash
|
||||||
fun attachmentFilter(elem: Any): Boolean = elem is SecureHash && elem == paramsHash
|
fun attachmentFilter(elem: Any): Boolean = elem is SecureHash && elem == paramsHash
|
||||||
val attachments = ComponentGroup(ATTACHMENTS_GROUP.ordinal, listOf(paramsHash.serialize())) // Same hash as network parameters
|
val attachments = ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, listOf(paramsHash.serialize())) // Same hash as network parameters
|
||||||
val componentGroups = listOf(
|
val componentGroups = listOf(
|
||||||
inputGroup,
|
inputGroup,
|
||||||
outputGroup,
|
outputGroup,
|
||||||
@ -577,12 +603,12 @@ class CompatibleTransactionTests {
|
|||||||
ftx1.verify()
|
ftx1.verify()
|
||||||
assertEquals(wtx.id, ftx1.id)
|
assertEquals(wtx.id, ftx1.id)
|
||||||
ftx1.checkAllComponentsVisible(PARAMETERS_GROUP)
|
ftx1.checkAllComponentsVisible(PARAMETERS_GROUP)
|
||||||
assertFailsWith<ComponentVisibilityException> { ftx1.checkAllComponentsVisible(ATTACHMENTS_GROUP) }
|
assertFailsWith<ComponentVisibilityException> { ftx1.checkAllComponentsVisible(ATTACHMENTS_V2_GROUP) }
|
||||||
// Filter only attachment.
|
// Filter only attachment.
|
||||||
val ftx2 = wtx.buildFilteredTransaction(Predicate(::attachmentFilter))
|
val ftx2 = wtx.buildFilteredTransaction(Predicate(::attachmentFilter))
|
||||||
ftx2.verify()
|
ftx2.verify()
|
||||||
assertEquals(wtx.id, ftx2.id)
|
assertEquals(wtx.id, ftx2.id)
|
||||||
ftx2.checkAllComponentsVisible(ATTACHMENTS_GROUP)
|
ftx2.checkAllComponentsVisible(ATTACHMENTS_V2_GROUP)
|
||||||
assertFailsWith<ComponentVisibilityException> { ftx2.checkAllComponentsVisible(PARAMETERS_GROUP) }
|
assertFailsWith<ComponentVisibilityException> { ftx2.checkAllComponentsVisible(PARAMETERS_GROUP) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
package net.corda.coretests.transactions
|
||||||
|
|
||||||
|
import net.corda.core.contracts.SignatureAttachmentConstraint
|
||||||
|
import net.corda.core.contracts.TransactionState
|
||||||
|
import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS
|
||||||
|
import net.corda.core.internal.RPC_UPLOADER
|
||||||
|
import net.corda.core.internal.copyToDirectory
|
||||||
|
import net.corda.core.internal.hash
|
||||||
|
import net.corda.core.internal.toPath
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.coretesting.internal.useZipFile
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.contracts.asset.Cash
|
||||||
|
import net.corda.finance.issuedBy
|
||||||
|
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.DummyCommandData
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||||
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
|
import net.corda.testing.node.internal.MockNodeArgs
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.copyTo
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.deleteExisting
|
||||||
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
|
||||||
|
@Suppress("INVISIBLE_MEMBER")
|
||||||
|
class TransactionBuilderMockNetworkTest {
|
||||||
|
companion object {
|
||||||
|
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val tempFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
private val mockNetwork = InternalMockNetwork(
|
||||||
|
cordappsForAllNodes = setOf(
|
||||||
|
FINANCE_CONTRACTS_CORDAPP,
|
||||||
|
cordappWithPackages("net.corda.testing.contracts").signed()
|
||||||
|
),
|
||||||
|
initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS)
|
||||||
|
)
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun close() {
|
||||||
|
mockNetwork.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `automatic signature constraint`() {
|
||||||
|
val services = mockNetwork.notaryNodes[0].services
|
||||||
|
|
||||||
|
val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0])
|
||||||
|
val attachmentSigner = attachment!!.signerKeys.single()
|
||||||
|
|
||||||
|
val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner)
|
||||||
|
assertThat(expectedConstraint.isSatisfiedBy(attachment)).isTrue()
|
||||||
|
|
||||||
|
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = mockNetwork.defaultNotaryIdentity)
|
||||||
|
val builder = TransactionBuilder()
|
||||||
|
.addOutputState(outputState)
|
||||||
|
.addCommand(DummyCommandData, mockNetwork.defaultNotaryIdentity.owningKey)
|
||||||
|
val wtx = builder.toWireTransaction(services)
|
||||||
|
|
||||||
|
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `contract overlap in explicit attachments`() {
|
||||||
|
val duplicateJar = tempFolder.newFile("duplicate.jar").toPath()
|
||||||
|
FINANCE_CONTRACTS_CORDAPP.jarFile.copyTo(duplicateJar, overwrite = true)
|
||||||
|
duplicateJar.unsignJar() // Change its hash
|
||||||
|
|
||||||
|
val node = mockNetwork.createNode()
|
||||||
|
val duplicateId = duplicateJar.inputStream().use {
|
||||||
|
node.services.attachments.privilegedImportAttachment(it, RPC_UPLOADER, null)
|
||||||
|
}
|
||||||
|
assertThat(FINANCE_CONTRACTS_CORDAPP.jarFile.hash).isNotEqualTo(duplicateId)
|
||||||
|
|
||||||
|
val builder = TransactionBuilder()
|
||||||
|
builder.addAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||||
|
builder.addAttachment(duplicateId)
|
||||||
|
val identity = node.info.singleIdentity()
|
||||||
|
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy { builder.toWireTransaction(node.services) }
|
||||||
|
.withMessageContaining("Multiple attachments specified for the same contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `populates legacy attachment group if legacy contract CorDapp is present`() {
|
||||||
|
val node = mockNetwork.createNode {
|
||||||
|
it.copyToLegacyContracts(legacyFinanceContractsJar)
|
||||||
|
InternalMockNetwork.MockNode(it)
|
||||||
|
}
|
||||||
|
val builder = TransactionBuilder()
|
||||||
|
val identity = node.info.singleIdentity()
|
||||||
|
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||||
|
val stx = node.services.signInitialTransaction(builder)
|
||||||
|
assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||||
|
assertThat(stx.tx.legacyAttachments).contains(legacyFinanceContractsJar.hash)
|
||||||
|
stx.verify(node.services)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
@Ignore // https://r3-cev.atlassian.net/browse/ENT-11445
|
||||||
|
fun `adds legacy CorDapp dependencies`() {
|
||||||
|
val cordapp1 = tempFolder.newFile("cordapp1.jar").toPath()
|
||||||
|
val cordapp2 = tempFolder.newFile("cordapp2.jar").toPath()
|
||||||
|
// Split the contracts CorDapp into two
|
||||||
|
legacyFinanceContractsJar.copyTo(cordapp1, overwrite = true)
|
||||||
|
cordapp1.useZipFile { zipFs1 ->
|
||||||
|
cordapp2.useZipFile { zipFs2 ->
|
||||||
|
val destinationDir = zipFs2.getPath("net/corda/finance/contracts/asset").createDirectories()
|
||||||
|
zipFs1.getPath("net/corda/finance/contracts/asset")
|
||||||
|
.listDirectoryEntries("OnLedgerAsset*")
|
||||||
|
.forEach {
|
||||||
|
it.copyToDirectory(destinationDir)
|
||||||
|
it.deleteExisting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reSignJar(cordapp1)
|
||||||
|
|
||||||
|
val node = mockNetwork.createNode {
|
||||||
|
it.copyToLegacyContracts(cordapp1, cordapp2)
|
||||||
|
InternalMockNetwork.MockNode(it)
|
||||||
|
}
|
||||||
|
val builder = TransactionBuilder()
|
||||||
|
val identity = node.info.singleIdentity()
|
||||||
|
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||||
|
val stx = node.services.signInitialTransaction(builder)
|
||||||
|
assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||||
|
assertThat(stx.tx.legacyAttachments).contains(cordapp1.hash, cordapp2.hash)
|
||||||
|
stx.verify(node.services)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reSignJar(jar: Path) {
|
||||||
|
jar.unsignJar()
|
||||||
|
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
||||||
|
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) {
|
||||||
|
val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories()
|
||||||
|
jars.forEach { it.copyToDirectory(legacyContractsDir) }
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ package net.corda.coretests.transactions
|
|||||||
import net.corda.core.contracts.Command
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.contracts.HashAttachmentConstraint
|
import net.corda.core.contracts.HashAttachmentConstraint
|
||||||
import net.corda.core.contracts.PrivacySalt
|
import net.corda.core.contracts.PrivacySalt
|
||||||
import net.corda.core.contracts.SignatureAttachmentConstraint
|
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
@ -16,7 +15,6 @@ import net.corda.core.internal.PLATFORM_VERSION
|
|||||||
import net.corda.core.internal.RPC_UPLOADER
|
import net.corda.core.internal.RPC_UPLOADER
|
||||||
import net.corda.core.internal.digestService
|
import net.corda.core.internal.digestService
|
||||||
import net.corda.core.node.ZoneVersionTooLowException
|
import net.corda.core.node.ZoneVersionTooLowException
|
||||||
import net.corda.core.serialization.internal._driverSerializationEnv
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
@ -26,15 +24,12 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME
|
|||||||
import net.corda.testing.core.DummyCommandData
|
import net.corda.testing.core.DummyCommandData
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
import net.corda.testing.core.TestIdentity
|
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.MockServices
|
||||||
import net.corda.testing.node.internal.cordappWithPackages
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -56,6 +51,7 @@ class TransactionBuilderTest {
|
|||||||
private val contractAttachmentId = services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]
|
private val contractAttachmentId = services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
|
@Suppress("INVISIBLE_MEMBER")
|
||||||
fun `bare minimum issuance tx`() {
|
fun `bare minimum issuance tx`() {
|
||||||
val outputState = TransactionState(
|
val outputState = TransactionState(
|
||||||
data = DummyState(),
|
data = DummyState(),
|
||||||
@ -70,6 +66,9 @@ class TransactionBuilderTest {
|
|||||||
assertThat(wtx.outputs).containsOnly(outputState)
|
assertThat(wtx.outputs).containsOnly(outputState)
|
||||||
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
|
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
|
||||||
assertThat(wtx.networkParametersHash).isEqualTo(services.networkParametersService.currentHash)
|
assertThat(wtx.networkParametersHash).isEqualTo(services.networkParametersService.currentHash)
|
||||||
|
// From 4.12 attachments are added to the new component group by default
|
||||||
|
assertThat(wtx.nonLegacyAttachments).isNotEmpty
|
||||||
|
assertThat(wtx.legacyAttachments).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
@ -105,41 +104,6 @@ class TransactionBuilderTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `automatic signature constraint`() {
|
|
||||||
// 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())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val services = mockNetwork.notaryNodes[0].services
|
|
||||||
|
|
||||||
val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0])
|
|
||||||
val attachmentSigner = attachment!!.signerKeys.single()
|
|
||||||
|
|
||||||
val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner)
|
|
||||||
assertTrue(expectedConstraint.isSatisfiedBy(attachment))
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `list accessors are mutable copies`() {
|
fun `list accessors are mutable copies`() {
|
||||||
val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||||
|
@ -8,10 +8,11 @@ enum class ComponentGroupEnum {
|
|||||||
INPUTS_GROUP, // ordinal = 0.
|
INPUTS_GROUP, // ordinal = 0.
|
||||||
OUTPUTS_GROUP, // ordinal = 1.
|
OUTPUTS_GROUP, // ordinal = 1.
|
||||||
COMMANDS_GROUP, // ordinal = 2.
|
COMMANDS_GROUP, // ordinal = 2.
|
||||||
ATTACHMENTS_GROUP, // ordinal = 3.
|
ATTACHMENTS_GROUP, // ordinal = 3. This is for legacy attachments. It's not been renamed for backwards compatibility.
|
||||||
NOTARY_GROUP, // ordinal = 4.
|
NOTARY_GROUP, // ordinal = 4.
|
||||||
TIMEWINDOW_GROUP, // ordinal = 5.
|
TIMEWINDOW_GROUP, // ordinal = 5.
|
||||||
SIGNERS_GROUP, // ordinal = 6.
|
SIGNERS_GROUP, // ordinal = 6.
|
||||||
REFERENCES_GROUP, // ordinal = 7.
|
REFERENCES_GROUP, // ordinal = 7.
|
||||||
PARAMETERS_GROUP // ordinal = 8.
|
PARAMETERS_GROUP, // ordinal = 8.
|
||||||
|
ATTACHMENTS_V2_GROUP // ordinal = 9. From 4.12+ this group is used for attachments.
|
||||||
}
|
}
|
||||||
|
@ -67,12 +67,12 @@ import java.security.PublicKey
|
|||||||
class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: SignedTransaction,
|
class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: SignedTransaction,
|
||||||
val sessionsToCollectFrom: Collection<FlowSession>,
|
val sessionsToCollectFrom: Collection<FlowSession>,
|
||||||
val myOptionalKeys: Iterable<PublicKey>?,
|
val myOptionalKeys: Iterable<PublicKey>?,
|
||||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(
|
constructor(
|
||||||
partiallySignedTx: SignedTransaction,
|
partiallySignedTx: SignedTransaction,
|
||||||
sessionsToCollectFrom: Collection<FlowSession>,
|
sessionsToCollectFrom: Collection<FlowSession>,
|
||||||
progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()
|
progressTracker: ProgressTracker = tracker()
|
||||||
) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -100,6 +100,7 @@ class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: Sig
|
|||||||
|
|
||||||
// The signatures must be valid and the transaction must be valid.
|
// The signatures must be valid and the transaction must be valid.
|
||||||
partiallySignedTx.verifySignaturesExcept(notSigned)
|
partiallySignedTx.verifySignaturesExcept(notSigned)
|
||||||
|
// TODO Should this be calling SignedTransaction.verify directly? https://r3-cev.atlassian.net/browse/ENT-11458
|
||||||
partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify()
|
partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify()
|
||||||
|
|
||||||
// Determine who still needs to sign.
|
// Determine who still needs to sign.
|
||||||
@ -235,7 +236,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
|||||||
* - Call the flow via [FlowLogic.subFlow]
|
* - Call the flow via [FlowLogic.subFlow]
|
||||||
* - The flow returns the transaction signed with the additional signature.
|
* - The flow returns the transaction signed with the additional signature.
|
||||||
*
|
*
|
||||||
* Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see
|
* Example - checking and signing a transaction involving a `DummyContract`, see
|
||||||
* CollectSignaturesFlowTests.kt for further examples:
|
* CollectSignaturesFlowTests.kt for further examples:
|
||||||
*
|
*
|
||||||
* class Responder(val otherPartySession: FlowSession): FlowLogic<SignedTransaction>() {
|
* class Responder(val otherPartySession: FlowSession): FlowLogic<SignedTransaction>() {
|
||||||
@ -259,7 +260,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
|||||||
* @param otherSideSession The session which is providing you a transaction to sign.
|
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||||
*/
|
*/
|
||||||
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
||||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
|
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
|
||||||
@ -287,6 +288,7 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
|
|||||||
checkMySignaturesRequired(stx, signingKeys)
|
checkMySignaturesRequired(stx, signingKeys)
|
||||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||||
checkSignatures(stx)
|
checkSignatures(stx)
|
||||||
|
// TODO Should this be calling SignedTransaction.verify directly? https://r3-cev.atlassian.net/browse/ENT-11458
|
||||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||||
// Perform some custom verification over the transaction.
|
// Perform some custom verification over the transaction.
|
||||||
try {
|
try {
|
||||||
|
@ -11,12 +11,14 @@ import net.corda.core.internal.PlatformVersionSwitches
|
|||||||
import net.corda.core.internal.ServiceHubCoreInternal
|
import net.corda.core.internal.ServiceHubCoreInternal
|
||||||
import net.corda.core.internal.pushToLoggingContext
|
import net.corda.core.internal.pushToLoggingContext
|
||||||
import net.corda.core.internal.telemetry.telemetryServiceInternal
|
import net.corda.core.internal.telemetry.telemetryServiceInternal
|
||||||
|
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||||
import net.corda.core.internal.warnOnce
|
import net.corda.core.internal.warnOnce
|
||||||
import net.corda.core.node.StatesToRecord
|
import net.corda.core.node.StatesToRecord
|
||||||
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
||||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.Try
|
import net.corda.core.utilities.Try
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
@ -170,6 +172,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
|||||||
@Suppress("ComplexMethod", "NestedBlockDepth")
|
@Suppress("ComplexMethod", "NestedBlockDepth")
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
|
require(transaction.coreTransaction is WireTransaction) // Sanity check
|
||||||
if (!newApi) {
|
if (!newApi) {
|
||||||
logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||||
"FinalityFlow with FlowSessions. (${serviceHub.getAppContext().cordapp.info})")
|
"FinalityFlow with FlowSessions. (${serviceHub.getAppContext().cordapp.info})")
|
||||||
@ -447,9 +450,12 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
|||||||
// The notary signature(s) are allowed to be missing but no others.
|
// The notary signature(s) are allowed to be missing but no others.
|
||||||
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
||||||
// TODO= [CORDA-3267] Remove duplicate signature verification
|
// TODO= [CORDA-3267] Remove duplicate signature verification
|
||||||
val ltx = transaction.toLedgerTransaction(serviceHub, false)
|
val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) as LedgerTransaction?
|
||||||
ltx.verify()
|
// verifyInternal returns null if the transaction was verified externally, which *could* happen on a very odd scenerio of a 4.11
|
||||||
return ltx
|
// node creating the transaction but a 4.12 kicking off finality. In that case, we still want a LedgerTransaction object for
|
||||||
|
// recording to the vault, etc. Note that calling verify() on this will fail as it doesn't have the necessary non-legacy attachments
|
||||||
|
// for verification by the node.
|
||||||
|
return ltx ?: transaction.toLedgerTransaction(serviceHub, checkSufficientSignatures = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package net.corda.core.internal
|
|
||||||
|
|
||||||
import net.corda.core.contracts.Attachment
|
|
||||||
import net.corda.core.contracts.ContractAttachment
|
|
||||||
|
|
||||||
interface InternalAttachment : Attachment {
|
|
||||||
/**
|
|
||||||
* The version of the Kotlin metadata, if this attachment has one. See `kotlinx.metadata.jvm.JvmMetadataVersion` for more information on
|
|
||||||
* how this maps to the Kotlin language version.
|
|
||||||
*/
|
|
||||||
val kotlinMetadataVersion: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Because [ContractAttachment] is public API, we can't make it implement [InternalAttachment] without also leaking it out.
|
|
||||||
*
|
|
||||||
* @see InternalAttachment.kotlinMetadataVersion
|
|
||||||
*/
|
|
||||||
val Attachment.kotlinMetadataVersion: String? get() {
|
|
||||||
var attachment = this
|
|
||||||
while (true) {
|
|
||||||
when (attachment) {
|
|
||||||
is InternalAttachment -> return attachment.kotlinMetadataVersion
|
|
||||||
is ContractAttachment -> attachment = attachment.attachment
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -79,7 +79,14 @@ import kotlin.math.roundToLong
|
|||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.full.createInstance
|
import kotlin.reflect.full.createInstance
|
||||||
|
|
||||||
val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this
|
val Throwable.rootCause: Throwable
|
||||||
|
get() {
|
||||||
|
var root = this
|
||||||
|
while (true) {
|
||||||
|
root = root.cause ?: return root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val Throwable.rootMessage: String? get() {
|
val Throwable.rootMessage: String? get() {
|
||||||
var message = this.message
|
var message = this.message
|
||||||
var throwable = cause
|
var throwable = cause
|
||||||
@ -231,8 +238,6 @@ inline fun elapsedTime(block: () -> Unit): Duration {
|
|||||||
|
|
||||||
fun <T> Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body)
|
fun <T> Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body)
|
||||||
|
|
||||||
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
|
||||||
// returns in the IRSSimulationTest. If not, commit the inline back.
|
|
||||||
fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
|
fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
|
||||||
// Use nanoTime as it's monotonic.
|
// Use nanoTime as it's monotonic.
|
||||||
val now = System.nanoTime()
|
val now = System.nanoTime()
|
||||||
@ -639,16 +644,10 @@ val Logger.level: Level
|
|||||||
else -> throw IllegalStateException("Unknown logging level")
|
else -> throw IllegalStateException("Unknown logging level")
|
||||||
}
|
}
|
||||||
|
|
||||||
const val JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION = 46
|
const val JAVA_1_2_CLASS_FILE_MAJOR_VERSION = 46
|
||||||
const val JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION = 61
|
const val JAVA_8_CLASS_FILE_MAJOR_VERSION = 52
|
||||||
|
const val JAVA_17_CLASS_FILE_MAJOR_VERSION = 61
|
||||||
|
|
||||||
/**
|
fun String.capitalize(): String = replaceFirstChar { it.titlecase(Locale.getDefault()) }
|
||||||
* String extension functions - to keep calling code readable following upgrade to Kotlin 1.9
|
|
||||||
*/
|
|
||||||
fun String.capitalize() : String {
|
|
||||||
return this.replaceFirstChar { it.titlecase(Locale.getDefault()) }
|
|
||||||
}
|
|
||||||
fun String.decapitalize() : String {
|
|
||||||
return this.replaceFirstChar { it.lowercase(Locale.getDefault()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun String.decapitalize(): String = replaceFirstChar { it.lowercase(Locale.getDefault()) }
|
||||||
|
@ -94,7 +94,7 @@ class ResolveTransactionsFlow private constructor(
|
|||||||
fun fetchMissingAttachments(transaction: SignedTransaction): Boolean {
|
fun fetchMissingAttachments(transaction: SignedTransaction): Boolean {
|
||||||
val tx = transaction.coreTransaction
|
val tx = transaction.coreTransaction
|
||||||
val attachmentIds = when (tx) {
|
val attachmentIds = when (tx) {
|
||||||
is WireTransaction -> tx.attachments.toSet()
|
is WireTransaction -> tx.allAttachments
|
||||||
is ContractUpgradeWireTransaction -> setOf(tx.legacyContractAttachmentId, tx.upgradedContractAttachmentId)
|
is ContractUpgradeWireTransaction -> setOf(tx.legacyContractAttachmentId, tx.upgradedContractAttachmentId)
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
package net.corda.core.internal
|
package net.corda.core.internal
|
||||||
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.Command
|
||||||
|
import net.corda.core.contracts.CommandData
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.ATTACHMENTS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.ATTACHMENTS_V2_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.INPUTS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.NOTARY_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.PARAMETERS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.REFERENCES_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP
|
||||||
|
import net.corda.core.contracts.ComponentGroupEnum.TIMEWINDOW_GROUP
|
||||||
|
import net.corda.core.contracts.ContractClassName
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.NamedByHash
|
||||||
|
import net.corda.core.contracts.PrivacySalt
|
||||||
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.contracts.StateRef
|
||||||
|
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.DigestService
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.algorithm
|
import net.corda.core.crypto.algorithm
|
||||||
@ -8,8 +29,20 @@ import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
|||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.MissingAttachmentsException
|
||||||
import net.corda.core.transactions.*
|
import net.corda.core.serialization.MissingAttachmentsRuntimeException
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.serialization.SerializationFactory
|
||||||
|
import net.corda.core.serialization.SerializedBytes
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
|
import net.corda.core.transactions.BaseTransaction
|
||||||
|
import net.corda.core.transactions.ComponentGroup
|
||||||
|
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||||
|
import net.corda.core.transactions.FilteredComponentGroup
|
||||||
|
import net.corda.core.transactions.FullTransaction
|
||||||
|
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -68,8 +101,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
|||||||
forceDeserialize: Boolean = false,
|
forceDeserialize: Boolean = false,
|
||||||
factory: SerializationFactory = SerializationFactory.defaultFactory,
|
factory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||||
context: SerializationContext = factory.defaultContext): List<T> {
|
context: SerializationContext = factory.defaultContext): List<T> {
|
||||||
val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal }
|
val group = componentGroups.getGroup(groupEnum)
|
||||||
|
|
||||||
if (group == null || group.components.isEmpty()) {
|
if (group == null || group.components.isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
@ -85,7 +117,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
|||||||
factory.deserialize(component, clazz.java, context)
|
factory.deserialize(component, clazz.java, context)
|
||||||
} catch (e: MissingAttachmentsException) {
|
} catch (e: MissingAttachmentsException) {
|
||||||
/**
|
/**
|
||||||
* [ServiceHub.signInitialTransaction] forgets to declare that
|
* `ServiceHub.signInitialTransaction` forgets to declare that
|
||||||
* it may throw any checked exceptions. Wrap this one inside
|
* it may throw any checked exceptions. Wrap this one inside
|
||||||
* an unchecked version to avoid breaking Java CorDapps.
|
* an unchecked version to avoid breaking Java CorDapps.
|
||||||
*/
|
*/
|
||||||
@ -96,7 +128,13 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun <T : ComponentGroup> List<T>.getGroup(type: ComponentGroupEnum): T? = firstOrNull { it.groupIndex == type.ordinal }
|
||||||
|
|
||||||
|
fun <T : ComponentGroup> List<T>.getRequiredGroup(type: ComponentGroupEnum): T {
|
||||||
|
return requireNotNull(getGroup(type)) { "Missing component group $type" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**x
|
||||||
* Exception raised if an error was encountered while attempting to deserialise a component group in a transaction.
|
* Exception raised if an error was encountered while attempting to deserialise a component group in a transaction.
|
||||||
*/
|
*/
|
||||||
class TransactionDeserialisationException(groupEnum: ComponentGroupEnum, index: Int, cause: Exception):
|
class TransactionDeserialisationException(groupEnum: ComponentGroupEnum, index: Int, cause: Exception):
|
||||||
@ -119,9 +157,9 @@ fun deserialiseCommands(
|
|||||||
// TODO: we could avoid deserialising unrelated signers.
|
// TODO: we could avoid deserialising unrelated signers.
|
||||||
// However, current approach ensures the transaction is not malformed
|
// However, current approach ensures the transaction is not malformed
|
||||||
// and it will throw if any of the signers objects is not List of public keys).
|
// and it will throw if any of the signers objects is not List of public keys).
|
||||||
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize, factory, context))
|
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, SIGNERS_GROUP, forceDeserialize, factory, context))
|
||||||
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize, factory, context)
|
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, COMMANDS_GROUP, forceDeserialize, factory, context)
|
||||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
val group = componentGroups.getGroup(COMMANDS_GROUP)
|
||||||
return if (group is FilteredComponentGroup) {
|
return if (group is FilteredComponentGroup) {
|
||||||
check(commandDataList.size <= signersList.size) {
|
check(commandDataList.size <= signersList.size) {
|
||||||
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
|
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
|
||||||
@ -141,10 +179,7 @@ fun deserialiseCommands(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Suppress("LongParameterList")
|
||||||
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
|
|
||||||
* for backwards compatibility purposes.
|
|
||||||
*/
|
|
||||||
fun createComponentGroups(inputs: List<StateRef>,
|
fun createComponentGroups(inputs: List<StateRef>,
|
||||||
outputs: List<TransactionState<ContractState>>,
|
outputs: List<TransactionState<ContractState>>,
|
||||||
commands: List<Command<*>>,
|
commands: List<Command<*>>,
|
||||||
@ -152,26 +187,37 @@ fun createComponentGroups(inputs: List<StateRef>,
|
|||||||
notary: Party?,
|
notary: Party?,
|
||||||
timeWindow: TimeWindow?,
|
timeWindow: TimeWindow?,
|
||||||
references: List<StateRef>,
|
references: List<StateRef>,
|
||||||
networkParametersHash: SecureHash?): List<ComponentGroup> {
|
networkParametersHash: SecureHash?,
|
||||||
|
// The old attachments group is now only used to create transaction compatible with 4.11 (or earlier) nodes
|
||||||
|
legacyAttachments: List<SecureHash> = emptyList()): List<ComponentGroup> {
|
||||||
val serializationFactory = SerializationFactory.defaultFactory
|
val serializationFactory = SerializationFactory.defaultFactory
|
||||||
val serializationContext = serializationFactory.defaultContext
|
val serializationContext = serializationFactory.defaultContext
|
||||||
val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) }
|
val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) }
|
||||||
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
|
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
|
||||||
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUTS_GROUP.ordinal, inputs.lazyMapped(serialize)))
|
componentGroupMap.addListGroup(INPUTS_GROUP, inputs, serialize)
|
||||||
if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCES_GROUP.ordinal, references.lazyMapped(serialize)))
|
componentGroupMap.addListGroup(REFERENCES_GROUP, references, serialize)
|
||||||
if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP.ordinal, outputs.lazyMapped(serialize)))
|
componentGroupMap.addListGroup(OUTPUTS_GROUP, outputs, serialize)
|
||||||
// Adding commandData only to the commands group. Signers are added in their own group.
|
// Adding commandData only to the commands group. Signers are added in their own group.
|
||||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.COMMANDS_GROUP.ordinal, commands.map { it.value }.lazyMapped(serialize)))
|
componentGroupMap.addListGroup(COMMANDS_GROUP, commands.map { it.value }, serialize)
|
||||||
if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, attachments.lazyMapped(serialize)))
|
// Attachments which can only be processed by 4.12 and later.
|
||||||
if (notary != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize)))
|
componentGroupMap.addListGroup(ATTACHMENTS_V2_GROUP, attachments, serialize)
|
||||||
if (timeWindow != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize)))
|
// The original attachments group now only contains attachments which can be processed by 4.11 and ealier (and the external verifier).
|
||||||
|
componentGroupMap.addListGroup(ATTACHMENTS_GROUP, legacyAttachments, serialize)
|
||||||
|
if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize)))
|
||||||
|
if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize)))
|
||||||
// Adding signers to their own group. This is required for command visibility purposes: a party receiving
|
// Adding signers to their own group. This is required for command visibility purposes: a party receiving
|
||||||
// a FilteredTransaction can now verify it sees all the commands it should sign.
|
// a FilteredTransaction can now verify it sees all the commands it should sign.
|
||||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize)))
|
componentGroupMap.addListGroup(SIGNERS_GROUP, commands.map { it.signers }, serialize)
|
||||||
if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize())))
|
if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize())))
|
||||||
return componentGroupMap
|
return componentGroupMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MutableList<ComponentGroup>.addListGroup(type: ComponentGroupEnum, list: List<Any>, serialize: (Any, Int) -> SerializedBytes<Any>) {
|
||||||
|
if (list.isNotEmpty()) {
|
||||||
|
add(ComponentGroup(type.ordinal, list.lazyMapped(serialize)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typealias SerializedTransactionState = SerializedBytes<TransactionState<ContractState>>
|
typealias SerializedTransactionState = SerializedBytes<TransactionState<ContractState>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,10 +313,3 @@ internal fun checkNotaryWhitelisted(ftx: FullTransaction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val CoreTransaction.attachmentIds: List<SecureHash>
|
|
||||||
get() = when (this) {
|
|
||||||
is WireTransaction -> attachments
|
|
||||||
is ContractUpgradeWireTransaction -> listOf(legacyContractAttachmentId, upgradedContractAttachmentId)
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
|
@ -36,6 +36,7 @@ data class CordappImpl(
|
|||||||
override val minimumPlatformVersion: Int,
|
override val minimumPlatformVersion: Int,
|
||||||
override val targetPlatformVersion: Int,
|
override val targetPlatformVersion: Int,
|
||||||
override val jarHash: SecureHash.SHA256 = jarFile.hash,
|
override val jarHash: SecureHash.SHA256 = jarFile.hash,
|
||||||
|
val languageVersion: LanguageVersion = LanguageVersion.Data,
|
||||||
val notaryService: Class<out NotaryService>? = null,
|
val notaryService: Class<out NotaryService>? = null,
|
||||||
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
|
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
|
||||||
val isLoaded: Boolean = true,
|
val isLoaded: Boolean = true,
|
||||||
|
@ -14,7 +14,10 @@ interface CordappProviderInternal : CordappProvider {
|
|||||||
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
|
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to [getContractAttachmentID] except it returns the [ContractAttachment] object.
|
* Similar to [getContractAttachmentID] except it returns the [ContractAttachment] object and also returns an optional second attachment
|
||||||
|
* representing the legacy version (4.11 or earlier) of the contract, if one exists.
|
||||||
*/
|
*/
|
||||||
fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment?
|
fun getContractAttachments(contractClassName: ContractClassName): ContractAttachmentWithLegacy?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ContractAttachmentWithLegacy(val currentAttachment: ContractAttachment, val legacyAttachment: ContractAttachment? = null)
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package net.corda.core.internal.cordapp
|
||||||
|
|
||||||
|
data class KotlinMetadataVersion(val major: Int, val minor: Int, val patch: Int = 0) : Comparable<KotlinMetadataVersion> {
|
||||||
|
companion object {
|
||||||
|
fun from(versionArray: IntArray): KotlinMetadataVersion {
|
||||||
|
val (major, minor, patch) = versionArray
|
||||||
|
return KotlinMetadataVersion(major, minor, patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(major >= 0) { "Major version should be not less than 0" }
|
||||||
|
require(minor >= 0) { "Minor version should be not less than 0" }
|
||||||
|
require(patch >= 0) { "Patch version should be not less than 0" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the equivalent [KotlinVersion] without the patch.
|
||||||
|
*/
|
||||||
|
val languageMinorVersion: KotlinVersion
|
||||||
|
// See `kotlinx.metadata.jvm.JvmMetadataVersion`
|
||||||
|
get() = if (major == 1 && minor == 1) KotlinVersion(1, 2) else KotlinVersion(major, minor)
|
||||||
|
|
||||||
|
override fun compareTo(other: KotlinMetadataVersion): Int {
|
||||||
|
val majors = this.major.compareTo(other.major)
|
||||||
|
if (majors != 0) return majors
|
||||||
|
val minors = this.minor.compareTo(other.minor)
|
||||||
|
return if (minors != 0) minors else this.patch.compareTo(other.patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "$major.$minor.$patch"
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package net.corda.core.internal.cordapp
|
||||||
|
|
||||||
|
import net.corda.core.internal.JAVA_17_CLASS_FILE_MAJOR_VERSION
|
||||||
|
import net.corda.core.internal.JAVA_1_2_CLASS_FILE_MAJOR_VERSION
|
||||||
|
import net.corda.core.internal.JAVA_8_CLASS_FILE_MAJOR_VERSION
|
||||||
|
|
||||||
|
sealed class LanguageVersion {
|
||||||
|
/**
|
||||||
|
* Returns true if this version is compatible with Corda 4.11 or earlier.
|
||||||
|
*/
|
||||||
|
abstract val isLegacyCompatible: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this version is compatible with Corda 4.12 or later.
|
||||||
|
*/
|
||||||
|
abstract val isNonLegacyCompatible: Boolean
|
||||||
|
|
||||||
|
@Suppress("ConvertObjectToDataObject") // External verifier uses Kotlin 1.2
|
||||||
|
object Data : LanguageVersion() {
|
||||||
|
override val isLegacyCompatible: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
override val isNonLegacyCompatible: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
override fun toString(): String = "Data"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Bytecode(val classFileMajorVersion: Int, val kotlinMetadataVersion: KotlinMetadataVersion?): LanguageVersion() {
|
||||||
|
companion object {
|
||||||
|
private val KOTLIN_1_2_VERSION = KotlinVersion(1, 2)
|
||||||
|
private val KOTLIN_1_9_VERSION = KotlinVersion(1, 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(classFileMajorVersion in JAVA_1_2_CLASS_FILE_MAJOR_VERSION..JAVA_17_CLASS_FILE_MAJOR_VERSION) {
|
||||||
|
"Unsupported class file major version $classFileMajorVersion"
|
||||||
|
}
|
||||||
|
val kotlinVersion = kotlinMetadataVersion?.languageMinorVersion
|
||||||
|
require(kotlinVersion == null || kotlinVersion == KOTLIN_1_2_VERSION || kotlinVersion == KOTLIN_1_9_VERSION) {
|
||||||
|
"Unsupported Kotlin metadata version $kotlinMetadataVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLegacyCompatible: Boolean
|
||||||
|
get() = when {
|
||||||
|
classFileMajorVersion > JAVA_8_CLASS_FILE_MAJOR_VERSION -> false
|
||||||
|
kotlinMetadataVersion == null -> true // Java 8 CorDapp is fine
|
||||||
|
else -> kotlinMetadataVersion.languageMinorVersion == KOTLIN_1_2_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isNonLegacyCompatible: Boolean
|
||||||
|
// Java-only CorDapp will always be compatible on 4.12
|
||||||
|
get() = if (kotlinMetadataVersion == null) true else kotlinMetadataVersion.languageMinorVersion == KOTLIN_1_9_VERSION
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package net.corda.core.internal.verification
|
package net.corda.core.internal.verification
|
||||||
|
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.contracts.ComponentGroupEnum
|
import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.contracts.TransactionResolutionException
|
import net.corda.core.contracts.TransactionResolutionException
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
@ -11,6 +11,7 @@ import net.corda.core.internal.SerializedTransactionState
|
|||||||
import net.corda.core.internal.TRUSTED_UPLOADERS
|
import net.corda.core.internal.TRUSTED_UPLOADERS
|
||||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
import net.corda.core.internal.cordapp.CordappProviderInternal
|
||||||
import net.corda.core.internal.entries
|
import net.corda.core.internal.entries
|
||||||
|
import net.corda.core.internal.getRequiredGroup
|
||||||
import net.corda.core.internal.getRequiredTransaction
|
import net.corda.core.internal.getRequiredTransaction
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
@ -85,9 +86,7 @@ interface NodeVerificationSupport : VerificationSupport {
|
|||||||
|
|
||||||
private fun getRegularOutput(coreTransaction: WireTransaction, outputIndex: Int): SerializedTransactionState {
|
private fun getRegularOutput(coreTransaction: WireTransaction, outputIndex: Int): SerializedTransactionState {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return coreTransaction.componentGroups
|
return coreTransaction.componentGroups.getRequiredGroup(OUTPUTS_GROUP).components[outputIndex] as SerializedTransactionState
|
||||||
.first { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }
|
|
||||||
.components[outputIndex] as SerializedTransactionState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,6 +136,8 @@ interface NodeVerificationSupport : VerificationSupport {
|
|||||||
override fun getTrustedClassAttachment(className: String): Attachment? {
|
override fun getTrustedClassAttachment(className: String): Attachment? {
|
||||||
val allTrusted = attachments.queryAttachments(
|
val allTrusted = attachments.queryAttachments(
|
||||||
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
|
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
|
||||||
|
// JarScanningCordappLoader makes sure legacy contract CorDapps have a coresponding non-legacy CorDapp, and that the
|
||||||
|
// legacy CorDapp has a smaller version number. Thus sorting by the version here ensures we never return the legacy attachment.
|
||||||
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
|
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import java.security.PublicKey
|
|||||||
* Represents the operations required to resolve and verify a transaction.
|
* Represents the operations required to resolve and verify a transaction.
|
||||||
*/
|
*/
|
||||||
interface VerificationSupport {
|
interface VerificationSupport {
|
||||||
val isResolutionLazy: Boolean get() = true
|
val isInProcess: Boolean get() = true
|
||||||
|
|
||||||
val appClassLoader: ClassLoader
|
val appClassLoader: ClassLoader
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ import net.corda.core.contracts.TransactionVerificationException
|
|||||||
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
||||||
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
|
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION
|
import net.corda.core.internal.JAVA_17_CLASS_FILE_MAJOR_VERSION
|
||||||
import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION
|
import net.corda.core.internal.JAVA_1_2_CLASS_FILE_MAJOR_VERSION
|
||||||
import net.corda.core.internal.JarSignatureCollector
|
import net.corda.core.internal.JarSignatureCollector
|
||||||
import net.corda.core.internal.NamedCacheFactory
|
import net.corda.core.internal.NamedCacheFactory
|
||||||
import net.corda.core.internal.PlatformVersionSwitches
|
import net.corda.core.internal.PlatformVersionSwitches
|
||||||
@ -340,7 +340,7 @@ object AttachmentsClassLoaderBuilder {
|
|||||||
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
|
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
|
||||||
val serializers = try {
|
val serializers = try {
|
||||||
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
||||||
JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
JAVA_1_2_CLASS_FILE_MAJOR_VERSION..JAVA_17_CLASS_FILE_MAJOR_VERSION)
|
||||||
} catch (ex: UnsupportedClassVersionError) {
|
} catch (ex: UnsupportedClassVersionError) {
|
||||||
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
|
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package net.corda.core.transactions
|
package net.corda.core.transactions
|
||||||
|
|
||||||
import net.corda.core.CordaException
|
import net.corda.core.CordaException
|
||||||
|
import net.corda.core.CordaInternal
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
import net.corda.core.contracts.ComponentGroupEnum.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.deserialiseCommands
|
import net.corda.core.internal.deserialiseCommands
|
||||||
import net.corda.core.internal.deserialiseComponentGroup
|
import net.corda.core.internal.deserialiseComponentGroup
|
||||||
|
import net.corda.core.internal.getGroup
|
||||||
|
import net.corda.core.internal.getRequiredGroup
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
@ -29,8 +32,34 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
|||||||
@DeprecatedConstructorForDeserialization(1)
|
@DeprecatedConstructorForDeserialization(1)
|
||||||
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, DigestService.sha2_256)
|
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, DigestService.sha2_256)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the attachments compatible with 4.11 and earlier. This may be empty, which means this transaction cannot be verified by a
|
||||||
|
* 4.11 node. On 4.12 and later these attachments are ignored.
|
||||||
|
*/
|
||||||
|
val legacyAttachments: List<SecureHash> = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the attachments compatible with 4.12 and later. This will be empty for transactions created on 4.11 or earlier.
|
||||||
|
*
|
||||||
|
* [legacyAttachments] and [nonLegacyAttachments] are independent of each other and may contain the same attachments. This is to provide backwards
|
||||||
|
* compatiblity and enable both 4.11 and 4.12 nodes to verify the same transaction.
|
||||||
|
*/
|
||||||
|
@CordaInternal
|
||||||
|
@JvmSynthetic
|
||||||
|
internal val nonLegacyAttachments: List<SecureHash> = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_V2_GROUP)
|
||||||
|
|
||||||
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
|
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
|
||||||
val attachments: List<SecureHash> = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP)
|
val attachments: List<SecureHash>
|
||||||
|
get() = when {
|
||||||
|
legacyAttachments.isEmpty() -> nonLegacyAttachments // 4.12+ only transaction
|
||||||
|
nonLegacyAttachments.isEmpty() -> legacyAttachments // 4.11 or earlier transaction
|
||||||
|
else -> nonLegacyAttachments // This is a backwards compatible transaction, but from an API PoV we're not concerned with the legacy attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
@CordaInternal
|
||||||
|
internal val allAttachments: Set<SecureHash>
|
||||||
|
@JvmSynthetic
|
||||||
|
get() = legacyAttachments.toMutableSet().apply { addAll(nonLegacyAttachments) }
|
||||||
|
|
||||||
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
||||||
override val inputs: List<StateRef> = deserialiseComponentGroup(componentGroups, StateRef::class, INPUTS_GROUP)
|
override val inputs: List<StateRef> = deserialiseComponentGroup(componentGroups, StateRef::class, INPUTS_GROUP)
|
||||||
@ -67,18 +96,20 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
|||||||
* - list of each input that is present
|
* - list of each input that is present
|
||||||
* - list of each output that is present
|
* - list of each output that is present
|
||||||
* - list of each command that is present
|
* - list of each command that is present
|
||||||
* - list of each attachment that is present
|
* - list of each legacy attachment that is present (only relevant if transaction is being verified on a legacy node)
|
||||||
* - The notary [Party], if present (list with one element)
|
* - The notary [Party], if present (list with one element)
|
||||||
* - The time-window of the transaction, if present (list with one element)
|
* - The time-window of the transaction, if present (list with one element)
|
||||||
* - list of each reference input that is present
|
* - list of each reference input that is present
|
||||||
* - network parameters hash if present
|
* - network parameters hash if present
|
||||||
|
* - list of each attachment that is present
|
||||||
*/
|
*/
|
||||||
val availableComponentGroups: List<List<Any>>
|
val availableComponentGroups: List<List<Any>>
|
||||||
get() {
|
get() {
|
||||||
val result = mutableListOf(inputs, outputs, commands, attachments, references)
|
val result = mutableListOf(inputs, outputs, commands, legacyAttachments, references)
|
||||||
notary?.let { result += listOf(it) }
|
notary?.let { result += listOf(it) }
|
||||||
timeWindow?.let { result += listOf(it) }
|
timeWindow?.let { result += listOf(it) }
|
||||||
networkParametersHash?.let { result += listOf(it) }
|
networkParametersHash?.let { result += listOf(it) }
|
||||||
|
result += nonLegacyAttachments
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,12 +184,10 @@ class FilteredTransaction internal constructor(
|
|||||||
// This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details.
|
// This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details.
|
||||||
if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) {
|
if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) {
|
||||||
signersIncluded = true
|
signersIncluded = true
|
||||||
val signersGroupIndex = SIGNERS_GROUP.ordinal
|
|
||||||
// There exist commands, thus the signers group is not empty.
|
// There exist commands, thus the signers group is not empty.
|
||||||
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
|
filteredSerialisedComponents[SIGNERS_GROUP.ordinal] = wtx.componentGroups.getRequiredGroup(SIGNERS_GROUP).components.toMutableList()
|
||||||
filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList()
|
filteredComponentNonces[SIGNERS_GROUP.ordinal] = wtx.availableComponentNonces[SIGNERS_GROUP.ordinal]!!.toMutableList()
|
||||||
filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList()
|
filteredComponentHashes[SIGNERS_GROUP.ordinal] = wtx.availableComponentHashes[SIGNERS_GROUP.ordinal]!!.toMutableList()
|
||||||
filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +195,8 @@ class FilteredTransaction internal constructor(
|
|||||||
wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_GROUP.ordinal, internalIndex) }
|
wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_GROUP.ordinal, internalIndex) }
|
||||||
wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, OUTPUTS_GROUP.ordinal, internalIndex) }
|
wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, OUTPUTS_GROUP.ordinal, internalIndex) }
|
||||||
wtx.commands.forEachIndexed { internalIndex, it -> filter(it, COMMANDS_GROUP.ordinal, internalIndex) }
|
wtx.commands.forEachIndexed { internalIndex, it -> filter(it, COMMANDS_GROUP.ordinal, internalIndex) }
|
||||||
wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) }
|
wtx.legacyAttachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) }
|
||||||
|
wtx.nonLegacyAttachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_V2_GROUP.ordinal, internalIndex) }
|
||||||
if (wtx.notary != null) filter(wtx.notary, NOTARY_GROUP.ordinal, 0)
|
if (wtx.notary != null) filter(wtx.notary, NOTARY_GROUP.ordinal, 0)
|
||||||
if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0)
|
if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0)
|
||||||
// Note that because [inputs] and [references] share the same type [StateRef], we use a wrapper for references [ReferenceStateRef],
|
// Note that because [inputs] and [references] share the same type [StateRef], we use a wrapper for references [ReferenceStateRef],
|
||||||
@ -269,7 +299,7 @@ class FilteredTransaction internal constructor(
|
|||||||
*/
|
*/
|
||||||
@Throws(ComponentVisibilityException::class)
|
@Throws(ComponentVisibilityException::class)
|
||||||
fun checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum) {
|
fun checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum) {
|
||||||
val group = filteredComponentGroups.firstOrNull { it.groupIndex == componentGroupEnum.ordinal }
|
val group = filteredComponentGroups.getGroup(componentGroupEnum)
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
// If we don't receive elements of a particular component, check if its ordinal is bigger that the
|
// If we don't receive elements of a particular component, check if its ordinal is bigger that the
|
||||||
// groupHashes.size or if the group hash is allOnesHash,
|
// groupHashes.size or if the group hash is allOnesHash,
|
||||||
@ -300,7 +330,7 @@ class FilteredTransaction internal constructor(
|
|||||||
*/
|
*/
|
||||||
@Throws(ComponentVisibilityException::class)
|
@Throws(ComponentVisibilityException::class)
|
||||||
fun checkCommandVisibility(publicKey: PublicKey) {
|
fun checkCommandVisibility(publicKey: PublicKey) {
|
||||||
val commandSigners = componentGroups.firstOrNull { it.groupIndex == SIGNERS_GROUP.ordinal }
|
val commandSigners = componentGroups.getGroup(SIGNERS_GROUP)
|
||||||
val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners)
|
val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners)
|
||||||
val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size
|
val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size
|
||||||
visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) {
|
visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) {
|
||||||
|
@ -18,10 +18,8 @@ import net.corda.core.crypto.toStringShort
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.TransactionDeserialisationException
|
import net.corda.core.internal.TransactionDeserialisationException
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.internal.attachmentIds
|
|
||||||
import net.corda.core.internal.equivalent
|
import net.corda.core.internal.equivalent
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
import net.corda.core.internal.isUploaderTrusted
|
||||||
import net.corda.core.internal.kotlinMetadataVersion
|
|
||||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||||
import net.corda.core.internal.verification.VerificationSupport
|
import net.corda.core.internal.verification.VerificationSupport
|
||||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||||
@ -33,6 +31,9 @@ import net.corda.core.serialization.SerializedBytes
|
|||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.internal.MissingSerializerException
|
import net.corda.core.serialization.internal.MissingSerializerException
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
|
import net.corda.core.utilities.Try
|
||||||
|
import net.corda.core.utilities.Try.Failure
|
||||||
|
import net.corda.core.utilities.Try.Success
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
@ -204,37 +205,58 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
*
|
*
|
||||||
* Depending on the contract attachments, this method will either verify this transaction in-process or send it to the external verifier
|
* Depending on the contract attachments, this method will either verify this transaction in-process or send it to the external verifier
|
||||||
* for out-of-process verification.
|
* for out-of-process verification.
|
||||||
|
*
|
||||||
|
* @return The [FullTransaction] that was successfully verified in-process. Returns null if the verification was successfully done externally.
|
||||||
*/
|
*/
|
||||||
@CordaInternal
|
@CordaInternal
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true) {
|
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? {
|
||||||
resolveAndCheckNetworkParameters(verificationSupport)
|
resolveAndCheckNetworkParameters(verificationSupport)
|
||||||
val verificationType = determineVerificationType(verificationSupport)
|
val verificationType = determineVerificationType()
|
||||||
log.debug { "Transaction $id has verification type $verificationType" }
|
log.debug { "Transaction $id has verification type $verificationType" }
|
||||||
if (verificationType == VerificationType.IN_PROCESS || verificationType == VerificationType.BOTH) {
|
return when (verificationType) {
|
||||||
verifyInProcess(verificationSupport, checkSufficientSignatures)
|
VerificationType.IN_PROCESS -> verifyInProcess(verificationSupport, checkSufficientSignatures)
|
||||||
|
VerificationType.BOTH -> {
|
||||||
|
val inProcessResult = Try.on { verifyInProcess(verificationSupport, checkSufficientSignatures) }
|
||||||
|
val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) }
|
||||||
|
ensureSameResult(inProcessResult, externalResult)
|
||||||
}
|
}
|
||||||
if (verificationType == VerificationType.EXTERNAL || verificationType == VerificationType.BOTH) {
|
VerificationType.EXTERNAL -> {
|
||||||
verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures)
|
verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures)
|
||||||
|
// We could create a LedgerTransaction here, and except for calling `verify()`, it would be valid to use. However, it's best
|
||||||
|
// we let the caller deal with that, since we can't control what they will do with it.
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun determineVerificationType(verificationSupport: VerificationSupport): VerificationType {
|
private fun determineVerificationType(): VerificationType {
|
||||||
var old = false
|
val ctx = coreTransaction
|
||||||
var new = false
|
return when (ctx) {
|
||||||
for (attachmentId in coreTransaction.attachmentIds) {
|
is WireTransaction -> {
|
||||||
val (major, minor) = verificationSupport.getAttachment(attachmentId)?.kotlinMetadataVersion?.split(".") ?: continue
|
when {
|
||||||
// Metadata version 1.1 maps to language versions 1.0 to 1.3
|
ctx.legacyAttachments.isEmpty() -> VerificationType.IN_PROCESS
|
||||||
if (major == "1" && minor == "1") {
|
ctx.nonLegacyAttachments.isEmpty() -> VerificationType.EXTERNAL
|
||||||
old = true
|
else -> VerificationType.BOTH
|
||||||
} else {
|
|
||||||
new = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return when {
|
// Contract upgrades only work on 4.11 and earlier
|
||||||
old && new -> VerificationType.BOTH
|
is ContractUpgradeWireTransaction -> VerificationType.EXTERNAL
|
||||||
old -> VerificationType.EXTERNAL
|
else -> VerificationType.IN_PROCESS // The default is always in-process
|
||||||
else -> VerificationType.IN_PROCESS
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureSameResult(inProcessResult: Try<FullTransaction>, externalResult: Try<*>): FullTransaction {
|
||||||
|
return when (externalResult) {
|
||||||
|
is Success -> when (inProcessResult) {
|
||||||
|
is Success -> inProcessResult.value
|
||||||
|
is Failure -> throw IllegalStateException("In-process verification of $id failed, but it succeeded in external verifier")
|
||||||
|
.apply { addSuppressed(inProcessResult.exception) }
|
||||||
|
}
|
||||||
|
is Failure -> throw when (inProcessResult) {
|
||||||
|
is Success -> IllegalStateException("In-process verification of $id succeeded, but it failed in external verifier")
|
||||||
|
is Failure -> inProcessResult.exception // Throw the in-process exception, with the external exception suppressed
|
||||||
|
}.apply { addSuppressed(externalResult.exception) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,11 +266,13 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies this transaction in-process. This assumes the current process has the correct classpath for all the contracts.
|
* Verifies this transaction in-process. This assumes the current process has the correct classpath for all the contracts.
|
||||||
|
*
|
||||||
|
* @return The [FullTransaction] that was successfully verified
|
||||||
*/
|
*/
|
||||||
@CordaInternal
|
@CordaInternal
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
|
internal fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): FullTransaction {
|
||||||
when (coreTransaction) {
|
return when (coreTransaction) {
|
||||||
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
|
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
|
||||||
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures)
|
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures)
|
||||||
else -> verifyRegularTransaction(verificationSupport, checkSufficientSignatures)
|
else -> verifyRegularTransaction(verificationSupport, checkSufficientSignatures)
|
||||||
@ -272,23 +296,25 @@ 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. */
|
/** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */
|
||||||
private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
|
private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): NotaryChangeLedgerTransaction {
|
||||||
val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs)
|
val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs)
|
||||||
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
|
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
|
||||||
else checkSignaturesAreValid()
|
else checkSignaturesAreValid()
|
||||||
|
return ntx
|
||||||
}
|
}
|
||||||
|
|
||||||
/** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */
|
/** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */
|
||||||
private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
|
private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): ContractUpgradeLedgerTransaction {
|
||||||
val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs)
|
val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs)
|
||||||
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
|
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
|
||||||
else checkSignaturesAreValid()
|
else checkSignaturesAreValid()
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
|
// 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
|
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
|
||||||
// objects from the TransactionState.
|
// objects from the TransactionState.
|
||||||
private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
|
private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
|
||||||
val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures)
|
val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures)
|
||||||
try {
|
try {
|
||||||
ltx.verify()
|
ltx.verify()
|
||||||
@ -304,6 +330,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
checkReverifyAllowed(e)
|
checkReverifyAllowed(e)
|
||||||
retryVerification(e.cause, e, ltx, verificationSupport)
|
retryVerification(e.cause, e, ltx, verificationSupport)
|
||||||
}
|
}
|
||||||
|
return ltx
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkReverifyAllowed(ex: Throwable) {
|
private fun checkReverifyAllowed(ex: Throwable) {
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.crypto.SignatureMetadata
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS
|
import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS
|
||||||
|
import net.corda.core.internal.cordapp.ContractAttachmentWithLegacy
|
||||||
import net.corda.core.internal.verification.VerifyingServiceHub
|
import net.corda.core.internal.verification.VerifyingServiceHub
|
||||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
@ -152,7 +153,7 @@ open class TransactionBuilder(
|
|||||||
*/
|
*/
|
||||||
@Throws(MissingContractAttachments::class)
|
@Throws(MissingContractAttachments::class)
|
||||||
fun toWireTransaction(services: ServicesForResolution, schemeId: Int): WireTransaction {
|
fun toWireTransaction(services: ServicesForResolution, schemeId: Int): WireTransaction {
|
||||||
return toWireTransaction(services, schemeId, emptyMap()).apply { checkSupportedHashType() }
|
return toWireTransaction(services, schemeId, emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,7 +196,7 @@ open class TransactionBuilder(
|
|||||||
|
|
||||||
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||||
// Sort the attachments to ensure transaction builds are stable.
|
// Sort the attachments to ensure transaction builds are stable.
|
||||||
val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.id }
|
val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id }
|
||||||
attachmentsBuilder.addAll(attachments)
|
attachmentsBuilder.addAll(attachments)
|
||||||
attachmentsBuilder.removeAll(excludedAttachments)
|
attachmentsBuilder.removeAll(excludedAttachments)
|
||||||
WireTransaction(
|
WireTransaction(
|
||||||
@ -207,7 +208,8 @@ open class TransactionBuilder(
|
|||||||
notary,
|
notary,
|
||||||
window,
|
window,
|
||||||
referenceStates,
|
referenceStates,
|
||||||
serviceHub.networkParametersService.currentHash
|
serviceHub.networkParametersService.currentHash,
|
||||||
|
allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.toList()
|
||||||
),
|
),
|
||||||
privacySalt,
|
privacySalt,
|
||||||
serviceHub.digestService
|
serviceHub.digestService
|
||||||
@ -237,6 +239,7 @@ open class TransactionBuilder(
|
|||||||
/**
|
/**
|
||||||
* @return true if a new dependency was successfully added.
|
* @return true if a new dependency was successfully added.
|
||||||
*/
|
*/
|
||||||
|
// TODO This entire code path needs to be updated to work with legacy attachments and automically adding their dependencies. ENT-11445
|
||||||
private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean {
|
private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean {
|
||||||
return try {
|
return try {
|
||||||
wireTx.toLedgerTransactionInternal(serviceHub).verify()
|
wireTx.toLedgerTransactionInternal(serviceHub).verify()
|
||||||
@ -249,11 +252,14 @@ open class TransactionBuilder(
|
|||||||
// Handle various exceptions that can be thrown during verification and drill down the wrappings.
|
// Handle various exceptions that can be thrown during verification and drill down the wrappings.
|
||||||
// Note: this is a best effort to preserve backwards compatibility.
|
// Note: this is a best effort to preserve backwards compatibility.
|
||||||
rootError is ClassNotFoundException -> {
|
rootError is ClassNotFoundException -> {
|
||||||
((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e))
|
// Using nonLegacyAttachments here as the verification above was done in-process and thus only the nonLegacyAttachments
|
||||||
|
// are used.
|
||||||
|
// TODO This might change with ENT-11445 where we add support for legacy contract dependencies.
|
||||||
|
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e))
|
||||||
|| addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e)
|
|| addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e)
|
||||||
}
|
}
|
||||||
rootError is NoClassDefFoundError -> {
|
rootError is NoClassDefFoundError -> {
|
||||||
((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e))
|
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e))
|
||||||
|| addMissingAttachment(rootError.message ?: throw e, serviceHub, e)
|
|| addMissingAttachment(rootError.message ?: throw e, serviceHub, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,7 +353,7 @@ open class TransactionBuilder(
|
|||||||
*/
|
*/
|
||||||
private fun selectContractAttachmentsAndOutputStateConstraints(
|
private fun selectContractAttachmentsAndOutputStateConstraints(
|
||||||
serviceHub: VerifyingServiceHub
|
serviceHub: VerifyingServiceHub
|
||||||
): Pair<List<ContractAttachment>, List<TransactionState<*>>> {
|
): Pair<List<ContractAttachmentWithLegacy>, List<TransactionState<*>>> {
|
||||||
// Determine the explicitly set contract attachments.
|
// Determine the explicitly set contract attachments.
|
||||||
val explicitContractToAttachments = attachments
|
val explicitContractToAttachments = attachments
|
||||||
.mapNotNull { serviceHub.attachments.openAttachment(it) as? ContractAttachment }
|
.mapNotNull { serviceHub.attachments.openAttachment(it) as? ContractAttachment }
|
||||||
@ -367,7 +373,7 @@ open class TransactionBuilder(
|
|||||||
= referencesWithTransactionState.groupBy { it.contract }
|
= referencesWithTransactionState.groupBy { it.contract }
|
||||||
val refStateContractAttachments = referenceStateGroups
|
val refStateContractAttachments = referenceStateGroups
|
||||||
.filterNot { it.key in allContracts }
|
.filterNot { it.key in allContracts }
|
||||||
.map { refStateEntry -> serviceHub.getInstalledContractAttachment(refStateEntry.key, refStateEntry::value) }
|
.map { refStateEntry -> serviceHub.getInstalledContractAttachments(refStateEntry.key, refStateEntry::value) }
|
||||||
|
|
||||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||||
val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract ->
|
val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract ->
|
||||||
@ -413,10 +419,10 @@ open class TransactionBuilder(
|
|||||||
outputStates: List<TransactionState<ContractState>>?,
|
outputStates: List<TransactionState<ContractState>>?,
|
||||||
explicitContractAttachment: ContractAttachment?,
|
explicitContractAttachment: ContractAttachment?,
|
||||||
serviceHub: VerifyingServiceHub
|
serviceHub: VerifyingServiceHub
|
||||||
): Pair<ContractAttachment, List<TransactionState<*>>> {
|
): Pair<ContractAttachmentWithLegacy, List<TransactionState<*>>> {
|
||||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
||||||
|
|
||||||
fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachment(contractClassName) {
|
fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachments(contractClassName) {
|
||||||
inputsAndOutputs.filterNot { it.constraint in automaticConstraints }
|
inputsAndOutputs.filterNot { it.constraint in automaticConstraints }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,14 +435,15 @@ open class TransactionBuilder(
|
|||||||
a system parameter that disables the hash constraint check.
|
a system parameter that disables the hash constraint check.
|
||||||
*/
|
*/
|
||||||
if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, serviceHub)) {
|
if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, serviceHub)) {
|
||||||
val attachment = selectAttachmentForContract()
|
val attachmentWithLegacy = selectAttachmentForContract()
|
||||||
|
val (attachment) = attachmentWithLegacy
|
||||||
if (attachment.isSigned && (explicitContractAttachment == null || explicitContractAttachment.id == attachment.id)) {
|
if (attachment.isSigned && (explicitContractAttachment == null || explicitContractAttachment.id == attachment.id)) {
|
||||||
val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys)
|
val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys)
|
||||||
require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" }
|
require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" }
|
||||||
val resolvedOutputStates = outputStates?.map {
|
val resolvedOutputStates = outputStates?.map {
|
||||||
if (it.constraint in automaticConstraints) it.copy(constraint = signatureConstraint) else it
|
if (it.constraint in automaticConstraints) it.copy(constraint = signatureConstraint) else it
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
return attachment to resolvedOutputStates
|
return attachmentWithLegacy to resolvedOutputStates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,9 +474,9 @@ open class TransactionBuilder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This *has* to be used by this transaction as it is explicit
|
// This *has* to be used by this transaction as it is explicit
|
||||||
explicitContractAttachment
|
ContractAttachmentWithLegacy(explicitContractAttachment, null) // By definition there can be no legacy version
|
||||||
} else {
|
} else {
|
||||||
hashAttachment ?: selectAttachmentForContract()
|
hashAttachment?.let { ContractAttachmentWithLegacy(it, null) } ?: selectAttachmentForContract()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
||||||
@ -491,10 +498,10 @@ open class TransactionBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
|
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
|
||||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment, serviceHub)
|
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment.currentAttachment, serviceHub)
|
||||||
|
|
||||||
// Sanity check that the selected attachment actually passes.
|
// Sanity check that the selected attachment actually passes.
|
||||||
val constraintAttachment = AttachmentWithContext(selectedAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations)
|
val constraintAttachment = AttachmentWithContext(selectedAttachment.currentAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations)
|
||||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
|
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
|
||||||
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
|
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
|
||||||
}
|
}
|
||||||
@ -506,7 +513,7 @@ open class TransactionBuilder(
|
|||||||
} else {
|
} else {
|
||||||
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
|
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
|
||||||
inputStates?.forEach { input ->
|
inputStates?.forEach { input ->
|
||||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment)) {
|
require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment)) {
|
||||||
"Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}"
|
"Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -629,12 +636,18 @@ open class TransactionBuilder(
|
|||||||
SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners)
|
SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners)
|
||||||
.build())
|
.build())
|
||||||
|
|
||||||
private inline fun VerifyingServiceHub.getInstalledContractAttachment(
|
private inline fun VerifyingServiceHub.getInstalledContractAttachments(
|
||||||
contractClassName: String,
|
contractClassName: String,
|
||||||
statesForException: () -> List<TransactionState<*>>
|
statesForException: () -> List<TransactionState<*>>
|
||||||
): ContractAttachment {
|
): ContractAttachmentWithLegacy {
|
||||||
return cordappProvider.getContractAttachment(contractClassName)
|
// TODO Stop using legacy attachments when the 4.12 min platform version is reached https://r3-cev.atlassian.net/browse/ENT-11479
|
||||||
|
val attachmentWithLegacy = cordappProvider.getContractAttachments(contractClassName)
|
||||||
?: throw MissingContractAttachments(statesForException(), contractClassName)
|
?: throw MissingContractAttachments(statesForException(), contractClassName)
|
||||||
|
if (attachmentWithLegacy.legacyAttachment == null) {
|
||||||
|
log.warnOnce("Contract $contractClassName does not have a legacy (4.11 or earlier) version installed. This means the " +
|
||||||
|
"transaction will not be compatible with older nodes.")
|
||||||
|
}
|
||||||
|
return attachmentWithLegacy
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean {
|
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean {
|
||||||
@ -646,6 +659,7 @@ open class TransactionBuilder(
|
|||||||
|
|
||||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
||||||
fun verify(services: ServiceHub) {
|
fun verify(services: ServiceHub) {
|
||||||
|
// TODO ENT-11445: Need to verify via SignedTransaction to ensure legacy components also work
|
||||||
toLedgerTransaction(services).verify()
|
toLedgerTransaction(services).verify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import net.corda.core.internal.SerializedStateAndRef
|
|||||||
import net.corda.core.internal.SerializedTransactionState
|
import net.corda.core.internal.SerializedTransactionState
|
||||||
import net.corda.core.internal.createComponentGroups
|
import net.corda.core.internal.createComponentGroups
|
||||||
import net.corda.core.internal.flatMapToSet
|
import net.corda.core.internal.flatMapToSet
|
||||||
|
import net.corda.core.internal.getGroup
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
import net.corda.core.internal.isUploaderTrusted
|
||||||
import net.corda.core.internal.lazyMapped
|
import net.corda.core.internal.lazyMapped
|
||||||
import net.corda.core.internal.mapToSet
|
import net.corda.core.internal.mapToSet
|
||||||
@ -162,7 +163,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
|
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
|
||||||
// Look up public keys to authenticated identities.
|
// Look up public keys to authenticated identities.
|
||||||
val authenticatedCommands = if (verificationSupport.isResolutionLazy) {
|
val authenticatedCommands = if (verificationSupport.isInProcess) {
|
||||||
commands.lazyMapped { cmd, _ ->
|
commands.lazyMapped { cmd, _ ->
|
||||||
val parties = verificationSupport.getParties(cmd.signers).filterNotNull()
|
val parties = verificationSupport.getParties(cmd.signers).filterNotNull()
|
||||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||||
@ -193,13 +194,15 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
}
|
}
|
||||||
val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef)
|
val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef)
|
||||||
|
|
||||||
val resolvedAttachments = if (verificationSupport.isResolutionLazy) {
|
val resolvedAttachments = if (verificationSupport.isInProcess) {
|
||||||
attachments.lazyMapped { id, _ ->
|
// The 4.12+ node only looks at the new attachments group
|
||||||
|
nonLegacyAttachments.lazyMapped { id, _ ->
|
||||||
verificationSupport.getAttachment(id) ?: throw AttachmentResolutionException(id)
|
verificationSupport.getAttachment(id) ?: throw AttachmentResolutionException(id)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
verificationSupport.getAttachments(attachments).mapIndexed { index, attachment ->
|
// The 4.11 external verifier only looks at the legacy attachments group since it will only contain attachments compatible with 4.11
|
||||||
attachment ?: throw AttachmentResolutionException(attachments[index])
|
verificationSupport.getAttachments(legacyAttachments).mapIndexed { index, attachment ->
|
||||||
|
attachment ?: throw AttachmentResolutionException(legacyAttachments[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +251,7 @@ 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.
|
// 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 {
|
fun componentGroupSize(componentGroup: ComponentGroupEnum): Int {
|
||||||
return this.componentGroups.firstOrNull { it.groupIndex == componentGroup.ordinal }?.let { cg -> cg.components.sumOf { it.size } + 4 } ?: 0
|
return componentGroups.getGroup(componentGroup)?.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
|
// Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances
|
||||||
@ -323,7 +326,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
if (digestService.hashAlgorithm == SecureHash.SHA2_256) {
|
if (digestService.hashAlgorithm == SecureHash.SHA2_256) {
|
||||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) } }
|
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) } }
|
||||||
} else {
|
} else {
|
||||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, _ -> digestService.computeNonce(privacySalt, it.groupIndex, internalIndex) } }
|
componentGroups.associate { it.groupIndex to List(it.components.size) { internalIndex -> digestService.computeNonce(privacySalt, it.groupIndex, internalIndex) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,23 +346,10 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
||||||
*/
|
*/
|
||||||
fun checkSignature(sig: TransactionSignature) {
|
fun checkSignature(sig: TransactionSignature) {
|
||||||
require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" }
|
require(commands.any { it.signers.any { signer -> sig.by in signer.keys } }) { "Signature key doesn't match any command" }
|
||||||
sig.verify(id)
|
sig.verify(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
@CordaInternal
|
|
||||||
@Deprecated("Do not use, this is internal API")
|
|
||||||
fun createComponentGroups(inputs: List<StateRef>,
|
|
||||||
outputs: List<TransactionState<ContractState>>,
|
|
||||||
commands: List<Command<*>>,
|
|
||||||
attachments: List<SecureHash>,
|
|
||||||
notary: Party?,
|
|
||||||
timeWindow: TimeWindow?): List<ComponentGroup> {
|
|
||||||
return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val buf = StringBuilder()
|
val buf = StringBuilder()
|
||||||
buf.appendLine("Transaction:")
|
buf.appendLine("Transaction:")
|
||||||
|
@ -51,7 +51,7 @@ cordapp {
|
|||||||
minimumPlatformVersion 1
|
minimumPlatformVersion 1
|
||||||
contract {
|
contract {
|
||||||
name "Corda Finance Demo"
|
name "Corda Finance Demo"
|
||||||
versionId 1
|
versionId 2
|
||||||
vendor "R3"
|
vendor "R3"
|
||||||
licence "Open Source (Apache 2)"
|
licence "Open Source (Apache 2)"
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,11 @@ interface CordappLoader : AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
val cordapps: List<CordappImpl>
|
val cordapps: List<CordappImpl>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all legacy (4.11 or older) contract CorDapps. These are used to form backward compatible transactions.
|
||||||
|
*/
|
||||||
|
val legacyContractCordapps: List<CordappImpl>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [ClassLoader] containing all types from all [Cordapp]s.
|
* Returns a [ClassLoader] containing all types from all [Cordapp]s.
|
||||||
*/
|
*/
|
||||||
|
@ -23,8 +23,10 @@ configurations {
|
|||||||
integrationTestImplementation.extendsFrom testImplementation
|
integrationTestImplementation.extendsFrom testImplementation
|
||||||
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||||
|
|
||||||
slowIntegrationTestCompile.extendsFrom testImplementation
|
slowIntegrationTestImplementation.extendsFrom testImplementation
|
||||||
slowIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
slowIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||||
|
|
||||||
|
corda4_11
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -89,6 +91,7 @@ processTestResources {
|
|||||||
from(tasks.getByPath(":testing:cordapps:cashobservers:jar")) {
|
from(tasks.getByPath(":testing:cordapps:cashobservers:jar")) {
|
||||||
rename 'testing-cashobservers-cordapp-.*.jar', 'testing-cashobservers-cordapp.jar'
|
rename 'testing-cashobservers-cordapp-.*.jar', 'testing-cashobservers-cordapp.jar'
|
||||||
}
|
}
|
||||||
|
from(configurations.corda4_11)
|
||||||
}
|
}
|
||||||
|
|
||||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||||
@ -104,30 +107,22 @@ dependencies {
|
|||||||
implementation project(':common-configuration-parsing')
|
implementation project(':common-configuration-parsing')
|
||||||
implementation project(':common-logging')
|
implementation project(':common-logging')
|
||||||
implementation project(':serialization')
|
implementation project(':serialization')
|
||||||
|
|
||||||
implementation "io.opentelemetry:opentelemetry-api:${open_telemetry_version}"
|
|
||||||
// Backwards compatibility goo: Apps expect confidential-identities to be loaded by default.
|
// Backwards compatibility goo: Apps expect confidential-identities to be loaded by default.
|
||||||
// We could eventually gate this on a target-version check.
|
// We could eventually gate this on a target-version check.
|
||||||
implementation project(':confidential-identities')
|
implementation project(':confidential-identities')
|
||||||
|
implementation "io.opentelemetry:opentelemetry-api:${open_telemetry_version}"
|
||||||
// Log4J: logging framework (with SLF4J bindings)
|
// Log4J: logging framework (with SLF4J bindings)
|
||||||
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
||||||
implementation "org.apache.logging.log4j:log4j-web:${log4j_version}"
|
implementation "org.apache.logging.log4j:log4j-web:${log4j_version}"
|
||||||
implementation "org.slf4j:jul-to-slf4j:$slf4j_version"
|
implementation "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
|
||||||
|
|
||||||
implementation "org.fusesource.jansi:jansi:$jansi_version"
|
implementation "org.fusesource.jansi:jansi:$jansi_version"
|
||||||
implementation "com.google.guava:guava:$guava_version"
|
implementation "com.google.guava:guava:$guava_version"
|
||||||
implementation "commons-io:commons-io:$commons_io_version"
|
implementation "commons-io:commons-io:$commons_io_version"
|
||||||
|
|
||||||
// For caches rather than guava
|
// For caches rather than guava
|
||||||
implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version"
|
implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version"
|
||||||
|
|
||||||
// For async logging
|
// For async logging
|
||||||
implementation "com.lmax:disruptor:$disruptor_version"
|
implementation "com.lmax:disruptor:$disruptor_version"
|
||||||
|
|
||||||
// Artemis: for reliable p2p message queues.
|
// Artemis: for reliable p2p message queues.
|
||||||
// TODO: remove the forced update of commons-collections and beanutils when artemis updates them
|
// TODO: remove the forced update of commons-collections and beanutils when artemis updates them
|
||||||
implementation "org.apache.commons:commons-collections4:${commons_collections_version}"
|
implementation "org.apache.commons:commons-collections4:${commons_collections_version}"
|
||||||
@ -142,92 +137,66 @@ dependencies {
|
|||||||
// Bouncy castle support needed for X509 certificate manipulation
|
// Bouncy castle support needed for X509 certificate manipulation
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
|
implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}"
|
implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}"
|
||||||
|
|
||||||
implementation "com.esotericsoftware:kryo:$kryo_version"
|
implementation "com.esotericsoftware:kryo:$kryo_version"
|
||||||
|
|
||||||
implementation "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}"
|
implementation "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
|
||||||
|
|
||||||
runtimeOnly("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
|
|
||||||
// Gains our proton-j version from core module.
|
|
||||||
exclude group: 'org.apache.qpid', module: 'proton-j'
|
|
||||||
exclude group: 'org.jgroups', module: 'jgroups'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manifests: for reading stuff from the manifest file
|
// Manifests: for reading stuff from the manifest file
|
||||||
implementation "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
implementation "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
||||||
|
|
||||||
// Coda Hale's Metrics: for monitoring of key statistics
|
// Coda Hale's Metrics: for monitoring of key statistics
|
||||||
implementation "io.dropwizard.metrics:metrics-jmx:$metrics_version"
|
implementation "io.dropwizard.metrics:metrics-jmx:$metrics_version"
|
||||||
implementation "io.github.classgraph:classgraph:$class_graph_version"
|
implementation "io.github.classgraph:classgraph:$class_graph_version"
|
||||||
implementation "org.liquibase:liquibase-core:$liquibase_version"
|
implementation "org.liquibase:liquibase-core:$liquibase_version"
|
||||||
|
|
||||||
// TypeSafe Config: for simple and human friendly config files.
|
// TypeSafe Config: for simple and human friendly config files.
|
||||||
implementation "com.typesafe:config:$typesafe_config_version"
|
implementation "com.typesafe:config:$typesafe_config_version"
|
||||||
|
|
||||||
implementation "io.reactivex:rxjava:$rxjava_version"
|
implementation "io.reactivex:rxjava:$rxjava_version"
|
||||||
|
|
||||||
implementation("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
|
implementation("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
|
||||||
// Gains our proton-j version from core module.
|
// Gains our proton-j version from core module.
|
||||||
exclude group: 'org.apache.qpid', module: 'proton-j'
|
exclude group: 'org.apache.qpid', module: 'proton-j'
|
||||||
exclude group: 'org.jgroups', module: 'jgroups'
|
exclude group: 'org.jgroups', module: 'jgroups'
|
||||||
}
|
}
|
||||||
|
// For H2 database support in persistence
|
||||||
|
implementation "com.h2database:h2:$h2_version"
|
||||||
|
// SQL connection pooling library
|
||||||
|
implementation "com.zaxxer:HikariCP:${hikari_version}"
|
||||||
|
// Hibernate: an object relational mapper for writing state objects to the database automatically.
|
||||||
|
implementation "org.hibernate:hibernate-core:$hibernate_version"
|
||||||
|
implementation "org.hibernate:hibernate-java8:$hibernate_version"
|
||||||
|
// OkHTTP: Simple HTTP library.
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
// Apache Shiro: authentication, authorization and session management.
|
||||||
|
implementation "org.apache.shiro:shiro-core:${shiro_version}"
|
||||||
|
//Picocli for command line interface
|
||||||
|
implementation "info.picocli:picocli:$picocli_version"
|
||||||
|
// BFT-Smart dependencies
|
||||||
|
implementation 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87'
|
||||||
|
// Java Atomix: RAFT library
|
||||||
|
implementation 'io.atomix.copycat:copycat-client:1.2.3'
|
||||||
|
implementation 'io.atomix.copycat:copycat-server:1.2.3'
|
||||||
|
implementation 'io.atomix.catalyst:catalyst-netty:1.1.2'
|
||||||
|
// Jolokia JVM monitoring agent, required to push logs through slf4j
|
||||||
|
implementation "org.jolokia:jolokia-jvm:${jolokia_version}:agent"
|
||||||
|
// Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic
|
||||||
|
implementation "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}"
|
||||||
|
// Adding native SSL library to allow using native SSL with Artemis and AMQP
|
||||||
|
implementation "io.netty:netty-tcnative-boringssl-static:$tcnative_version"
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.8.0'
|
||||||
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
testImplementation(project(':test-cli'))
|
||||||
testImplementation "junit:junit:$junit_version"
|
testImplementation(project(':test-utils'))
|
||||||
|
|
||||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
|
||||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
|
||||||
|
|
||||||
// Unit testing helpers.
|
// Unit testing helpers.
|
||||||
testImplementation "org.assertj:assertj-core:${assertj_version}"
|
|
||||||
testImplementation project(':node-driver')
|
testImplementation project(':node-driver')
|
||||||
testImplementation project(':core-test-utils')
|
testImplementation project(':core-test-utils')
|
||||||
testImplementation project(':test-utils')
|
testImplementation project(':test-utils')
|
||||||
testImplementation project(':client:jfx')
|
testImplementation project(':client:jfx')
|
||||||
testImplementation project(':finance:contracts')
|
testImplementation project(':finance:contracts')
|
||||||
testImplementation project(':finance:workflows')
|
testImplementation project(':finance:workflows')
|
||||||
|
|
||||||
// sample test schemas
|
// sample test schemas
|
||||||
testImplementation project(path: ':finance:contracts', configuration: 'testArtifacts')
|
testImplementation project(path: ':finance:contracts', configuration: 'testArtifacts')
|
||||||
|
testImplementation project(':testing:cordapps:dbfailure:dbfworkflows')
|
||||||
// For H2 database support in persistence
|
testImplementation "org.assertj:assertj-core:${assertj_version}"
|
||||||
implementation "com.h2database:h2:$h2_version"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||||
// SQL connection pooling library
|
testImplementation "junit:junit:$junit_version"
|
||||||
implementation "com.zaxxer:HikariCP:${hikari_version}"
|
|
||||||
|
|
||||||
// Hibernate: an object relational mapper for writing state objects to the database automatically.
|
|
||||||
implementation "org.hibernate:hibernate-core:$hibernate_version"
|
|
||||||
implementation "org.hibernate:hibernate-java8:$hibernate_version"
|
|
||||||
|
|
||||||
// OkHTTP: Simple HTTP library.
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
|
||||||
|
|
||||||
// Apache Shiro: authentication, authorization and session management.
|
|
||||||
implementation "org.apache.shiro:shiro-core:${shiro_version}"
|
|
||||||
|
|
||||||
//Picocli for command line interface
|
|
||||||
implementation "info.picocli:picocli:$picocli_version"
|
|
||||||
|
|
||||||
integrationTestImplementation project(":testing:cordapps:dbfailure:dbfcontracts")
|
|
||||||
|
|
||||||
// Integration test helpers
|
|
||||||
integrationTestImplementation "de.javakaffee:kryo-serializers:$kryo_serializer_version"
|
|
||||||
integrationTestImplementation "junit:junit:$junit_version"
|
|
||||||
integrationTestImplementation "org.assertj:assertj-core:${assertj_version}"
|
|
||||||
integrationTestImplementation "org.apache.qpid:qpid-jms-client:${protonj_version}"
|
|
||||||
integrationTestImplementation "net.i2p.crypto:eddsa:$eddsa_version"
|
|
||||||
|
|
||||||
// BFT-Smart dependencies
|
|
||||||
implementation 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87'
|
|
||||||
|
|
||||||
// Java Atomix: RAFT library
|
|
||||||
implementation 'io.atomix.copycat:copycat-client:1.2.3'
|
|
||||||
implementation 'io.atomix.copycat:copycat-server:1.2.3'
|
|
||||||
implementation 'io.atomix.catalyst:catalyst-netty:1.1.2'
|
|
||||||
|
|
||||||
// Jetty dependencies for NetworkMapClient test.
|
// Jetty dependencies for NetworkMapClient test.
|
||||||
// Web stuff: for HTTP[S] servlets
|
// Web stuff: for HTTP[S] servlets
|
||||||
testImplementation "org.hamcrest:hamcrest-library:2.1"
|
testImplementation "org.hamcrest:hamcrest-library:2.1"
|
||||||
@ -238,43 +207,33 @@ dependencies {
|
|||||||
testImplementation "com.google.jimfs:jimfs:1.1"
|
testImplementation "com.google.jimfs:jimfs:1.1"
|
||||||
testImplementation "co.paralleluniverse:quasar-core:$quasar_version"
|
testImplementation "co.paralleluniverse:quasar-core:$quasar_version"
|
||||||
testImplementation "com.natpryce:hamkrest:$hamkrest_version"
|
testImplementation "com.natpryce:hamkrest:$hamkrest_version"
|
||||||
|
|
||||||
// Jersey for JAX-RS implementation for use in Jetty
|
// Jersey for JAX-RS implementation for use in Jetty
|
||||||
testImplementation "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
testImplementation "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||||
testImplementation "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
testImplementation "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
||||||
testImplementation "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
testImplementation "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
||||||
|
|
||||||
// Jolokia JVM monitoring agent, required to push logs through slf4j
|
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||||
implementation "org.jolokia:jolokia-jvm:${jolokia_version}:agent"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||||
// Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic
|
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||||
implementation "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}"
|
|
||||||
|
|
||||||
// Adding native SSL library to allow using native SSL with Artemis and AMQP
|
|
||||||
implementation "io.netty:netty-tcnative-boringssl-static:$tcnative_version"
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.8.0'
|
|
||||||
|
|
||||||
// Byteman for runtime (termination) rules injection on the running node
|
|
||||||
// Submission tool allowing to install rules on running nodes
|
|
||||||
slowIntegrationTestCompile "org.jboss.byteman:byteman-submit:4.0.22"
|
|
||||||
// The actual Byteman agent which should only be in the classpath of the out of process nodes
|
|
||||||
slowIntegrationTestCompile "org.jboss.byteman:byteman:4.0.22"
|
|
||||||
|
|
||||||
testImplementation(project(':test-cli'))
|
|
||||||
testImplementation(project(':test-utils'))
|
|
||||||
|
|
||||||
slowIntegrationTestCompile sourceSets.main.output
|
|
||||||
slowIntegrationTestCompile sourceSets.test.output
|
|
||||||
slowIntegrationTestCompile configurations.implementation
|
|
||||||
slowIntegrationTestCompile configurations.testImplementation
|
|
||||||
slowIntegrationTestRuntimeOnly configurations.runtimeOnly
|
|
||||||
slowIntegrationTestRuntimeOnly configurations.testRuntimeOnly
|
|
||||||
|
|
||||||
|
integrationTestImplementation project(":testing:cordapps:dbfailure:dbfcontracts")
|
||||||
integrationTestImplementation project(":testing:cordapps:missingmigration")
|
integrationTestImplementation project(":testing:cordapps:missingmigration")
|
||||||
|
// Integration test helpers
|
||||||
testImplementation project(':testing:cordapps:dbfailure:dbfworkflows')
|
integrationTestImplementation "de.javakaffee:kryo-serializers:$kryo_serializer_version"
|
||||||
|
integrationTestImplementation "junit:junit:$junit_version"
|
||||||
|
integrationTestImplementation "org.assertj:assertj-core:${assertj_version}"
|
||||||
|
integrationTestImplementation "org.apache.qpid:qpid-jms-client:${protonj_version}"
|
||||||
|
integrationTestImplementation "net.i2p.crypto:eddsa:$eddsa_version"
|
||||||
|
|
||||||
// used by FinalityFlowErrorHandlingTest
|
// used by FinalityFlowErrorHandlingTest
|
||||||
slowIntegrationTestImplementation project(':testing:cordapps:cashobservers')
|
slowIntegrationTestImplementation project(':testing:cordapps:cashobservers')
|
||||||
|
// Byteman for runtime (termination) rules injection on the running node
|
||||||
|
// Submission tool allowing to install rules on running nodes
|
||||||
|
slowIntegrationTestImplementation "org.jboss.byteman:byteman-submit:4.0.22"
|
||||||
|
// The actual Byteman agent which should only be in the classpath of the out of process nodes
|
||||||
|
slowIntegrationTestImplementation "org.jboss.byteman:byteman:4.0.22"
|
||||||
|
|
||||||
|
corda4_11 "net.corda:corda-finance-contracts:4.11"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
@ -1,51 +1,42 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.flows.FinalityFlow
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.flows.FlowException
|
||||||
|
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.ReceiveFinalityFlow
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
|
||||||
import net.corda.core.internal.concurrent.transpose
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.testing.common.internal.checkNotOnClasspath
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
|
import net.corda.finance.workflows.asset.CashUtils
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.driver.DriverDSL
|
|
||||||
import net.corda.testing.driver.DriverParameters
|
import net.corda.testing.driver.DriverParameters
|
||||||
|
import net.corda.testing.driver.NodeParameters
|
||||||
import net.corda.testing.driver.driver
|
import net.corda.testing.driver.driver
|
||||||
import net.corda.testing.node.NotarySpec
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||||
|
import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP
|
||||||
import net.corda.testing.node.internal.enclosedCordapp
|
import net.corda.testing.node.internal.enclosedCordapp
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.net.URL
|
import java.util.Currency
|
||||||
import java.net.URLClassLoader
|
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.div
|
|
||||||
|
|
||||||
class AttachmentLoadingTests {
|
class AttachmentLoadingTests {
|
||||||
private companion object {
|
|
||||||
val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")!!
|
|
||||||
val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar))
|
|
||||||
val issuanceFlowClass: Class<FlowLogic<StateRef>> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
|
|
||||||
|
|
||||||
init {
|
|
||||||
checkNotOnClasspath("net.corda.isolated.contracts.AnotherDummyContract") {
|
|
||||||
"isolated module cannot be on the classpath as otherwise it will be pulled into the nodes the driver creates and " +
|
|
||||||
"contaminate the tests. This is a known issue with the driver and we must work around it until it's fixed."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadFromIsolated(className: String): Class<*> = Class.forName(className, false, isolatedClassLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `contracts downloaded from the network are not executed`() {
|
fun `contracts downloaded from the network are not executed`() {
|
||||||
driver(DriverParameters(
|
driver(DriverParameters(
|
||||||
@ -53,61 +44,36 @@ class AttachmentLoadingTests {
|
|||||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
||||||
cordappsForAllNodes = listOf(enclosedCordapp())
|
cordappsForAllNodes = listOf(enclosedCordapp())
|
||||||
)) {
|
)) {
|
||||||
installIsolatedCordapp(ALICE_NAME)
|
|
||||||
|
|
||||||
val (alice, bob) = listOf(
|
val (alice, bob) = listOf(
|
||||||
startNode(providedName = ALICE_NAME),
|
startNode(NodeParameters(ALICE_NAME, additionalCordapps = FINANCE_CORDAPPS)),
|
||||||
startNode(providedName = BOB_NAME)
|
startNode(NodeParameters(BOB_NAME, additionalCordapps = listOf(FINANCE_WORKFLOWS_CORDAPP)))
|
||||||
).transpose().getOrThrow()
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0x00), defaultNotaryIdentity).returnValue.getOrThrow()
|
||||||
|
|
||||||
assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
|
assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, 10.DOLLARS, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
|
||||||
// ConsumeAndBroadcastResponderFlow re-throws any non-FlowExceptions with just their class name in the message so that
|
// ConsumeAndBroadcastResponderFlow re-throws any non-FlowExceptions with just their class name in the message so that
|
||||||
// we can verify here Bob threw the correct exception
|
// we can verify here Bob threw the correct exception
|
||||||
.hasMessage(TransactionVerificationException.UntrustedAttachmentsException::class.java.name)
|
.hasMessage(TransactionVerificationException.UntrustedAttachmentsException::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `contract is executed if installed locally`() {
|
|
||||||
driver(DriverParameters(
|
|
||||||
startNodesInProcess = false,
|
|
||||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
|
||||||
cordappsForAllNodes = listOf(enclosedCordapp())
|
|
||||||
)) {
|
|
||||||
installIsolatedCordapp(ALICE_NAME)
|
|
||||||
installIsolatedCordapp(BOB_NAME)
|
|
||||||
|
|
||||||
val (alice, bob) = listOf(
|
|
||||||
startNode(providedName = ALICE_NAME),
|
|
||||||
startNode(providedName = BOB_NAME)
|
|
||||||
).transpose().getOrThrow()
|
|
||||||
|
|
||||||
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
|
||||||
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
|
|
||||||
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
|
|
||||||
isolatedJar.toPath().copyToDirectory(cordappsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class ConsumeAndBroadcastFlow(private val stateRef: StateRef, private val otherSide: Party) : FlowLogic<Unit>() {
|
class ConsumeAndBroadcastFlow(private val amount: Amount<Currency>, private val otherSide: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
val stateAndRef = serviceHub.toStateAndRef<ContractState>(stateRef)
|
val builder = TransactionBuilder(notary)
|
||||||
val stx = serviceHub.signInitialTransaction(
|
val (_, keysForSigning) = CashUtils.generateSpend(
|
||||||
TransactionBuilder(notary)
|
serviceHub,
|
||||||
.addInputState(stateAndRef)
|
builder,
|
||||||
.addOutputState(ConsumeContract.State())
|
amount,
|
||||||
.addCommand(Command(ConsumeContract.Cmd, ourIdentity.owningKey))
|
ourIdentityAndCert,
|
||||||
|
otherSide,
|
||||||
|
anonymous = false
|
||||||
)
|
)
|
||||||
stx.verify(serviceHub, checkSufficientSignatures = false)
|
val stx = serviceHub.signInitialTransaction(builder, keysForSigning)
|
||||||
val session = initiateFlow(otherSide)
|
val session = initiateFlow(otherSide)
|
||||||
subFlow(FinalityFlow(stx, session))
|
subFlow(FinalityFlow(stx, session))
|
||||||
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
|
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
|
||||||
@ -129,16 +95,4 @@ class AttachmentLoadingTests {
|
|||||||
otherSide.send("OK")
|
otherSide.send("OK")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConsumeContract : Contract {
|
|
||||||
override fun verify(tx: LedgerTransaction) {
|
|
||||||
// Accept everything
|
|
||||||
}
|
|
||||||
|
|
||||||
class State : ContractState {
|
|
||||||
override val participants: List<AbstractParty> get() = emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
object Cmd : TypeOnlyCommandData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -79,6 +79,7 @@ import net.corda.node.internal.classloading.requireAnnotation
|
|||||||
import net.corda.node.internal.cordapp.CordappConfigFileProvider
|
import net.corda.node.internal.cordapp.CordappConfigFileProvider
|
||||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||||
|
import net.corda.node.internal.cordapp.JarScanningCordappLoader.Companion.LEGACY_CONTRACTS_DIR_NAME
|
||||||
import net.corda.node.internal.cordapp.VirtualCordapp
|
import net.corda.node.internal.cordapp.VirtualCordapp
|
||||||
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
|
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
|
||||||
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
|
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
|
||||||
@ -187,6 +188,7 @@ import java.util.function.Consumer
|
|||||||
import javax.persistence.EntityManager
|
import javax.persistence.EntityManager
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base node implementation that can be customised either for production (with real implementations that do real
|
* A base node implementation that can be customised either for production (with real implementations that do real
|
||||||
@ -853,6 +855,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
return JarScanningCordappLoader.fromDirectories(
|
return JarScanningCordappLoader.fromDirectories(
|
||||||
configuration.cordappDirectories,
|
configuration.cordappDirectories,
|
||||||
|
(configuration.baseDirectory / LEGACY_CONTRACTS_DIR_NAME).takeIf { it.exists() },
|
||||||
versionInfo,
|
versionInfo,
|
||||||
extraCordapps = generatedCordapps,
|
extraCordapps = generatedCordapps,
|
||||||
signerKeyFingerprintBlacklist = blacklistedKeys
|
signerKeyFingerprintBlacklist = blacklistedKeys
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.cordapp.Cordapp
|
|||||||
import net.corda.core.cordapp.CordappContext
|
import net.corda.core.cordapp.CordappContext
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||||
|
import net.corda.core.internal.cordapp.ContractAttachmentWithLegacy
|
||||||
import net.corda.core.internal.cordapp.CordappImpl
|
import net.corda.core.internal.cordapp.CordappImpl
|
||||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
import net.corda.core.internal.cordapp.CordappProviderInternal
|
||||||
import net.corda.core.internal.groupByMultipleKeys
|
import net.corda.core.internal.groupByMultipleKeys
|
||||||
@ -38,6 +39,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
|
|||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
loadContractsIntoAttachmentStore(cordappLoader.cordapps)
|
loadContractsIntoAttachmentStore(cordappLoader.cordapps)
|
||||||
|
loadContractsIntoAttachmentStore(cordappLoader.legacyContractCordapps)
|
||||||
flowToCordapp = makeFlowToCordapp()
|
flowToCordapp = makeFlowToCordapp()
|
||||||
// Load the fix-ups after uploading any new contracts into attachment storage.
|
// Load the fix-ups after uploading any new contracts into attachment storage.
|
||||||
attachmentFixups.load(cordappLoader.appClassLoader)
|
attachmentFixups.load(cordappLoader.appClassLoader)
|
||||||
@ -56,12 +58,18 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? {
|
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? {
|
||||||
// loadContractsIntoAttachmentStore makes sure the jarHash is the attachment ID
|
return cordappLoader.cordapps.findCordapp(contractClassName)
|
||||||
return cordappLoader.cordapps.find { contractClassName in it.contractClassNames }?.jarHash
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? {
|
override fun getContractAttachments(contractClassName: ContractClassName): ContractAttachmentWithLegacy? {
|
||||||
return getContractAttachmentID(contractClassName)?.let(::getContractAttachment)
|
val currentAttachmentId = getContractAttachmentID(contractClassName) ?: return null
|
||||||
|
val legacyAttachmentId = cordappLoader.legacyContractCordapps.findCordapp(contractClassName)
|
||||||
|
return ContractAttachmentWithLegacy(getContractAttachment(currentAttachmentId), legacyAttachmentId?.let(::getContractAttachment))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<CordappImpl>.findCordapp(contractClassName: ContractClassName): AttachmentId? {
|
||||||
|
// loadContractsIntoAttachmentStore makes sure the jarHash is the attachment ID
|
||||||
|
return find { contractClassName in it.contractClassNames }?.jarHash
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadContractsIntoAttachmentStore(cordapps: List<CordappImpl>) {
|
private fun loadContractsIntoAttachmentStore(cordapps: List<CordappImpl>) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.node.internal.cordapp
|
package net.corda.node.internal.cordapp
|
||||||
|
|
||||||
import io.github.classgraph.ClassGraph
|
import io.github.classgraph.ClassGraph
|
||||||
|
import io.github.classgraph.ClassInfo
|
||||||
import io.github.classgraph.ClassInfoList
|
import io.github.classgraph.ClassInfoList
|
||||||
import io.github.classgraph.ScanResult
|
import io.github.classgraph.ScanResult
|
||||||
import net.corda.common.logging.errorReporting.CordappErrors
|
import net.corda.common.logging.errorReporting.CordappErrors
|
||||||
@ -18,13 +19,15 @@ import net.corda.core.internal.JarSignatureCollector
|
|||||||
import net.corda.core.internal.PlatformVersionSwitches
|
import net.corda.core.internal.PlatformVersionSwitches
|
||||||
import net.corda.core.internal.cordapp.CordappImpl
|
import net.corda.core.internal.cordapp.CordappImpl
|
||||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO
|
import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO
|
||||||
|
import net.corda.core.internal.cordapp.KotlinMetadataVersion
|
||||||
|
import net.corda.core.internal.cordapp.LanguageVersion
|
||||||
import net.corda.core.internal.cordapp.get
|
import net.corda.core.internal.cordapp.get
|
||||||
import net.corda.core.internal.flatMapToSet
|
import net.corda.core.internal.flatMapToSet
|
||||||
|
import net.corda.core.internal.groupByMultipleKeys
|
||||||
import net.corda.core.internal.hash
|
import net.corda.core.internal.hash
|
||||||
import net.corda.core.internal.isAbstractClass
|
import net.corda.core.internal.isAbstractClass
|
||||||
import net.corda.core.internal.loadClassOfType
|
import net.corda.core.internal.loadClassOfType
|
||||||
import net.corda.core.internal.location
|
import net.corda.core.internal.location
|
||||||
import net.corda.core.internal.groupByMultipleKeys
|
|
||||||
import net.corda.core.internal.mapToSet
|
import net.corda.core.internal.mapToSet
|
||||||
import net.corda.core.internal.notary.NotaryService
|
import net.corda.core.internal.notary.NotaryService
|
||||||
import net.corda.core.internal.notary.SinglePartyNotaryService
|
import net.corda.core.internal.notary.SinglePartyNotaryService
|
||||||
@ -42,6 +45,7 @@ import net.corda.core.serialization.SerializationWhitelist
|
|||||||
import net.corda.core.serialization.SerializeAsToken
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.node.VersionInfo
|
import net.corda.node.VersionInfo
|
||||||
import net.corda.nodeapi.internal.cordapp.CordappLoader
|
import net.corda.nodeapi.internal.cordapp.CordappLoader
|
||||||
import net.corda.nodeapi.internal.coreContractClasses
|
import net.corda.nodeapi.internal.coreContractClasses
|
||||||
@ -50,6 +54,7 @@ import java.lang.reflect.Modifier
|
|||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.ServiceLoader
|
import java.util.ServiceLoader
|
||||||
|
import java.util.TreeSet
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
import java.util.jar.Manifest
|
import java.util.jar.Manifest
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
@ -57,27 +62,35 @@ import kotlin.io.path.exists
|
|||||||
import kotlin.io.path.inputStream
|
import kotlin.io.path.inputStream
|
||||||
import kotlin.io.path.isSameFileAs
|
import kotlin.io.path.isSameFileAs
|
||||||
import kotlin.io.path.listDirectoryEntries
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
import kotlin.io.path.useDirectoryEntries
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles CorDapp loading and classpath scanning of CorDapp JARs
|
* Handles CorDapp loading and classpath scanning of CorDapp JARs
|
||||||
*
|
*
|
||||||
* @property cordappJars The classpath of cordapp JARs
|
* @property cordappJars The classpath of cordapp JARs
|
||||||
|
* @property legacyContractJars Legacy contract CorDapps (4.11 or earlier) needed for backwards compatibility with 4.11 nodes.
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
||||||
|
private val legacyContractJars: Set<Path> = emptySet(),
|
||||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||||
private val extraCordapps: List<CordappImpl> = emptyList(),
|
private val extraCordapps: List<CordappImpl> = emptyList(),
|
||||||
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoader {
|
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoader {
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = contextLogger()
|
private val logger = contextLogger()
|
||||||
|
|
||||||
|
const val LEGACY_CONTRACTS_DIR_NAME = "legacy-contracts"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a CordappLoader from multiple directories.
|
* Creates a CordappLoader from multiple directories.
|
||||||
*
|
*
|
||||||
* @param cordappDirs Directories used to scan for CorDapp JARs.
|
* @param cordappDirs Directories used to scan for CorDapp JARs.
|
||||||
|
* @param legacyContractsDir Directory containing legacy contract CorDapps (4.11 or earlier).
|
||||||
*/
|
*/
|
||||||
fun fromDirectories(cordappDirs: Collection<Path>,
|
fun fromDirectories(cordappDirs: Collection<Path>,
|
||||||
|
legacyContractsDir: Path? = null,
|
||||||
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||||
extraCordapps: List<CordappImpl> = emptyList(),
|
extraCordapps: List<CordappImpl> = emptyList(),
|
||||||
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
|
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
|
||||||
@ -86,12 +99,14 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
.asSequence()
|
.asSequence()
|
||||||
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
|
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
|
||||||
.toSet()
|
.toSet()
|
||||||
return JarScanningCordappLoader(cordappJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
|
val legacyContractJars = legacyContractsDir?.useDirectoryEntries("*.jar") { it.toSet() } ?: emptySet()
|
||||||
|
return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
logger.debug { "cordappJars: $cordappJars" }
|
logger.debug { "cordappJars: $cordappJars" }
|
||||||
|
logger.debug { "legacyContractJars: $legacyContractJars" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader)
|
override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader)
|
||||||
@ -99,21 +114,46 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
private val internal by lazy(::InternalHolder)
|
private val internal by lazy(::InternalHolder)
|
||||||
|
|
||||||
override val cordapps: List<CordappImpl>
|
override val cordapps: List<CordappImpl>
|
||||||
get() = internal.cordapps
|
get() = internal.nonLegacyCordapps
|
||||||
|
|
||||||
|
override val legacyContractCordapps: List<CordappImpl>
|
||||||
|
get() = internal.legacyContractCordapps
|
||||||
|
|
||||||
override fun close() = appClassLoader.close()
|
override fun close() = appClassLoader.close()
|
||||||
|
|
||||||
private inner class InternalHolder {
|
private inner class InternalHolder {
|
||||||
val cordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp)
|
val nonLegacyCordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp)
|
||||||
|
val legacyContractCordapps = legacyContractJars.map(::scanCordapp)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
checkInvalidCordapps()
|
commonChecks(nonLegacyCordapps, LanguageVersion::isNonLegacyCompatible)
|
||||||
checkDuplicateCordapps()
|
nonLegacyCordapps += extraCordapps
|
||||||
checkContractOverlap()
|
if (legacyContractCordapps.isNotEmpty()) {
|
||||||
cordapps += extraCordapps
|
commonChecks(legacyContractCordapps, LanguageVersion::isLegacyCompatible)
|
||||||
|
checkLegacyContracts()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkInvalidCordapps() {
|
private fun commonChecks(cordapps: List<CordappImpl>, compatibilityProperty: KProperty1<LanguageVersion, Boolean>) {
|
||||||
|
for (cordapp in cordapps) {
|
||||||
|
check(compatibilityProperty(cordapp.languageVersion)) {
|
||||||
|
val isLegacyCompatibleCheck = compatibilityProperty == LanguageVersion::isLegacyCompatible
|
||||||
|
val msg = when {
|
||||||
|
isLegacyCompatibleCheck -> "not legacy; please remove or place it in the node's CorDapps directory."
|
||||||
|
cordapp.contractClassNames.isEmpty() -> "legacy (should be 4.12 or later)"
|
||||||
|
else -> "legacy contracts; please place it in the node's '$LEGACY_CONTRACTS_DIR_NAME' directory."
|
||||||
|
}
|
||||||
|
"CorDapp ${cordapp.jarFile} is $msg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkInvalidCordapps(cordapps)
|
||||||
|
checkDuplicateCordapps(cordapps)
|
||||||
|
// The same contract may occur in both 4.11 and 4.12 CorDapps for ledger compatibility, so we only check for overlap within each
|
||||||
|
// compatibility group
|
||||||
|
checkContractOverlap(cordapps)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkInvalidCordapps(cordapps: List<CordappImpl>) {
|
||||||
val invalidCordapps = LinkedHashMap<String, CordappImpl>()
|
val invalidCordapps = LinkedHashMap<String, CordappImpl>()
|
||||||
|
|
||||||
for (cordapp in cordapps) {
|
for (cordapp in cordapps) {
|
||||||
@ -139,7 +179,7 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkDuplicateCordapps() {
|
private fun checkDuplicateCordapps(cordapps: List<CordappImpl>) {
|
||||||
for (group in cordapps.groupBy { it.jarHash }.values) {
|
for (group in cordapps.groupBy { it.jarHash }.values) {
|
||||||
if (group.size > 1) {
|
if (group.size > 1) {
|
||||||
throw DuplicateCordappsInstalledException(group[0], group.drop(1))
|
throw DuplicateCordappsInstalledException(group[0], group.drop(1))
|
||||||
@ -147,12 +187,38 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkContractOverlap() {
|
private fun checkContractOverlap(cordapps: List<CordappImpl>) {
|
||||||
cordapps.groupByMultipleKeys(CordappImpl::contractClassNames) { contract, cordapp1, cordapp2 ->
|
cordapps.groupByMultipleKeys(CordappImpl::contractClassNames) { contract, cordapp1, cordapp2 ->
|
||||||
throw IllegalStateException("Contract $contract occuring in multiple CorDapps (${cordapp1.name}, ${cordapp2.name}). " +
|
throw IllegalStateException("Contract $contract occuring in multiple CorDapps (${cordapp1.name}, ${cordapp2.name}). " +
|
||||||
"Please remove the previous version when upgrading to a new version.")
|
"Please remove the previous version when upgrading to a new version.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkLegacyContracts() {
|
||||||
|
for (legacyCordapp in legacyContractCordapps) {
|
||||||
|
if (legacyCordapp.contractClassNames.isEmpty()) continue
|
||||||
|
logger.debug { "Contracts CorDapp ${legacyCordapp.name} is legacy (4.11 or older), searching for corresponding 4.12+ contracts" }
|
||||||
|
for (legacyContract in legacyCordapp.contractClassNames) {
|
||||||
|
val newerCordapp = nonLegacyCordapps.find { legacyContract in it.contractClassNames }
|
||||||
|
checkNotNull(newerCordapp) {
|
||||||
|
"Contract $legacyContract in legacy CorDapp (4.11 or older) '${legacyCordapp.jarFile}' does not have a " +
|
||||||
|
"corresponding newer version (4.12 or later). Please add this corresponding CorDapp or remove the legacy one."
|
||||||
|
}
|
||||||
|
check(newerCordapp.contractVersionId > legacyCordapp.contractVersionId) {
|
||||||
|
"Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher version number " +
|
||||||
|
"(${newerCordapp.contractVersionId}) compared to corresponding legacy contract CorDapp " +
|
||||||
|
"'${legacyCordapp.jarFile}' (${legacyCordapp.contractVersionId})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CordappImpl.contractVersionId: Int
|
||||||
|
get() = when (val info = info) {
|
||||||
|
is Cordapp.Info.Contract -> info.versionId
|
||||||
|
is Cordapp.Info.ContractAndWorkflow -> info.contract.versionId
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ScanResult.toCordapp(path: Path): CordappImpl {
|
private fun ScanResult.toCordapp(path: Path): CordappImpl {
|
||||||
@ -160,6 +226,8 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
|
val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
|
||||||
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
|
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
|
||||||
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
|
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
|
||||||
|
val languageVersion = determineLanguageVersion(path)
|
||||||
|
logger.debug { "$path: $languageVersion" }
|
||||||
return CordappImpl(
|
return CordappImpl(
|
||||||
path,
|
path,
|
||||||
findContractClassNames(this),
|
findContractClassNames(this),
|
||||||
@ -177,6 +245,7 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
info,
|
info,
|
||||||
minPlatformVersion,
|
minPlatformVersion,
|
||||||
targetPlatformVersion,
|
targetPlatformVersion,
|
||||||
|
languageVersion = languageVersion,
|
||||||
notaryService = findNotaryService(this),
|
notaryService = findNotaryService(this),
|
||||||
explicitCordappClasses = findAllCordappClasses(this)
|
explicitCordappClasses = findAllCordappClasses(this)
|
||||||
)
|
)
|
||||||
@ -360,6 +429,36 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
|||||||
private fun <T : Any> ClassInfoList.getAllConcreteClasses(type: KClass<T>): List<Class<out T>> {
|
private fun <T : Any> ClassInfoList.getAllConcreteClasses(type: KClass<T>): List<Class<out T>> {
|
||||||
return mapNotNull { loadClass(it.name, type)?.takeUnless(Class<*>::isAbstractClass) }
|
return mapNotNull { loadClass(it.name, type)?.takeUnless(Class<*>::isAbstractClass) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ScanResult.determineLanguageVersion(cordappJar: Path): LanguageVersion {
|
||||||
|
val allClasses = allClassesAsMap.values
|
||||||
|
if (allClasses.isEmpty()) {
|
||||||
|
return LanguageVersion.Data
|
||||||
|
}
|
||||||
|
val classFileMajorVersion = allClasses.maxOf { it.classfileMajorVersion }
|
||||||
|
val kotlinMetadataVersion = allClasses
|
||||||
|
.mapNotNullTo(TreeSet()) { it.kotlinMetadataVersion() }
|
||||||
|
.let { kotlinMetadataVersions ->
|
||||||
|
// If there's more than one minor version of Kotlin
|
||||||
|
if (kotlinMetadataVersions.size > 1 && kotlinMetadataVersions.mapToSet { it.copy(patch = 0) }.size > 1) {
|
||||||
|
logger.warn("CorDapp $cordappJar comprised of multiple Kotlin versions (kotlinMetadataVersions=$kotlinMetadataVersions). " +
|
||||||
|
"This may cause compatibility issues.")
|
||||||
|
}
|
||||||
|
kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LanguageVersion.Bytecode(classFileMajorVersion, kotlinMetadataVersion)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw IllegalStateException("Unable to load CorDapp $cordappJar: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ClassInfo.kotlinMetadataVersion(): KotlinMetadataVersion? {
|
||||||
|
val kotlinMetadata = getAnnotationInfo(Metadata::class.java) ?: return null
|
||||||
|
val kotlinMetadataVersion = KotlinMetadataVersion.from(kotlinMetadata.parameterValues.get("mv").value as IntArray)
|
||||||
|
logger.trace { "$name: $kotlinMetadataVersion" }
|
||||||
|
return kotlinMetadataVersion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,8 +6,6 @@ import com.google.common.hash.HashCode
|
|||||||
import com.google.common.hash.Hashing
|
import com.google.common.hash.Hashing
|
||||||
import com.google.common.hash.HashingInputStream
|
import com.google.common.hash.HashingInputStream
|
||||||
import com.google.common.io.CountingInputStream
|
import com.google.common.io.CountingInputStream
|
||||||
import kotlinx.metadata.jvm.KotlinModuleMetadata
|
|
||||||
import kotlinx.metadata.jvm.UnstableMetadataApi
|
|
||||||
import net.corda.core.CordaRuntimeException
|
import net.corda.core.CordaRuntimeException
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.contracts.ContractAttachment
|
import net.corda.core.contracts.ContractAttachment
|
||||||
@ -18,7 +16,6 @@ import net.corda.core.internal.AbstractAttachment
|
|||||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||||
import net.corda.core.internal.FetchAttachmentsFlow
|
import net.corda.core.internal.FetchAttachmentsFlow
|
||||||
import net.corda.core.internal.JarSignatureCollector
|
import net.corda.core.internal.JarSignatureCollector
|
||||||
import net.corda.core.internal.InternalAttachment
|
|
||||||
import net.corda.core.internal.NamedCacheFactory
|
import net.corda.core.internal.NamedCacheFactory
|
||||||
import net.corda.core.internal.P2P_UPLOADER
|
import net.corda.core.internal.P2P_UPLOADER
|
||||||
import net.corda.core.internal.RPC_UPLOADER
|
import net.corda.core.internal.RPC_UPLOADER
|
||||||
@ -28,7 +25,6 @@ import net.corda.core.internal.Version
|
|||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION
|
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION
|
||||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||||
import net.corda.core.internal.entries
|
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
import net.corda.core.internal.isUploaderTrusted
|
||||||
import net.corda.core.internal.readFully
|
import net.corda.core.internal.readFully
|
||||||
import net.corda.core.internal.utilities.ZipBombDetector
|
import net.corda.core.internal.utilities.ZipBombDetector
|
||||||
@ -266,8 +262,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
private val checkOnLoad: Boolean,
|
private val checkOnLoad: Boolean,
|
||||||
uploader: String?,
|
uploader: String?,
|
||||||
override val signerKeys: List<PublicKey>,
|
override val signerKeys: List<PublicKey>,
|
||||||
override val kotlinMetadataVersion: String?
|
) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
|
||||||
) : AbstractAttachment(dataLoader, uploader), InternalAttachment, SerializeAsToken {
|
|
||||||
|
|
||||||
override fun open(): InputStream {
|
override fun open(): InputStream {
|
||||||
val stream = super.open()
|
val stream = super.open()
|
||||||
@ -280,7 +275,6 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
private val checkOnLoad: Boolean,
|
private val checkOnLoad: Boolean,
|
||||||
private val uploader: String?,
|
private val uploader: String?,
|
||||||
private val signerKeys: List<PublicKey>,
|
private val signerKeys: List<PublicKey>,
|
||||||
private val kotlinMetadataVersion: String?
|
|
||||||
) : SerializationToken {
|
) : SerializationToken {
|
||||||
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
|
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
|
||||||
id,
|
id,
|
||||||
@ -288,12 +282,10 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
checkOnLoad,
|
checkOnLoad,
|
||||||
uploader,
|
uploader,
|
||||||
signerKeys,
|
signerKeys,
|
||||||
kotlinMetadataVersion
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toToken(context: SerializeAsTokenContext) =
|
override fun toToken(context: SerializeAsTokenContext) = Token(id, checkOnLoad, uploader, signerKeys)
|
||||||
Token(id, checkOnLoad, uploader, signerKeys, kotlinMetadataVersion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
|
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
|
||||||
@ -311,24 +303,13 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableMetadataApi::class)
|
|
||||||
private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment {
|
private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment {
|
||||||
// TODO Cache this as a column in the database
|
|
||||||
val jis = JarInputStream(attachment.content.inputStream())
|
|
||||||
val kotlinMetadataVersions = jis.entries()
|
|
||||||
.filter { it.name.endsWith(".kotlin_module") }
|
|
||||||
.map { KotlinModuleMetadata.read(jis.readAllBytes()).version }
|
|
||||||
.toSortedSet()
|
|
||||||
if (kotlinMetadataVersions.size > 1) {
|
|
||||||
log.warn("Attachment ${attachment.attId} seems to be comprised of multiple Kotlin versions: $kotlinMetadataVersions")
|
|
||||||
}
|
|
||||||
val attachmentImpl = AttachmentImpl(
|
val attachmentImpl = AttachmentImpl(
|
||||||
id = SecureHash.create(attachment.attId),
|
id = SecureHash.create(attachment.attId),
|
||||||
dataLoader = { attachment.content },
|
dataLoader = { attachment.content },
|
||||||
checkOnLoad = checkAttachmentsOnLoad,
|
checkOnLoad = checkAttachmentsOnLoad,
|
||||||
uploader = attachment.uploader,
|
uploader = attachment.uploader,
|
||||||
signerKeys = attachment.signers?.toList() ?: emptyList(),
|
signerKeys = attachment.signers?.toList() ?: emptyList()
|
||||||
kotlinMetadataVersion = kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last()?.toString()
|
|
||||||
)
|
)
|
||||||
val contracts = attachment.contractClassNames
|
val contracts = attachment.contractClassNames
|
||||||
return if (!contracts.isNullOrEmpty()) {
|
return if (!contracts.isNullOrEmpty()) {
|
||||||
@ -376,14 +357,6 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
return import(jar, uploader, filename)
|
return import(jar, uploader, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
|
||||||
return try {
|
|
||||||
import(jar, uploader, filename)
|
|
||||||
} catch (faee: FileAlreadyExistsException) {
|
|
||||||
AttachmentId.create(faee.message!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction {
|
override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction {
|
||||||
currentDBSession().find(DBAttachment::class.java, attachmentId.toString()) != null
|
currentDBSession().find(DBAttachment::class.java, attachmentId.toString()) != null
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ class ExternalVerifierHandleImpl(
|
|||||||
"${server.localPort}",
|
"${server.localPort}",
|
||||||
log.level.name.lowercase()
|
log.level.name.lowercase()
|
||||||
)
|
)
|
||||||
log.debug { "Verifier command: $command" }
|
log.debug { "External verifier command: $command" }
|
||||||
val logsDirectory = (baseDirectory / "logs").createDirectories()
|
val logsDirectory = (baseDirectory / "logs").createDirectories()
|
||||||
verifierProcess = ProcessBuilder(command)
|
verifierProcess = ProcessBuilder(command)
|
||||||
.redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile()))
|
.redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile()))
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
package net.corda.node.internal
|
package net.corda.node.internal
|
||||||
|
|
||||||
import org.mockito.kotlin.atLeast
|
|
||||||
import org.mockito.kotlin.mock
|
|
||||||
import org.mockito.kotlin.verify
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.serialization.SerializeAsToken
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
@ -20,12 +16,17 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.h2.tools.Server
|
import org.h2.tools.Server
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.mockito.kotlin.atLeast
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.DatabaseMetaData
|
import java.sql.DatabaseMetaData
|
||||||
import java.util.*
|
import java.util.Properties
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
import kotlin.io.path.Path
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class NodeH2SecurityTests {
|
class NodeH2SecurityTests {
|
||||||
@ -133,7 +134,7 @@ class NodeH2SecurityTests {
|
|||||||
init {
|
init {
|
||||||
whenever(config.database).thenReturn(database)
|
whenever(config.database).thenReturn(database)
|
||||||
whenever(config.dataSourceProperties).thenReturn(hikaryProperties)
|
whenever(config.dataSourceProperties).thenReturn(hikaryProperties)
|
||||||
whenever(config.baseDirectory).thenReturn(mock())
|
whenever(config.baseDirectory).thenReturn(Path("."))
|
||||||
whenever(config.effectiveH2Settings).thenAnswer { NodeH2Settings(address) }
|
whenever(config.effectiveH2Settings).thenAnswer { NodeH2Settings(address) }
|
||||||
whenever(config.telemetry).thenReturn(mock())
|
whenever(config.telemetry).thenReturn(mock())
|
||||||
whenever(config.myLegalName).thenReturn(CordaX500Name(null, "client-${address.toString()}", "Corda", "London", null, "GB"))
|
whenever(config.myLegalName).thenReturn(CordaX500Name(null, "client-${address.toString()}", "Corda", "London", null, "GB"))
|
||||||
|
@ -36,8 +36,9 @@ import kotlin.test.assertFailsWith
|
|||||||
|
|
||||||
class CordappProviderImplTests {
|
class CordappProviderImplTests {
|
||||||
private companion object {
|
private companion object {
|
||||||
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
|
val currentFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
|
||||||
val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
|
val currentFinanceWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
|
||||||
|
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val ID1 = AttachmentId.randomSHA256()
|
val ID1 = AttachmentId.randomSHA256()
|
||||||
@ -83,7 +84,7 @@ class CordappProviderImplTests {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test that we find a cordapp class that is loaded into the store`() {
|
fun `test that we find a cordapp class that is loaded into the store`() {
|
||||||
val provider = newCordappProvider(setOf(financeContractsJar))
|
val provider = newCordappProvider(setOf(currentFinanceContractsJar))
|
||||||
|
|
||||||
val expected = provider.cordapps.first()
|
val expected = provider.cordapps.first()
|
||||||
val actual = provider.getCordappForClass(Cash::class.java.name)
|
val actual = provider.getCordappForClass(Cash::class.java.name)
|
||||||
@ -94,7 +95,7 @@ class CordappProviderImplTests {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test that we find an attachment for a cordapp contract class`() {
|
fun `test that we find an attachment for a cordapp contract class`() {
|
||||||
val provider = newCordappProvider(setOf(financeContractsJar))
|
val provider = newCordappProvider(setOf(currentFinanceContractsJar))
|
||||||
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
|
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
|
||||||
val actual = provider.getContractAttachmentID(Cash::class.java.name)
|
val actual = provider.getContractAttachmentID(Cash::class.java.name)
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ class CordappProviderImplTests {
|
|||||||
fun `test cordapp configuration`() {
|
fun `test cordapp configuration`() {
|
||||||
val configProvider = MockCordappConfigProvider()
|
val configProvider = MockCordappConfigProvider()
|
||||||
configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value")
|
configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value")
|
||||||
val provider = newCordappProvider(setOf(financeContractsJar), cordappConfigProvider = configProvider)
|
val provider = newCordappProvider(setOf(currentFinanceContractsJar), cordappConfigProvider = configProvider)
|
||||||
|
|
||||||
val expected = provider.getAppContext(provider.cordapps.first()).config
|
val expected = provider.getAppContext(provider.cordapps.first()).config
|
||||||
|
|
||||||
@ -115,23 +116,33 @@ class CordappProviderImplTests {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun getCordappForFlow() {
|
fun getCordappForFlow() {
|
||||||
val provider = newCordappProvider(setOf(financeWorkflowsJar))
|
val provider = newCordappProvider(setOf(currentFinanceWorkflowsJar))
|
||||||
val cashIssueFlow = CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x00), TestIdentity(ALICE_NAME).party)
|
val cashIssueFlow = CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x00), TestIdentity(ALICE_NAME).party)
|
||||||
assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(financeWorkflowsJar)
|
assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(currentFinanceWorkflowsJar)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `does not load the same flow across different CorDapps`() {
|
fun `does not load the same flow across different CorDapps`() {
|
||||||
val unsignedJar = tempFolder.newFile("duplicate.jar").toPath()
|
val unsignedJar = tempFolder.newFile("duplicate.jar").toPath()
|
||||||
financeWorkflowsJar.copyTo(unsignedJar, overwrite = true)
|
currentFinanceWorkflowsJar.copyTo(unsignedJar, overwrite = true)
|
||||||
// We just need to change the file's hash and thus avoid the duplicate CorDapp check
|
// We just need to change the file's hash and thus avoid the duplicate CorDapp check
|
||||||
unsignedJar.unsignJar()
|
unsignedJar.unsignJar()
|
||||||
assertThat(unsignedJar.hash).isNotEqualTo(financeWorkflowsJar.hash)
|
assertThat(unsignedJar.hash).isNotEqualTo(currentFinanceWorkflowsJar.hash)
|
||||||
assertFailsWith<MultipleCordappsForFlowException> {
|
assertFailsWith<MultipleCordappsForFlowException> {
|
||||||
newCordappProvider(setOf(financeWorkflowsJar, unsignedJar))
|
newCordappProvider(setOf(currentFinanceWorkflowsJar, unsignedJar))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `retrieving legacy attachment for contract`() {
|
||||||
|
val provider = newCordappProvider(setOf(currentFinanceContractsJar), setOf(legacyFinanceContractsJar))
|
||||||
|
val (current, legacy) = provider.getContractAttachments(Cash::class.java.name)!!
|
||||||
|
assertThat(current.id).isEqualTo(currentFinanceContractsJar.hash)
|
||||||
|
assertThat(legacy?.id).isEqualTo(legacyFinanceContractsJar.hash)
|
||||||
|
// getContractAttachmentID should always return the non-legacy attachment ID
|
||||||
|
assertThat(provider.getContractAttachmentID(Cash::class.java.name)).isEqualTo(currentFinanceContractsJar.hash)
|
||||||
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test fixup rule that adds attachment`() {
|
fun `test fixup rule that adds attachment`() {
|
||||||
val fixupJar = File.createTempFile("fixup", ".jar")
|
val fixupJar = File.createTempFile("fixup", ".jar")
|
||||||
@ -220,8 +231,10 @@ class CordappProviderImplTests {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newCordappProvider(cordappJars: Set<Path>, cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl {
|
private fun newCordappProvider(cordappJars: Set<Path>,
|
||||||
val loader = JarScanningCordappLoader(cordappJars)
|
legacyContractJars: Set<Path> = emptySet(),
|
||||||
|
cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl {
|
||||||
|
val loader = JarScanningCordappLoader(cordappJars, legacyContractJars)
|
||||||
return CordappProviderImpl(loader, cordappConfigProvider, attachmentStore).apply { start() }
|
return CordappProviderImpl(loader, cordappConfigProvider, attachmentStore).apply { start() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
|
|||||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||||
import net.corda.testing.core.internal.JarSignatureTestUtils.getJarSigners
|
import net.corda.testing.core.internal.JarSignatureTestUtils.getJarSigners
|
||||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||||
import net.corda.testing.internal.LogHelper
|
import net.corda.testing.internal.LogHelper
|
||||||
import net.corda.testing.node.internal.cordappWithPackages
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
@ -69,8 +70,9 @@ class DummyRPCFlow : FlowLogic<Unit>() {
|
|||||||
|
|
||||||
class JarScanningCordappLoaderTest {
|
class JarScanningCordappLoaderTest {
|
||||||
private companion object {
|
private companion object {
|
||||||
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
|
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
|
||||||
val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
|
val currentFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
|
||||||
|
val currentFinanceWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LogHelper.setLevel(JarScanningCordappLoaderTest::class)
|
LogHelper.setLevel(JarScanningCordappLoaderTest::class)
|
||||||
@ -90,20 +92,20 @@ class JarScanningCordappLoaderTest {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `constructed CordappImpls contains the right classes`() {
|
fun `constructed CordappImpls contains the right classes`() {
|
||||||
val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar))
|
val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar, currentFinanceWorkflowsJar))
|
||||||
val (contractsCordapp, workflowsCordapp) = loader.cordapps
|
val (contractsCordapp, workflowsCordapp) = loader.cordapps
|
||||||
|
|
||||||
assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name)
|
assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name)
|
||||||
assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1)
|
assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1)
|
||||||
assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java)
|
assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java)
|
||||||
assertThat(contractsCordapp.allFlows).isEmpty()
|
assertThat(contractsCordapp.allFlows).isEmpty()
|
||||||
assertThat(contractsCordapp.jarFile).isEqualTo(financeContractsJar)
|
assertThat(contractsCordapp.jarFile).isEqualTo(currentFinanceContractsJar)
|
||||||
|
|
||||||
assertThat(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java)
|
assertThat(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java)
|
||||||
assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java)
|
assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java)
|
||||||
assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java)
|
assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java)
|
||||||
assertThat(workflowsCordapp.contractClassNames).isEmpty()
|
assertThat(workflowsCordapp.contractClassNames).isEmpty()
|
||||||
assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar)
|
assertThat(workflowsCordapp.jarFile).isEqualTo(currentFinanceWorkflowsJar)
|
||||||
|
|
||||||
for (actualCordapp in loader.cordapps) {
|
for (actualCordapp in loader.cordapps) {
|
||||||
assertThat(actualCordapp.cordappClasses)
|
assertThat(actualCordapp.cordappClasses)
|
||||||
@ -196,22 +198,32 @@ class JarScanningCordappLoaderTest {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `loads app signed by allowed certificate`() {
|
fun `loads app signed by allowed certificate`() {
|
||||||
val loader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = emptyList())
|
val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), signerKeyFingerprintBlacklist = emptyList())
|
||||||
assertThat(loader.cordapps).hasSize(1)
|
assertThat(loader.cordapps).hasSize(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout = 300_000)
|
@Test(timeout = 300_000)
|
||||||
fun `does not load app signed by blacklisted certificate`() {
|
fun `does not load app signed by blacklisted certificate`() {
|
||||||
val cordappLoader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
val cordappLoader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||||
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
|
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
|
||||||
cordappLoader.cordapps
|
cordappLoader.cordapps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `does not load legacy contract CorDapp signed by blacklisted certificate`() {
|
||||||
|
val unsignedJar = currentFinanceContractsJar.duplicate { unsignJar() }
|
||||||
|
val loader = JarScanningCordappLoader(setOf(unsignedJar), setOf(legacyFinanceContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||||
|
assertThatExceptionOfType(InvalidCordappException::class.java)
|
||||||
|
.isThrownBy { loader.cordapps }
|
||||||
|
.withMessageContaining("Corresponding contracts are signed by blacklisted key(s)")
|
||||||
|
.withMessageContaining(legacyFinanceContractsJar.name)
|
||||||
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `does not load duplicate CorDapps`() {
|
fun `does not load duplicate CorDapps`() {
|
||||||
val duplicateJar = financeWorkflowsJar.duplicate()
|
val duplicateJar = currentFinanceWorkflowsJar.duplicate()
|
||||||
val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar))
|
val loader = JarScanningCordappLoader(setOf(currentFinanceWorkflowsJar, duplicateJar))
|
||||||
assertFailsWith<DuplicateCordappsInstalledException> {
|
assertFailsWith<DuplicateCordappsInstalledException> {
|
||||||
loader.cordapps
|
loader.cordapps
|
||||||
}
|
}
|
||||||
@ -235,7 +247,7 @@ class JarScanningCordappLoaderTest {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `loads app signed by both allowed and non-blacklisted certificate`() {
|
fun `loads app signed by both allowed and non-blacklisted certificate`() {
|
||||||
val jar = financeWorkflowsJar.duplicate {
|
val jar = currentFinanceWorkflowsJar.duplicate {
|
||||||
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
||||||
tempFolder.root.toPath().signJar(absolutePathString(), "testAlias", "testPassword")
|
tempFolder.root.toPath().signJar(absolutePathString(), "testAlias", "testPassword")
|
||||||
}
|
}
|
||||||
@ -244,6 +256,38 @@ class JarScanningCordappLoaderTest {
|
|||||||
assertThat(loader.cordapps).hasSize(1)
|
assertThat(loader.cordapps).hasSize(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `loads both legacy and current versions of the same contracts CorDapp`() {
|
||||||
|
val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), setOf(legacyFinanceContractsJar))
|
||||||
|
assertThat(loader.cordapps).hasSize(1) // Legacy contract CorDapps are not part of the main list
|
||||||
|
assertThat(loader.legacyContractCordapps).hasSize(1)
|
||||||
|
assertThat(loader.legacyContractCordapps.single().jarFile).isEqualTo(legacyFinanceContractsJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `does not load legacy contracts CorDapp without the corresponding current version`() {
|
||||||
|
val loader = JarScanningCordappLoader(setOf(currentFinanceWorkflowsJar), setOf(legacyFinanceContractsJar))
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy { loader.legacyContractCordapps }
|
||||||
|
.withMessageContaining("does not have a corresponding newer version (4.12 or later). Please add this corresponding CorDapp or remove the legacy one.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `checks if legacy contract CorDapp is actually legacy`() {
|
||||||
|
val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), setOf(currentFinanceContractsJar))
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy { loader.legacyContractCordapps }
|
||||||
|
.withMessageContaining("${currentFinanceContractsJar.name} is not legacy; please remove or place it in the node's CorDapps directory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `does not load if legacy CorDapp present in general list`() {
|
||||||
|
val loader = JarScanningCordappLoader(setOf(legacyFinanceContractsJar))
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy { loader.cordapps }
|
||||||
|
.withMessageContaining("${legacyFinanceContractsJar.name} is legacy contracts; please place it in the node's 'legacy-contracts' directory.")
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun Path.duplicate(name: String = "duplicate.jar", modify: Path.() -> Unit = { }): Path {
|
private inline fun Path.duplicate(name: String = "duplicate.jar", modify: Path.() -> Unit = { }): Path {
|
||||||
val copy = tempFolder.newFile(name).toPath()
|
val copy = tempFolder.newFile(name).toPath()
|
||||||
copyTo(copy, overwrite = true)
|
copyTo(copy, overwrite = true)
|
||||||
@ -252,7 +296,7 @@ class JarScanningCordappLoaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun minAndTargetCordapp(minVersion: Int?, targetVersion: Int?): Path {
|
private fun minAndTargetCordapp(minVersion: Int?, targetVersion: Int?): Path {
|
||||||
return financeWorkflowsJar.duplicate {
|
return currentFinanceWorkflowsJar.duplicate {
|
||||||
modifyJarManifest { manifest ->
|
modifyJarManifest { manifest ->
|
||||||
manifest.setOrDeleteAttribute("Min-Platform-Version", minVersion?.toString())
|
manifest.setOrDeleteAttribute("Min-Platform-Version", minVersion?.toString())
|
||||||
manifest.setOrDeleteAttribute("Target-Platform-Version", targetVersion?.toString())
|
manifest.setOrDeleteAttribute("Target-Platform-Version", targetVersion?.toString())
|
||||||
|
@ -18,6 +18,7 @@ class NodeParams @JvmOverloads constructor(
|
|||||||
val rpcAdminPort: Int,
|
val rpcAdminPort: Int,
|
||||||
val users: List<User>,
|
val users: List<User>,
|
||||||
val cordappJars: List<Path> = emptyList(),
|
val cordappJars: List<Path> = emptyList(),
|
||||||
|
val legacyContractJars: List<Path> = emptyList(),
|
||||||
val jarDirs: List<Path> = emptyList(),
|
val jarDirs: List<Path> = emptyList(),
|
||||||
val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||||
val devMode: Boolean = true,
|
val devMode: Boolean = true,
|
||||||
|
@ -138,6 +138,10 @@ class NodeProcess(
|
|||||||
log.info("Node directory: {}", nodeDir)
|
log.info("Node directory: {}", nodeDir)
|
||||||
val cordappsDir = (nodeDir / CORDAPPS_DIR_NAME).createDirectory()
|
val cordappsDir = (nodeDir / CORDAPPS_DIR_NAME).createDirectory()
|
||||||
params.cordappJars.forEach { it.copyToDirectory(cordappsDir) }
|
params.cordappJars.forEach { it.copyToDirectory(cordappsDir) }
|
||||||
|
if (params.legacyContractJars.isNotEmpty()) {
|
||||||
|
val legacyContractsDir = (nodeDir / "legacy-contracts").createDirectories()
|
||||||
|
params.legacyContractJars.forEach { it.copyToDirectory(legacyContractsDir) }
|
||||||
|
}
|
||||||
(nodeDir / "node.conf").writeText(params.createNodeConfig(isNotary))
|
(nodeDir / "node.conf").writeText(params.createNodeConfig(isNotary))
|
||||||
networkParametersCopier.install(nodeDir)
|
networkParametersCopier.install(nodeDir)
|
||||||
nodeInfoFilesCopier.addConfig(nodeDir)
|
nodeInfoFilesCopier.addConfig(nodeDir)
|
||||||
|
@ -7,7 +7,6 @@ import net.corda.core.contracts.StateRef
|
|||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.contracts.TransactionState
|
import net.corda.core.contracts.TransactionState
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.Crypto.generateKeyPair
|
|
||||||
import net.corda.core.crypto.DigestService
|
import net.corda.core.crypto.DigestService
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
@ -49,13 +48,11 @@ import net.corda.testing.core.ALICE_NAME
|
|||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
import net.corda.testing.core.TestIdentity
|
import net.corda.testing.core.TestIdentity
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
|
||||||
import java.net.ServerSocket
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.cert.X509CRL
|
import java.security.cert.X509CRL
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.Properties
|
||||||
import java.util.jar.JarOutputStream
|
import java.util.jar.JarOutputStream
|
||||||
import java.util.jar.Manifest
|
import java.util.jar.Manifest
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@ -111,7 +108,7 @@ fun createDevIntermediateCaCertPath(
|
|||||||
*/
|
*/
|
||||||
fun createDevNodeCaCertPath(
|
fun createDevNodeCaCertPath(
|
||||||
legalName: CordaX500Name,
|
legalName: CordaX500Name,
|
||||||
nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
|
nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
|
||||||
rootCaName: X500Principal = defaultRootCaName,
|
rootCaName: X500Principal = defaultRootCaName,
|
||||||
intermediateCaName: X500Principal = defaultIntermediateCaName
|
intermediateCaName: X500Principal = defaultIntermediateCaName
|
||||||
): Triple<CertificateAndKeyPair, CertificateAndKeyPair, CertificateAndKeyPair> {
|
): Triple<CertificateAndKeyPair, CertificateAndKeyPair, CertificateAndKeyPair> {
|
||||||
@ -156,7 +153,6 @@ fun fixedCrlSource(crls: Set<X509CRL>): CrlSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is the same as the deprecated [WireTransaction] c'tor but avoids the deprecation warning. */
|
|
||||||
@SuppressWarnings("LongParameterList")
|
@SuppressWarnings("LongParameterList")
|
||||||
fun createWireTransaction(inputs: List<StateRef>,
|
fun createWireTransaction(inputs: List<StateRef>,
|
||||||
attachments: List<SecureHash>,
|
attachments: List<SecureHash>,
|
||||||
@ -164,9 +160,10 @@ fun createWireTransaction(inputs: List<StateRef>,
|
|||||||
commands: List<Command<*>>,
|
commands: List<Command<*>>,
|
||||||
notary: Party?,
|
notary: Party?,
|
||||||
timeWindow: TimeWindow?,
|
timeWindow: TimeWindow?,
|
||||||
|
legacyAttachments: List<SecureHash> = emptyList(),
|
||||||
privacySalt: PrivacySalt = PrivacySalt(),
|
privacySalt: PrivacySalt = PrivacySalt(),
|
||||||
digestService: DigestService = DigestService.default): WireTransaction {
|
digestService: DigestService = DigestService.default): WireTransaction {
|
||||||
val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null)
|
val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null, legacyAttachments)
|
||||||
return WireTransaction(componentGroups, privacySalt, digestService)
|
return WireTransaction(componentGroups, privacySalt, digestService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,20 +248,5 @@ fun <R> withTestSerializationEnvIfNotSet(block: () -> R): R {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to check if particular port is already bound i.e. not vacant
|
|
||||||
*/
|
|
||||||
fun isLocalPortBound(port: Int): Boolean {
|
|
||||||
return try {
|
|
||||||
ServerSocket(port).use {
|
|
||||||
// Successful means that the port was vacant
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Failed to open server socket means that it is already bound by someone
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val IS_S390X = System.getProperty("os.arch") == "s390x"
|
val IS_S390X = System.getProperty("os.arch") == "s390x"
|
||||||
|
@ -16,7 +16,7 @@ class ExternalVerificationContext(
|
|||||||
private val externalVerifier: ExternalVerifier,
|
private val externalVerifier: ExternalVerifier,
|
||||||
private val transactionInputsAndReferences: Map<StateRef, SerializedTransactionState>
|
private val transactionInputsAndReferences: Map<StateRef, SerializedTransactionState>
|
||||||
) : VerificationSupport {
|
) : VerificationSupport {
|
||||||
override val isResolutionLazy: Boolean get() = false
|
override val isInProcess: Boolean get() = false
|
||||||
|
|
||||||
override fun getParties(keys: Collection<PublicKey>): List<Party?> = externalVerifier.getParties(keys)
|
override fun getParties(keys: Collection<PublicKey>): List<Party?> = externalVerifier.getParties(keys)
|
||||||
|
|
||||||
|
@ -132,6 +132,7 @@ class ExternalVerifier(
|
|||||||
return URLClassLoader(cordappJarUrls, javaClass.classLoader)
|
return URLClassLoader(cordappJarUrls, javaClass.classLoader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("INVISIBLE_MEMBER")
|
||||||
private fun verifyTransaction(request: VerificationRequest) {
|
private fun verifyTransaction(request: VerificationRequest) {
|
||||||
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
|
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
|
||||||
val result: Try<Unit> = try {
|
val result: Try<Unit> = try {
|
||||||
|
Loading…
Reference in New Issue
Block a user