ENT-11355: Backwards compatibility with older nodes via new attachments component group

This commit is contained in:
Shams Asari 2024-02-19 14:58:52 +00:00
parent c2742ba6a5
commit 200333b198
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)
@NotNull
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
@NotNull

View File

@ -95,7 +95,8 @@ import java.math.BigDecimal
import java.security.PublicKey
import java.security.cert.CertPath
import java.time.Instant
import java.util.*
import java.util.Currency
import java.util.UUID
class CordaModule : SimpleModule("corda-core") {
override fun setupModule(context: SetupContext) {
@ -256,6 +257,7 @@ private data class StxJson(
private interface WireTransactionMixin
private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
@Suppress("INVISIBLE_MEMBER")
override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeObject(WireTransactionJson(
value.digestService,
@ -265,7 +267,7 @@ private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
value.outputs,
value.commands,
value.timeWindow,
value.attachments,
value.legacyAttachments.map { "$it-legacy" } + value.nonLegacyAttachments.map { it.toString() },
value.references,
value.privacySalt,
value.networkParametersHash
@ -276,15 +278,18 @@ private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction {
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(
wrapper.inputs,
wrapper.outputs,
wrapper.commands,
wrapper.attachments,
newerAttachments.map(SecureHash::parse),
wrapper.notary,
wrapper.timeWindow,
wrapper.references,
wrapper.networkParametersHash
wrapper.networkParametersHash,
legacyAttachments.map { SecureHash.parse(it.removeSuffix("-legacy")) }
)
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 commands: List<Command<*>>,
val timeWindow: TimeWindow?,
val attachments: List<SecureHash>,
val attachments: List<String>,
val references: List<StateRef>,
val privacySalt: PrivacySalt,
val networkParametersHash: SecureHash?)
val networkParametersHash: SecureHash?
)
private interface TransactionStateMixin {
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)

View File

@ -57,6 +57,10 @@ processSmokeTestResources {
from(configurations.corda4_11)
}
processTestResources {
from(configurations.corda4_11)
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version"

View File

@ -1,6 +1,5 @@
package net.corda.coretests.verification
import co.paralleluniverse.strands.concurrent.CountDownLatch
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.notUsed
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.startFlow
import net.corda.core.node.NodeInfo
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
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.USD
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.AbstractCashFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.workflows.getCashBalance
import net.corda.nodeapi.internal.config.User
import net.corda.smoketesting.NodeParams
import net.corda.smoketesting.NodeProcess
@ -31,15 +36,16 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import rx.Observable
import java.net.InetAddress
import java.nio.file.Path
import java.util.Currency
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.Path
import kotlin.io.path.copyTo
import kotlin.io.path.div
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name
import kotlin.io.path.readText
class ExternalVerificationSignedCordappsTest {
@ -48,27 +54,30 @@ class ExternalVerificationSignedCordappsTest {
private lateinit var notaries: List<NodeProcess>
private lateinit var oldNode: NodeProcess
private lateinit var newNode: NodeProcess
private lateinit var currentNode: NodeProcess
@BeforeClass
@JvmStatic
fun startNodes() {
// The 4.11 finance CorDapp jars
val oldCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") }
val (legacyContractsCordapp, legacyWorkflowsCordapp) = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") }
// 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(
nodeParams(DUMMY_NOTARY_NAME, oldCordapps),
nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), newCordapps)
nodeParams(DUMMY_NOTARY_NAME, cordappJars = currentCordapps, legacyContractJars = listOf(legacyContractsCordapp)),
nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), currentCordapps)
)
oldNode = factory.createNode(nodeParams(
CordaX500Name("Old", "Delhi", "IN"),
oldCordapps + listOf(smokeTestResource("4.11-workflows-cordapp.jar")),
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
listOf(legacyContractsCordapp, legacyWorkflowsCordapp, smokeTestResource("4.11-workflows-cordapp.jar")),
clientRpcConfig = CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
version = "4.11"
))
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
currentNode = factory.createNode(nodeParams(
CordaX500Name("New", "York", "US"),
currentCordapps,
listOf(legacyContractsCordapp)
))
}
@AfterClass
@ -79,8 +88,17 @@ class ExternalVerificationSignedCordappsTest {
}
@Test(timeout=300_000)
fun `transaction containing 4_11 contract sent to new node`() {
assertCashIssuanceAndPayment(issuer = oldNode, recipient = newNode)
fun `transaction containing 4_11 contract attachment only sent to current node`() {
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)
@ -94,12 +112,14 @@ class ExternalVerificationSignedCordappsTest {
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 recipientRpc = recipient.connect(superUser).proxy
val recipientNodeInfo = recipientRpc.nodeInfo()
val notaryIdentity = issuerRpc.notaryIdentities()[0]
val beforeAmount = recipientRpc.getCashBalance(USD)
val (issuanceTx) = issuerRpc.startFlow(
::CashIssueFlow,
10.DOLLARS,
@ -110,6 +130,9 @@ class ExternalVerificationSignedCordappsTest {
issuerRpc.waitForVisibility(recipientNodeInfo)
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
val (_, update) = recipientRpc.vaultTrack(Cash.State::class.java)
val cashArrived = update.waitForFirst { true }
val (paymentTx) = issuerRpc.startFlow(
::CashPaymentFlow,
10.DOLLARS,
@ -117,8 +140,10 @@ class ExternalVerificationSignedCordappsTest {
false,
).returnValue.getOrThrow()
notaries[0].assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
recipient.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
cashArrived.getOrThrow()
assertThat(recipientRpc.getCashBalance(USD) - beforeAmount).isEqualTo(10.DOLLARS)
return Pair(issuanceTx, paymentTx)
}
}
@ -134,18 +159,18 @@ class ExternalVerificationUnsignedCordappsTest {
@JvmStatic
fun startNodes() {
// 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
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(
CordaX500Name("Old", "Delhi", "IN"),
oldCordapps,
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
legacyCordapps,
clientRpcConfig = CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
version = "4.11"
))
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), currentCordapps))
}
@AfterClass
@ -200,6 +225,7 @@ private fun smokeTestResource(name: String): Path = ExternalVerificationSignedCo
private fun nodeParams(
legalName: CordaX500Name,
cordappJars: List<Path> = emptyList(),
legacyContractJars: List<Path> = emptyList(),
clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
version: String? = null
): NodeParams {
@ -210,6 +236,7 @@ private fun nodeParams(
rpcAdminPort = portCounter.andIncrement,
users = listOf(superUser),
cordappJars = cordappJars,
legacyContractJars = legacyContractJars,
clientRpcConfig = clientRpcConfig,
version = version
)
@ -220,28 +247,41 @@ private fun CordaRPCOps.waitForVisibility(other: NodeInfo) {
if (other in snapshot) {
updates.notUsed()
} else {
val found = CountDownLatch(1)
val subscription = updates.subscribe {
if (it.node == other) {
found.countDown()
}
updates.waitForFirst { it.node == other }.getOrThrow()
}
}
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) {
val verifierLogContent = externalVerifierLogs()
private fun NodeProcess.assertTransactionsWereVerified(verificationType: VerificationType, vararg txIds: SecureHash) {
val nodeLogs = logs("node")!!
val externalVerifierLogs = externalVerifierLogs()
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 {
val verifierLogs = (nodeDir / "logs")
.listDirectoryEntries()
.filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" }
assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1)
return verifierLogs[0].readText()
private fun NodeProcess.externalVerifierLogs(): String? = logs("verifier")
private fun NodeProcess.logs(name: String): String? {
return (nodeDir / "logs")
.listDirectoryEntries("$name-${InetAddress.getLocalHost().hostName}.log")
.singleOrNull()
?.readText()
}
private enum class VerificationType {
IN_PROCESS, EXTERNAL, BOTH
}

View File

@ -1,23 +1,56 @@
package net.corda.coretests.transactions
import net.corda.core.contracts.*
import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.*
import net.corda.core.contracts.Command
import net.corda.core.contracts.ComponentGroupEnum
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.accessGroupHashes
import net.corda.core.internal.accessGroupMerkleRoots
import net.corda.core.internal.createComponentGroups
import net.corda.core.internal.getRequiredGroup
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.testing.contracts.DummyContract
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.Test
import java.time.Instant
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 {
private companion object {
@ -47,7 +80,7 @@ class CompatibleTransactionTests {
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 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 timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.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.
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(
inputShuffledGroup,
outputGroup,
@ -114,8 +147,8 @@ class CompatibleTransactionTests {
// 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()[NOTARY_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal])
assertNull(wireTransactionA.accessGroupMerkleRoots()[ATTACHMENTS_GROUP.ordinal])
assertNull(wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[ATTACHMENTS_GROUP.ordinal])
assertNull(wireTransactionA.accessGroupMerkleRoots()[ATTACHMENTS_V2_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.
val shuffledComponentGroupsA = listOf(
@ -140,7 +173,7 @@ class CompatibleTransactionTests {
inputGroup,
outputGroup,
commandGroup,
ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components),
ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, inputGroup.components),
notaryGroup,
timeWindowGroup,
signersGroup
@ -201,23 +234,16 @@ class CompatibleTransactionTests {
@Test(timeout=300_000)
fun `FilteredTransaction constructors and compatibility`() {
// 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.
// 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)
ftxNothing.verify()
// Include all of the components.
val ftxAll = wireTransactionA.buildFilteredTransaction(Predicate { true }) // All filtered.
val ftxAll = wireTransactionA.buildFilteredTransaction { true } // All filtered.
ftxAll.verify()
ftxAll.checkAllComponentsVisible(INPUTS_GROUP)
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)
ComponentGroupEnum.entries.forEach(ftxAll::checkAllComponentsVisible)
// Filter inputs only.
fun filtering(elem: Any): Boolean {
@ -232,9 +258,9 @@ class CompatibleTransactionTests {
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(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // All 3 inputs are present.
assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And their corresponding nonces.
assertNotNull(ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
assertEquals(3, ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) // All 3 inputs are present.
assertEquals(3, ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) // And their corresponding nonces.
assertNotNull(ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) // And the Merkle tree.
// Filter one input only.
fun filteringOneInput(elem: Any) = elem == inputs[0]
@ -244,9 +270,9 @@ class CompatibleTransactionTests {
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.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // 1 input is present.
assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And its corresponding nonce.
assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
assertEquals(1, ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) // 1 input is present.
assertEquals(1, ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) // And its corresponding nonce.
assertNotNull(ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) // And the Merkle tree.
// The old client (receiving more component types than expected) is still compatible.
val componentGroupsCompatibleA = listOf(
@ -265,14 +291,14 @@ class CompatibleTransactionTests {
assertEquals(wireTransactionCompatibleA.id, ftxCompatible.id)
assertEquals(1, ftxCompatible.filteredComponentGroups.size)
assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size)
assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size)
assertNotNull(ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree)
assertEquals(3, ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size)
assertEquals(3, ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size)
assertNotNull(ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree)
assertNull(wireTransactionCompatibleA.networkParametersHash)
assertNull(ftxCompatible.networkParametersHash)
// 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()
assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id)
@ -292,7 +318,7 @@ class CompatibleTransactionTests {
ftxCompatibleNoInputs.verify()
assertFailsWith<ComponentVisibilityException> { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) }
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)
@ -451,7 +477,7 @@ class CompatibleTransactionTests {
val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands))
// 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(
MerkleTree.getMerkleTree(commandDataHashes, wtx.digestService),
commandDataHashes.subList(0, 1)
@ -466,7 +492,7 @@ class CompatibleTransactionTests {
)
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(
MerkleTree.getMerkleTree(signerHashes, wtx.digestService),
signerHashes.subList(0, 2)
@ -527,7 +553,7 @@ class CompatibleTransactionTests {
// Modify last signer (we have a pointer from commandData).
// 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 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 alterSignerPMTK = PartialMerkleTree.build(
alterMTree,
@ -561,7 +587,7 @@ class CompatibleTransactionTests {
fun `parameters hash visibility`() {
fun paramsFilter(elem: Any): Boolean = elem is NetworkParametersHash && elem.hash == 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(
inputGroup,
outputGroup,
@ -577,12 +603,12 @@ class CompatibleTransactionTests {
ftx1.verify()
assertEquals(wtx.id, ftx1.id)
ftx1.checkAllComponentsVisible(PARAMETERS_GROUP)
assertFailsWith<ComponentVisibilityException> { ftx1.checkAllComponentsVisible(ATTACHMENTS_GROUP) }
assertFailsWith<ComponentVisibilityException> { ftx1.checkAllComponentsVisible(ATTACHMENTS_V2_GROUP) }
// Filter only attachment.
val ftx2 = wtx.buildFilteredTransaction(Predicate(::attachmentFilter))
ftx2.verify()
assertEquals(wtx.id, ftx2.id)
ftx2.checkAllComponentsVisible(ATTACHMENTS_GROUP)
ftx2.checkAllComponentsVisible(ATTACHMENTS_V2_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.HashAttachmentConstraint
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.SignatureAttachmentConstraint
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
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.digestService
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.serialization.internal._driverSerializationEnv
import net.corda.core.transactions.TransactionBuilder
import net.corda.testing.common.internal.testNetworkParameters
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.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.MockServices
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@ -56,6 +51,7 @@ class TransactionBuilderTest {
private val contractAttachmentId = services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]
@Test(timeout=300_000)
@Suppress("INVISIBLE_MEMBER")
fun `bare minimum issuance tx`() {
val outputState = TransactionState(
data = DummyState(),
@ -70,6 +66,9 @@ class TransactionBuilderTest {
assertThat(wtx.outputs).containsOnly(outputState)
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
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)
@ -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)
fun `list accessors are mutable copies`() {
val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)

View File

@ -8,10 +8,11 @@ enum class ComponentGroupEnum {
INPUTS_GROUP, // ordinal = 0.
OUTPUTS_GROUP, // ordinal = 1.
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.
TIMEWINDOW_GROUP, // ordinal = 5.
SIGNERS_GROUP, // ordinal = 6.
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,
val sessionsToCollectFrom: Collection<FlowSession>,
val myOptionalKeys: Iterable<PublicKey>?,
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SignedTransaction>() {
@JvmOverloads
constructor(
partiallySignedTx: SignedTransaction,
sessionsToCollectFrom: Collection<FlowSession>,
progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()
progressTracker: ProgressTracker = tracker()
) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
companion object {
@ -100,6 +100,7 @@ class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: Sig
// The signatures must be valid and the transaction must be valid.
partiallySignedTx.verifySignaturesExcept(notSigned)
// TODO Should this be calling SignedTransaction.verify directly? https://r3-cev.atlassian.net/browse/ENT-11458
partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify()
// Determine who still needs to sign.
@ -235,7 +236,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
* - Call the flow via [FlowLogic.subFlow]
* - 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:
*
* 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.
*/
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 {
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
@ -287,6 +288,7 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
checkMySignaturesRequired(stx, signingKeys)
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(stx)
// TODO Should this be calling SignedTransaction.verify directly? https://r3-cev.atlassian.net/browse/ENT-11458
stx.tx.toLedgerTransaction(serviceHub).verify()
// Perform some custom verification over the transaction.
try {

View File

@ -11,12 +11,14 @@ import net.corda.core.internal.PlatformVersionSwitches
import net.corda.core.internal.ServiceHubCoreInternal
import net.corda.core.internal.pushToLoggingContext
import net.corda.core.internal.telemetry.telemetryServiceInternal
import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.internal.warnOnce
import net.corda.core.node.StatesToRecord
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.Try
import net.corda.core.utilities.debug
@ -170,6 +172,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
@Suppress("ComplexMethod", "NestedBlockDepth")
@Throws(NotaryException::class)
override fun call(): SignedTransaction {
require(transaction.coreTransaction is WireTransaction) // Sanity check
if (!newApi) {
logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
"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.
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
// TODO= [CORDA-3267] Remove duplicate signature verification
val ltx = transaction.toLedgerTransaction(serviceHub, false)
ltx.verify()
return ltx
val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) as LedgerTransaction?
// verifyInternal returns null if the transaction was verified externally, which *could* happen on a very odd scenerio of a 4.11
// 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.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() {
var message = this.message
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)
// 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 {
// Use nanoTime as it's monotonic.
val now = System.nanoTime()
@ -639,16 +644,10 @@ val Logger.level: Level
else -> throw IllegalStateException("Unknown logging level")
}
const val JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION = 46
const val JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION = 61
const val JAVA_1_2_CLASS_FILE_MAJOR_VERSION = 46
const val JAVA_8_CLASS_FILE_MAJOR_VERSION = 52
const val JAVA_17_CLASS_FILE_MAJOR_VERSION = 61
/**
* 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.capitalize(): String = replaceFirstChar { it.titlecase(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 {
val tx = transaction.coreTransaction
val attachmentIds = when (tx) {
is WireTransaction -> tx.attachments.toSet()
is WireTransaction -> tx.allAttachments
is ContractUpgradeWireTransaction -> setOf(tx.legacyContractAttachmentId, tx.upgradedContractAttachmentId)
else -> return false
}

View File

@ -1,6 +1,27 @@
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.SecureHash
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.identity.Party
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.*
import net.corda.core.transactions.*
import net.corda.core.serialization.MissingAttachmentsException
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 java.io.ByteArrayOutputStream
import java.security.PublicKey
@ -68,8 +101,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
forceDeserialize: Boolean = false,
factory: SerializationFactory = SerializationFactory.defaultFactory,
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()) {
return emptyList()
}
@ -85,7 +117,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
factory.deserialize(component, clazz.java, context)
} 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
* 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.
*/
class TransactionDeserialisationException(groupEnum: ComponentGroupEnum, index: Int, cause: Exception):
@ -119,9 +157,9 @@ fun deserialiseCommands(
// TODO: we could avoid deserialising unrelated signers.
// 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).
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize, factory, context))
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize, factory, context)
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, SIGNERS_GROUP, forceDeserialize, factory, context))
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, COMMANDS_GROUP, forceDeserialize, factory, context)
val group = componentGroups.getGroup(COMMANDS_GROUP)
return if (group is FilteredComponentGroup) {
check(commandDataList.size <= signersList.size) {
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
@ -141,10 +179,7 @@ fun deserialiseCommands(
}
}
/**
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
* for backwards compatibility purposes.
*/
@Suppress("LongParameterList")
fun createComponentGroups(inputs: List<StateRef>,
outputs: List<TransactionState<ContractState>>,
commands: List<Command<*>>,
@ -152,26 +187,37 @@ fun createComponentGroups(inputs: List<StateRef>,
notary: Party?,
timeWindow: TimeWindow?,
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 serializationContext = serializationFactory.defaultContext
val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) }
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUTS_GROUP.ordinal, inputs.lazyMapped(serialize)))
if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCES_GROUP.ordinal, references.lazyMapped(serialize)))
if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP.ordinal, outputs.lazyMapped(serialize)))
componentGroupMap.addListGroup(INPUTS_GROUP, inputs, serialize)
componentGroupMap.addListGroup(REFERENCES_GROUP, references, serialize)
componentGroupMap.addListGroup(OUTPUTS_GROUP, outputs, serialize)
// 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)))
if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, attachments.lazyMapped(serialize)))
if (notary != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize)))
if (timeWindow != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize)))
componentGroupMap.addListGroup(COMMANDS_GROUP, commands.map { it.value }, serialize)
// Attachments which can only be processed by 4.12 and later.
componentGroupMap.addListGroup(ATTACHMENTS_V2_GROUP, attachments, 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
// 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)))
if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize())))
componentGroupMap.addListGroup(SIGNERS_GROUP, commands.map { it.signers }, serialize)
if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize())))
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>>
/**
@ -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 targetPlatformVersion: Int,
override val jarHash: SecureHash.SHA256 = jarFile.hash,
val languageVersion: LanguageVersion = LanguageVersion.Data,
val notaryService: Class<out NotaryService>? = null,
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
val isLoaded: Boolean = true,

View File

@ -14,7 +14,10 @@ interface CordappProviderInternal : CordappProvider {
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
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.TransactionResolutionException
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.cordapp.CordappProviderInternal
import net.corda.core.internal.entries
import net.corda.core.internal.getRequiredGroup
import net.corda.core.internal.getRequiredTransaction
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentStorage
@ -85,9 +86,7 @@ interface NodeVerificationSupport : VerificationSupport {
private fun getRegularOutput(coreTransaction: WireTransaction, outputIndex: Int): SerializedTransactionState {
@Suppress("UNCHECKED_CAST")
return coreTransaction.componentGroups
.first { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }
.components[outputIndex] as SerializedTransactionState
return coreTransaction.componentGroups.getRequiredGroup(OUTPUTS_GROUP).components[outputIndex] as SerializedTransactionState
}
/**
@ -137,6 +136,8 @@ interface NodeVerificationSupport : VerificationSupport {
override fun getTrustedClassAttachment(className: String): Attachment? {
val allTrusted = attachments.queryAttachments(
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)))
)

View File

@ -18,7 +18,7 @@ import java.security.PublicKey
* Represents the operations required to resolve and verify a transaction.
*/
interface VerificationSupport {
val isResolutionLazy: Boolean get() = true
val isInProcess: Boolean get() = true
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.PackageOwnershipException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION
import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION
import net.corda.core.internal.JAVA_17_CLASS_FILE_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.NamedCacheFactory
import net.corda.core.internal.PlatformVersionSwitches
@ -340,7 +340,7 @@ object AttachmentsClassLoaderBuilder {
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
val serializers = try {
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) {
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
}

View File

@ -1,12 +1,15 @@
package net.corda.core.transactions
import net.corda.core.CordaException
import net.corda.core.CordaInternal
import net.corda.core.contracts.*
import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.deserialiseCommands
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.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializedBytes
@ -29,8 +32,34 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
@DeprecatedConstructorForDeserialization(1)
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. */
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). */
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 output 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 time-window of the transaction, if present (list with one element)
* - list of each reference input that is present
* - network parameters hash if present
* - list of each attachment that is present
*/
val availableComponentGroups: List<List<Any>>
get() {
val result = mutableListOf(inputs, outputs, commands, attachments, references)
val result = mutableListOf(inputs, outputs, commands, legacyAttachments, references)
notary?.let { result += listOf(it) }
timeWindow?.let { result += listOf(it) }
networkParametersHash?.let { result += listOf(it) }
result += nonLegacyAttachments
return result
}
}
@ -153,12 +184,10 @@ class FilteredTransaction internal constructor(
// This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details.
if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) {
signersIncluded = true
val signersGroupIndex = SIGNERS_GROUP.ordinal
// There exist commands, thus the signers group is not empty.
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList()
filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList()
filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList()
filteredSerialisedComponents[SIGNERS_GROUP.ordinal] = wtx.componentGroups.getRequiredGroup(SIGNERS_GROUP).components.toMutableList()
filteredComponentNonces[SIGNERS_GROUP.ordinal] = wtx.availableComponentNonces[SIGNERS_GROUP.ordinal]!!.toMutableList()
filteredComponentHashes[SIGNERS_GROUP.ordinal] = wtx.availableComponentHashes[SIGNERS_GROUP.ordinal]!!.toMutableList()
}
}
@ -166,7 +195,8 @@ class FilteredTransaction internal constructor(
wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_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.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.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],
@ -269,7 +299,7 @@ class FilteredTransaction internal constructor(
*/
@Throws(ComponentVisibilityException::class)
fun checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum) {
val group = filteredComponentGroups.firstOrNull { it.groupIndex == componentGroupEnum.ordinal }
val group = filteredComponentGroups.getGroup(componentGroupEnum)
if (group == null) {
// 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,
@ -300,7 +330,7 @@ class FilteredTransaction internal constructor(
*/
@Throws(ComponentVisibilityException::class)
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 receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size
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.internal.TransactionDeserialisationException
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.attachmentIds
import net.corda.core.internal.equivalent
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.VerificationSupport
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.internal.MissingSerializerException
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.debug
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
* for out-of-process verification.
*
* @return The [FullTransaction] that was successfully verified in-process. Returns null if the verification was successfully done externally.
*/
@CordaInternal
@JvmSynthetic
fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true) {
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? {
resolveAndCheckNetworkParameters(verificationSupport)
val verificationType = determineVerificationType(verificationSupport)
val verificationType = determineVerificationType()
log.debug { "Transaction $id has verification type $verificationType" }
if (verificationType == VerificationType.IN_PROCESS || verificationType == VerificationType.BOTH) {
verifyInProcess(verificationSupport, checkSufficientSignatures)
}
if (verificationType == VerificationType.EXTERNAL || verificationType == VerificationType.BOTH) {
verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures)
return when (verificationType) {
VerificationType.IN_PROCESS -> verifyInProcess(verificationSupport, checkSufficientSignatures)
VerificationType.BOTH -> {
val inProcessResult = Try.on { verifyInProcess(verificationSupport, checkSufficientSignatures) }
val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) }
ensureSameResult(inProcessResult, externalResult)
}
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 {
var old = false
var new = false
for (attachmentId in coreTransaction.attachmentIds) {
val (major, minor) = verificationSupport.getAttachment(attachmentId)?.kotlinMetadataVersion?.split(".") ?: continue
// Metadata version 1.1 maps to language versions 1.0 to 1.3
if (major == "1" && minor == "1") {
old = true
} else {
new = true
private fun determineVerificationType(): VerificationType {
val ctx = coreTransaction
return when (ctx) {
is WireTransaction -> {
when {
ctx.legacyAttachments.isEmpty() -> VerificationType.IN_PROCESS
ctx.nonLegacyAttachments.isEmpty() -> VerificationType.EXTERNAL
else -> VerificationType.BOTH
}
}
// 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
else -> VerificationType.IN_PROCESS
}
private fun ensureSameResult(inProcessResult: Try<FullTransaction>, externalResult: Try<*>): FullTransaction {
return when (externalResult) {
is Success -> when (inProcessResult) {
is Success -> inProcessResult.value
is Failure -> throw IllegalStateException("In-process verification of $id failed, but it succeeded in external verifier")
.apply { addSuppressed(inProcessResult.exception) }
}
is Failure -> throw when (inProcessResult) {
is Success -> IllegalStateException("In-process verification of $id succeeded, but it failed in external verifier")
is Failure -> inProcessResult.exception // Throw the in-process exception, with the external exception suppressed
}.apply { addSuppressed(externalResult.exception) }
}
}
@ -244,11 +266,13 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
/**
* Verifies this transaction in-process. This assumes the current process has the correct classpath for all the contracts.
*
* @return The [FullTransaction] that was successfully verified
*/
@CordaInternal
@JvmSynthetic
fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
when (coreTransaction) {
internal fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): FullTransaction {
return when (coreTransaction) {
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(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. */
private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): NotaryChangeLedgerTransaction {
val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
else checkSignaturesAreValid()
return ntx
}
/** 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)
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
else checkSignaturesAreValid()
return ctx
}
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
// objects from the TransactionState.
private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures)
try {
ltx.verify()
@ -304,6 +330,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
checkReverifyAllowed(e)
retryVerification(e.cause, e, ltx, verificationSupport)
}
return ltx
}
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.internal.*
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.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters
@ -152,7 +153,7 @@ open class TransactionBuilder(
*/
@Throws(MissingContractAttachments::class)
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) {
// 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.removeAll(excludedAttachments)
WireTransaction(
@ -207,7 +208,8 @@ open class TransactionBuilder(
notary,
window,
referenceStates,
serviceHub.networkParametersService.currentHash
serviceHub.networkParametersService.currentHash,
allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.toList()
),
privacySalt,
serviceHub.digestService
@ -237,6 +239,7 @@ open class TransactionBuilder(
/**
* @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 {
return try {
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.
// Note: this is a best effort to preserve backwards compatibility.
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)
}
rootError is NoClassDefFoundError -> {
((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e))
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e))
|| addMissingAttachment(rootError.message ?: throw e, serviceHub, e)
}
@ -347,7 +353,7 @@ open class TransactionBuilder(
*/
private fun selectContractAttachmentsAndOutputStateConstraints(
serviceHub: VerifyingServiceHub
): Pair<List<ContractAttachment>, List<TransactionState<*>>> {
): Pair<List<ContractAttachmentWithLegacy>, List<TransactionState<*>>> {
// Determine the explicitly set contract attachments.
val explicitContractToAttachments = attachments
.mapNotNull { serviceHub.attachments.openAttachment(it) as? ContractAttachment }
@ -367,7 +373,7 @@ open class TransactionBuilder(
= referencesWithTransactionState.groupBy { it.contract }
val refStateContractAttachments = referenceStateGroups
.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.
val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract ->
@ -413,10 +419,10 @@ open class TransactionBuilder(
outputStates: List<TransactionState<ContractState>>?,
explicitContractAttachment: ContractAttachment?,
serviceHub: VerifyingServiceHub
): Pair<ContractAttachment, List<TransactionState<*>>> {
): Pair<ContractAttachmentWithLegacy, List<TransactionState<*>>> {
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachment(contractClassName) {
fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachments(contractClassName) {
inputsAndOutputs.filterNot { it.constraint in automaticConstraints }
}
@ -429,14 +435,15 @@ open class TransactionBuilder(
a system parameter that disables the hash constraint check.
*/
if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, serviceHub)) {
val attachment = selectAttachmentForContract()
val attachmentWithLegacy = selectAttachmentForContract()
val (attachment) = attachmentWithLegacy
if (attachment.isSigned && (explicitContractAttachment == null || explicitContractAttachment.id == attachment.id)) {
val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys)
require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" }
val resolvedOutputStates = outputStates?.map {
if (it.constraint in automaticConstraints) it.copy(constraint = signatureConstraint) else it
} ?: 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
explicitContractAttachment
ContractAttachmentWithLegacy(explicitContractAttachment, null) // By definition there can be no legacy version
} else {
hashAttachment ?: selectAttachmentForContract()
hashAttachment?.let { ContractAttachmentWithLegacy(it, null) } ?: selectAttachmentForContract()
}
// 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.
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment, serviceHub)
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment.currentAttachment, serviceHub)
// 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)) {
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
}
@ -506,7 +513,7 @@ open class TransactionBuilder(
} 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.
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}"
}
}
@ -629,12 +636,18 @@ open class TransactionBuilder(
SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners)
.build())
private inline fun VerifyingServiceHub.getInstalledContractAttachment(
private inline fun VerifyingServiceHub.getInstalledContractAttachments(
contractClassName: String,
statesForException: () -> List<TransactionState<*>>
): ContractAttachment {
return cordappProvider.getContractAttachment(contractClassName)
): ContractAttachmentWithLegacy {
// 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)
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 {
@ -646,6 +659,7 @@ open class TransactionBuilder(
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub) {
// TODO ENT-11445: Need to verify via SignedTransaction to ensure legacy components also work
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.createComponentGroups
import net.corda.core.internal.flatMapToSet
import net.corda.core.internal.getGroup
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.internal.lazyMapped
import net.corda.core.internal.mapToSet
@ -162,7 +163,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
@JvmSynthetic
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
// Look up public keys to authenticated identities.
val authenticatedCommands = if (verificationSupport.isResolutionLazy) {
val authenticatedCommands = if (verificationSupport.isInProcess) {
commands.lazyMapped { cmd, _ ->
val parties = verificationSupport.getParties(cmd.signers).filterNotNull()
CommandWithParties(cmd.signers, parties, cmd.value)
@ -193,13 +194,15 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
}
val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef)
val resolvedAttachments = if (verificationSupport.isResolutionLazy) {
attachments.lazyMapped { id, _ ->
val resolvedAttachments = if (verificationSupport.isInProcess) {
// The 4.12+ node only looks at the new attachments group
nonLegacyAttachments.lazyMapped { id, _ ->
verificationSupport.getAttachment(id) ?: throw AttachmentResolutionException(id)
}
} else {
verificationSupport.getAttachments(attachments).mapIndexed { index, attachment ->
attachment ?: throw AttachmentResolutionException(attachments[index])
// The 4.11 external verifier only looks at the legacy attachments group since it will only contain attachments compatible with 4.11
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.
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
@ -320,10 +323,10 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
* nothing about the rest.
*/
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) } }
} 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.
*/
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)
}
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 {
val buf = StringBuilder()
buf.appendLine("Transaction:")

View File

@ -51,7 +51,7 @@ cordapp {
minimumPlatformVersion 1
contract {
name "Corda Finance Demo"
versionId 1
versionId 2
vendor "R3"
licence "Open Source (Apache 2)"
}

View File

@ -14,6 +14,11 @@ interface CordappLoader : AutoCloseable {
*/
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.
*/

View File

@ -23,8 +23,10 @@ configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
slowIntegrationTestCompile.extendsFrom testImplementation
slowIntegrationTestImplementation.extendsFrom testImplementation
slowIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
corda4_11
}
sourceSets {
@ -89,6 +91,7 @@ processTestResources {
from(tasks.getByPath(":testing:cordapps:cashobservers: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
@ -104,30 +107,22 @@ dependencies {
implementation project(':common-configuration-parsing')
implementation project(':common-logging')
implementation project(':serialization')
implementation "io.opentelemetry:opentelemetry-api:${open_telemetry_version}"
// Backwards compatibility goo: Apps expect confidential-identities to be loaded by default.
// We could eventually gate this on a target-version check.
implementation project(':confidential-identities')
implementation "io.opentelemetry:opentelemetry-api:${open_telemetry_version}"
// Log4J: logging framework (with SLF4J bindings)
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
implementation "org.apache.logging.log4j:log4j-web:${log4j_version}"
implementation "org.slf4j:jul-to-slf4j:$slf4j_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 "com.google.guava:guava:$guava_version"
implementation "commons-io:commons-io:$commons_io_version"
// For caches rather than guava
implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version"
// For async logging
implementation "com.lmax:disruptor:$disruptor_version"
// Artemis: for reliable p2p message queues.
// TODO: remove the forced update of commons-collections and beanutils when artemis updates them
implementation "org.apache.commons:commons-collections4:${commons_collections_version}"
@ -142,92 +137,66 @@ dependencies {
// Bouncy castle support needed for X509 certificate manipulation
implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}"
implementation "com.esotericsoftware:kryo:$kryo_version"
implementation "com.fasterxml.jackson.core:jackson-annotations:${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
implementation "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
// Coda Hale's Metrics: for monitoring of key statistics
implementation "io.dropwizard.metrics:metrics-jmx:$metrics_version"
implementation "io.github.classgraph:classgraph:$class_graph_version"
implementation "org.liquibase:liquibase-core:$liquibase_version"
// TypeSafe Config: for simple and human friendly config files.
implementation "com.typesafe:config:$typesafe_config_version"
implementation "io.reactivex:rxjava:$rxjava_version"
implementation("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'
}
// 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 "junit:junit:$junit_version"
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}"
testImplementation(project(':test-cli'))
testImplementation(project(':test-utils'))
// Unit testing helpers.
testImplementation "org.assertj:assertj-core:${assertj_version}"
testImplementation project(':node-driver')
testImplementation project(':core-test-utils')
testImplementation project(':test-utils')
testImplementation project(':client:jfx')
testImplementation project(':finance:contracts')
testImplementation project(':finance:workflows')
// sample test schemas
testImplementation project(path: ':finance:contracts', configuration: 'testArtifacts')
// 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"
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'
testImplementation project(':testing:cordapps:dbfailure:dbfworkflows')
testImplementation "org.assertj:assertj-core:${assertj_version}"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version"
// Jetty dependencies for NetworkMapClient test.
// Web stuff: for HTTP[S] servlets
testImplementation "org.hamcrest:hamcrest-library:2.1"
@ -238,43 +207,33 @@ dependencies {
testImplementation "com.google.jimfs:jimfs:1.1"
testImplementation "co.paralleluniverse:quasar-core:$quasar_version"
testImplementation "com.natpryce:hamkrest:$hamkrest_version"
// Jersey for JAX-RS implementation for use in Jetty
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-jetty-http:${jersey_version}"
// 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'
// 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
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}"
integrationTestImplementation project(":testing:cordapps:dbfailure:dbfcontracts")
integrationTestImplementation project(":testing:cordapps:missingmigration")
testImplementation project(':testing:cordapps:dbfailure:dbfworkflows')
// 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"
// used by FinalityFlowErrorHandlingTest
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 {

View File

@ -1,51 +1,42 @@
package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.contracts.Amount
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.flows.FinalityFlow
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.internal.*
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
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.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver
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 org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import java.net.URL
import java.net.URLClassLoader
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import java.util.Currency
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)
fun `contracts downloaded from the network are not executed`() {
driver(DriverParameters(
@ -53,61 +44,36 @@ class AttachmentLoadingTests {
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = listOf(enclosedCordapp())
)) {
installIsolatedCordapp(ALICE_NAME)
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME)
startNode(NodeParameters(ALICE_NAME, additionalCordapps = FINANCE_CORDAPPS)),
startNode(NodeParameters(BOB_NAME, additionalCordapps = listOf(FINANCE_WORKFLOWS_CORDAPP)))
).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
// we can verify here Bob threw the correct exception
.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
@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
override fun call() {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stateAndRef = serviceHub.toStateAndRef<ContractState>(stateRef)
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary)
.addInputState(stateAndRef)
.addOutputState(ConsumeContract.State())
.addCommand(Command(ConsumeContract.Cmd, ourIdentity.owningKey))
val builder = TransactionBuilder(notary)
val (_, keysForSigning) = CashUtils.generateSpend(
serviceHub,
builder,
amount,
ourIdentityAndCert,
otherSide,
anonymous = false
)
stx.verify(serviceHub, checkSufficientSignatures = false)
val stx = serviceHub.signInitialTransaction(builder, keysForSigning)
val session = initiateFlow(otherSide)
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
@ -129,16 +95,4 @@ class AttachmentLoadingTests {
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.CordappProviderImpl
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.rpc.proxies.AuthenticatedRpcOpsProxy
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
@ -187,6 +188,7 @@ import java.util.function.Consumer
import javax.persistence.EntityManager
import javax.sql.DataSource
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
@ -853,6 +855,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
return JarScanningCordappLoader.fromDirectories(
configuration.cordappDirectories,
(configuration.baseDirectory / LEGACY_CONTRACTS_DIR_NAME).takeIf { it.exists() },
versionInfo,
extraCordapps = generatedCordapps,
signerKeyFingerprintBlacklist = blacklistedKeys

View File

@ -6,6 +6,7 @@ import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext
import net.corda.core.flows.FlowLogic
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.CordappProviderInternal
import net.corda.core.internal.groupByMultipleKeys
@ -38,6 +39,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
fun start() {
loadContractsIntoAttachmentStore(cordappLoader.cordapps)
loadContractsIntoAttachmentStore(cordappLoader.legacyContractCordapps)
flowToCordapp = makeFlowToCordapp()
// Load the fix-ups after uploading any new contracts into attachment storage.
attachmentFixups.load(cordappLoader.appClassLoader)
@ -56,12 +58,18 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader,
}
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? {
// loadContractsIntoAttachmentStore makes sure the jarHash is the attachment ID
return cordappLoader.cordapps.find { contractClassName in it.contractClassNames }?.jarHash
return cordappLoader.cordapps.findCordapp(contractClassName)
}
override fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? {
return getContractAttachmentID(contractClassName)?.let(::getContractAttachment)
override fun getContractAttachments(contractClassName: ContractClassName): ContractAttachmentWithLegacy? {
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>) {

View File

@ -1,6 +1,7 @@
package net.corda.node.internal.cordapp
import io.github.classgraph.ClassGraph
import io.github.classgraph.ClassInfo
import io.github.classgraph.ClassInfoList
import io.github.classgraph.ScanResult
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.cordapp.CordappImpl
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.flatMapToSet
import net.corda.core.internal.groupByMultipleKeys
import net.corda.core.internal.hash
import net.corda.core.internal.isAbstractClass
import net.corda.core.internal.loadClassOfType
import net.corda.core.internal.location
import net.corda.core.internal.groupByMultipleKeys
import net.corda.core.internal.mapToSet
import net.corda.core.internal.notary.NotaryService
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.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.nodeapi.internal.coreContractClasses
@ -50,6 +54,7 @@ import java.lang.reflect.Modifier
import java.net.URLClassLoader
import java.nio.file.Path
import java.util.ServiceLoader
import java.util.TreeSet
import java.util.jar.JarInputStream
import java.util.jar.Manifest
import kotlin.io.path.absolutePathString
@ -57,27 +62,35 @@ import kotlin.io.path.exists
import kotlin.io.path.inputStream
import kotlin.io.path.isSameFileAs
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.useDirectoryEntries
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
/**
* Handles CorDapp loading and classpath scanning 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")
class JarScanningCordappLoader(private val cordappJars: Set<Path>,
private val legacyContractJars: Set<Path> = emptySet(),
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
private val extraCordapps: List<CordappImpl> = emptyList(),
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoader {
companion object {
private val logger = contextLogger()
const val LEGACY_CONTRACTS_DIR_NAME = "legacy-contracts"
/**
* Creates a CordappLoader from multiple directories.
*
* @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>,
legacyContractsDir: Path? = null,
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(),
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
@ -86,12 +99,14 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
.asSequence()
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
.toSet()
return JarScanningCordappLoader(cordappJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
val legacyContractJars = legacyContractsDir?.useDirectoryEntries("*.jar") { it.toSet() } ?: emptySet()
return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
}
}
init {
logger.debug { "cordappJars: $cordappJars" }
logger.debug { "legacyContractJars: $legacyContractJars" }
}
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)
override val cordapps: List<CordappImpl>
get() = internal.cordapps
get() = internal.nonLegacyCordapps
override val legacyContractCordapps: List<CordappImpl>
get() = internal.legacyContractCordapps
override fun close() = appClassLoader.close()
private inner class InternalHolder {
val cordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp)
val nonLegacyCordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp)
val legacyContractCordapps = legacyContractJars.map(::scanCordapp)
init {
checkInvalidCordapps()
checkDuplicateCordapps()
checkContractOverlap()
cordapps += extraCordapps
commonChecks(nonLegacyCordapps, LanguageVersion::isNonLegacyCompatible)
nonLegacyCordapps += extraCordapps
if (legacyContractCordapps.isNotEmpty()) {
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>()
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) {
if (group.size > 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 ->
throw IllegalStateException("Contract $contract occuring in multiple CorDapps (${cordapp1.name}, ${cordapp2.name}). " +
"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 {
@ -160,6 +226,8 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
val languageVersion = determineLanguageVersion(path)
logger.debug { "$path: $languageVersion" }
return CordappImpl(
path,
findContractClassNames(this),
@ -177,6 +245,7 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
info,
minPlatformVersion,
targetPlatformVersion,
languageVersion = languageVersion,
notaryService = findNotaryService(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>> {
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.HashingInputStream
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.contracts.Attachment
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.FetchAttachmentsFlow
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.InternalAttachment
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.P2P_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.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_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.readFully
import net.corda.core.internal.utilities.ZipBombDetector
@ -266,8 +262,7 @@ class NodeAttachmentService @JvmOverloads constructor(
private val checkOnLoad: Boolean,
uploader: String?,
override val signerKeys: List<PublicKey>,
override val kotlinMetadataVersion: String?
) : AbstractAttachment(dataLoader, uploader), InternalAttachment, SerializeAsToken {
) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
override fun open(): InputStream {
val stream = super.open()
@ -280,7 +275,6 @@ class NodeAttachmentService @JvmOverloads constructor(
private val checkOnLoad: Boolean,
private val uploader: String?,
private val signerKeys: List<PublicKey>,
private val kotlinMetadataVersion: String?
) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
id,
@ -288,12 +282,10 @@ class NodeAttachmentService @JvmOverloads constructor(
checkOnLoad,
uploader,
signerKeys,
kotlinMetadataVersion
)
}
override fun toToken(context: SerializeAsTokenContext) =
Token(id, checkOnLoad, uploader, signerKeys, kotlinMetadataVersion)
override fun toToken(context: SerializeAsTokenContext) = Token(id, checkOnLoad, uploader, signerKeys)
}
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
@ -311,24 +303,13 @@ class NodeAttachmentService @JvmOverloads constructor(
}
}
@OptIn(UnstableMetadataApi::class)
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(
id = SecureHash.create(attachment.attId),
dataLoader = { attachment.content },
checkOnLoad = checkAttachmentsOnLoad,
uploader = attachment.uploader,
signerKeys = attachment.signers?.toList() ?: emptyList(),
kotlinMetadataVersion = kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last()?.toString()
signerKeys = attachment.signers?.toList() ?: emptyList()
)
val contracts = attachment.contractClassNames
return if (!contracts.isNullOrEmpty()) {
@ -376,14 +357,6 @@ class NodeAttachmentService @JvmOverloads constructor(
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 {
currentDBSession().find(DBAttachment::class.java, attachmentId.toString()) != null
}

View File

@ -195,7 +195,7 @@ class ExternalVerifierHandleImpl(
"${server.localPort}",
log.level.name.lowercase()
)
log.debug { "Verifier command: $command" }
log.debug { "External verifier command: $command" }
val logsDirectory = (baseDirectory / "logs").createDirectories()
verifierProcess = ProcessBuilder(command)
.redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile()))

View File

@ -1,9 +1,5 @@
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.serialization.SerializeAsToken
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.h2.tools.Server
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.sql.Connection
import java.sql.DatabaseMetaData
import java.util.*
import java.util.Properties
import java.util.concurrent.ExecutorService
import javax.sql.DataSource
import kotlin.io.path.Path
import kotlin.test.assertFailsWith
class NodeH2SecurityTests {
@ -133,13 +134,13 @@ class NodeH2SecurityTests {
init {
whenever(config.database).thenReturn(database)
whenever(config.dataSourceProperties).thenReturn(hikaryProperties)
whenever(config.baseDirectory).thenReturn(mock())
whenever(config.baseDirectory).thenReturn(Path("."))
whenever(config.effectiveH2Settings).thenAnswer { NodeH2Settings(address) }
whenever(config.telemetry).thenReturn(mock())
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()
override fun makeMessagingService(): MessagingService {

View File

@ -36,8 +36,9 @@ import kotlin.test.assertFailsWith
class CordappProviderImplTests {
private companion object {
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.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()
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
@JvmField
val ID1 = AttachmentId.randomSHA256()
@ -83,7 +84,7 @@ class CordappProviderImplTests {
@Test(timeout=300_000)
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 actual = provider.getCordappForClass(Cash::class.java.name)
@ -94,7 +95,7 @@ class CordappProviderImplTests {
@Test(timeout=300_000)
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 actual = provider.getContractAttachmentID(Cash::class.java.name)
@ -106,7 +107,7 @@ class CordappProviderImplTests {
fun `test cordapp configuration`() {
val configProvider = MockCordappConfigProvider()
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
@ -115,23 +116,33 @@ class CordappProviderImplTests {
@Test(timeout=300_000)
fun getCordappForFlow() {
val provider = newCordappProvider(setOf(financeWorkflowsJar))
val provider = newCordappProvider(setOf(currentFinanceWorkflowsJar))
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)
fun `does not load the same flow across different CorDapps`() {
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
unsignedJar.unsignJar()
assertThat(unsignedJar.hash).isNotEqualTo(financeWorkflowsJar.hash)
assertThat(unsignedJar.hash).isNotEqualTo(currentFinanceWorkflowsJar.hash)
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)
fun `test fixup rule that adds attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar")
@ -220,8 +231,10 @@ class CordappProviderImplTests {
return this
}
private fun newCordappProvider(cordappJars: Set<Path>, cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl {
val loader = JarScanningCordappLoader(cordappJars)
private fun newCordappProvider(cordappJars: Set<Path>,
legacyContractJars: Set<Path> = emptySet(),
cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl {
val loader = JarScanningCordappLoader(cordappJars, legacyContractJars)
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.getJarSigners
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.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
@ -69,8 +70,9 @@ class DummyRPCFlow : FlowLogic<Unit>() {
class JarScanningCordappLoaderTest {
private companion object {
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.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 {
LogHelper.setLevel(JarScanningCordappLoaderTest::class)
@ -90,20 +92,20 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000)
fun `constructed CordappImpls contains the right classes`() {
val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar))
val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar, currentFinanceWorkflowsJar))
val (contractsCordapp, workflowsCordapp) = loader.cordapps
assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name)
assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1)
assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java)
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.services).contains(ConfigHolder::class.java)
assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java)
assertThat(workflowsCordapp.contractClassNames).isEmpty()
assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar)
assertThat(workflowsCordapp.jarFile).isEqualTo(currentFinanceWorkflowsJar)
for (actualCordapp in loader.cordapps) {
assertThat(actualCordapp.cordappClasses)
@ -196,22 +198,32 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000)
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)
}
@Test(timeout = 300_000)
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 {
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)
fun `does not load duplicate CorDapps`() {
val duplicateJar = financeWorkflowsJar.duplicate()
val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar))
val duplicateJar = currentFinanceWorkflowsJar.duplicate()
val loader = JarScanningCordappLoader(setOf(currentFinanceWorkflowsJar, duplicateJar))
assertFailsWith<DuplicateCordappsInstalledException> {
loader.cordapps
}
@ -235,7 +247,7 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000)
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().signJar(absolutePathString(), "testAlias", "testPassword")
}
@ -244,6 +256,38 @@ class JarScanningCordappLoaderTest {
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 {
val copy = tempFolder.newFile(name).toPath()
copyTo(copy, overwrite = true)
@ -252,7 +296,7 @@ class JarScanningCordappLoaderTest {
}
private fun minAndTargetCordapp(minVersion: Int?, targetVersion: Int?): Path {
return financeWorkflowsJar.duplicate {
return currentFinanceWorkflowsJar.duplicate {
modifyJarManifest { manifest ->
manifest.setOrDeleteAttribute("Min-Platform-Version", minVersion?.toString())
manifest.setOrDeleteAttribute("Target-Platform-Version", targetVersion?.toString())

View File

@ -18,6 +18,7 @@ class NodeParams @JvmOverloads constructor(
val rpcAdminPort: Int,
val users: List<User>,
val cordappJars: List<Path> = emptyList(),
val legacyContractJars: List<Path> = emptyList(),
val jarDirs: List<Path> = emptyList(),
val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
val devMode: Boolean = true,

View File

@ -138,6 +138,10 @@ class NodeProcess(
log.info("Node directory: {}", nodeDir)
val cordappsDir = (nodeDir / CORDAPPS_DIR_NAME).createDirectory()
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))
networkParametersCopier.install(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.TransactionState
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.crypto.DigestService
import net.corda.core.crypto.SecureHash
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.TestIdentity
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.net.ServerSocket
import java.nio.file.Path
import java.security.KeyPair
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.util.*
import java.util.Properties
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import java.util.zip.ZipEntry
@ -111,7 +108,7 @@ fun createDevIntermediateCaCertPath(
*/
fun createDevNodeCaCertPath(
legalName: CordaX500Name,
nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
rootCaName: X500Principal = defaultRootCaName,
intermediateCaName: X500Principal = defaultIntermediateCaName
): 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")
fun createWireTransaction(inputs: List<StateRef>,
attachments: List<SecureHash>,
@ -164,9 +160,10 @@ fun createWireTransaction(inputs: List<StateRef>,
commands: List<Command<*>>,
notary: Party?,
timeWindow: TimeWindow?,
legacyAttachments: List<SecureHash> = emptyList(),
privacySalt: PrivacySalt = PrivacySalt(),
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)
}
@ -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
val IS_S390X = System.getProperty("os.arch") == "s390x"

View File

@ -16,7 +16,7 @@ class ExternalVerificationContext(
private val externalVerifier: ExternalVerifier,
private val transactionInputsAndReferences: Map<StateRef, SerializedTransactionState>
) : VerificationSupport {
override val isResolutionLazy: Boolean get() = false
override val isInProcess: Boolean get() = false
override fun getParties(keys: Collection<PublicKey>): List<Party?> = externalVerifier.getParties(keys)

View File

@ -132,6 +132,7 @@ class ExternalVerifier(
return URLClassLoader(cordappJarUrls, javaClass.classLoader)
}
@Suppress("INVISIBLE_MEMBER")
private fun verifyTransaction(request: VerificationRequest) {
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
val result: Try<Unit> = try {