Merge pull request #7673 from corda/shams-contracts-component-group

ENT-11355: Backwards compatibility with older nodes via new attachments component group
This commit is contained in:
Adel El-Beik 2024-02-21 15:11:20 +00:00 committed by GitHub
commit 9babf8d801
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 995 additions and 574 deletions

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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()
} private fun <T> Observable<T>.waitForFirst(predicate: (T) -> Boolean): CompletableFuture<Unit> {
val found = CompletableFuture<Unit>()
val subscription = subscribe {
if (predicate(it)) {
found.complete(Unit)
} }
found.await()
subscription.unsubscribe()
} }
return found.whenComplete { _, _ -> subscription.unsubscribe() }
} }
private fun NodeProcess.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) { private fun NodeProcess.assertTransactionsWereVerified(verificationType: VerificationType, vararg txIds: SecureHash) {
val verifierLogContent = externalVerifierLogs() 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
} }

View File

@ -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) }
} }
} }

View File

@ -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) }
}
}

View File

@ -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)

View File

@ -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.
} }

View File

@ -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 {

View File

@ -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)
} }
} }

View File

@ -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
}
}
}

View File

@ -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()) }

View File

@ -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
} }

View File

@ -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()
}

View File

@ -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,

View File

@ -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)

View File

@ -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"
}

View File

@ -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
}
}

View File

@ -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)))
) )

View File

@ -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

View File

@ -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)
} }

View File

@ -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) {

View File

@ -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 -> {
if (verificationType == VerificationType.EXTERNAL || verificationType == VerificationType.BOTH) { val inProcessResult = Try.on { verifyInProcess(verificationSupport, checkSufficientSignatures) }
verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) }
ensureSameResult(inProcessResult, externalResult)
}
VerificationType.EXTERNAL -> {
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
} }
// Contract upgrades only work on 4.11 and earlier
is ContractUpgradeWireTransaction -> VerificationType.EXTERNAL
else -> VerificationType.IN_PROCESS // The default is always in-process
} }
return when { }
old && new -> VerificationType.BOTH
old -> VerificationType.EXTERNAL private fun ensureSameResult(inProcessResult: Try<FullTransaction>, externalResult: Try<*>): FullTransaction {
else -> VerificationType.IN_PROCESS 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) {

View File

@ -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()
} }

View File

@ -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
@ -320,10 +323,10 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
* nothing about the rest. * nothing about the rest.
*/ */
internal val availableComponentNonces: Map<Int, List<SecureHash>> by lazy { internal val availableComponentNonces: Map<Int, List<SecureHash>> by lazy {
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:")

View File

@ -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)"
} }

View File

@ -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.
*/ */

View File

@ -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 {

View File

@ -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()
}
} }

View File

@ -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

View File

@ -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>) {

View File

@ -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
}
} }
/** /**

View File

@ -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
} }

View File

@ -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()))

View File

@ -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,13 +134,13 @@ 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"))
} }
private inner class MockNode: Node(config, VersionInfo.UNKNOWN, false) { private inner class MockNode : Node(config, VersionInfo.UNKNOWN, false) {
fun startDb() = startDatabase() fun startDb() = startDatabase()
override fun makeMessagingService(): MessagingService { override fun makeMessagingService(): MessagingService {

View File

@ -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() }
} }
} }

View File

@ -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())

View File

@ -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,

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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 {