ENT-11255: Scan attachments to determine if they are Kotlin 1.2 or later

The node now sends a transaction to the verifier if any of its attachments were compiled with Kotlin 1.2 (the net.corda.node.verification.external system property has been removed). It uses kotlinx-metadata to read the Kotlin metadata in the attachment to determine this. For now this scanning is done each time the attachment is loaded from the database.

The existing external verification integration tests were converted into smoke tests so that 4.11 nodes could be involved. This required various improvements to NodeProcess.Factory. A new JAVA_8_HOME environment variable, pointing to JDK 8, is required to run these tests.

There is still some follow-up work that needs to be done:

Sending transactions from a 4.11 node to a 4.12 node works, but not the other way round. A new WireTransaction component group needs to be introduced for storing 4.12 attachments so that they can be safely ignored by 4.11 nodes, and the 4.12 node needs to be able to load both 4.11 and 4.12 versions of the same contracts CorDapp so that they can be both attached to the transaction.
Even though attachments are cached when retrieved from the database, the Kotlin metadata version should be stored in the attachments db table, rather than being scanned each time.
Finally, VerificationService was refactored into NodeVerificationSupport and can be passed into SignedTransaction.verifyInternal, instead of needing the much heavier VerifyingServiceHub. This makes it easier for internal tools to verify transactions and spawn the verifier if necessary.
This commit is contained in:
Shams Asari 2024-01-22 11:31:51 +00:00
parent 1ff853b421
commit f30ba33929
37 changed files with 828 additions and 695 deletions

View File

