mirror of
https://github.com/corda/corda.git
synced 2025-02-15 15:12:46 +00:00
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:
parent
1ff853b421
commit
f30ba33929
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
@ -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 {
|
||||
|
1
.ci/dev/regression/Jenkinsfile
vendored
1
.ci/dev/regression/Jenkinsfile
vendored
@ -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
1
Jenkinsfile
vendored
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>>() {
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|