mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge pull request #7654 from corda/shams-external-verifier-analyse-txs
ENT-11255: Scan attachments to determine if they are Kotlin 1.2 or later
This commit is contained in:
commit
3abb218bca
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(
|
||||
@Before
|
||||
public void setUp() {
|
||||
NodeProcess notary = factory.createNotaries(new NodeParams(
|
||||
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();
|
||||
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,45 +73,56 @@ class StandaloneCordaRPClientTest {
|
||||
val port = AtomicInteger(15200)
|
||||
const val ATTACHMENT_SIZE = 2116
|
||||
val timeout = 60.seconds
|
||||
}
|
||||
|
||||
private lateinit var factory: NodeProcess.Factory
|
||||
private val 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 notaryNodeIdentity: Party
|
||||
|
||||
private val notaryConfig = NodeConfig(
|
||||
private val notaryConfig = NodeParams(
|
||||
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)
|
||||
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 connection: CordaRPCConnection
|
||||
private lateinit var rpcProxy: CordaRPCOps
|
||||
private lateinit var notaryNodeIdentity: Party
|
||||
|
||||
@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`() {
|
||||
@ -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,58 +27,41 @@ class NodeVersioningTest {
|
||||
|
||||
private val factory = NodeProcess.Factory()
|
||||
|
||||
private val notaryConfig = NodeConfig(
|
||||
private lateinit var notary: NodeProcess
|
||||
|
||||
@Before
|
||||
fun startNotary() {
|
||||
notary = factory.createNotaries(NodeParams(
|
||||
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
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
notary = factory.create(notaryConfig)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class GetPlatformVersionFlow : FlowLogic<Int>() {
|
||||
|
@ -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,23 +88,16 @@ class CordappSmokeTest {
|
||||
val additionalNodeInfoDir = (baseDir / "additional-node-infos").createDirectories()
|
||||
createDummyNodeInfo(additionalNodeInfoDir)
|
||||
|
||||
factory.create(aliceConfig).use { alice ->
|
||||
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 = selfCordapp.name.removeSuffix(".jar")
|
||||
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
|
||||
|
@ -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)
|
||||
|
@ -242,6 +242,7 @@ dependencies {
|
||||
|
||||
// Adding native SSL library to allow using native SSL with Artemis and AMQP
|
||||
implementation "io.netty:netty-tcnative-boringssl-static:$tcnative_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.8.0'
|
||||
|
||||
// Byteman for runtime (termination) rules injection on the running node
|
||||
// Submission tool allowing to install rules on running nodes
|
||||
|
@ -1,286 +0,0 @@
|
||||
package net.corda.node.verification
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.NotaryChangeFlow
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.UnexpectedFlowEndException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.BOC_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
import net.corda.testing.node.internal.enclosedCordapp
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class ExternalVerificationTest {
|
||||
@Test(timeout=300_000)
|
||||
fun `regular transactions are verified in external verifier`() {
|
||||
internalDriver(
|
||||
systemProperties = mapOf("net.corda.node.verification.external" to "true"),
|
||||
cordappsForAllNodes = FINANCE_CORDAPPS,
|
||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true, startInProcess = false))
|
||||
) {
|
||||
val (notary, alice, bob) = listOf(
|
||||
defaultNotaryNode,
|
||||
startNode(NodeParameters(providedName = ALICE_NAME)),
|
||||
startNode(NodeParameters(providedName = BOB_NAME))
|
||||
).transpose().getOrThrow()
|
||||
|
||||
val (issuanceTx) = alice.rpc.startFlow(
|
||||
::CashIssueFlow,
|
||||
10.DOLLARS,
|
||||
OpaqueBytes.of(0x01),
|
||||
defaultNotaryIdentity
|
||||
).returnValue.getOrThrow()
|
||||
|
||||
val (paymentTx) = alice.rpc.startFlow(
|
||||
::CashPaymentFlow,
|
||||
10.DOLLARS,
|
||||
bob.nodeInfo.singleIdentity(),
|
||||
false,
|
||||
).returnValue.getOrThrow()
|
||||
|
||||
notary.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
|
||||
bob.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `external verifier is unable to verify contracts which use new Kotlin APIs`() {
|
||||
check(!IntArray::maxOrNull.isInline)
|
||||
|
||||
internalDriver(
|
||||
systemProperties = mapOf("net.corda.node.verification.external" to "true"),
|
||||
cordappsForAllNodes = listOf(cordappWithPackages("net.corda.node.verification"))
|
||||
) {
|
||||
val (alice, bob) = listOf(
|
||||
startNode(NodeParameters(providedName = ALICE_NAME)),
|
||||
startNode(NodeParameters(providedName = BOB_NAME)),
|
||||
).transpose().getOrThrow()
|
||||
|
||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||
alice.rpc.startFlow(::NewKotlinApiFlow, bob.nodeInfo).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
assertThat(bob.externalVerifierLogs()).contains("""
|
||||
java.lang.NoSuchMethodError: 'java.lang.Integer kotlin.collections.ArraysKt.maxOrNull(int[])'
|
||||
at net.corda.node.verification.ExternalVerificationTest${'$'}NewKotlinApiContract.verify(ExternalVerificationTest.kt:
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `regular transactions can fail verification in external verifier`() {
|
||||
internalDriver(
|
||||
systemProperties = mapOf("net.corda.node.verification.external" to "true"),
|
||||
cordappsForAllNodes = listOf(cordappWithPackages("net.corda.node.verification", "com.typesafe.config"))
|
||||
) {
|
||||
val (alice, bob, charlie) = listOf(
|
||||
startNode(NodeParameters(providedName = ALICE_NAME)),
|
||||
startNode(NodeParameters(providedName = BOB_NAME)),
|
||||
startNode(NodeParameters(providedName = CHARLIE_NAME))
|
||||
).transpose().getOrThrow()
|
||||
|
||||
// Create a transaction from Alice to Bob, where Charlie is specified as the contract verification trigger
|
||||
val firstState = alice.rpc.startFlow(::FailExternallyFlow, null, charlie.nodeInfo, bob.nodeInfo).returnValue.getOrThrow()
|
||||
// When the transaction chain tries to moves onto Charlie, it will trigger the failure
|
||||
assertThatExceptionOfType(ContractRejection::class.java)
|
||||
.isThrownBy { bob.rpc.startFlow(::FailExternallyFlow, firstState, charlie.nodeInfo, charlie.nodeInfo).returnValue.getOrThrow() }
|
||||
.withMessageContaining("Fail in external verifier: ${firstState.ref.txhash}")
|
||||
|
||||
// Make sure Charlie tried to verify the first transaction externally
|
||||
assertThat(charlie.externalVerifierLogs()).contains("Fail in external verifier: ${firstState.ref.txhash}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `notary change transactions are verified in external verifier`() {
|
||||
internalDriver(
|
||||
systemProperties = mapOf("net.corda.node.verification.external" to "true"),
|
||||
cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(),
|
||||
notarySpecs = listOf(DUMMY_NOTARY_NAME, BOC_NAME).map { NotarySpec(it, validating = true, startInProcess = false) }
|
||||
) {
|
||||
val (notary1, notary2) = notaryHandles.map { handle -> handle.nodeHandles.map { it[0] } }.transpose().getOrThrow()
|
||||
val alice = startNode(NodeParameters(providedName = ALICE_NAME)).getOrThrow()
|
||||
|
||||
val txId = alice.rpc.startFlow(
|
||||
::IssueAndChangeNotaryFlow,
|
||||
notary1.nodeInfo.singleIdentity(),
|
||||
notary2.nodeInfo.singleIdentity()
|
||||
).returnValue.getOrThrow()
|
||||
|
||||
notary1.assertTransactionsWereVerifiedExternally(txId)
|
||||
alice.assertTransactionsWereVerifiedExternally(txId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeHandle.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) {
|
||||
val verifierLogContent = externalVerifierLogs()
|
||||
for (txId in txIds) {
|
||||
assertThat(verifierLogContent).contains("SignedTransaction(id=$txId) verified")
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeHandle.externalVerifierLogs(): String {
|
||||
val verifierLogs = (baseDirectory / "logs")
|
||||
.listDirectoryEntries()
|
||||
.filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" }
|
||||
assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1)
|
||||
return verifierLogs[0].readText()
|
||||
}
|
||||
|
||||
|
||||
class FailExternallyContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commandsOfType<Command>().single()
|
||||
if (insideExternalVerifier()) {
|
||||
// The current directory for the external verifier is the node's base directory
|
||||
val localName = CordaX500Name.parse(ConfigFactory.parseFile(File("node.conf")).getString("myLegalName"))
|
||||
check(localName != command.value.failForParty.name) { "Fail in external verifier: ${tx.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun insideExternalVerifier(): Boolean {
|
||||
return StackWalker.getInstance().walk { frames ->
|
||||
frames.anyMatch { it.className.startsWith("net.corda.verifier.") }
|
||||
}
|
||||
}
|
||||
|
||||
data class State(override val party: Party) : TestState
|
||||
data class Command(val failForParty: Party) : CommandData
|
||||
}
|
||||
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FailExternallyFlow(inputState: StateAndRef<FailExternallyContract.State>?,
|
||||
private val failForParty: NodeInfo,
|
||||
recipient: NodeInfo) : TestFlow<FailExternallyContract.State>(inputState, recipient) {
|
||||
override fun newOutput() = FailExternallyContract.State(serviceHub.myInfo.legalIdentities[0])
|
||||
override fun newCommand() = FailExternallyContract.Command(failForParty.legalIdentities[0])
|
||||
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(FailExternallyFlow::class)
|
||||
class ReceiverFlow(otherSide: FlowSession) : TestReceiverFlow(otherSide)
|
||||
}
|
||||
|
||||
|
||||
class NewKotlinApiContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
check(tx.commandsOfType<Command>().isNotEmpty())
|
||||
// New post-1.2 API which is non-inlined
|
||||
intArrayOf().maxOrNull()
|
||||
}
|
||||
|
||||
data class State(override val party: Party) : TestState
|
||||
object Command : TypeOnlyCommandData()
|
||||
}
|
||||
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class NewKotlinApiFlow(recipient: NodeInfo) : TestFlow<NewKotlinApiContract.State>(null, recipient) {
|
||||
override fun newOutput() = NewKotlinApiContract.State(serviceHub.myInfo.legalIdentities[0])
|
||||
override fun newCommand() = NewKotlinApiContract.Command
|
||||
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(NewKotlinApiFlow::class)
|
||||
class ReceiverFlow(otherSide: FlowSession) : TestReceiverFlow(otherSide)
|
||||
}
|
||||
|
||||
|
||||
@StartableByRPC
|
||||
class IssueAndChangeNotaryFlow(private val oldNotary: Party, private val newNotary: Party) : FlowLogic<SecureHash>() {
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
subFlow(CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x01), oldNotary))
|
||||
val oldState = serviceHub.vaultService.queryBy(Cash.State::class.java).states.single()
|
||||
assertThat(oldState.state.notary).isEqualTo(oldNotary)
|
||||
val newState = subFlow(NotaryChangeFlow(oldState, newNotary))
|
||||
assertThat(newState.state.notary).isEqualTo(newNotary)
|
||||
val notaryChangeTx = serviceHub.validatedTransactions.getTransaction(newState.ref.txhash)
|
||||
assertThat(notaryChangeTx?.coreTransaction).isInstanceOf(NotaryChangeWireTransaction::class.java)
|
||||
return notaryChangeTx!!.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class TestFlow<T : TestState>(
|
||||
private val inputState: StateAndRef<T>?,
|
||||
private val recipient: NodeInfo
|
||||
) : FlowLogic<StateAndRef<T>>() {
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<T> {
|
||||
val myParty = serviceHub.myInfo.legalIdentities[0]
|
||||
val txBuilder = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities[0])
|
||||
inputState?.let(txBuilder::addInputState)
|
||||
txBuilder.addOutputState(newOutput())
|
||||
txBuilder.addCommand(newCommand(), myParty.owningKey)
|
||||
val initialTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
val sessions = arrayListOf(initiateFlow(recipient.legalIdentities[0]))
|
||||
inputState?.let { sessions += initiateFlow(it.state.data.party) }
|
||||
val notarisedTx = subFlow(FinalityFlow(initialTx, sessions))
|
||||
return notarisedTx.toLedgerTransaction(serviceHub).outRef(0)
|
||||
}
|
||||
|
||||
protected abstract fun newOutput(): T
|
||||
protected abstract fun newCommand(): CommandData
|
||||
}
|
||||
|
||||
abstract class TestReceiverFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
interface TestState : ContractState {
|
||||
val party: Party
|
||||
override val participants: List<AbstractParty> get() = listOf(party)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.corda.node.verification
|
||||
|
||||
import io.github.classgraph.ClassGraph
|
||||
import net.corda.core.internal.pooledScan
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class ExternalVerifierTest {
|
||||
@Test(timeout=300_000)
|
||||
fun `external verifier does not have newer Kotlin`() {
|
||||
val kotlinClasses = ClassGraph()
|
||||
.overrideClasspath(javaClass.getResource("external-verifier.jar")!!)
|
||||
.enableAnnotationInfo()
|
||||
.pooledScan()
|
||||
.use { result ->
|
||||
result.getClassesWithAnnotation(Metadata::class.java).associateBy({ it.name }, {
|
||||
val annotationInfo = it.getAnnotationInfo(Metadata::class.java)
|
||||
val metadataVersion = annotationInfo.parameterValues.get("mv").value as IntArray
|
||||
"${metadataVersion[0]}.${metadataVersion[1]}"
|
||||
})
|
||||
}
|
||||
|
||||
// First make sure we're capturing the right data
|
||||
assertThat(kotlinClasses).containsKeys("net.corda.verifier.ExternalVerifier")
|
||||
// Kotlin metadata version 1.1 maps to language versions 1.0 to 1.3
|
||||
val newerKotlinClasses = kotlinClasses.filterValues { metadataVersion -> metadataVersion != "1.1" }
|
||||
assertThat(newerKotlinClasses).isEmpty()
|
||||
}
|
||||
}
|
@ -46,7 +46,6 @@ import net.corda.core.internal.telemetry.SimpleLogTelemetryComponent
|
||||
import net.corda.core.internal.telemetry.TelemetryComponent
|
||||
import net.corda.core.internal.telemetry.TelemetryServiceImpl
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.verification.VerifyingServiceHub
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.RPCOps
|
||||
@ -68,7 +67,6 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
@ -144,6 +142,7 @@ import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.BindableNamedCacheFactory
|
||||
import net.corda.node.utilities.NamedThreadFactory
|
||||
import net.corda.node.utilities.NotaryLoader
|
||||
import net.corda.node.verification.ExternalVerifierHandleImpl
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.NodeStatus
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
@ -1152,14 +1151,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
return NodeVaultService(platformClock, keyManagementService, services, database, schemaService, cordappLoader.appClassLoader)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dy default only internal verification is done.
|
||||
* @see VerifyingServiceHub.tryExternalVerification
|
||||
*/
|
||||
protected open fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
// JDK 11: switch to directly instantiating jolokia server (rather than indirectly via dynamically self attaching Java Agents,
|
||||
// which is no longer supported from JDK 9 onwards (https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8180425).
|
||||
// No longer need to use https://github.com/electronicarts/ea-agent-loader either (which is also deprecated)
|
||||
@ -1175,6 +1166,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
inner class ServiceHubImpl : SingletonSerializeAsToken(), ServiceHubInternal, NetworkParameterUpdateListener {
|
||||
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
||||
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
|
||||
override val externalVerifierHandle = ExternalVerifierHandleImpl(this, configuration.baseDirectory).also { runOnStop += it::close }
|
||||
override val identityService: IdentityService get() = this@AbstractNode.identityService
|
||||
override val keyManagementService: KeyManagementService get() = this@AbstractNode.keyManagementService
|
||||
override val schemaService: SchemaService get() = this@AbstractNode.schemaService
|
||||
@ -1298,10 +1290,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
|
||||
this.networkParameters = networkParameters
|
||||
}
|
||||
|
||||
override fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean {
|
||||
return this@AbstractNode.tryExternalVerification(stx, checkSufficientSignatures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,6 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.CordaClock
|
||||
@ -61,7 +60,6 @@ import net.corda.node.utilities.BindableNamedCacheFactory
|
||||
import net.corda.node.utilities.DefaultNamedCacheFactory
|
||||
import net.corda.node.utilities.DemoClock
|
||||
import net.corda.node.utilities.errorAndTerminate
|
||||
import net.corda.node.verification.ExternalVerifierHandle
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
import net.corda.nodeapi.internal.ShutdownHook
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
@ -201,8 +199,6 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
protected open val journalBufferTimeout : Int? = null
|
||||
|
||||
private val externalVerifierHandle = ExternalVerifierHandle(services).also { runOnStop += it::close }
|
||||
|
||||
private var shutdownHook: ShutdownHook? = null
|
||||
|
||||
// DISCUSSION
|
||||
@ -588,17 +584,6 @@ open class Node(configuration: NodeConfiguration,
|
||||
)
|
||||
}
|
||||
|
||||
override fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean {
|
||||
// TODO Determine from transaction whether it should be verified externally
|
||||
// TODO If both old and new attachments are present then return true so that internal verification is also done.
|
||||
return if (java.lang.Boolean.getBoolean("net.corda.node.verification.external")) {
|
||||
externalVerifierHandle.verifyTransaction(stx, checkSufficientSignatures)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts a blocking event loop for message dispatch. */
|
||||
fun run() {
|
||||
internalRpcMessagingClient?.start(rpcBroker!!.serverControl)
|
||||
|
@ -6,6 +6,8 @@ import com.google.common.hash.HashCode
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import com.google.common.io.CountingInputStream
|
||||
import kotlinx.metadata.jvm.KotlinModuleMetadata
|
||||
import kotlinx.metadata.jvm.UnstableMetadataApi
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
@ -16,6 +18,7 @@ import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.InternalAttachment
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.P2P_UPLOADER
|
||||
import net.corda.core.internal.RPC_UPLOADER
|
||||
@ -25,6 +28,7 @@ import net.corda.core.internal.Version
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||
import net.corda.core.internal.entries
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.internal.utilities.ZipBombDetector
|
||||
@ -53,6 +57,7 @@ import java.io.ByteArrayInputStream
|
||||
import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -109,7 +114,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
// Can be null for not-signed JARs.
|
||||
val allManifestEntries = jar.manifest?.entries?.keys?.toMutableList()
|
||||
val extraFilesNotFoundInEntries = mutableListOf<JarEntry>()
|
||||
val manifestHasEntries= allManifestEntries != null && allManifestEntries.isNotEmpty()
|
||||
val manifestHasEntries = !allManifestEntries.isNullOrEmpty()
|
||||
|
||||
while (true) {
|
||||
val cursor = jar.nextJarEntry ?: break
|
||||
@ -225,7 +230,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
|
||||
// This is invoked by [InputStreamSerializer], which does NOT close the stream afterwards.
|
||||
@Throws(IOException::class)
|
||||
override fun read(b: ByteArray?, off: Int, len: Int): Int {
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
return super.read(b, off, len).apply {
|
||||
if (this == -1) {
|
||||
validate()
|
||||
@ -260,8 +265,9 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
dataLoader: () -> ByteArray,
|
||||
private val checkOnLoad: Boolean,
|
||||
uploader: String?,
|
||||
override val signerKeys: List<PublicKey>
|
||||
) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
|
||||
override val signerKeys: List<PublicKey>,
|
||||
override val kotlinMetadataVersion: String?
|
||||
) : AbstractAttachment(dataLoader, uploader), InternalAttachment, SerializeAsToken {
|
||||
|
||||
override fun open(): InputStream {
|
||||
val stream = super.open()
|
||||
@ -273,19 +279,21 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
private val id: SecureHash,
|
||||
private val checkOnLoad: Boolean,
|
||||
private val uploader: String?,
|
||||
private val signerKeys: List<PublicKey>
|
||||
private val signerKeys: List<PublicKey>,
|
||||
private val kotlinMetadataVersion: String?
|
||||
) : SerializationToken {
|
||||
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
|
||||
id,
|
||||
context.attachmentDataLoader(id),
|
||||
checkOnLoad,
|
||||
uploader,
|
||||
signerKeys
|
||||
signerKeys,
|
||||
kotlinMetadataVersion
|
||||
)
|
||||
}
|
||||
|
||||
override fun toToken(context: SerializeAsTokenContext) =
|
||||
Token(id, checkOnLoad, uploader, signerKeys)
|
||||
Token(id, checkOnLoad, uploader, signerKeys, kotlinMetadataVersion)
|
||||
}
|
||||
|
||||
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
|
||||
@ -303,16 +311,27 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableMetadataApi::class)
|
||||
private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment {
|
||||
// TODO Cache this as a column in the database
|
||||
val jis = JarInputStream(attachment.content.inputStream())
|
||||
val kotlinMetadataVersions = jis.entries()
|
||||
.filter { it.name.endsWith(".kotlin_module") }
|
||||
.map { KotlinModuleMetadata.read(jis.readAllBytes()).version }
|
||||
.toSortedSet()
|
||||
if (kotlinMetadataVersions.size > 1) {
|
||||
log.warn("Attachment ${attachment.attId} seems to be comprised of multiple Kotlin versions: $kotlinMetadataVersions")
|
||||
}
|
||||
val attachmentImpl = AttachmentImpl(
|
||||
id = SecureHash.create(attachment.attId),
|
||||
dataLoader = { attachment.content },
|
||||
checkOnLoad = checkAttachmentsOnLoad,
|
||||
uploader = attachment.uploader,
|
||||
signerKeys = attachment.signers?.toList() ?: emptyList()
|
||||
signerKeys = attachment.signers?.toList() ?: emptyList(),
|
||||
kotlinMetadataVersion = kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last()?.toString()
|
||||
)
|
||||
val contracts = attachment.contractClassNames
|
||||
return if (contracts != null && contracts.isNotEmpty()) {
|
||||
return if (!contracts.isNullOrEmpty()) {
|
||||
ContractAttachment.create(
|
||||
attachment = attachmentImpl,
|
||||
contract = contracts.first(),
|
||||
@ -336,7 +355,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun importAttachment(jar: InputStream): AttachmentId {
|
||||
return import(jar, UNKNOWN_UPLOADER, null)
|
||||
}
|
||||
@ -360,7 +379,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
||||
return try {
|
||||
import(jar, uploader, filename)
|
||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
||||
} catch (faee: FileAlreadyExistsException) {
|
||||
AttachmentId.create(faee.message!!)
|
||||
}
|
||||
}
|
||||
@ -447,18 +466,14 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
|
||||
private fun getVersion(attachmentBytes: ByteArray) =
|
||||
JarInputStream(attachmentBytes.inputStream()).use {
|
||||
try {
|
||||
it.manifest?.mainAttributes?.getValue(CORDAPP_CONTRACT_VERSION)?.toInt() ?: DEFAULT_CORDAPP_VERSION
|
||||
} catch (e: NumberFormatException) {
|
||||
DEFAULT_CORDAPP_VERSION
|
||||
}
|
||||
it.manifest?.mainAttributes?.getValue(CORDAPP_CONTRACT_VERSION)?.toIntOrNull() ?: DEFAULT_CORDAPP_VERSION
|
||||
}
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||
return try {
|
||||
import(jar, UNKNOWN_UPLOADER, null)
|
||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
||||
} catch (faee: FileAlreadyExistsException) {
|
||||
AttachmentId.create(faee.message!!)
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,16 @@ package net.corda.node.verification
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.level
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.internal.verification.ExternalVerifierHandle
|
||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.serialization.internal.GeneratedAttachment
|
||||
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.customSerializers
|
||||
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.serializationWhitelists
|
||||
@ -49,7 +51,10 @@ import kotlin.io.path.div
|
||||
/**
|
||||
* Handle to the node's external verifier. The verifier process is started lazily on the first verification request.
|
||||
*/
|
||||
class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoCloseable {
|
||||
class ExternalVerifierHandleImpl(
|
||||
private val verificationSupport: NodeVerificationSupport,
|
||||
private val baseDirectory: Path
|
||||
) : ExternalVerifierHandle {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
@ -69,12 +74,12 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
||||
@Volatile
|
||||
private var connection: Connection? = null
|
||||
|
||||
fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) {
|
||||
override fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) {
|
||||
log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures")
|
||||
// By definition input states are unique, and so it makes sense to eagerly send them across with the transaction.
|
||||
// Reference states are not, but for now we'll send them anyway and assume they aren't used often. If this assumption is not
|
||||
// correct, and there's a benefit, then we can send them lazily.
|
||||
val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(serviceHub::getSerializedState)
|
||||
val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(verificationSupport::getSerializedState)
|
||||
val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures)
|
||||
|
||||
// To keep things simple the verifier only supports one verification request at a time.
|
||||
@ -140,11 +145,11 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
||||
|
||||
private fun processVerifierRequest(request: VerifierRequest, connection: Connection) {
|
||||
val result = when (request) {
|
||||
is GetParties -> PartiesResult(serviceHub.getParties(request.keys))
|
||||
is GetAttachment -> AttachmentResult(prepare(serviceHub.attachments.openAttachment(request.id)))
|
||||
is GetAttachments -> AttachmentsResult(serviceHub.getAttachments(request.ids).map(::prepare))
|
||||
is GetNetworkParameters -> NetworkParametersResult(serviceHub.getNetworkParameters(request.id))
|
||||
is GetTrustedClassAttachment -> TrustedClassAttachmentResult(serviceHub.getTrustedClassAttachment(request.className)?.id)
|
||||
is GetParties -> PartiesResult(verificationSupport.getParties(request.keys))
|
||||
is GetAttachment -> AttachmentResult(prepare(verificationSupport.getAttachment(request.id)))
|
||||
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map(::prepare))
|
||||
is GetNetworkParameters -> NetworkParametersResult(verificationSupport.getNetworkParameters(request.id))
|
||||
is GetTrustedClassAttachment -> TrustedClassAttachmentResult(verificationSupport.getTrustedClassAttachment(request.className)?.id)
|
||||
}
|
||||
log.debug { "Sending response to external verifier: $result" }
|
||||
connection.toVerifier.writeCordaSerializable(result)
|
||||
@ -152,7 +157,7 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
||||
|
||||
private fun prepare(attachment: Attachment?): AttachmentWithTrust? {
|
||||
if (attachment == null) return null
|
||||
val isTrusted = serviceHub.isAttachmentTrusted(attachment)
|
||||
val isTrusted = verificationSupport.isAttachmentTrusted(attachment)
|
||||
val attachmentForSer = when (attachment) {
|
||||
// The Attachment retrieved from the database is not serialisable, so we have to convert it into one
|
||||
is AbstractAttachment -> GeneratedAttachment(attachment.open().readFully(), attachment.uploader)
|
||||
@ -188,20 +193,20 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
||||
"-jar",
|
||||
"$verifierJar",
|
||||
"${server.localPort}",
|
||||
System.getProperty("log4j2.level")?.lowercase() ?: "info"
|
||||
log.level.name.lowercase()
|
||||
)
|
||||
log.debug { "Verifier command: $command" }
|
||||
val logsDirectory = (serviceHub.configuration.baseDirectory / "logs").createDirectories()
|
||||
val logsDirectory = (baseDirectory / "logs").createDirectories()
|
||||
verifierProcess = ProcessBuilder(command)
|
||||
.redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile()))
|
||||
.redirectError(Redirect.appendTo((logsDirectory / "verifier-stderr.log").toFile()))
|
||||
.directory(serviceHub.configuration.baseDirectory.toFile())
|
||||
.directory(baseDirectory.toFile())
|
||||
.start()
|
||||
log.info("External verifier process started; PID ${verifierProcess.pid()}")
|
||||
|
||||
verifierProcess.onExit().whenComplete { _, _ ->
|
||||
if (connection != null) {
|
||||
log.error("The external verifier has unexpectedly terminated with error code ${verifierProcess.exitValue()}. " +
|
||||
log.warn("The external verifier has unexpectedly terminated with error code ${verifierProcess.exitValue()}. " +
|
||||
"Please check verifier logs for more details.")
|
||||
}
|
||||
// Allow a new process to be started on the next verification request
|
||||
@ -212,12 +217,12 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
||||
toVerifier = DataOutputStream(socket.outputStream)
|
||||
fromVerifier = DataInputStream(socket.inputStream)
|
||||
|
||||
val cordapps = serviceHub.cordappProvider.cordapps
|
||||
val cordapps = verificationSupport.cordappProvider.cordapps
|
||||
val initialisation = Initialisation(
|
||||
customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name },
|
||||
serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name },
|
||||
System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization
|
||||
serializedCurrentNetworkParameters = serviceHub.networkParameters.serialize()
|
||||
serializedCurrentNetworkParameters = verificationSupport.networkParameters.serialize()
|
||||
)
|
||||
toVerifier.writeCordaSerializable(initialisation)
|
||||
}
|
@ -110,6 +110,7 @@ include 'testing:cordapps:dbfailure:dbfworkflows'
|
||||
include 'testing:cordapps:missingmigration'
|
||||
include 'testing:cordapps:sleeping'
|
||||
include 'testing:cordapps:cashobservers'
|
||||
include 'testing:cordapps:4.11-workflows'
|
||||
|
||||
// Common libraries - start
|
||||
include 'common-validation'
|
||||
|
17
testing/cordapps/4.11-workflows/build.gradle
Normal file
17
testing/cordapps/4.11-workflows/build.gradle
Normal file
@ -0,0 +1,17 @@
|
||||
apply plugin: 'corda.kotlin-1.2'
|
||||
|
||||
dependencies {
|
||||
compileOnly "net.corda:corda-core:4.11.+"
|
||||
compileOnly "net.corda:corda-finance-contracts:4.11.+"
|
||||
compileOnly "net.corda:corda-finance-workflows:4.11.+"
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveBaseName = "4.11-workflows-cordapp"
|
||||
archiveVersion = ""
|
||||
manifest {
|
||||
// This JAR is part of Corda's testing framework.
|
||||
// Driver will not include it as part of an out-of-process node.
|
||||
attributes('Corda-Testing': true)
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.corda.testing.cordapps.workflows411
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.NotaryChangeFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
|
||||
// We need a separate flow as NotaryChangeFlow is not StartableByRPC
|
||||
@StartableByRPC
|
||||
class IssueAndChangeNotaryFlow(private val oldNotary: Party, private val newNotary: Party) : FlowLogic<SecureHash>() {
|
||||
@Suppress("MagicNumber")
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
subFlow(CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x01), oldNotary))
|
||||
val oldState = serviceHub.vaultService.queryBy(Cash.State::class.java).states.single()
|
||||
check(oldState.state.notary == oldNotary) { oldState.state.notary }
|
||||
val newState = subFlow(NotaryChangeFlow(oldState, newNotary))
|
||||
check(newState.state.notary == newNotary) { newState.state.notary }
|
||||
val notaryChangeTx = checkNotNull(serviceHub.validatedTransactions.getTransaction(newState.ref.txhash))
|
||||
check(notaryChangeTx.coreTransaction is NotaryChangeWireTransaction) { notaryChangeTx.coreTransaction }
|
||||
return notaryChangeTx.id
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import java.io.Closeable
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
@ -17,7 +18,11 @@ import java.util.jar.Attributes
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Manifest
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.outputStream
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
@ -36,7 +41,6 @@ object JarSignatureTestUtils {
|
||||
private fun Path.executeProcess(vararg command: String) {
|
||||
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
|
||||
assertEquals(0, ProcessBuilder()
|
||||
.inheritIO()
|
||||
.redirectOutput(shredder)
|
||||
.redirectError(shredder)
|
||||
.directory(this.toFile())
|
||||
@ -69,6 +73,16 @@ object JarSignatureTestUtils {
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.unsignJar() {
|
||||
FileSystems.newFileSystem(this).use { zipFs ->
|
||||
zipFs.getPath("META-INF").listDirectoryEntries("*.{SF,DSA,RSA,EC}").forEach(Path::deleteExisting)
|
||||
val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF")
|
||||
val manifest = manifestFile.inputStream().use(::Manifest)
|
||||
manifest.entries.clear() // Remove all the hash information of the jar contents
|
||||
manifestFile.outputStream().use(manifest::write)
|
||||
}
|
||||
}
|
||||
|
||||
fun Path.getPublicKey(alias: String, storeName: String, storePassword: String) : PublicKey {
|
||||
val ks = loadKeyStore(this.resolve(storeName), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
|
@ -10,11 +10,12 @@ import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
||||
@ -23,6 +24,7 @@ import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.requireSupportedHashType
|
||||
import net.corda.core.internal.telemetry.TelemetryComponent
|
||||
import net.corda.core.internal.telemetry.TelemetryServiceImpl
|
||||
import net.corda.core.internal.verification.ExternalVerifierHandle
|
||||
import net.corda.core.internal.verification.VerifyingServiceHub
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.messaging.FlowHandle
|
||||
@ -302,22 +304,19 @@ open class MockServices private constructor(
|
||||
// Because Kotlin is dumb and makes not publicly visible objects public, thus changing the public API.
|
||||
private val mockStateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()
|
||||
|
||||
private val dummyAttachment by lazy {
|
||||
val inputStream = ByteArrayOutputStream().apply {
|
||||
ZipOutputStream(this).use {
|
||||
with(it) {
|
||||
putNextEntry(ZipEntry(JarFile.MANIFEST_NAME))
|
||||
private val dummyAttachment: Attachment by lazy {
|
||||
object : AbstractAttachment(
|
||||
{
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use { zip ->
|
||||
zip.putNextEntry(ZipEntry(JarFile.MANIFEST_NAME))
|
||||
}
|
||||
baos.toByteArray()
|
||||
},
|
||||
null
|
||||
) {
|
||||
override val id: SecureHash by lazy(attachmentData::sha256)
|
||||
}
|
||||
}.toByteArray().inputStream()
|
||||
val attachment = object : Attachment {
|
||||
override val id get() = throw UnsupportedOperationException()
|
||||
override fun open() = inputStream
|
||||
override val signerKeys get() = throw UnsupportedOperationException()
|
||||
override val signers: List<Party> get() = throw UnsupportedOperationException()
|
||||
override val size: Int = 512
|
||||
}
|
||||
attachment
|
||||
}
|
||||
}
|
||||
|
||||
@ -576,6 +575,9 @@ open class MockServices private constructor(
|
||||
override fun loadState(stateRef: StateRef): TransactionState<*> = mockServices.loadState(stateRef)
|
||||
|
||||
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = mockServices.loadStates(stateRefs)
|
||||
|
||||
override val externalVerifierHandle: ExternalVerifierHandle
|
||||
get() = throw UnsupportedOperationException("External verification is not supported by MockServices")
|
||||
}
|
||||
|
||||
|
||||
|
@ -9,6 +9,7 @@ dependencies {
|
||||
implementation project(':test-common')
|
||||
implementation project(':client:rpc')
|
||||
|
||||
implementation "com.google.guava:guava:$guava_version"
|
||||
implementation "com.typesafe:config:$typesafe_config_version"
|
||||
implementation "org.slf4j:slf4j-api:$slf4j_version"
|
||||
}
|
||||
|
@ -1,22 +1,25 @@
|
||||
package net.corda.smoketesting
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory.empty
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import com.typesafe.config.ConfigValue
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.config.toConfig
|
||||
import java.nio.file.Path
|
||||
|
||||
class NodeConfig(
|
||||
class NodeParams @JvmOverloads constructor(
|
||||
val legalName: CordaX500Name,
|
||||
val p2pPort: Int,
|
||||
val rpcPort: Int,
|
||||
val rpcAdminPort: Int,
|
||||
val isNotary: Boolean,
|
||||
val users: List<User>,
|
||||
val devMode: Boolean = true
|
||||
val cordappJars: List<Path> = emptyList(),
|
||||
val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||
val devMode: Boolean = true,
|
||||
val version: String? = null
|
||||
) {
|
||||
companion object {
|
||||
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
||||
@ -24,12 +27,7 @@ class NodeConfig(
|
||||
|
||||
val commonName: String get() = legalName.organisation
|
||||
|
||||
/*
|
||||
* The configuration object depends upon the networkMap,
|
||||
* which is mutable.
|
||||
*/
|
||||
//TODO Make use of Any.toConfig
|
||||
private fun toFileConfig(): Config {
|
||||
fun createNodeConfig(isNotary: Boolean): String {
|
||||
val config = empty()
|
||||
.withValue("myLegalName", valueFor(legalName.toString()))
|
||||
.withValue("p2pAddress", addressValueFor(p2pPort))
|
||||
@ -44,10 +42,8 @@ class NodeConfig(
|
||||
config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true)))
|
||||
} else {
|
||||
config
|
||||
}.root().render(renderOptions)
|
||||
}
|
||||
}
|
||||
|
||||
fun toText(): String = toFileConfig().root().render(renderOptions)
|
||||
|
||||
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
|
||||
|
@ -1,16 +1,20 @@
|
||||
package net.corda.smoketesting
|
||||
|
||||
import com.google.common.collect.Lists
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||
import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme
|
||||
import net.corda.testing.common.internal.asContextEnv
|
||||
import net.corda.testing.common.internal.checkNotOnClasspath
|
||||
@ -20,15 +24,18 @@ import java.nio.file.Paths
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId.systemDefault
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createDirectory
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
class NodeProcess(
|
||||
private val config: NodeConfig,
|
||||
private val nodeDir: Path,
|
||||
private val config: NodeParams,
|
||||
val nodeDir: Path,
|
||||
private val node: Process,
|
||||
private val client: CordaRPCClient
|
||||
) : AutoCloseable {
|
||||
@ -43,6 +50,7 @@ class NodeProcess(
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!node.isAlive) return
|
||||
log.info("Stopping node '${config.commonName}'")
|
||||
node.destroy()
|
||||
if (!node.waitFor(60, SECONDS)) {
|
||||
@ -56,65 +64,94 @@ class NodeProcess(
|
||||
|
||||
// TODO All use of this factory have duplicate code which is either bundling the calling module or a 3rd party module
|
||||
// as a CorDapp for the nodes.
|
||||
class Factory(private val buildDirectory: Path = Paths.get("build")) {
|
||||
val cordaJar: Path by lazy {
|
||||
val cordaJarUrl = requireNotNull(this::class.java.getResource("/corda.jar")) {
|
||||
"corda.jar could not be found in classpath"
|
||||
}
|
||||
cordaJarUrl.toPath()
|
||||
}
|
||||
class Factory(
|
||||
private val baseNetworkParameters: NetworkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION),
|
||||
private val buildDirectory: Path = Paths.get("build")
|
||||
) : AutoCloseable {
|
||||
companion object {
|
||||
private val formatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(systemDefault())
|
||||
private val cordaJars = ConcurrentHashMap<String, CordaJar>()
|
||||
|
||||
private companion object {
|
||||
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
|
||||
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(systemDefault())
|
||||
init {
|
||||
checkNotOnClasspath("net.corda.node.Corda") {
|
||||
"Smoke test has the node in its classpath. Please remove the offending dependency."
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getCordaJarInfo(version: String): CordaJar {
|
||||
return cordaJars.computeIfAbsent(version) {
|
||||
val (javaHome, versionSuffix) = if (version.isEmpty()) {
|
||||
System.getProperty("java.home") to ""
|
||||
} else {
|
||||
val javaHome = if (version.split(".")[1].toInt() > 11) {
|
||||
System.getProperty("java.home")
|
||||
} else {
|
||||
// 4.11 and below need JDK 8 to run
|
||||
checkNotNull(System.getenv("JAVA_8_HOME")) { "Please set JAVA_8_HOME env variable to home directory of JDK 8" }
|
||||
}
|
||||
javaHome to "-$version"
|
||||
}
|
||||
val cordaJar = this::class.java.getResource("/corda$versionSuffix.jar")!!.toPath()
|
||||
CordaJar(cordaJar, Path(javaHome, "bin", "java"))
|
||||
}
|
||||
}
|
||||
|
||||
fun getCordaJar(version: String? = null): Path = getCordaJarInfo(version ?: "").jarPath
|
||||
}
|
||||
|
||||
private val nodesDirectory: Path = (buildDirectory / "smoke-testing" / formatter.format(Instant.now())).createDirectories()
|
||||
private val nodeInfoFilesCopier = NodeInfoFilesCopier()
|
||||
private var nodes: MutableList<NodeProcess>? = ArrayList()
|
||||
private lateinit var networkParametersCopier: NetworkParametersCopier
|
||||
|
||||
private val nodesDirectory = (buildDirectory / formatter.format(Instant.now())).createDirectories()
|
||||
fun baseDirectory(config: NodeParams): Path = nodesDirectory / config.commonName
|
||||
|
||||
private var notaryParty: Party? = null
|
||||
fun createNotaries(first: NodeParams, vararg rest: NodeParams): List<NodeProcess> {
|
||||
check(!::networkParametersCopier.isInitialized) { "Notaries have already been created" }
|
||||
|
||||
private fun createNetworkParameters(notaryInfo: NotaryInfo, nodeDir: Path) {
|
||||
try {
|
||||
networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaries = listOf(notaryInfo)))
|
||||
val notariesParams = Lists.asList(first, rest)
|
||||
val notaryInfos = notariesParams.map { notaryParams ->
|
||||
val nodeDir = baseDirectory(notaryParams).createDirectories()
|
||||
val notaryParty = DevIdentityGenerator.installKeyStoreWithNodeIdentity(nodeDir, notaryParams.legalName)
|
||||
NotaryInfo(notaryParty, true)
|
||||
}
|
||||
val networkParameters = baseNetworkParameters.copy(notaries = notaryInfos)
|
||||
networkParametersCopier = try {
|
||||
NetworkParametersCopier(networkParameters)
|
||||
} catch (_: IllegalStateException) {
|
||||
// Assuming serialization env not in context.
|
||||
AMQPClientSerializationScheme.createSerializationEnv().asContextEnv {
|
||||
networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaries = listOf(notaryInfo)))
|
||||
NetworkParametersCopier(networkParameters)
|
||||
}
|
||||
}
|
||||
networkParametersCopier.install(nodeDir)
|
||||
}
|
||||
|
||||
fun baseDirectory(config: NodeConfig): Path = nodesDirectory / config.commonName
|
||||
return notariesParams.map { createNode(it, isNotary = true) }
|
||||
}
|
||||
|
||||
fun create(config: NodeConfig): NodeProcess {
|
||||
val nodeDir = baseDirectory(config).createDirectories()
|
||||
fun createNode(params: NodeParams): NodeProcess = createNode(params, isNotary = false)
|
||||
|
||||
private fun createNode(params: NodeParams, isNotary: Boolean): NodeProcess {
|
||||
check(::networkParametersCopier.isInitialized) { "Notary not created. Please call `creatNotaries` first." }
|
||||
|
||||
val nodeDir = baseDirectory(params).createDirectories()
|
||||
log.info("Node directory: {}", nodeDir)
|
||||
if (config.isNotary) {
|
||||
require(notaryParty == null) { "Only one notary can be created." }
|
||||
notaryParty = DevIdentityGenerator.installKeyStoreWithNodeIdentity(nodeDir, config.legalName)
|
||||
} else {
|
||||
require(notaryParty != null) { "Notary not created. Please call `create` with a notary config first." }
|
||||
val cordappsDir = (nodeDir / CORDAPPS_DIR_NAME).createDirectory()
|
||||
params.cordappJars.forEach { it.copyToDirectory(cordappsDir) }
|
||||
(nodeDir / "node.conf").writeText(params.createNodeConfig(isNotary))
|
||||
networkParametersCopier.install(nodeDir)
|
||||
nodeInfoFilesCopier.addConfig(nodeDir)
|
||||
|
||||
createSchema(nodeDir, params.version)
|
||||
val process = startNode(nodeDir, params.version)
|
||||
val client = CordaRPCClient(NetworkHostAndPort("localhost", params.rpcPort), params.clientRpcConfig)
|
||||
waitForNode(process, params, client)
|
||||
val nodeProcess = NodeProcess(params, nodeDir, process, client)
|
||||
nodes!! += nodeProcess
|
||||
return nodeProcess
|
||||
}
|
||||
|
||||
(nodeDir / "node.conf").writeText(config.toText())
|
||||
createNetworkParameters(NotaryInfo(notaryParty!!, true), nodeDir)
|
||||
|
||||
createSchema(nodeDir)
|
||||
val process = startNode(nodeDir)
|
||||
val client = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort))
|
||||
waitForNode(process, config, client)
|
||||
return NodeProcess(config, nodeDir, process, client)
|
||||
}
|
||||
|
||||
private fun waitForNode(process: Process, config: NodeConfig, client: CordaRPCClient) {
|
||||
private fun waitForNode(process: Process, config: NodeParams, client: CordaRPCClient) {
|
||||
val executor = Executors.newSingleThreadScheduledExecutor()
|
||||
try {
|
||||
executor.scheduleWithFixedDelay({
|
||||
@ -129,7 +166,7 @@ class NodeProcess(
|
||||
// Cancel the "setup" task now that we've created the RPC client.
|
||||
executor.shutdown()
|
||||
} catch (e: Exception) {
|
||||
log.warn("Node '{}' not ready yet (Error: {})", config.commonName, e.message)
|
||||
log.debug("Node '{}' not ready yet (Error: {})", config.commonName, e.message)
|
||||
}
|
||||
}, 5, 1, SECONDS)
|
||||
|
||||
@ -147,10 +184,10 @@ class NodeProcess(
|
||||
class SchemaCreationFailedError(nodeDir: Path) : Exception("Creating node schema failed for $nodeDir")
|
||||
|
||||
|
||||
private fun createSchema(nodeDir: Path){
|
||||
val process = startNode(nodeDir, "run-migration-scripts", "--core-schemas", "--app-schemas")
|
||||
private fun createSchema(nodeDir: Path, version: String?) {
|
||||
val process = startNode(nodeDir, version, "run-migration-scripts", "--core-schemas", "--app-schemas")
|
||||
if (!process.waitFor(schemaCreationTimeOutSeconds, SECONDS)) {
|
||||
process.destroy()
|
||||
process.destroyForcibly()
|
||||
throw SchemaCreationTimedOutError(nodeDir)
|
||||
}
|
||||
if (process.exitValue() != 0) {
|
||||
@ -158,8 +195,9 @@ class NodeProcess(
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNode(nodeDir: Path, vararg extraArgs: String): Process {
|
||||
val command = arrayListOf(javaPath.toString(), "-Dcapsule.log=verbose", "-jar", cordaJar.toString())
|
||||
private fun startNode(nodeDir: Path, version: String?, vararg extraArgs: String): Process {
|
||||
val cordaJar = getCordaJarInfo(version ?: "")
|
||||
val command = arrayListOf("${cordaJar.javaPath}", "-Dcapsule.log=verbose", "-jar", "${cordaJar.jarPath}", "--logging-level=debug")
|
||||
command += extraArgs
|
||||
val now = formatter.format(Instant.now())
|
||||
val builder = ProcessBuilder()
|
||||
@ -171,7 +209,17 @@ class NodeProcess(
|
||||
"CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString()
|
||||
))
|
||||
|
||||
return builder.start()
|
||||
}
|
||||
val process = builder.start()
|
||||
Runtime.getRuntime().addShutdownHook(Thread(process::destroyForcibly))
|
||||
return process
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
nodes?.parallelStream()?.forEach { it.close() }
|
||||
nodes = null
|
||||
nodeInfoFilesCopier.close()
|
||||
}
|
||||
|
||||
private data class CordaJar(val jarPath: Path, val javaPath: Path)
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
||||
import net.corda.core.internal.notary.NotaryService
|
||||
import net.corda.core.internal.verification.ExternalVerifierHandle
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.StatesToRecord
|
||||
@ -139,6 +140,9 @@ data class TestTransactionDSLInterpreter private constructor(
|
||||
return ledgerInterpreter.services.loadContractAttachment(stateRef)
|
||||
}
|
||||
|
||||
override val externalVerifierHandle: ExternalVerifierHandle
|
||||
get() = throw UnsupportedOperationException("External verification is not supported by TestTransactionDSLInterpreter")
|
||||
|
||||
override fun recordUnnotarisedTransaction(txn: SignedTransaction) {}
|
||||
|
||||
override fun removeUnnotarisedTransaction(id: SecureHash) {}
|
||||
|
@ -12,12 +12,16 @@ import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VER
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
import net.corda.core.node.services.vault.ColumnPredicate
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.nodeapi.internal.withContractsInJar
|
||||
import java.io.InputStream
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
@ -33,7 +37,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
/** A map of the currently stored files by their [SecureHash] */
|
||||
val files: Map<SecureHash, Pair<Attachment, ByteArray>> get() = _files
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun importAttachment(jar: InputStream): AttachmentId = importAttachment(jar, UNKNOWN_UPLOADER, null)
|
||||
|
||||
override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
||||
@ -78,11 +82,11 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
|
||||
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||
return try {
|
||||
importAttachment(jar, UNKNOWN_UPLOADER, null)
|
||||
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
AttachmentId.create(e.message!!)
|
||||
}
|
||||
}
|
||||
@ -109,7 +113,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
val baseAttachment = MockAttachment({ bytes }, sha256, signers, uploader)
|
||||
val version = try { Integer.parseInt(baseAttachment.openAsJAR().manifest?.mainAttributes?.getValue(Attributes.Name.IMPLEMENTATION_VERSION)) } catch (e: Exception) { DEFAULT_CORDAPP_VERSION }
|
||||
val attachment =
|
||||
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment
|
||||
if (contractClassNames.isNullOrEmpty()) baseAttachment
|
||||
else {
|
||||
contractClassNames.map {contractClassName ->
|
||||
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty(), signers, uploader)
|
||||
|
@ -124,9 +124,8 @@ class ExternalVerifier(
|
||||
}
|
||||
|
||||
private fun createAppClassLoader(): ClassLoader {
|
||||
val cordappJarUrls = (baseDirectory / "cordapps").listDirectoryEntries()
|
||||
val cordappJarUrls = (baseDirectory / "cordapps").listDirectoryEntries("*.jar")
|
||||
.stream()
|
||||
.filter { it.toString().endsWith(".jar") }
|
||||
.map { it.toUri().toURL() }
|
||||
.toTypedArray()
|
||||
log.debug { "CorDapps: ${cordappJarUrls?.joinToString()}" }
|
||||
@ -136,7 +135,7 @@ class ExternalVerifier(
|
||||
private fun verifyTransaction(request: VerificationRequest) {
|
||||
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
|
||||
val result: Try<Unit> = try {
|
||||
request.stx.verifyInternal(verificationContext, request.checkSufficientSignatures)
|
||||
request.stx.verifyInProcess(verificationContext, request.checkSufficientSignatures)
|
||||
log.info("${request.stx} verified")
|
||||
Try.Success(Unit)
|
||||
} catch (t: Throwable) {
|
||||
|
@ -10,15 +10,14 @@ import kotlin.io.path.div
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
object Main {
|
||||
private val log = loggerFor<Main>()
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
val port = args[0].toInt()
|
||||
val loggingLevel = args[0]
|
||||
val loggingLevel = args[1]
|
||||
val baseDirectory = Path.of("").toAbsolutePath()
|
||||
|
||||
initLogging(baseDirectory, loggingLevel)
|
||||
val log = loggerFor<Main>()
|
||||
|
||||
log.info("External verifier started; PID ${ProcessHandle.current().pid()}")
|
||||
log.info("Node base directory: $baseDirectory")
|
||||
|
Loading…
Reference in New Issue
Block a user