@ -46,6 +46,7 @@ pipeline {
CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}"
CORDA_USE_CACHE = "corda-remotes"
JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto"
JAVA_8_HOME = "/usr/lib/jvm/java-1.8.0-amazon-corretto"
}
stages {

View File

@ -71,6 +71,7 @@ pipeline {
SNYK_TOKEN = credentials('c4-os-snyk-api-token-secret') //Jenkins credential type: Secret text
C4_OS_SNYK_ORG_ID = credentials('corda4-os-snyk-org-id')
JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto"
JAVA_8_HOME = "/usr/lib/jvm/java-1.8.0-amazon-corretto"
}
stages {

1
Jenkinsfile vendored
View File

@ -54,6 +54,7 @@ pipeline {
CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key')
CORDA_USE_CACHE = "corda-remotes"
JAVA_HOME="/usr/lib/jvm/java-17-amazon-corretto"
JAVA_8_HOME = "/usr/lib/jvm/java-1.8.0-amazon-corretto"
}
stages {

View File

@ -1,6 +1,5 @@
package net.corda.java.rpc;
import net.corda.client.rpc.CordaRPCConnection;
import net.corda.core.contracts.Amount;
import net.corda.core.identity.CordaX500Name;
import net.corda.core.identity.Party;
@ -10,88 +9,48 @@ import net.corda.core.utilities.OpaqueBytes;
import net.corda.finance.flows.AbstractCashFlow;
import net.corda.finance.flows.CashIssueFlow;
import net.corda.nodeapi.internal.config.User;
import net.corda.smoketesting.NodeConfig;
import net.corda.smoketesting.NodeParams;
import net.corda.smoketesting.NodeProcess;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Currency;
import java.util.HashSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static java.util.Collections.singletonList;
import static kotlin.test.AssertionsKt.assertEquals;
import static kotlin.test.AssertionsKt.fail;
import static net.corda.finance.workflows.GetBalances.getCashBalance;
import static net.corda.kotlin.rpc.StandaloneCordaRPClientTest.gatherCordapps;
public class StandaloneCordaRPCJavaClientTest {
private final User superUser = new User("superUser", "test", new HashSet<>(singletonList("ALL")));
public static void copyCordapps(NodeProcess.Factory factory, NodeConfig notaryConfig) {
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
try {
Files.createDirectories(cordappsDir);
} catch (IOException ex) {
fail("Failed to create directories");
}
try (Stream<Path> paths = Files.walk(Paths.get("build", "resources", "smokeTest"))) {
paths.filter(path -> path.toFile().getName().startsWith("cordapp")).forEach(file -> {
try {
Files.copy(file, cordappsDir.resolve(file.getFileName()));
} catch (IOException ex) {
fail("Failed to copy cordapp jar");
}
});
} catch (IOException e) {
fail("Failed to walk files");
}
}
private final AtomicInteger port = new AtomicInteger(15000);
private final NodeProcess.Factory factory = new NodeProcess.Factory();
private List<String> perms = singletonList("ALL");
private Set<String> permSet = new HashSet<>(perms);
private User superUser = new User("superUser", "test", permSet);
private AtomicInteger port = new AtomicInteger(15000);
private NodeProcess notary;
private CordaRPCOps rpcProxy;
private CordaRPCConnection connection;
private Party notaryNodeIdentity;
private NodeConfig notaryConfig = new NodeConfig(
new CordaX500Name("Notary Service", "Zurich", "CH"),
port.getAndIncrement(),
port.getAndIncrement(),
port.getAndIncrement(),
true,
singletonList(superUser),
true
);
@Before
public void setUp() {
NodeProcess.Factory factory = new NodeProcess.Factory();
copyCordapps(factory, notaryConfig);
notary = factory.create(notaryConfig);
connection = notary.connect(superUser);
rpcProxy = connection.getProxy();
NodeProcess notary = factory.createNotaries(new NodeParams(
new CordaX500Name("Notary Service", "Zurich", "CH"),
port.getAndIncrement(),
port.getAndIncrement(),
port.getAndIncrement(),
singletonList(superUser),
gatherCordapps()
)).get(0);
rpcProxy = notary.connect(superUser).getProxy();
notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0);
}
@After
public void done() {
try {
connection.close();
} finally {
if (notary != null) {
notary.close();
}
}
factory.close();
}
@Test

View File

@ -27,6 +27,7 @@ import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.finance.DOLLARS
import net.corda.finance.GBP
import net.corda.finance.POUNDS
import net.corda.finance.SWISS_FRANCS
import net.corda.finance.USD
@ -35,14 +36,15 @@ import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.workflows.getCashBalance
import net.corda.finance.workflows.getCashBalances
import net.corda.java.rpc.StandaloneCordaRPCJavaClientTest
import net.corda.nodeapi.internal.config.User
import net.corda.sleeping.SleepingFlow
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeParams
import net.corda.smoketesting.NodeProcess
import org.hamcrest.text.MatchesPattern
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@ -50,17 +52,19 @@ import org.junit.rules.ExpectedException
import java.io.FilterInputStream
import java.io.InputStream
import java.io.OutputStream.nullOutputStream
import java.util.Currency
import java.nio.file.Path
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
import java.util.regex.Pattern
import kotlin.io.path.Path
import kotlin.io.path.listDirectoryEntries
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
class StandaloneCordaRPClientTest {
private companion object {
companion object {
private val log = contextLogger()
val superUser = User("superUser", "test", permissions = setOf("ALL"))
val nonUser = User("nonUser", "test", permissions = emptySet())
@ -69,46 +73,57 @@ class StandaloneCordaRPClientTest {
val port = AtomicInteger(15200)
const val ATTACHMENT_SIZE = 2116
val timeout = 60.seconds
private val factory = NodeProcess.Factory()
private lateinit var notary: NodeProcess
private val notaryConfig = NodeParams(
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
users = listOf(superUser, nonUser, rpcUser, flowUser),
cordappJars = gatherCordapps()
)
@BeforeClass
@JvmStatic
fun startNotary() {
notary = factory.createNotaries(notaryConfig)[0]
}
@AfterClass
@JvmStatic
fun close() {
factory.close()
}
@JvmStatic
fun gatherCordapps(): List<Path> {
return Path("build", "resources", "smokeTest").listDirectoryEntries("cordapp*.jar")
}
}
private lateinit var factory: NodeProcess.Factory
private lateinit var notary: NodeProcess
private lateinit var rpcProxy: CordaRPCOps
private lateinit var connection: CordaRPCConnection
private lateinit var notaryNode: NodeInfo
private lateinit var rpcProxy: CordaRPCOps
private lateinit var notaryNodeIdentity: Party
private val notaryConfig = NodeConfig(
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
isNotary = true,
users = listOf(superUser, nonUser, rpcUser, flowUser)
)
@get:Rule
val exception: ExpectedException = ExpectedException.none()
@Before
fun setUp() {
factory = NodeProcess.Factory()
StandaloneCordaRPCJavaClientTest.copyCordapps(factory, notaryConfig)
notary = factory.create(notaryConfig)
connection = notary.connect(superUser)
rpcProxy = connection.proxy
notaryNode = fetchNotaryIdentity()
notaryNodeIdentity = rpcProxy.nodeInfo().legalIdentitiesAndCerts.first().party
}
@After
fun done() {
connection.use {
notary.close()
}
fun closeConnection() {
connection.close()
}
@Test(timeout=300_000)
fun `test attachments`() {
val attachment = InputStreamAndHash.createInMemoryTestZip(ATTACHMENT_SIZE, 1)
@ -168,8 +183,7 @@ class StandaloneCordaRPClientTest {
@Test(timeout=300_000)
fun `test state machines`() {
val (stateMachines, updates) = rpcProxy.stateMachinesFeed()
assertEquals(0, stateMachines.size)
val (_, updates) = rpcProxy.stateMachinesFeed()
val updateLatch = CountDownLatch(1)
val updateCount = AtomicInteger(0)
@ -190,8 +204,9 @@ class StandaloneCordaRPClientTest {
@Test(timeout=300_000)
fun `test vault track by`() {
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
assertEquals(0, vault.totalStatesAvailable)
val initialGbpBalance = rpcProxy.getCashBalance(GBP)
val (_, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
val updateLatch = CountDownLatch(1)
vaultUpdates.subscribe { update ->
@ -207,34 +222,35 @@ class StandaloneCordaRPClientTest {
// Check that this cash exists in the vault
val cashBalance = rpcProxy.getCashBalances()
log.info("Cash Balances: $cashBalance")
assertEquals(1, cashBalance.size)
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
assertEquals(629.POUNDS, cashBalance[GBP]!! - initialGbpBalance)
}
@Test(timeout=300_000)
fun `test vault query by`() {
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity)
.returnValue.getOrThrow(timeout)
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
val paging = PageSpecification(DEFAULT_PAGE_NUM, 10)
val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC)))
val initialStateCount = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting).totalStatesAvailable
val initialGbpBalance = rpcProxy.getCashBalance(GBP)
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity)
.returnValue.getOrThrow(timeout)
val queryResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
assertEquals(1, queryResults.totalStatesAvailable)
assertEquals(1, queryResults.totalStatesAvailable - initialStateCount)
assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity)
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNodeIdentity, true, notaryNodeIdentity).returnValue.getOrThrow()
val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
assertEquals(3, moreResults.totalStatesAvailable - initialStateCount) // 629 - 100 + 100
// Check that this cash exists in the vault
val cashBalances = rpcProxy.getCashBalances()
log.info("Cash Balances: $cashBalances")
assertEquals(1, cashBalances.size)
assertEquals(629.POUNDS, cashBalances[Currency.getInstance("GBP")])
assertEquals(629.POUNDS, cashBalances[GBP]!! - initialGbpBalance)
}
@Test(timeout=300_000)

View File

@ -9,11 +9,13 @@ configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
smokeTestCompile.extendsFrom compile
smokeTestImplementation.extendsFrom implementation
smokeTestRuntimeOnly.extendsFrom runtimeOnly
}
evaluationDependsOn(':node:capsule')
testArtifacts.extendsFrom testRuntimeOnlyClasspath
corda4_11
}
sourceSets {
integrationTest {
@ -42,9 +44,17 @@ sourceSets {
processSmokeTestResources {
// Bring in the fully built corda.jar for use by NodeFactory in the smoke tests
from(project(":node:capsule").tasks['buildCordaJAR']) {
from(tasks.getByPath(":node:capsule:buildCordaJAR")) {
rename 'corda-(.*)', 'corda.jar'
}
from(tasks.getByPath(":finance:workflows:jar")) {
rename 'corda-finance-workflows-.*.jar', 'corda-finance-workflows.jar'
}
from(tasks.getByPath(":finance:contracts:jar")) {
rename 'corda-finance-contracts-.*.jar', 'corda-finance-contracts.jar'
}
from(tasks.getByPath(":testing:cordapps:4.11-workflows:jar"))
from(configurations.corda4_11)
}
dependencies {
@ -69,7 +79,6 @@ dependencies {
testImplementation project(":test-utils")
testImplementation project(path: ':core', configuration: 'testArtifacts')
// Guava: Google test library (collections test suite)
testImplementation "com.google.guava:guava-testlib:$guava_version"
testImplementation "com.google.jimfs:jimfs:1.1"
@ -98,7 +107,12 @@ dependencies {
smokeTestImplementation project(":core")
smokeTestImplementation project(":node-api")
smokeTestImplementation project(":client:rpc")
smokeTestImplementation project(':smoke-test-utils')
smokeTestImplementation project(':core-test-utils')
smokeTestImplementation project(":finance:contracts")
smokeTestImplementation project(":finance:workflows")
smokeTestImplementation project(":testing:cordapps:4.11-workflows")
smokeTestImplementation "org.assertj:assertj-core:${assertj_version}"
smokeTestImplementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
smokeTestImplementation "co.paralleluniverse:quasar-core:$quasar_version"
smokeTestImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
@ -109,15 +123,12 @@ dependencies {
smokeTestRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_version"
smokeTestCompile project(':smoke-test-utils')
smokeTestCompile "org.assertj:assertj-core:${assertj_version}"
// used by FinalityFlowTests
testImplementation project(':testing:cordapps:cashobservers')
}
configurations {
testArtifacts.extendsFrom testRuntimeOnlyClasspath
corda4_11 "net.corda:corda-finance-contracts:4.11"
corda4_11 "net.corda:corda-finance-workflows:4.11"
corda4_11 "net.corda:corda:4.11"
}
tasks.withType(Test).configureEach {
@ -125,22 +136,24 @@ tasks.withType(Test).configureEach {
forkEvery = 10
}
task testJar(type: Jar) {
tasks.register('testJar', Jar) {
classifier "tests"
from sourceSets.test.output
}
task integrationTest(type: Test) {
tasks.register('integrationTest', Test) {
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}
task smokeTestJar(type: Jar) {
tasks.register('smokeTestJar', Jar) {
classifier 'smokeTests'
from sourceSets.smokeTest.output
from(sourceSets.smokeTest.output) {
exclude("*.jar")
}
}
task smokeTest(type: Test) {
tasks.register('smokeTest', Test) {
dependsOn smokeTestJar
testClassesDirs = sourceSets.smokeTest.output.classesDirs
classpath = sourceSets.smokeTest.runtimeClasspath

View File

@ -5,11 +5,10 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.copyToDirectory
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.nodeapi.internal.config.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeParams
import net.corda.smoketesting.NodeProcess
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
@ -18,8 +17,6 @@ import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
import java.util.jar.JarFile
import kotlin.io.path.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.listDirectoryEntries
class NodeVersioningTest {
@ -30,56 +27,39 @@ class NodeVersioningTest {
private val factory = NodeProcess.Factory()
private val notaryConfig = NodeConfig(
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
isNotary = true,
users = listOf(superUser)
)
private val aliceConfig = NodeConfig(
legalName = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
isNotary = false,
users = listOf(superUser)
)
private var notary: NodeProcess? = null
private lateinit var notary: NodeProcess
@Before
fun setUp() {
notary = factory.create(notaryConfig)
fun startNotary() {
notary = factory.createNotaries(NodeParams(
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
users = listOf(superUser),
// Find the jar file for the smoke tests of this module
cordappJars = Path("build", "libs").listDirectoryEntries("*-smokeTests*")
))[0]
}
@After
fun done() {
notary?.close()
factory.close()
}
@Test(timeout=300_000)
fun `platform version in manifest file`() {
val manifest = JarFile(factory.cordaJar.toFile()).manifest
val manifest = JarFile(NodeProcess.Factory.getCordaJar().toFile()).manifest
assertThat(manifest.mainAttributes.getValue("Corda-Platform-Version").toInt()).isEqualTo(PLATFORM_VERSION)
}
@Test(timeout=300_000)
fun `platform version from RPC`() {
val cordappsDir = (factory.baseDirectory(aliceConfig) / NodeProcess.CORDAPPS_DIR_NAME).createDirectories()
// Find the jar file for the smoke tests of this module
val selfCordapp = Path("build", "libs").listDirectoryEntries("*-smokeTests*").single()
selfCordapp.copyToDirectory(cordappsDir)
factory.create(aliceConfig).use { alice ->
alice.connect(superUser).use {
val rpc = it.proxy
assertThat(rpc.protocolVersion).isEqualTo(PLATFORM_VERSION)
assertThat(rpc.nodeInfo().platformVersion).isEqualTo(PLATFORM_VERSION)
assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(PLATFORM_VERSION)
}
notary.connect(superUser).use {
val rpc = it.proxy
assertThat(rpc.protocolVersion).isEqualTo(PLATFORM_VERSION)
assertThat(rpc.nodeInfo().platformVersion).isEqualTo(PLATFORM_VERSION)
assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(PLATFORM_VERSION)
}
}

View File

@ -12,7 +12,6 @@ import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.copyToDirectory
import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
@ -26,9 +25,8 @@ import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeParams
import net.corda.smoketesting.NodeProcess
import net.corda.smoketesting.NodeProcess.Companion.CORDAPPS_DIR_NAME
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
@ -41,8 +39,8 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name
import kotlin.io.path.useDirectoryEntries
import kotlin.io.path.writeBytes
class CordappSmokeTest {
@ -53,43 +51,35 @@ class CordappSmokeTest {
private val factory = NodeProcess.Factory()
private val notaryConfig = NodeConfig(
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
isNotary = true,
users = listOf(superUser)
)
private val aliceConfig = NodeConfig(
private val aliceConfig = NodeParams(
legalName = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
isNotary = false,
users = listOf(superUser)
users = listOf(superUser),
// Find the jar file for the smoke tests of this module
cordappJars = Path("build", "libs").listDirectoryEntries("*-smokeTests*")
)
private lateinit var notary: NodeProcess
@Before
fun setUp() {
notary = factory.create(notaryConfig)
fun startNotary() {
factory.createNotaries(NodeParams(
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
rpcAdminPort = port.andIncrement,
users = listOf(superUser)
))
}
@After
fun done() {
notary.close()
factory.close()
}
@Test(timeout=300_000)
fun `FlowContent appName returns the filename of the CorDapp jar`() {
val baseDir = factory.baseDirectory(aliceConfig)
val cordappsDir = (baseDir / CORDAPPS_DIR_NAME).createDirectories()
// Find the jar file for the smoke tests of this module
val selfCordapp = Path("build", "libs").useDirectoryEntries { it.single { "-smokeTests" in it.toString() } }
selfCordapp.copyToDirectory(cordappsDir)
// The `nodeReadyFuture` in the persistent network map cache will not complete unless there is at least one other
// node in the network. We work around this limitation by putting another node info file in the additional-node-info
@ -98,24 +88,17 @@ class CordappSmokeTest {
val additionalNodeInfoDir = (baseDir / "additional-node-infos").createDirectories()
createDummyNodeInfo(additionalNodeInfoDir)
factory.create(aliceConfig).use { alice ->
alice.connect(superUser).use { connectionToAlice ->
val aliceIdentity = connectionToAlice.proxy.nodeInfo().legalIdentitiesAndCerts.first().party
val future = connectionToAlice.proxy.startFlow(CordappSmokeTest::GatherContextsFlow, aliceIdentity).returnValue
val (sessionInitContext, sessionConfirmContext) = future.getOrThrow()
val selfCordappName = selfCordapp.name.removeSuffix(".jar")
assertThat(sessionInitContext.appName).isEqualTo(selfCordappName)
assertThat(sessionConfirmContext.appName).isEqualTo(selfCordappName)
}
val alice = factory.createNode(aliceConfig)
alice.connect(superUser).use { connectionToAlice ->
val aliceIdentity = connectionToAlice.proxy.nodeInfo().legalIdentitiesAndCerts.first().party
val future = connectionToAlice.proxy.startFlow(CordappSmokeTest::GatherContextsFlow, aliceIdentity).returnValue
val (sessionInitContext, sessionConfirmContext) = future.getOrThrow()
val selfCordappName = aliceConfig.cordappJars[0].name.removeSuffix(".jar")
assertThat(sessionInitContext.appName).isEqualTo(selfCordappName)
assertThat(sessionConfirmContext.appName).isEqualTo(selfCordappName)
}
}
@Test(timeout=300_000)
fun `empty cordapps directory`() {
(factory.baseDirectory(aliceConfig) / CORDAPPS_DIR_NAME).createDirectories()
factory.create(aliceConfig).close()
}
@InitiatingFlow
@StartableByRPC
class GatherContextsFlow(private val otherParty: Party) : FlowLogic<Pair<FlowInfo, FlowInfo>>() {

View File

@ -0,0 +1,247 @@
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
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.UnexpectedFlowEndException
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS
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.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.flows.AbstractCashFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.nodeapi.internal.config.User
import net.corda.smoketesting.NodeParams
import net.corda.smoketesting.NodeProcess
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.cordapps.workflows411.IssueAndChangeNotaryFlow
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import java.net.InetAddress
import java.nio.file.Path
import java.util.Currency
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 {
private companion object {
private val factory = NodeProcess.Factory(testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS))
private lateinit var notaries: List<NodeProcess>
private lateinit var oldNode: NodeProcess
private lateinit var newNode: NodeProcess
@BeforeClass
@JvmStatic
fun startNodes() {
// The 4.11 finance CorDapp jars
val oldCordapps = 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") }
notaries = factory.createNotaries(
nodeParams(DUMMY_NOTARY_NAME, oldCordapps),
nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), newCordapps)
)
oldNode = factory.createNode(nodeParams(
CordaX500Name("Old", "Delhi", "IN"),
oldCordapps + listOf(smokeTestResource("4.11-workflows-cordapp.jar")),
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
version = "4.11"
))
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
}
@AfterClass
@JvmStatic
fun close() {
factory.close()
}
}
@Test(timeout=300_000)
fun `transaction containing 4_11 contract sent to new node`() {
assertCashIssuanceAndPayment(issuer = oldNode, recipient = newNode)
}
@Test(timeout=300_000)
fun `notary change transaction`() {
val oldRpc = oldNode.connect(superUser).proxy
val oldNodeInfo = oldRpc.nodeInfo()
val notaryIdentities = oldRpc.notaryIdentities()
for (notary in notaries) {
notary.connect(superUser).use { it.proxy.waitForVisibility(oldNodeInfo) }
}
oldRpc.startFlow(::IssueAndChangeNotaryFlow, notaryIdentities[0], notaryIdentities[1]).returnValue.getOrThrow()
}
private fun assertCashIssuanceAndPayment(issuer: NodeProcess, recipient: NodeProcess) {
val issuerRpc = issuer.connect(superUser).proxy
val recipientRpc = recipient.connect(superUser).proxy
val recipientNodeInfo = recipientRpc.nodeInfo()
val notaryIdentity = issuerRpc.notaryIdentities()[0]
val (issuanceTx) = issuerRpc.startFlow(
::CashIssueFlow,
10.DOLLARS,
OpaqueBytes.of(0x01),
notaryIdentity
).returnValue.getOrThrow()
issuerRpc.waitForVisibility(recipientNodeInfo)
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
val (paymentTx) = issuerRpc.startFlow(
::CashPaymentFlow,
10.DOLLARS,
recipientNodeInfo.legalIdentities[0],
false,
).returnValue.getOrThrow()
notaries[0].assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
recipient.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
}
}
class ExternalVerificationUnsignedCordappsTest {
private companion object {
private val factory = NodeProcess.Factory(testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS))
private lateinit var notary: NodeProcess
private lateinit var oldNode: NodeProcess
private lateinit var newNode: NodeProcess
@BeforeClass
@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"))
// The current version finance CorDapp jars
val newCordapps = listOf(unsignedResourceJar("corda-finance-contracts.jar"), smokeTestResource("corda-finance-workflows.jar"))
notary = factory.createNotaries(nodeParams(DUMMY_NOTARY_NAME, oldCordapps))[0]
oldNode = factory.createNode(nodeParams(
CordaX500Name("Old", "Delhi", "IN"),
oldCordapps,
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
version = "4.11"
))
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
}
@AfterClass
@JvmStatic
fun close() {
factory.close()
}
private fun unsignedResourceJar(name: String): Path {
val signedJar = smokeTestResource(name)
val copy = signedJar.copyTo(Path("${signedJar.toString().substringBeforeLast(".")}-UNSIGNED.jar"), overwrite = true)
copy.unsignJar()
return copy
}
}
@Test(timeout = 300_000)
fun `transactions can fail verification in external verifier`() {
val issuerRpc = oldNode.connect(superUser).proxy
val recipientRpc = newNode.connect(superUser).proxy
val recipientNodeInfo = recipientRpc.nodeInfo()
val notaryIdentity = issuerRpc.notaryIdentities()[0]
val (issuanceTx) = issuerRpc.startFlow(
::CashIssueFlow,
10.DOLLARS,
OpaqueBytes.of(0x01),
notaryIdentity
).returnValue.getOrThrow()
issuerRpc.waitForVisibility(recipientNodeInfo)
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
issuerRpc.startFlow<AbstractCashFlow.Result, Amount<Currency>, Party, Boolean, CashPaymentFlow>(
::CashPaymentFlow,
10.DOLLARS,
recipientNodeInfo.legalIdentities[0],
false,
).returnValue.getOrThrow()
}
assertThat(newNode.externalVerifierLogs()).contains("$issuanceTx failed to verify")
}
}
private val superUser = User("superUser", "test", permissions = setOf("ALL"))
private val portCounter = AtomicInteger(15100)
private fun smokeTestResource(name: String): Path = ExternalVerificationSignedCordappsTest::class.java.getResource("/$name")!!.toPath()
private fun nodeParams(
legalName: CordaX500Name,
cordappJars: List<Path> = emptyList(),
clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
version: String? = null
): NodeParams {
return NodeParams(
legalName = legalName,
p2pPort = portCounter.andIncrement,
rpcPort = portCounter.andIncrement,
rpcAdminPort = portCounter.andIncrement,
users = listOf(superUser),
cordappJars = cordappJars,
clientRpcConfig = clientRpcConfig,
version = version
)
}
private fun CordaRPCOps.waitForVisibility(other: NodeInfo) {
val (snapshot, updates) = networkMapFeed()
if (other in snapshot) {
updates.notUsed()
} else {
val found = CountDownLatch(1)
val subscription = updates.subscribe {
if (it.node == other) {
found.countDown()
}
}
found.await()
subscription.unsubscribe()
}
}
private fun NodeProcess.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) {
val verifierLogContent = externalVerifierLogs()
for (txId in txIds) {
assertThat(verifierLogContent).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()
}

View File

@ -11,6 +11,7 @@ import net.corda.core.internal.utilities.Internable
import net.corda.core.internal.utilities.PrivateInterner
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import java.lang.annotation.Inherited
import java.security.PublicKey
@ -70,7 +71,7 @@ object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
return if (attachment is AttachmentWithContext) {
val whitelist = attachment.whitelistedContractImplementations
log.debug("Checking ${attachment.contract} is in CZ whitelist $whitelist")
log.debug { "Checking ${attachment.contract} is in CZ whitelist $whitelist" }
attachment.id in (whitelist[attachment.contract] ?: emptyList())
} else {
log.warn("CZ whitelisted constraint check failed: ${attachment.id} not in CZ whitelist")
@ -111,8 +112,8 @@ object AutomaticPlaceholderConstraint : AttachmentConstraint {
*/
data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
log.debug("Checking signature constraints: verifying $key in contract attachment signer keys: ${attachment.signerKeys}")
return if (!key.isFulfilledBy(attachment.signerKeys.map { it })) {
log.debug { "Checking signature constraints: verifying $key in contract attachment signer keys: ${attachment.signerKeys}" }
return if (!key.isFulfilledBy(attachment.signerKeys)) {
log.warn("Untrusted signing key: expected $key. but contract attachment contains ${attachment.signerKeys}")
false
} else true

View File

@ -0,0 +1,28 @@
package net.corda.core.internal
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
interface InternalAttachment : Attachment {
/**
* The version of the Kotlin metadata, if this attachment has one. See `kotlinx.metadata.jvm.JvmMetadataVersion` for more information on
* how this maps to the Kotlin language version.
*/
val kotlinMetadataVersion: String?
}
/**
* Because [ContractAttachment] is public API, we can't make it implement [InternalAttachment] without also leaking it out.
*
* @see InternalAttachment.kotlinMetadataVersion
*/
val Attachment.kotlinMetadataVersion: String? get() {
var attachment = this
while (true) {
when (attachment) {
is InternalAttachment -> return attachment.kotlinMetadataVersion
is ContractAttachment -> attachment = attachment.attachment
else -> return null
}
}
}

View File

@ -16,6 +16,7 @@ import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.seconds
import org.slf4j.Logger
import org.slf4j.event.Level
import rx.Observable
import rx.Observer
import rx.observers.Subscribers
@ -619,5 +620,15 @@ fun Logger.warnOnce(warning: String) {
}
}
val Logger.level: Level
get() = when {
isTraceEnabled -> Level.TRACE
isDebugEnabled -> Level.DEBUG
isInfoEnabled -> Level.INFO
isWarnEnabled -> Level.WARN
isErrorEnabled -> Level.ERROR
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

View File

@ -268,5 +268,9 @@ internal fun checkNotaryWhitelisted(ftx: FullTransaction) {
}
}
val CoreTransaction.attachmentIds: List<SecureHash>
get() = when (this) {
is WireTransaction -> attachments
is ContractUpgradeWireTransaction -> listOf(legacyContractAttachmentId, upgradedContractAttachmentId)
else -> emptyList()
}

View File

@ -0,0 +1,7 @@
package net.corda.core.internal.verification
import net.corda.core.transactions.SignedTransaction
interface ExternalVerifierHandle : AutoCloseable {
fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean)
}

View File

@ -9,6 +9,7 @@ import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentTrustCalculator
import net.corda.core.internal.SerializedTransactionState
import net.corda.core.internal.TRUSTED_UPLOADERS
import net.corda.core.internal.cordapp.CordappProviderInternal
import net.corda.core.internal.entries
import net.corda.core.internal.getRequiredTransaction
import net.corda.core.node.NetworkParameters
@ -36,26 +37,33 @@ import java.security.PublicKey
/**
* Implements [VerificationSupport] in terms of node-based services.
*/
interface VerificationService : VerificationSupport {
val transactionStorage: TransactionStorage
interface NodeVerificationSupport : VerificationSupport {
val networkParameters: NetworkParameters
val validatedTransactions: TransactionStorage
val identityService: IdentityService
val attachmentStorage: AttachmentStorage
val attachments: AttachmentStorage
val networkParametersService: NetworkParametersService
val cordappProvider: CordappProviderInternal
val attachmentTrustCalculator: AttachmentTrustCalculator
val attachmentFixups: AttachmentFixups
val externalVerifierHandle: ExternalVerifierHandle
override val appClassLoader: ClassLoader
get() = cordappProvider.appClassLoader
// TODO Bulk party lookup?
override fun getParties(keys: Collection<PublicKey>): List<Party?> = keys.map(identityService::partyFromKey)
override fun getAttachment(id: SecureHash): Attachment? = attachmentStorage.openAttachment(id)
override fun getAttachment(id: SecureHash): Attachment? = attachments.openAttachment(id)
override fun getNetworkParameters(id: SecureHash?): NetworkParameters? {
return networkParametersService.lookup(id ?: networkParametersService.defaultHash)
return if (id != null) networkParametersService.lookup(id) else networkParameters
}
/**
@ -65,7 +73,7 @@ interface VerificationService : VerificationSupport {
* correct classloader independent of the node's classpath.
*/
override fun getSerializedState(stateRef: StateRef): SerializedTransactionState {
val coreTransaction = transactionStorage.getRequiredTransaction(stateRef.txhash).coreTransaction
val coreTransaction = validatedTransactions.getRequiredTransaction(stateRef.txhash).coreTransaction
return when (coreTransaction) {
is WireTransaction -> getRegularOutput(coreTransaction, stateRef.index)
is ContractUpgradeWireTransaction -> getContractUpdateOutput(coreTransaction, stateRef.index)
@ -127,14 +135,14 @@ interface VerificationService : VerificationSupport {
*/
// TODO Should throw when the class is found in multiple contract attachments (not different versions).
override fun getTrustedClassAttachment(className: String): Attachment? {
val allTrusted = attachmentStorage.queryAttachments(
val allTrusted = attachments.queryAttachments(
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
)
// TODO - add caching if performance is affected.
for (attId in allTrusted) {
val attch = attachmentStorage.openAttachment(attId)!!
val attch = attachments.openAttachment(attId)!!
if (attch.hasFile("$className.class")) return attch
}
return null
@ -145,6 +153,6 @@ interface VerificationService : VerificationSupport {
override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachmentTrustCalculator.calculate(attachment)
override fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>): Set<SecureHash> {
return attachmentFixups.fixupAttachmentIds(attachmentIds)
return cordappProvider.attachmentFixups.fixupAttachmentIds(attachmentIds)
}
}

View File

@ -8,30 +8,16 @@ import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.cordapp.CordappProviderInternal
import net.corda.core.internal.getRequiredTransaction
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.TransactionStorage
import net.corda.core.serialization.deserialize
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
@Suppress("TooManyFunctions", "ThrowsCount")
interface VerifyingServiceHub : ServiceHub, VerificationService {
override val cordappProvider: CordappProviderInternal
override val transactionStorage: TransactionStorage get() = validatedTransactions
override val attachmentStorage: AttachmentStorage get() = attachments
override val appClassLoader: ClassLoader get() = cordappProvider.appClassLoader
override val attachmentFixups: AttachmentFixups get() = cordappProvider.attachmentFixups
interface VerifyingServiceHub : ServiceHub, NodeVerificationSupport {
override fun loadContractAttachment(stateRef: StateRef): Attachment {
// We may need to recursively chase transactions if there are notary changes.
return loadContractAttachment(stateRef, null)
@ -72,18 +58,6 @@ interface VerifyingServiceHub : ServiceHub, VerificationService {
fun <T : ContractState, C : MutableCollection<StateAndRef<T>>> loadStatesInternal(input: Iterable<StateRef>, output: C): C {
return input.mapTo(output, ::toStateAndRef)
}
/**
* Try to verify the given transaction on the external verifier, assuming it is available. It is not required to verify externally even
* if the verifier is available.
*
* The default implementation is to only do internal verification.
*
* @return true if the transaction should (also) be verified internally, regardless of whether it was verified externally.
*/
fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean {
return true
}
}
fun ServicesForResolution.toVerifyingServiceHub(): VerifyingServiceHub {

View File

@ -18,8 +18,11 @@ 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
import net.corda.core.node.ServiceHub
@ -31,6 +34,7 @@ import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import java.io.NotSerializableException
import java.security.KeyPair
import java.security.PublicKey
@ -155,9 +159,10 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@JvmOverloads
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction {
val verifyingServiceHub = services.toVerifyingServiceHub()
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
resolveAndCheckNetworkParameters(services)
return toLedgerTransactionInternal(services.toVerifyingServiceHub(), checkSufficientSignatures)
resolveAndCheckNetworkParameters(verifyingServiceHub)
return toLedgerTransactionInternal(verifyingServiceHub, checkSufficientSignatures)
}
@JvmSynthetic
@ -191,16 +196,58 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@JvmOverloads
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
resolveAndCheckNetworkParameters(services)
val verifyingServiceHub = services.toVerifyingServiceHub()
if (verifyingServiceHub.tryExternalVerification(this, checkSufficientSignatures)) {
verifyInternal(verifyingServiceHub, checkSufficientSignatures)
verifyInternal(services.toVerifyingServiceHub(), checkSufficientSignatures)
}
/**
* Internal version of the public [verify] which takes in a [NodeVerificationSupport] instead of the heavier [ServiceHub].
*
* 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.
*/
@CordaInternal
@JvmSynthetic
fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true) {
resolveAndCheckNetworkParameters(verificationSupport)
val verificationType = determineVerificationType(verificationSupport)
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)
}
}
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
}
}
return when {
old && new -> VerificationType.BOTH
old -> VerificationType.EXTERNAL
else -> VerificationType.IN_PROCESS
}
}
private enum class VerificationType {
IN_PROCESS, EXTERNAL, BOTH
}
/**
* Verifies this transaction in-process. This assumes the current process has the correct classpath for all the contracts.
*/
@CordaInternal
@JvmSynthetic
fun verifyInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
when (coreTransaction) {
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures)
@ -209,7 +256,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
}
@Suppress("ThrowsCount")
private fun resolveAndCheckNetworkParameters(services: ServiceHub) {
private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) {
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault)
?: throw TransactionResolutionException(id)

View File