mirror of
https://github.com/corda/corda.git
synced 2024-12-18 12:46:29 +00:00
ENT-11355: Backwards compatibility with older nodes via new attachments component group
This commit is contained in:
parent
c2742ba6a5
commit
200333b198
@ -7924,11 +7924,6 @@ public final class net.corda.core.transactions.WireTransaction extends net.corda
|
||||
public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution)
|
||||
@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
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,166 @@
|
||||
package net.corda.coretests.transactions
|
||||
|
||||
import net.corda.core.contracts.SignatureAttachmentConstraint
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS
|
||||
import net.corda.core.internal.RPC_UPLOADER
|
||||
import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.coretesting.internal.useZipFile
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.DummyCommandData
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.MockNodeArgs
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.junit.After
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER")
|
||||
class TransactionBuilderMockNetworkTest {
|
||||
companion object {
|
||||
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private val mockNetwork = InternalMockNetwork(
|
||||
cordappsForAllNodes = setOf(
|
||||
FINANCE_CONTRACTS_CORDAPP,
|
||||
cordappWithPackages("net.corda.testing.contracts").signed()
|
||||
),
|
||||
initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS)
|
||||
)
|
||||
|
||||
@After
|
||||
fun close() {
|
||||
mockNetwork.close()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `automatic signature constraint`() {
|
||||
val services = mockNetwork.notaryNodes[0].services
|
||||
|
||||
val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0])
|
||||
val attachmentSigner = attachment!!.signerKeys.single()
|
||||
|
||||
val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner)
|
||||
assertThat(expectedConstraint.isSatisfiedBy(attachment)).isTrue()
|
||||
|
||||
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = mockNetwork.defaultNotaryIdentity)
|
||||
val builder = TransactionBuilder()
|
||||
.addOutputState(outputState)
|
||||
.addCommand(DummyCommandData, mockNetwork.defaultNotaryIdentity.owningKey)
|
||||
val wtx = builder.toWireTransaction(services)
|
||||
|
||||
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `contract overlap in explicit attachments`() {
|
||||
val duplicateJar = tempFolder.newFile("duplicate.jar").toPath()
|
||||
FINANCE_CONTRACTS_CORDAPP.jarFile.copyTo(duplicateJar, overwrite = true)
|
||||
duplicateJar.unsignJar() // Change its hash
|
||||
|
||||
val node = mockNetwork.createNode()
|
||||
val duplicateId = duplicateJar.inputStream().use {
|
||||
node.services.attachments.privilegedImportAttachment(it, RPC_UPLOADER, null)
|
||||
}
|
||||
assertThat(FINANCE_CONTRACTS_CORDAPP.jarFile.hash).isNotEqualTo(duplicateId)
|
||||
|
||||
val builder = TransactionBuilder()
|
||||
builder.addAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||
builder.addAttachment(duplicateId)
|
||||
val identity = node.info.singleIdentity()
|
||||
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy { builder.toWireTransaction(node.services) }
|
||||
.withMessageContaining("Multiple attachments specified for the same contract")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `populates legacy attachment group if legacy contract CorDapp is present`() {
|
||||
val node = mockNetwork.createNode {
|
||||
it.copyToLegacyContracts(legacyFinanceContractsJar)
|
||||
InternalMockNetwork.MockNode(it)
|
||||
}
|
||||
val builder = TransactionBuilder()
|
||||
val identity = node.info.singleIdentity()
|
||||
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||
val stx = node.services.signInitialTransaction(builder)
|
||||
assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||
assertThat(stx.tx.legacyAttachments).contains(legacyFinanceContractsJar.hash)
|
||||
stx.verify(node.services)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Ignore // https://r3-cev.atlassian.net/browse/ENT-11445
|
||||
fun `adds legacy CorDapp dependencies`() {
|
||||
val cordapp1 = tempFolder.newFile("cordapp1.jar").toPath()
|
||||
val cordapp2 = tempFolder.newFile("cordapp2.jar").toPath()
|
||||
// Split the contracts CorDapp into two
|
||||
legacyFinanceContractsJar.copyTo(cordapp1, overwrite = true)
|
||||
cordapp1.useZipFile { zipFs1 ->
|
||||
cordapp2.useZipFile { zipFs2 ->
|
||||
val destinationDir = zipFs2.getPath("net/corda/finance/contracts/asset").createDirectories()
|
||||
zipFs1.getPath("net/corda/finance/contracts/asset")
|
||||
.listDirectoryEntries("OnLedgerAsset*")
|
||||
.forEach {
|
||||
it.copyToDirectory(destinationDir)
|
||||
it.deleteExisting()
|
||||
}
|
||||
}
|
||||
}
|
||||
reSignJar(cordapp1)
|
||||
|
||||
val node = mockNetwork.createNode {
|
||||
it.copyToLegacyContracts(cordapp1, cordapp2)
|
||||
InternalMockNetwork.MockNode(it)
|
||||
}
|
||||
val builder = TransactionBuilder()
|
||||
val identity = node.info.singleIdentity()
|
||||
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||
val stx = node.services.signInitialTransaction(builder)
|
||||
assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||
assertThat(stx.tx.legacyAttachments).contains(cordapp1.hash, cordapp2.hash)
|
||||
stx.verify(node.services)
|
||||
}
|
||||
|
||||
private fun reSignJar(jar: Path) {
|
||||
jar.unsignJar()
|
||||
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
||||
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
|
||||
}
|
||||
|
||||
private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) {
|
||||
val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories()
|
||||
jars.forEach { it.copyToDirectory(legacyContractsDir) }
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package net.corda.coretests.transactions
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.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)
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
|
||||
interface InternalAttachment : Attachment {
|
||||
/**
|
||||
* The version of the Kotlin metadata, if this attachment has one. See `kotlinx.metadata.jvm.JvmMetadataVersion` for more information on
|
||||
* how this maps to the Kotlin language version.
|
||||
*/
|
||||
val kotlinMetadataVersion: String?
|
||||
}
|
||||
|
||||
/**
|
||||
* Because [ContractAttachment] is public API, we can't make it implement [InternalAttachment] without also leaking it out.
|
||||
*
|
||||
* @see InternalAttachment.kotlinMetadataVersion
|
||||
*/
|
||||
val Attachment.kotlinMetadataVersion: String? get() {
|
||||
var attachment = this
|
||||
while (true) {
|
||||
when (attachment) {
|
||||
is InternalAttachment -> return attachment.kotlinMetadataVersion
|
||||
is ContractAttachment -> attachment = attachment.attachment
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
}
|
@ -79,7 +79,14 @@ import kotlin.math.roundToLong
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.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()) }
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,32 @@
|
||||
package net.corda.core.internal.cordapp
|
||||
|
||||
data class KotlinMetadataVersion(val major: Int, val minor: Int, val patch: Int = 0) : Comparable<KotlinMetadataVersion> {
|
||||
companion object {
|
||||
fun from(versionArray: IntArray): KotlinMetadataVersion {
|
||||
val (major, minor, patch) = versionArray
|
||||
return KotlinMetadataVersion(major, minor, patch)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
require(major >= 0) { "Major version should be not less than 0" }
|
||||
require(minor >= 0) { "Minor version should be not less than 0" }
|
||||
require(patch >= 0) { "Patch version should be not less than 0" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the equivalent [KotlinVersion] without the patch.
|
||||
*/
|
||||
val languageMinorVersion: KotlinVersion
|
||||
// See `kotlinx.metadata.jvm.JvmMetadataVersion`
|
||||
get() = if (major == 1 && minor == 1) KotlinVersion(1, 2) else KotlinVersion(major, minor)
|
||||
|
||||
override fun compareTo(other: KotlinMetadataVersion): Int {
|
||||
val majors = this.major.compareTo(other.major)
|
||||
if (majors != 0) return majors
|
||||
val minors = this.minor.compareTo(other.minor)
|
||||
return if (minors != 0) minors else this.patch.compareTo(other.patch)
|
||||
}
|
||||
|
||||
override fun toString(): String = "$major.$minor.$patch"
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.corda.core.internal.cordapp
|
||||
|
||||
import net.corda.core.internal.JAVA_17_CLASS_FILE_MAJOR_VERSION
|
||||
import net.corda.core.internal.JAVA_1_2_CLASS_FILE_MAJOR_VERSION
|
||||
import net.corda.core.internal.JAVA_8_CLASS_FILE_MAJOR_VERSION
|
||||
|
||||
sealed class LanguageVersion {
|
||||
/**
|
||||
* Returns true if this version is compatible with Corda 4.11 or earlier.
|
||||
*/
|
||||
abstract val isLegacyCompatible: Boolean
|
||||
|
||||
/**
|
||||
* Returns true if this version is compatible with Corda 4.12 or later.
|
||||
*/
|
||||
abstract val isNonLegacyCompatible: Boolean
|
||||
|
||||
@Suppress("ConvertObjectToDataObject") // External verifier uses Kotlin 1.2
|
||||
object Data : LanguageVersion() {
|
||||
override val isLegacyCompatible: Boolean
|
||||
get() = true
|
||||
|
||||
override val isNonLegacyCompatible: Boolean
|
||||
get() = true
|
||||
|
||||
override fun toString(): String = "Data"
|
||||
}
|
||||
|
||||
data class Bytecode(val classFileMajorVersion: Int, val kotlinMetadataVersion: KotlinMetadataVersion?): LanguageVersion() {
|
||||
companion object {
|
||||
private val KOTLIN_1_2_VERSION = KotlinVersion(1, 2)
|
||||
private val KOTLIN_1_9_VERSION = KotlinVersion(1, 9)
|
||||
}
|
||||
|
||||
init {
|
||||
require(classFileMajorVersion in JAVA_1_2_CLASS_FILE_MAJOR_VERSION..JAVA_17_CLASS_FILE_MAJOR_VERSION) {
|
||||
"Unsupported class file major version $classFileMajorVersion"
|
||||
}
|
||||
val kotlinVersion = kotlinMetadataVersion?.languageMinorVersion
|
||||
require(kotlinVersion == null || kotlinVersion == KOTLIN_1_2_VERSION || kotlinVersion == KOTLIN_1_9_VERSION) {
|
||||
"Unsupported Kotlin metadata version $kotlinMetadataVersion"
|
||||
}
|
||||
}
|
||||
|
||||
override val isLegacyCompatible: Boolean
|
||||
get() = when {
|
||||
classFileMajorVersion > JAVA_8_CLASS_FILE_MAJOR_VERSION -> false
|
||||
kotlinMetadataVersion == null -> true // Java 8 CorDapp is fine
|
||||
else -> kotlinMetadataVersion.languageMinorVersion == KOTLIN_1_2_VERSION
|
||||
}
|
||||
|
||||
override val isNonLegacyCompatible: Boolean
|
||||
// Java-only CorDapp will always be compatible on 4.12
|
||||
get() = if (kotlinMetadataVersion == null) true else kotlinMetadataVersion.languageMinorVersion == KOTLIN_1_9_VERSION
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package net.corda.core.internal.verification
|
||||
|
||||
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)))
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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:")
|
||||
|
@ -51,7 +51,7 @@ cordapp {
|
||||
minimumPlatformVersion 1
|
||||
contract {
|
||||
name "Corda Finance Demo"
|
||||
versionId 1
|
||||
versionId 2
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
@ -79,6 +79,7 @@ import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.internal.cordapp.CordappConfigFileProvider
|
||||
import net.corda.node.internal.cordapp.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
|
||||
|
@ -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>) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()))
|
||||
|
@ -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 {
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user