mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
ENT-11255: Scan attachments to determine if they are Kotlin 1.2 or later
The node now sends a transaction to the verifier if any of its attachments were compiled with Kotlin 1.2 (the net.corda.node.verification.external system property has been removed). It uses kotlinx-metadata to read the Kotlin metadata in the attachment to determine this. For now this scanning is done each time the attachment is loaded from the database. The existing external verification integration tests were converted into smoke tests so that 4.11 nodes could be involved. This required various improvements to NodeProcess.Factory. A new JAVA_8_HOME environment variable, pointing to JDK 8, is required to run these tests. There is still some follow-up work that needs to be done: Sending transactions from a 4.11 node to a 4.12 node works, but not the other way round. A new WireTransaction component group needs to be introduced for storing 4.12 attachments so that they can be safely ignored by 4.11 nodes, and the 4.12 node needs to be able to load both 4.11 and 4.12 versions of the same contracts CorDapp so that they can be both attached to the transaction. Even though attachments are cached when retrieved from the database, the Kotlin metadata version should be stored in the attachments db table, rather than being scanned each time. Finally, VerificationService was refactored into NodeVerificationSupport and can be passed into SignedTransaction.verifyInternal, instead of needing the much heavier VerifyingServiceHub. This makes it easier for internal tools to verify transactions and spawn the verifier if necessary.
This commit is contained in:
parent
1ff853b421
commit
f30ba33929
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
@ -46,6 +46,7 @@ pipeline {
|
|||||||
CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}"
|
CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}"
|
||||||
CORDA_USE_CACHE = "corda-remotes"
|
CORDA_USE_CACHE = "corda-remotes"
|
||||||
JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto"
|
JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto"
|
||||||
|
JAVA_8_HOME = "/usr/lib/jvm/java-1.8.0-amazon-corretto"
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
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
|
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')
|
C4_OS_SNYK_ORG_ID = credentials('corda4-os-snyk-org-id')
|
||||||
JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto"
|
JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto"
|
||||||
|
JAVA_8_HOME = "/usr/lib/jvm/java-1.8.0-amazon-corretto"
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
1
Jenkinsfile
vendored
1
Jenkinsfile
vendored
@ -54,6 +54,7 @@ pipeline {
|
|||||||
CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key')
|
CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key')
|
||||||
CORDA_USE_CACHE = "corda-remotes"
|
CORDA_USE_CACHE = "corda-remotes"
|
||||||
JAVA_HOME="/usr/lib/jvm/java-17-amazon-corretto"
|
JAVA_HOME="/usr/lib/jvm/java-17-amazon-corretto"
|
||||||
|
JAVA_8_HOME = "/usr/lib/jvm/java-1.8.0-amazon-corretto"
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.java.rpc;
|
package net.corda.java.rpc;
|
||||||
|
|
||||||
import net.corda.client.rpc.CordaRPCConnection;
|
|
||||||
import net.corda.core.contracts.Amount;
|
import net.corda.core.contracts.Amount;
|
||||||
import net.corda.core.identity.CordaX500Name;
|
import net.corda.core.identity.CordaX500Name;
|
||||||
import net.corda.core.identity.Party;
|
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.AbstractCashFlow;
|
||||||
import net.corda.finance.flows.CashIssueFlow;
|
import net.corda.finance.flows.CashIssueFlow;
|
||||||
import net.corda.nodeapi.internal.config.User;
|
import net.corda.nodeapi.internal.config.User;
|
||||||
import net.corda.smoketesting.NodeConfig;
|
import net.corda.smoketesting.NodeParams;
|
||||||
import net.corda.smoketesting.NodeProcess;
|
import net.corda.smoketesting.NodeProcess;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.util.Currency;
|
||||||
import java.nio.file.Files;
|
import java.util.HashSet;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static kotlin.test.AssertionsKt.assertEquals;
|
import static kotlin.test.AssertionsKt.assertEquals;
|
||||||
import static kotlin.test.AssertionsKt.fail;
|
|
||||||
import static net.corda.finance.workflows.GetBalances.getCashBalance;
|
import static net.corda.finance.workflows.GetBalances.getCashBalance;
|
||||||
|
import static net.corda.kotlin.rpc.StandaloneCordaRPClientTest.gatherCordapps;
|
||||||
|
|
||||||
public class StandaloneCordaRPCJavaClientTest {
|
public class StandaloneCordaRPCJavaClientTest {
|
||||||
|
private final User superUser = new User("superUser", "test", new HashSet<>(singletonList("ALL")));
|
||||||
|
|
||||||
public static void copyCordapps(NodeProcess.Factory factory, NodeConfig notaryConfig) {
|
private final AtomicInteger port = new AtomicInteger(15000);
|
||||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
|
private final NodeProcess.Factory factory = new NodeProcess.Factory();
|
||||||
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 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 CordaRPCOps rpcProxy;
|
||||||
private CordaRPCConnection connection;
|
|
||||||
private Party notaryNodeIdentity;
|
private Party notaryNodeIdentity;
|
||||||
|
|
||||||
private NodeConfig notaryConfig = new NodeConfig(
|
|
||||||
new CordaX500Name("Notary Service", "Zurich", "CH"),
|
|
||||||
port.getAndIncrement(),
|
|
||||||
port.getAndIncrement(),
|
|
||||||
port.getAndIncrement(),
|
|
||||||
true,
|
|
||||||
singletonList(superUser),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
NodeProcess.Factory factory = new NodeProcess.Factory();
|
NodeProcess notary = factory.createNotaries(new NodeParams(
|
||||||
copyCordapps(factory, notaryConfig);
|
new CordaX500Name("Notary Service", "Zurich", "CH"),
|
||||||
notary = factory.create(notaryConfig);
|
port.getAndIncrement(),
|
||||||
connection = notary.connect(superUser);
|
port.getAndIncrement(),
|
||||||
rpcProxy = connection.getProxy();
|
port.getAndIncrement(),
|
||||||
|
singletonList(superUser),
|
||||||
|
gatherCordapps()
|
||||||
|
)).get(0);
|
||||||
|
rpcProxy = notary.connect(superUser).getProxy();
|
||||||
notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0);
|
notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void done() {
|
public void done() {
|
||||||
try {
|
factory.close();
|
||||||
connection.close();
|
|
||||||
} finally {
|
|
||||||
if (notary != null) {
|
|
||||||
notary.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -27,6 +27,7 @@ import net.corda.core.utilities.getOrThrow
|
|||||||
import net.corda.core.utilities.minutes
|
import net.corda.core.utilities.minutes
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.GBP
|
||||||
import net.corda.finance.POUNDS
|
import net.corda.finance.POUNDS
|
||||||
import net.corda.finance.SWISS_FRANCS
|
import net.corda.finance.SWISS_FRANCS
|
||||||
import net.corda.finance.USD
|
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.flows.CashPaymentFlow
|
||||||
import net.corda.finance.workflows.getCashBalance
|
import net.corda.finance.workflows.getCashBalance
|
||||||
import net.corda.finance.workflows.getCashBalances
|
import net.corda.finance.workflows.getCashBalances
|
||||||
import net.corda.java.rpc.StandaloneCordaRPCJavaClientTest
|
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.sleeping.SleepingFlow
|
import net.corda.sleeping.SleepingFlow
|
||||||
import net.corda.smoketesting.NodeConfig
|
import net.corda.smoketesting.NodeParams
|
||||||
import net.corda.smoketesting.NodeProcess
|
import net.corda.smoketesting.NodeProcess
|
||||||
import org.hamcrest.text.MatchesPattern
|
import org.hamcrest.text.MatchesPattern
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
import org.junit.AfterClass
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.BeforeClass
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -50,17 +52,19 @@ import org.junit.rules.ExpectedException
|
|||||||
import java.io.FilterInputStream
|
import java.io.FilterInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream.nullOutputStream
|
import java.io.OutputStream.nullOutputStream
|
||||||
import java.util.Currency
|
import java.nio.file.Path
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.listDirectoryEntries
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertNotEquals
|
import kotlin.test.assertNotEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class StandaloneCordaRPClientTest {
|
class StandaloneCordaRPClientTest {
|
||||||
private companion object {
|
companion object {
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
val superUser = User("superUser", "test", permissions = setOf("ALL"))
|
val superUser = User("superUser", "test", permissions = setOf("ALL"))
|
||||||
val nonUser = User("nonUser", "test", permissions = emptySet())
|
val nonUser = User("nonUser", "test", permissions = emptySet())
|
||||||
@ -69,46 +73,57 @@ class StandaloneCordaRPClientTest {
|
|||||||
val port = AtomicInteger(15200)
|
val port = AtomicInteger(15200)
|
||||||
const val ATTACHMENT_SIZE = 2116
|
const val ATTACHMENT_SIZE = 2116
|
||||||
val timeout = 60.seconds
|
val timeout = 60.seconds
|
||||||
|
|
||||||
|
private val factory = NodeProcess.Factory()
|
||||||
|
|
||||||
|
private lateinit var notary: NodeProcess
|
||||||
|
|
||||||
|
private val notaryConfig = NodeParams(
|
||||||
|
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
|
||||||
|
p2pPort = port.andIncrement,
|
||||||
|
rpcPort = port.andIncrement,
|
||||||
|
rpcAdminPort = port.andIncrement,
|
||||||
|
users = listOf(superUser, nonUser, rpcUser, flowUser),
|
||||||
|
cordappJars = gatherCordapps()
|
||||||
|
)
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun startNotary() {
|
||||||
|
notary = factory.createNotaries(notaryConfig)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
@JvmStatic
|
||||||
|
fun close() {
|
||||||
|
factory.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun gatherCordapps(): List<Path> {
|
||||||
|
return Path("build", "resources", "smokeTest").listDirectoryEntries("cordapp*.jar")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var factory: NodeProcess.Factory
|
|
||||||
private lateinit var notary: NodeProcess
|
|
||||||
private lateinit var rpcProxy: CordaRPCOps
|
|
||||||
private lateinit var connection: CordaRPCConnection
|
private lateinit var connection: CordaRPCConnection
|
||||||
private lateinit var notaryNode: NodeInfo
|
private lateinit var rpcProxy: CordaRPCOps
|
||||||
private lateinit var notaryNodeIdentity: Party
|
private lateinit var notaryNodeIdentity: Party
|
||||||
|
|
||||||
private val notaryConfig = NodeConfig(
|
|
||||||
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
|
|
||||||
p2pPort = port.andIncrement,
|
|
||||||
rpcPort = port.andIncrement,
|
|
||||||
rpcAdminPort = port.andIncrement,
|
|
||||||
isNotary = true,
|
|
||||||
users = listOf(superUser, nonUser, rpcUser, flowUser)
|
|
||||||
)
|
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val exception: ExpectedException = ExpectedException.none()
|
val exception: ExpectedException = ExpectedException.none()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
factory = NodeProcess.Factory()
|
|
||||||
StandaloneCordaRPCJavaClientTest.copyCordapps(factory, notaryConfig)
|
|
||||||
notary = factory.create(notaryConfig)
|
|
||||||
connection = notary.connect(superUser)
|
connection = notary.connect(superUser)
|
||||||
rpcProxy = connection.proxy
|
rpcProxy = connection.proxy
|
||||||
notaryNode = fetchNotaryIdentity()
|
|
||||||
notaryNodeIdentity = rpcProxy.nodeInfo().legalIdentitiesAndCerts.first().party
|
notaryNodeIdentity = rpcProxy.nodeInfo().legalIdentitiesAndCerts.first().party
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun done() {
|
fun closeConnection() {
|
||||||
connection.use {
|
connection.close()
|
||||||
notary.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test attachments`() {
|
fun `test attachments`() {
|
||||||
val attachment = InputStreamAndHash.createInMemoryTestZip(ATTACHMENT_SIZE, 1)
|
val attachment = InputStreamAndHash.createInMemoryTestZip(ATTACHMENT_SIZE, 1)
|
||||||
@ -168,8 +183,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test state machines`() {
|
fun `test state machines`() {
|
||||||
val (stateMachines, updates) = rpcProxy.stateMachinesFeed()
|
val (_, updates) = rpcProxy.stateMachinesFeed()
|
||||||
assertEquals(0, stateMachines.size)
|
|
||||||
|
|
||||||
val updateLatch = CountDownLatch(1)
|
val updateLatch = CountDownLatch(1)
|
||||||
val updateCount = AtomicInteger(0)
|
val updateCount = AtomicInteger(0)
|
||||||
@ -190,8 +204,9 @@ class StandaloneCordaRPClientTest {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test vault track by`() {
|
fun `test vault track by`() {
|
||||||
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
|
val initialGbpBalance = rpcProxy.getCashBalance(GBP)
|
||||||
assertEquals(0, vault.totalStatesAvailable)
|
|
||||||
|
val (_, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
|
||||||
|
|
||||||
val updateLatch = CountDownLatch(1)
|
val updateLatch = CountDownLatch(1)
|
||||||
vaultUpdates.subscribe { update ->
|
vaultUpdates.subscribe { update ->
|
||||||
@ -207,34 +222,35 @@ class StandaloneCordaRPClientTest {
|
|||||||
// Check that this cash exists in the vault
|
// Check that this cash exists in the vault
|
||||||
val cashBalance = rpcProxy.getCashBalances()
|
val cashBalance = rpcProxy.getCashBalances()
|
||||||
log.info("Cash Balances: $cashBalance")
|
log.info("Cash Balances: $cashBalance")
|
||||||
assertEquals(1, cashBalance.size)
|
assertEquals(629.POUNDS, cashBalance[GBP]!! - initialGbpBalance)
|
||||||
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test vault query by`() {
|
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 criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
||||||
val paging = PageSpecification(DEFAULT_PAGE_NUM, 10)
|
val paging = PageSpecification(DEFAULT_PAGE_NUM, 10)
|
||||||
val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC)))
|
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)
|
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)
|
assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity)
|
||||||
|
|
||||||
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNodeIdentity, true, notaryNodeIdentity).returnValue.getOrThrow()
|
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNodeIdentity, true, notaryNodeIdentity).returnValue.getOrThrow()
|
||||||
|
|
||||||
val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
|
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
|
// Check that this cash exists in the vault
|
||||||
val cashBalances = rpcProxy.getCashBalances()
|
val cashBalances = rpcProxy.getCashBalances()
|
||||||
log.info("Cash Balances: $cashBalances")
|
log.info("Cash Balances: $cashBalances")
|
||||||
assertEquals(1, cashBalances.size)
|
assertEquals(629.POUNDS, cashBalances[GBP]!! - initialGbpBalance)
|
||||||
assertEquals(629.POUNDS, cashBalances[Currency.getInstance("GBP")])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
|
@ -9,11 +9,13 @@ configurations {
|
|||||||
integrationTestImplementation.extendsFrom testImplementation
|
integrationTestImplementation.extendsFrom testImplementation
|
||||||
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||||
|
|
||||||
smokeTestCompile.extendsFrom compile
|
smokeTestImplementation.extendsFrom implementation
|
||||||
smokeTestRuntimeOnly.extendsFrom runtimeOnly
|
smokeTestRuntimeOnly.extendsFrom runtimeOnly
|
||||||
}
|
|
||||||
|
|
||||||
evaluationDependsOn(':node:capsule')
|
testArtifacts.extendsFrom testRuntimeOnlyClasspath
|
||||||
|
|
||||||
|
corda4_11
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
integrationTest {
|
integrationTest {
|
||||||
@ -42,9 +44,17 @@ sourceSets {
|
|||||||
|
|
||||||
processSmokeTestResources {
|
processSmokeTestResources {
|
||||||
// Bring in the fully built corda.jar for use by NodeFactory in the smoke tests
|
// 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'
|
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 {
|
dependencies {
|
||||||
@ -69,7 +79,6 @@ dependencies {
|
|||||||
testImplementation project(":test-utils")
|
testImplementation project(":test-utils")
|
||||||
testImplementation project(path: ':core', configuration: 'testArtifacts')
|
testImplementation project(path: ':core', configuration: 'testArtifacts')
|
||||||
|
|
||||||
|
|
||||||
// Guava: Google test library (collections test suite)
|
// Guava: Google test library (collections test suite)
|
||||||
testImplementation "com.google.guava:guava-testlib:$guava_version"
|
testImplementation "com.google.guava:guava-testlib:$guava_version"
|
||||||
testImplementation "com.google.jimfs:jimfs:1.1"
|
testImplementation "com.google.jimfs:jimfs:1.1"
|
||||||
@ -98,7 +107,12 @@ dependencies {
|
|||||||
smokeTestImplementation project(":core")
|
smokeTestImplementation project(":core")
|
||||||
smokeTestImplementation project(":node-api")
|
smokeTestImplementation project(":node-api")
|
||||||
smokeTestImplementation project(":client:rpc")
|
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 "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
|
||||||
smokeTestImplementation "co.paralleluniverse:quasar-core:$quasar_version"
|
smokeTestImplementation "co.paralleluniverse:quasar-core:$quasar_version"
|
||||||
smokeTestImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_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.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||||
smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_version"
|
smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_version"
|
||||||
|
|
||||||
smokeTestCompile project(':smoke-test-utils')
|
|
||||||
smokeTestCompile "org.assertj:assertj-core:${assertj_version}"
|
|
||||||
|
|
||||||
// used by FinalityFlowTests
|
// used by FinalityFlowTests
|
||||||
testImplementation project(':testing:cordapps:cashobservers')
|
testImplementation project(':testing:cordapps:cashobservers')
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
corda4_11 "net.corda:corda-finance-contracts:4.11"
|
||||||
testArtifacts.extendsFrom testRuntimeOnlyClasspath
|
corda4_11 "net.corda:corda-finance-workflows:4.11"
|
||||||
|
corda4_11 "net.corda:corda:4.11"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(Test).configureEach {
|
tasks.withType(Test).configureEach {
|
||||||
@ -125,22 +136,24 @@ tasks.withType(Test).configureEach {
|
|||||||
forkEvery = 10
|
forkEvery = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
task testJar(type: Jar) {
|
tasks.register('testJar', Jar) {
|
||||||
classifier "tests"
|
classifier "tests"
|
||||||
from sourceSets.test.output
|
from sourceSets.test.output
|
||||||
}
|
}
|
||||||
|
|
||||||
task integrationTest(type: Test) {
|
tasks.register('integrationTest', Test) {
|
||||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||||
}
|
}
|
||||||
|
|
||||||
task smokeTestJar(type: Jar) {
|
tasks.register('smokeTestJar', Jar) {
|
||||||
classifier 'smokeTests'
|
classifier 'smokeTests'
|
||||||
from sourceSets.smokeTest.output
|
from(sourceSets.smokeTest.output) {
|
||||||
|
exclude("*.jar")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task smokeTest(type: Test) {
|
tasks.register('smokeTest', Test) {
|
||||||
dependsOn smokeTestJar
|
dependsOn smokeTestJar
|
||||||
testClassesDirs = sourceSets.smokeTest.output.classesDirs
|
testClassesDirs = sourceSets.smokeTest.output.classesDirs
|
||||||
classpath = sourceSets.smokeTest.runtimeClasspath
|
classpath = sourceSets.smokeTest.runtimeClasspath
|
||||||
|
@ -5,11 +5,10 @@ import net.corda.core.flows.FlowLogic
|
|||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.PLATFORM_VERSION
|
import net.corda.core.internal.PLATFORM_VERSION
|
||||||
import net.corda.core.internal.copyToDirectory
|
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.smoketesting.NodeConfig
|
import net.corda.smoketesting.NodeParams
|
||||||
import net.corda.smoketesting.NodeProcess
|
import net.corda.smoketesting.NodeProcess
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -18,8 +17,6 @@ import org.junit.Test
|
|||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.jar.JarFile
|
import java.util.jar.JarFile
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.div
|
|
||||||
import kotlin.io.path.listDirectoryEntries
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
|
||||||
class NodeVersioningTest {
|
class NodeVersioningTest {
|
||||||
@ -30,56 +27,39 @@ class NodeVersioningTest {
|
|||||||
|
|
||||||
private val factory = NodeProcess.Factory()
|
private val factory = NodeProcess.Factory()
|
||||||
|
|
||||||
private val notaryConfig = NodeConfig(
|
private lateinit var notary: NodeProcess
|
||||||
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
|
@Before
|
||||||
fun setUp() {
|
fun startNotary() {
|
||||||
notary = factory.create(notaryConfig)
|
notary = factory.createNotaries(NodeParams(
|
||||||
|
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
|
||||||
|
p2pPort = port.andIncrement,
|
||||||
|
rpcPort = port.andIncrement,
|
||||||
|
rpcAdminPort = port.andIncrement,
|
||||||
|
users = listOf(superUser),
|
||||||
|
// Find the jar file for the smoke tests of this module
|
||||||
|
cordappJars = Path("build", "libs").listDirectoryEntries("*-smokeTests*")
|
||||||
|
))[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun done() {
|
fun done() {
|
||||||
notary?.close()
|
factory.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `platform version in manifest file`() {
|
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)
|
assertThat(manifest.mainAttributes.getValue("Corda-Platform-Version").toInt()).isEqualTo(PLATFORM_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `platform version from RPC`() {
|
fun `platform version from RPC`() {
|
||||||
val cordappsDir = (factory.baseDirectory(aliceConfig) / NodeProcess.CORDAPPS_DIR_NAME).createDirectories()
|
notary.connect(superUser).use {
|
||||||
// Find the jar file for the smoke tests of this module
|
val rpc = it.proxy
|
||||||
val selfCordapp = Path("build", "libs").listDirectoryEntries("*-smokeTests*").single()
|
assertThat(rpc.protocolVersion).isEqualTo(PLATFORM_VERSION)
|
||||||
selfCordapp.copyToDirectory(cordappsDir)
|
assertThat(rpc.nodeInfo().platformVersion).isEqualTo(PLATFORM_VERSION)
|
||||||
|
assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(PLATFORM_VERSION)
|
||||||
factory.create(aliceConfig).use { alice ->
|
|
||||||
alice.connect(superUser).use {
|
|
||||||
val rpc = it.proxy
|
|
||||||
assertThat(rpc.protocolVersion).isEqualTo(PLATFORM_VERSION)
|
|
||||||
assertThat(rpc.nodeInfo().platformVersion).isEqualTo(PLATFORM_VERSION)
|
|
||||||
assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(PLATFORM_VERSION)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import net.corda.core.flows.StartableByRPC
|
|||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.copyToDirectory
|
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.serialize
|
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.createDevNodeCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
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
|
||||||
import net.corda.smoketesting.NodeProcess.Companion.CORDAPPS_DIR_NAME
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -41,8 +39,8 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.createDirectories
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.listDirectoryEntries
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
import kotlin.io.path.useDirectoryEntries
|
|
||||||
import kotlin.io.path.writeBytes
|
import kotlin.io.path.writeBytes
|
||||||
|
|
||||||
class CordappSmokeTest {
|
class CordappSmokeTest {
|
||||||
@ -53,43 +51,35 @@ class CordappSmokeTest {
|
|||||||
|
|
||||||
private val factory = NodeProcess.Factory()
|
private val factory = NodeProcess.Factory()
|
||||||
|
|
||||||
private val notaryConfig = NodeConfig(
|
private val aliceConfig = 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"),
|
legalName = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"),
|
||||||
p2pPort = port.andIncrement,
|
p2pPort = port.andIncrement,
|
||||||
rpcPort = port.andIncrement,
|
rpcPort = port.andIncrement,
|
||||||
rpcAdminPort = 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
|
@Before
|
||||||
fun setUp() {
|
fun startNotary() {
|
||||||
notary = factory.create(notaryConfig)
|
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
|
@After
|
||||||
fun done() {
|
fun done() {
|
||||||
notary.close()
|
factory.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `FlowContent appName returns the filename of the CorDapp jar`() {
|
fun `FlowContent appName returns the filename of the CorDapp jar`() {
|
||||||
val baseDir = factory.baseDirectory(aliceConfig)
|
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
|
// 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
|
// node in the network. We work around this limitation by putting another node info file in the additional-node-info
|
||||||
@ -98,24 +88,17 @@ class CordappSmokeTest {
|
|||||||
val additionalNodeInfoDir = (baseDir / "additional-node-infos").createDirectories()
|
val additionalNodeInfoDir = (baseDir / "additional-node-infos").createDirectories()
|
||||||
createDummyNodeInfo(additionalNodeInfoDir)
|
createDummyNodeInfo(additionalNodeInfoDir)
|
||||||
|
|
||||||
factory.create(aliceConfig).use { alice ->
|
val alice = factory.createNode(aliceConfig)
|
||||||
alice.connect(superUser).use { connectionToAlice ->
|
alice.connect(superUser).use { connectionToAlice ->
|
||||||
val aliceIdentity = connectionToAlice.proxy.nodeInfo().legalIdentitiesAndCerts.first().party
|
val aliceIdentity = connectionToAlice.proxy.nodeInfo().legalIdentitiesAndCerts.first().party
|
||||||
val future = connectionToAlice.proxy.startFlow(CordappSmokeTest::GatherContextsFlow, aliceIdentity).returnValue
|
val future = connectionToAlice.proxy.startFlow(CordappSmokeTest::GatherContextsFlow, aliceIdentity).returnValue
|
||||||
val (sessionInitContext, sessionConfirmContext) = future.getOrThrow()
|
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(sessionInitContext.appName).isEqualTo(selfCordappName)
|
||||||
assertThat(sessionConfirmContext.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
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class GatherContextsFlow(private val otherParty: Party) : FlowLogic<Pair<FlowInfo, FlowInfo>>() {
|
class GatherContextsFlow(private val otherParty: Party) : FlowLogic<Pair<FlowInfo, FlowInfo>>() {
|
||||||
|
@ -0,0 +1,247 @@
|
|||||||
|
package net.corda.coretests.verification
|
||||||
|
|
||||||
|
import co.paralleluniverse.strands.concurrent.CountDownLatch
|
||||||
|
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||||
|
import net.corda.client.rpc.notUsed
|
||||||
|
import net.corda.core.contracts.Amount
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.UnexpectedFlowEndException
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS
|
||||||
|
import net.corda.core.internal.toPath
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.flows.AbstractCashFlow
|
||||||
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
|
import net.corda.finance.flows.CashPaymentFlow
|
||||||
|
import net.corda.nodeapi.internal.config.User
|
||||||
|
import net.corda.smoketesting.NodeParams
|
||||||
|
import net.corda.smoketesting.NodeProcess
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
|
import net.corda.testing.cordapps.workflows411.IssueAndChangeNotaryFlow
|
||||||
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||||
|
import org.junit.AfterClass
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Test
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.Currency
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.copyTo
|
||||||
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.io.path.readText
|
||||||
|
|
||||||
|
class ExternalVerificationSignedCordappsTest {
|
||||||
|
private companion object {
|
||||||
|
private val factory = NodeProcess.Factory(testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS))
|
||||||
|
|
||||||
|
private lateinit var notaries: List<NodeProcess>
|
||||||
|
private lateinit var oldNode: NodeProcess
|
||||||
|
private lateinit var newNode: NodeProcess
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun startNodes() {
|
||||||
|
// The 4.11 finance CorDapp jars
|
||||||
|
val oldCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") }
|
||||||
|
// The current version finance CorDapp jars
|
||||||
|
val newCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it.jar") }
|
||||||
|
|
||||||
|
notaries = factory.createNotaries(
|
||||||
|
nodeParams(DUMMY_NOTARY_NAME, oldCordapps),
|
||||||
|
nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), newCordapps)
|
||||||
|
)
|
||||||
|
oldNode = factory.createNode(nodeParams(
|
||||||
|
CordaX500Name("Old", "Delhi", "IN"),
|
||||||
|
oldCordapps + listOf(smokeTestResource("4.11-workflows-cordapp.jar")),
|
||||||
|
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
|
||||||
|
version = "4.11"
|
||||||
|
))
|
||||||
|
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
@JvmStatic
|
||||||
|
fun close() {
|
||||||
|
factory.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `transaction containing 4_11 contract sent to new node`() {
|
||||||
|
assertCashIssuanceAndPayment(issuer = oldNode, recipient = newNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `notary change transaction`() {
|
||||||
|
val oldRpc = oldNode.connect(superUser).proxy
|
||||||
|
val oldNodeInfo = oldRpc.nodeInfo()
|
||||||
|
val notaryIdentities = oldRpc.notaryIdentities()
|
||||||
|
for (notary in notaries) {
|
||||||
|
notary.connect(superUser).use { it.proxy.waitForVisibility(oldNodeInfo) }
|
||||||
|
}
|
||||||
|
oldRpc.startFlow(::IssueAndChangeNotaryFlow, notaryIdentities[0], notaryIdentities[1]).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertCashIssuanceAndPayment(issuer: NodeProcess, recipient: NodeProcess) {
|
||||||
|
val issuerRpc = issuer.connect(superUser).proxy
|
||||||
|
val recipientRpc = recipient.connect(superUser).proxy
|
||||||
|
val recipientNodeInfo = recipientRpc.nodeInfo()
|
||||||
|
val notaryIdentity = issuerRpc.notaryIdentities()[0]
|
||||||
|
|
||||||
|
val (issuanceTx) = issuerRpc.startFlow(
|
||||||
|
::CashIssueFlow,
|
||||||
|
10.DOLLARS,
|
||||||
|
OpaqueBytes.of(0x01),
|
||||||
|
notaryIdentity
|
||||||
|
).returnValue.getOrThrow()
|
||||||
|
|
||||||
|
issuerRpc.waitForVisibility(recipientNodeInfo)
|
||||||
|
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
|
||||||
|
|
||||||
|
val (paymentTx) = issuerRpc.startFlow(
|
||||||
|
::CashPaymentFlow,
|
||||||
|
10.DOLLARS,
|
||||||
|
recipientNodeInfo.legalIdentities[0],
|
||||||
|
false,
|
||||||
|
).returnValue.getOrThrow()
|
||||||
|
|
||||||
|
notaries[0].assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
|
||||||
|
recipient.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExternalVerificationUnsignedCordappsTest {
|
||||||
|
private companion object {
|
||||||
|
private val factory = NodeProcess.Factory(testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS))
|
||||||
|
|
||||||
|
private lateinit var notary: NodeProcess
|
||||||
|
private lateinit var oldNode: NodeProcess
|
||||||
|
private lateinit var newNode: NodeProcess
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun startNodes() {
|
||||||
|
// The 4.11 finance CorDapp jars
|
||||||
|
val oldCordapps = listOf(unsignedResourceJar("corda-finance-contracts-4.11.jar"), smokeTestResource("corda-finance-workflows-4.11.jar"))
|
||||||
|
// The current version finance CorDapp jars
|
||||||
|
val newCordapps = listOf(unsignedResourceJar("corda-finance-contracts.jar"), smokeTestResource("corda-finance-workflows.jar"))
|
||||||
|
|
||||||
|
notary = factory.createNotaries(nodeParams(DUMMY_NOTARY_NAME, oldCordapps))[0]
|
||||||
|
oldNode = factory.createNode(nodeParams(
|
||||||
|
CordaX500Name("Old", "Delhi", "IN"),
|
||||||
|
oldCordapps,
|
||||||
|
CordaRPCClientConfiguration(minimumServerProtocolVersion = 13),
|
||||||
|
version = "4.11"
|
||||||
|
))
|
||||||
|
newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps))
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
@JvmStatic
|
||||||
|
fun close() {
|
||||||
|
factory.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unsignedResourceJar(name: String): Path {
|
||||||
|
val signedJar = smokeTestResource(name)
|
||||||
|
val copy = signedJar.copyTo(Path("${signedJar.toString().substringBeforeLast(".")}-UNSIGNED.jar"), overwrite = true)
|
||||||
|
copy.unsignJar()
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 300_000)
|
||||||
|
fun `transactions can fail verification in external verifier`() {
|
||||||
|
val issuerRpc = oldNode.connect(superUser).proxy
|
||||||
|
val recipientRpc = newNode.connect(superUser).proxy
|
||||||
|
val recipientNodeInfo = recipientRpc.nodeInfo()
|
||||||
|
val notaryIdentity = issuerRpc.notaryIdentities()[0]
|
||||||
|
|
||||||
|
val (issuanceTx) = issuerRpc.startFlow(
|
||||||
|
::CashIssueFlow,
|
||||||
|
10.DOLLARS,
|
||||||
|
OpaqueBytes.of(0x01),
|
||||||
|
notaryIdentity
|
||||||
|
).returnValue.getOrThrow()
|
||||||
|
|
||||||
|
issuerRpc.waitForVisibility(recipientNodeInfo)
|
||||||
|
recipientRpc.waitForVisibility(issuerRpc.nodeInfo())
|
||||||
|
|
||||||
|
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||||
|
issuerRpc.startFlow<AbstractCashFlow.Result, Amount<Currency>, Party, Boolean, CashPaymentFlow>(
|
||||||
|
::CashPaymentFlow,
|
||||||
|
10.DOLLARS,
|
||||||
|
recipientNodeInfo.legalIdentities[0],
|
||||||
|
false,
|
||||||
|
).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(newNode.externalVerifierLogs()).contains("$issuanceTx failed to verify")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val superUser = User("superUser", "test", permissions = setOf("ALL"))
|
||||||
|
private val portCounter = AtomicInteger(15100)
|
||||||
|
|
||||||
|
private fun smokeTestResource(name: String): Path = ExternalVerificationSignedCordappsTest::class.java.getResource("/$name")!!.toPath()
|
||||||
|
|
||||||
|
private fun nodeParams(
|
||||||
|
legalName: CordaX500Name,
|
||||||
|
cordappJars: List<Path> = emptyList(),
|
||||||
|
clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||||
|
version: String? = null
|
||||||
|
): NodeParams {
|
||||||
|
return NodeParams(
|
||||||
|
legalName = legalName,
|
||||||
|
p2pPort = portCounter.andIncrement,
|
||||||
|
rpcPort = portCounter.andIncrement,
|
||||||
|
rpcAdminPort = portCounter.andIncrement,
|
||||||
|
users = listOf(superUser),
|
||||||
|
cordappJars = cordappJars,
|
||||||
|
clientRpcConfig = clientRpcConfig,
|
||||||
|
version = version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CordaRPCOps.waitForVisibility(other: NodeInfo) {
|
||||||
|
val (snapshot, updates) = networkMapFeed()
|
||||||
|
if (other in snapshot) {
|
||||||
|
updates.notUsed()
|
||||||
|
} else {
|
||||||
|
val found = CountDownLatch(1)
|
||||||
|
val subscription = updates.subscribe {
|
||||||
|
if (it.node == other) {
|
||||||
|
found.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found.await()
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NodeProcess.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) {
|
||||||
|
val verifierLogContent = externalVerifierLogs()
|
||||||
|
for (txId in txIds) {
|
||||||
|
assertThat(verifierLogContent).contains("SignedTransaction(id=$txId) verified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NodeProcess.externalVerifierLogs(): String {
|
||||||
|
val verifierLogs = (nodeDir / "logs")
|
||||||
|
.listDirectoryEntries()
|
||||||
|
.filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" }
|
||||||
|
assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1)
|
||||||
|
return verifierLogs[0].readText()
|
||||||
|
}
|
@ -11,6 +11,7 @@ import net.corda.core.internal.utilities.Internable
|
|||||||
import net.corda.core.internal.utilities.PrivateInterner
|
import net.corda.core.internal.utilities.PrivateInterner
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import java.lang.annotation.Inherited
|
import java.lang.annotation.Inherited
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -70,7 +71,7 @@ object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
|
|||||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||||
return if (attachment is AttachmentWithContext) {
|
return if (attachment is AttachmentWithContext) {
|
||||||
val whitelist = attachment.whitelistedContractImplementations
|
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())
|
attachment.id in (whitelist[attachment.contract] ?: emptyList())
|
||||||
} else {
|
} else {
|
||||||
log.warn("CZ whitelisted constraint check failed: ${attachment.id} not in CZ whitelist")
|
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 {
|
data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstraint {
|
||||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||||
log.debug("Checking signature constraints: verifying $key in contract attachment signer keys: ${attachment.signerKeys}")
|
log.debug { "Checking signature constraints: verifying $key in contract attachment signer keys: ${attachment.signerKeys}" }
|
||||||
return if (!key.isFulfilledBy(attachment.signerKeys.map { it })) {
|
return if (!key.isFulfilledBy(attachment.signerKeys)) {
|
||||||
log.warn("Untrusted signing key: expected $key. but contract attachment contains ${attachment.signerKeys}")
|
log.warn("Untrusted signing key: expected $key. but contract attachment contains ${attachment.signerKeys}")
|
||||||
false
|
false
|
||||||
} else true
|
} 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.UntrustworthyData
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.event.Level
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Observer
|
import rx.Observer
|
||||||
import rx.observers.Subscribers
|
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_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION = 46
|
||||||
const val JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION = 61
|
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.AttachmentTrustCalculator
|
||||||
import net.corda.core.internal.SerializedTransactionState
|
import net.corda.core.internal.SerializedTransactionState
|
||||||
import net.corda.core.internal.TRUSTED_UPLOADERS
|
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.entries
|
||||||
import net.corda.core.internal.getRequiredTransaction
|
import net.corda.core.internal.getRequiredTransaction
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
@ -36,26 +37,33 @@ import java.security.PublicKey
|
|||||||
/**
|
/**
|
||||||
* Implements [VerificationSupport] in terms of node-based services.
|
* Implements [VerificationSupport] in terms of node-based services.
|
||||||
*/
|
*/
|
||||||
interface VerificationService : VerificationSupport {
|
interface NodeVerificationSupport : VerificationSupport {
|
||||||
val transactionStorage: TransactionStorage
|
val networkParameters: NetworkParameters
|
||||||
|
|
||||||
|
val validatedTransactions: TransactionStorage
|
||||||
|
|
||||||
val identityService: IdentityService
|
val identityService: IdentityService
|
||||||
|
|
||||||
val attachmentStorage: AttachmentStorage
|
val attachments: AttachmentStorage
|
||||||
|
|
||||||
val networkParametersService: NetworkParametersService
|
val networkParametersService: NetworkParametersService
|
||||||
|
|
||||||
|
val cordappProvider: CordappProviderInternal
|
||||||
|
|
||||||
val attachmentTrustCalculator: AttachmentTrustCalculator
|
val attachmentTrustCalculator: AttachmentTrustCalculator
|
||||||
|
|
||||||
val attachmentFixups: AttachmentFixups
|
val externalVerifierHandle: ExternalVerifierHandle
|
||||||
|
|
||||||
|
override val appClassLoader: ClassLoader
|
||||||
|
get() = cordappProvider.appClassLoader
|
||||||
|
|
||||||
// TODO Bulk party lookup?
|
// TODO Bulk party lookup?
|
||||||
override fun getParties(keys: Collection<PublicKey>): List<Party?> = keys.map(identityService::partyFromKey)
|
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? {
|
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.
|
* correct classloader independent of the node's classpath.
|
||||||
*/
|
*/
|
||||||
override fun getSerializedState(stateRef: StateRef): SerializedTransactionState {
|
override fun getSerializedState(stateRef: StateRef): SerializedTransactionState {
|
||||||
val coreTransaction = transactionStorage.getRequiredTransaction(stateRef.txhash).coreTransaction
|
val coreTransaction = validatedTransactions.getRequiredTransaction(stateRef.txhash).coreTransaction
|
||||||
return when (coreTransaction) {
|
return when (coreTransaction) {
|
||||||
is WireTransaction -> getRegularOutput(coreTransaction, stateRef.index)
|
is WireTransaction -> getRegularOutput(coreTransaction, stateRef.index)
|
||||||
is ContractUpgradeWireTransaction -> getContractUpdateOutput(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).
|
// TODO Should throw when the class is found in multiple contract attachments (not different versions).
|
||||||
override fun getTrustedClassAttachment(className: String): Attachment? {
|
override fun getTrustedClassAttachment(className: String): Attachment? {
|
||||||
val allTrusted = attachmentStorage.queryAttachments(
|
val allTrusted = attachments.queryAttachments(
|
||||||
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
|
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
|
||||||
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
|
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO - add caching if performance is affected.
|
// TODO - add caching if performance is affected.
|
||||||
for (attId in allTrusted) {
|
for (attId in allTrusted) {
|
||||||
val attch = attachmentStorage.openAttachment(attId)!!
|
val attch = attachments.openAttachment(attId)!!
|
||||||
if (attch.hasFile("$className.class")) return attch
|
if (attch.hasFile("$className.class")) return attch
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@ -145,6 +153,6 @@ interface VerificationService : VerificationSupport {
|
|||||||
override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachmentTrustCalculator.calculate(attachment)
|
override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachmentTrustCalculator.calculate(attachment)
|
||||||
|
|
||||||
override fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>): Set<SecureHash> {
|
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.StateRef
|
||||||
import net.corda.core.contracts.TransactionState
|
import net.corda.core.contracts.TransactionState
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
|
||||||
import net.corda.core.internal.getRequiredTransaction
|
import net.corda.core.internal.getRequiredTransaction
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.ServicesForResolution
|
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.serialization.deserialize
|
||||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
|
|
||||||
@Suppress("TooManyFunctions", "ThrowsCount")
|
@Suppress("TooManyFunctions", "ThrowsCount")
|
||||||
interface VerifyingServiceHub : ServiceHub, VerificationService {
|
interface VerifyingServiceHub : ServiceHub, NodeVerificationSupport {
|
||||||
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
|
|
||||||
|
|
||||||
override fun loadContractAttachment(stateRef: StateRef): Attachment {
|
override fun loadContractAttachment(stateRef: StateRef): Attachment {
|
||||||
// We may need to recursively chase transactions if there are notary changes.
|
// We may need to recursively chase transactions if there are notary changes.
|
||||||
return loadContractAttachment(stateRef, null)
|
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 {
|
fun <T : ContractState, C : MutableCollection<StateAndRef<T>>> loadStatesInternal(input: Iterable<StateRef>, output: C): C {
|
||||||
return input.mapTo(output, ::toStateAndRef)
|
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 {
|
fun ServicesForResolution.toVerifyingServiceHub(): VerifyingServiceHub {
|
||||||
|
@ -18,8 +18,11 @@ import net.corda.core.crypto.toStringShort
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.TransactionDeserialisationException
|
import net.corda.core.internal.TransactionDeserialisationException
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
|
import net.corda.core.internal.attachmentIds
|
||||||
import net.corda.core.internal.equivalent
|
import net.corda.core.internal.equivalent
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
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.VerificationSupport
|
||||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||||
import net.corda.core.node.ServiceHub
|
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.internal.MissingSerializerException
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.core.utilities.debug
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -155,9 +159,10 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class)
|
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||||
fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction {
|
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.
|
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
|
||||||
resolveAndCheckNetworkParameters(services)
|
resolveAndCheckNetworkParameters(verifyingServiceHub)
|
||||||
return toLedgerTransactionInternal(services.toVerifyingServiceHub(), checkSufficientSignatures)
|
return toLedgerTransactionInternal(verifyingServiceHub, checkSufficientSignatures)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
@ -191,16 +196,58 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
||||||
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
|
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
|
||||||
resolveAndCheckNetworkParameters(services)
|
verifyInternal(services.toVerifyingServiceHub(), checkSufficientSignatures)
|
||||||
val verifyingServiceHub = services.toVerifyingServiceHub()
|
}
|
||||||
if (verifyingServiceHub.tryExternalVerification(this, checkSufficientSignatures)) {
|
|
||||||
verifyInternal(verifyingServiceHub, 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
|
@CordaInternal
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
fun verifyInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
|
fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) {
|
||||||
when (coreTransaction) {
|
when (coreTransaction) {
|
||||||
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
|
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
|
||||||
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures)
|
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures)
|
||||||
@ -209,7 +256,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ThrowsCount")
|
@Suppress("ThrowsCount")
|
||||||
private fun resolveAndCheckNetworkParameters(services: ServiceHub) {
|
private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) {
|
||||||
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
|
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
|
||||||
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault)
|
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault)
|
||||||
?: throw TransactionResolutionException(id)
|
?: throw TransactionResolutionException(id)
|
||||||
|
@ -242,6 +242,7 @@ dependencies {
|
|||||||
|
|
||||||
// Adding native SSL library to allow using native SSL with Artemis and AMQP
|
// Adding native SSL library to allow using native SSL with Artemis and AMQP
|
||||||
implementation "io.netty:netty-tcnative-boringssl-static:$tcnative_version"
|
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
|
// Byteman for runtime (termination) rules injection on the running node
|
||||||
// Submission tool allowing to install rules on running nodes
|
// 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.TelemetryComponent
|
||||||
import net.corda.core.internal.telemetry.TelemetryServiceImpl
|
import net.corda.core.internal.telemetry.TelemetryServiceImpl
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.internal.verification.VerifyingServiceHub
|
|
||||||
import net.corda.core.messaging.ClientRpcSslOptions
|
import net.corda.core.messaging.ClientRpcSslOptions
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.RPCOps
|
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.AttachmentsClassLoaderCache
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.days
|
import net.corda.core.utilities.days
|
||||||
import net.corda.core.utilities.millis
|
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.BindableNamedCacheFactory
|
||||||
import net.corda.node.utilities.NamedThreadFactory
|
import net.corda.node.utilities.NamedThreadFactory
|
||||||
import net.corda.node.utilities.NotaryLoader
|
import net.corda.node.utilities.NotaryLoader
|
||||||
|
import net.corda.node.verification.ExternalVerifierHandleImpl
|
||||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.NodeStatus
|
import net.corda.nodeapi.internal.NodeStatus
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
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)
|
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,
|
// 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).
|
// 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)
|
// 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 {
|
inner class ServiceHubImpl : SingletonSerializeAsToken(), ServiceHubInternal, NetworkParameterUpdateListener {
|
||||||
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
||||||
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
|
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 identityService: IdentityService get() = this@AbstractNode.identityService
|
||||||
override val keyManagementService: KeyManagementService get() = this@AbstractNode.keyManagementService
|
override val keyManagementService: KeyManagementService get() = this@AbstractNode.keyManagementService
|
||||||
override val schemaService: SchemaService get() = this@AbstractNode.schemaService
|
override val schemaService: SchemaService get() = this@AbstractNode.schemaService
|
||||||
@ -1298,10 +1290,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
|
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
|
||||||
this.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.node.ServiceHub
|
||||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.node.CordaClock
|
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.DefaultNamedCacheFactory
|
||||||
import net.corda.node.utilities.DemoClock
|
import net.corda.node.utilities.DemoClock
|
||||||
import net.corda.node.utilities.errorAndTerminate
|
import net.corda.node.utilities.errorAndTerminate
|
||||||
import net.corda.node.verification.ExternalVerifierHandle
|
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||||
import net.corda.nodeapi.internal.ShutdownHook
|
import net.corda.nodeapi.internal.ShutdownHook
|
||||||
import net.corda.nodeapi.internal.addShutdownHook
|
import net.corda.nodeapi.internal.addShutdownHook
|
||||||
@ -201,8 +199,6 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
|
|
||||||
protected open val journalBufferTimeout : Int? = null
|
protected open val journalBufferTimeout : Int? = null
|
||||||
|
|
||||||
private val externalVerifierHandle = ExternalVerifierHandle(services).also { runOnStop += it::close }
|
|
||||||
|
|
||||||
private var shutdownHook: ShutdownHook? = null
|
private var shutdownHook: ShutdownHook? = null
|
||||||
|
|
||||||
// DISCUSSION
|
// 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. */
|
/** Starts a blocking event loop for message dispatch. */
|
||||||
fun run() {
|
fun run() {
|
||||||
internalRpcMessagingClient?.start(rpcBroker!!.serverControl)
|
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.Hashing
|
||||||
import com.google.common.hash.HashingInputStream
|
import com.google.common.hash.HashingInputStream
|
||||||
import com.google.common.io.CountingInputStream
|
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.CordaRuntimeException
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.contracts.ContractAttachment
|
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.DEPLOYED_CORDAPP_UPLOADER
|
||||||
import net.corda.core.internal.FetchAttachmentsFlow
|
import net.corda.core.internal.FetchAttachmentsFlow
|
||||||
import net.corda.core.internal.JarSignatureCollector
|
import net.corda.core.internal.JarSignatureCollector
|
||||||
|
import net.corda.core.internal.InternalAttachment
|
||||||
import net.corda.core.internal.NamedCacheFactory
|
import net.corda.core.internal.NamedCacheFactory
|
||||||
import net.corda.core.internal.P2P_UPLOADER
|
import net.corda.core.internal.P2P_UPLOADER
|
||||||
import net.corda.core.internal.RPC_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.VisibleForTesting
|
||||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION
|
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.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||||
|
import net.corda.core.internal.entries
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
import net.corda.core.internal.isUploaderTrusted
|
||||||
import net.corda.core.internal.readFully
|
import net.corda.core.internal.readFully
|
||||||
import net.corda.core.internal.utilities.ZipBombDetector
|
import net.corda.core.internal.utilities.ZipBombDetector
|
||||||
@ -53,6 +57,7 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.FilterInputStream
|
import java.io.FilterInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.FileAlreadyExistsException
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -109,7 +114,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
// Can be null for not-signed JARs.
|
// Can be null for not-signed JARs.
|
||||||
val allManifestEntries = jar.manifest?.entries?.keys?.toMutableList()
|
val allManifestEntries = jar.manifest?.entries?.keys?.toMutableList()
|
||||||
val extraFilesNotFoundInEntries = mutableListOf<JarEntry>()
|
val extraFilesNotFoundInEntries = mutableListOf<JarEntry>()
|
||||||
val manifestHasEntries= allManifestEntries != null && allManifestEntries.isNotEmpty()
|
val manifestHasEntries = !allManifestEntries.isNullOrEmpty()
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val cursor = jar.nextJarEntry ?: break
|
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.
|
// This is invoked by [InputStreamSerializer], which does NOT close the stream afterwards.
|
||||||
@Throws(IOException::class)
|
@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 {
|
return super.read(b, off, len).apply {
|
||||||
if (this == -1) {
|
if (this == -1) {
|
||||||
validate()
|
validate()
|
||||||
@ -256,12 +261,13 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class AttachmentImpl(
|
private class AttachmentImpl(
|
||||||
override val id: SecureHash,
|
override val id: SecureHash,
|
||||||
dataLoader: () -> ByteArray,
|
dataLoader: () -> ByteArray,
|
||||||
private val checkOnLoad: Boolean,
|
private val checkOnLoad: Boolean,
|
||||||
uploader: String?,
|
uploader: String?,
|
||||||
override val signerKeys: List<PublicKey>
|
override val signerKeys: List<PublicKey>,
|
||||||
) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
|
override val kotlinMetadataVersion: String?
|
||||||
|
) : AbstractAttachment(dataLoader, uploader), InternalAttachment, SerializeAsToken {
|
||||||
|
|
||||||
override fun open(): InputStream {
|
override fun open(): InputStream {
|
||||||
val stream = super.open()
|
val stream = super.open()
|
||||||
@ -270,22 +276,24 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class Token(
|
private class Token(
|
||||||
private val id: SecureHash,
|
private val id: SecureHash,
|
||||||
private val checkOnLoad: Boolean,
|
private val checkOnLoad: Boolean,
|
||||||
private val uploader: String?,
|
private val uploader: String?,
|
||||||
private val signerKeys: List<PublicKey>
|
private val signerKeys: List<PublicKey>,
|
||||||
|
private val kotlinMetadataVersion: String?
|
||||||
) : SerializationToken {
|
) : SerializationToken {
|
||||||
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
|
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
|
||||||
id,
|
id,
|
||||||
context.attachmentDataLoader(id),
|
context.attachmentDataLoader(id),
|
||||||
checkOnLoad,
|
checkOnLoad,
|
||||||
uploader,
|
uploader,
|
||||||
signerKeys
|
signerKeys,
|
||||||
|
kotlinMetadataVersion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toToken(context: SerializeAsTokenContext) =
|
override fun toToken(context: SerializeAsTokenContext) =
|
||||||
Token(id, checkOnLoad, uploader, signerKeys)
|
Token(id, checkOnLoad, uploader, signerKeys, kotlinMetadataVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
|
private val attachmentContentCache = NonInvalidatingWeightBasedCache(
|
||||||
@ -303,16 +311,27 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableMetadataApi::class)
|
||||||
private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment {
|
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(
|
val attachmentImpl = AttachmentImpl(
|
||||||
id = SecureHash.create(attachment.attId),
|
id = SecureHash.create(attachment.attId),
|
||||||
dataLoader = { attachment.content },
|
dataLoader = { attachment.content },
|
||||||
checkOnLoad = checkAttachmentsOnLoad,
|
checkOnLoad = checkAttachmentsOnLoad,
|
||||||
uploader = attachment.uploader,
|
uploader = attachment.uploader,
|
||||||
signerKeys = attachment.signers?.toList() ?: emptyList()
|
signerKeys = attachment.signers?.toList() ?: emptyList(),
|
||||||
|
kotlinMetadataVersion = kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last()?.toString()
|
||||||
)
|
)
|
||||||
val contracts = attachment.contractClassNames
|
val contracts = attachment.contractClassNames
|
||||||
return if (contracts != null && contracts.isNotEmpty()) {
|
return if (!contracts.isNullOrEmpty()) {
|
||||||
ContractAttachment.create(
|
ContractAttachment.create(
|
||||||
attachment = attachmentImpl,
|
attachment = attachmentImpl,
|
||||||
contract = contracts.first(),
|
contract = contracts.first(),
|
||||||
@ -336,7 +355,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
override fun importAttachment(jar: InputStream): AttachmentId {
|
override fun importAttachment(jar: InputStream): AttachmentId {
|
||||||
return import(jar, UNKNOWN_UPLOADER, null)
|
return import(jar, UNKNOWN_UPLOADER, null)
|
||||||
}
|
}
|
||||||
@ -360,7 +379,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
||||||
return try {
|
return try {
|
||||||
import(jar, uploader, filename)
|
import(jar, uploader, filename)
|
||||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
} catch (faee: FileAlreadyExistsException) {
|
||||||
AttachmentId.create(faee.message!!)
|
AttachmentId.create(faee.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -447,18 +466,14 @@ class NodeAttachmentService @JvmOverloads constructor(
|
|||||||
|
|
||||||
private fun getVersion(attachmentBytes: ByteArray) =
|
private fun getVersion(attachmentBytes: ByteArray) =
|
||||||
JarInputStream(attachmentBytes.inputStream()).use {
|
JarInputStream(attachmentBytes.inputStream()).use {
|
||||||
try {
|
it.manifest?.mainAttributes?.getValue(CORDAPP_CONTRACT_VERSION)?.toIntOrNull() ?: DEFAULT_CORDAPP_VERSION
|
||||||
it.manifest?.mainAttributes?.getValue(CORDAPP_CONTRACT_VERSION)?.toInt() ?: DEFAULT_CORDAPP_VERSION
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
DEFAULT_CORDAPP_VERSION
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||||
return try {
|
return try {
|
||||||
import(jar, UNKNOWN_UPLOADER, null)
|
import(jar, UNKNOWN_UPLOADER, null)
|
||||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
} catch (faee: FileAlreadyExistsException) {
|
||||||
AttachmentId.create(faee.message!!)
|
AttachmentId.create(faee.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,16 @@ package net.corda.node.verification
|
|||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.internal.AbstractAttachment
|
import net.corda.core.internal.AbstractAttachment
|
||||||
import net.corda.core.internal.copyTo
|
import net.corda.core.internal.copyTo
|
||||||
|
import net.corda.core.internal.level
|
||||||
import net.corda.core.internal.mapToSet
|
import net.corda.core.internal.mapToSet
|
||||||
import net.corda.core.internal.readFully
|
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.serialization.serialize
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.Try
|
import net.corda.core.utilities.Try
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
|
||||||
import net.corda.serialization.internal.GeneratedAttachment
|
import net.corda.serialization.internal.GeneratedAttachment
|
||||||
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.customSerializers
|
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.customSerializers
|
||||||
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.serializationWhitelists
|
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.
|
* 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 {
|
companion object {
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
|
|
||||||
@ -69,12 +74,12 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var connection: Connection? = null
|
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")
|
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.
|
// 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
|
// 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.
|
// 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)
|
val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures)
|
||||||
|
|
||||||
// To keep things simple the verifier only supports one verification request at a time.
|
// 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) {
|
private fun processVerifierRequest(request: VerifierRequest, connection: Connection) {
|
||||||
val result = when (request) {
|
val result = when (request) {
|
||||||
is GetParties -> PartiesResult(serviceHub.getParties(request.keys))
|
is GetParties -> PartiesResult(verificationSupport.getParties(request.keys))
|
||||||
is GetAttachment -> AttachmentResult(prepare(serviceHub.attachments.openAttachment(request.id)))
|
is GetAttachment -> AttachmentResult(prepare(verificationSupport.getAttachment(request.id)))
|
||||||
is GetAttachments -> AttachmentsResult(serviceHub.getAttachments(request.ids).map(::prepare))
|
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map(::prepare))
|
||||||
is GetNetworkParameters -> NetworkParametersResult(serviceHub.getNetworkParameters(request.id))
|
is GetNetworkParameters -> NetworkParametersResult(verificationSupport.getNetworkParameters(request.id))
|
||||||
is GetTrustedClassAttachment -> TrustedClassAttachmentResult(serviceHub.getTrustedClassAttachment(request.className)?.id)
|
is GetTrustedClassAttachment -> TrustedClassAttachmentResult(verificationSupport.getTrustedClassAttachment(request.className)?.id)
|
||||||
}
|
}
|
||||||
log.debug { "Sending response to external verifier: $result" }
|
log.debug { "Sending response to external verifier: $result" }
|
||||||
connection.toVerifier.writeCordaSerializable(result)
|
connection.toVerifier.writeCordaSerializable(result)
|
||||||
@ -152,7 +157,7 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
|||||||
|
|
||||||
private fun prepare(attachment: Attachment?): AttachmentWithTrust? {
|
private fun prepare(attachment: Attachment?): AttachmentWithTrust? {
|
||||||
if (attachment == null) return null
|
if (attachment == null) return null
|
||||||
val isTrusted = serviceHub.isAttachmentTrusted(attachment)
|
val isTrusted = verificationSupport.isAttachmentTrusted(attachment)
|
||||||
val attachmentForSer = when (attachment) {
|
val attachmentForSer = when (attachment) {
|
||||||
// The Attachment retrieved from the database is not serialisable, so we have to convert it into one
|
// 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)
|
is AbstractAttachment -> GeneratedAttachment(attachment.open().readFully(), attachment.uploader)
|
||||||
@ -188,20 +193,20 @@ class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoC
|
|||||||
"-jar",
|
"-jar",
|
||||||
"$verifierJar",
|
"$verifierJar",
|
||||||
"${server.localPort}",
|
"${server.localPort}",
|
||||||
System.getProperty("log4j2.level")?.lowercase() ?: "info"
|
log.level.name.lowercase()
|
||||||
)
|
)
|
||||||
log.debug { "Verifier command: $command" }
|
log.debug { "Verifier command: $command" }
|
||||||
val logsDirectory = (serviceHub.configuration.baseDirectory / "logs").createDirectories()
|
val logsDirectory = (baseDirectory / "logs").createDirectories()
|
||||||
verifierProcess = ProcessBuilder(command)
|
verifierProcess = ProcessBuilder(command)
|
||||||
.redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile()))
|
.redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile()))
|
||||||
.redirectError(Redirect.appendTo((logsDirectory / "verifier-stderr.log").toFile()))
|
.redirectError(Redirect.appendTo((logsDirectory / "verifier-stderr.log").toFile()))
|
||||||
.directory(serviceHub.configuration.baseDirectory.toFile())
|
.directory(baseDirectory.toFile())
|
||||||
.start()
|
.start()
|
||||||
log.info("External verifier process started; PID ${verifierProcess.pid()}")
|
log.info("External verifier process started; PID ${verifierProcess.pid()}")
|
||||||
|
|
||||||
verifierProcess.onExit().whenComplete { _, _ ->
|
verifierProcess.onExit().whenComplete { _, _ ->
|
||||||
if (connection != null) {
|
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.")
|
"Please check verifier logs for more details.")
|
||||||
}
|
}
|
||||||
// Allow a new process to be started on the next verification request
|
// 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)
|
toVerifier = DataOutputStream(socket.outputStream)
|
||||||
fromVerifier = DataInputStream(socket.inputStream)
|
fromVerifier = DataInputStream(socket.inputStream)
|
||||||
|
|
||||||
val cordapps = serviceHub.cordappProvider.cordapps
|
val cordapps = verificationSupport.cordappProvider.cordapps
|
||||||
val initialisation = Initialisation(
|
val initialisation = Initialisation(
|
||||||
customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name },
|
customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name },
|
||||||
serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name },
|
serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name },
|
||||||
System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization
|
System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization
|
||||||
serializedCurrentNetworkParameters = serviceHub.networkParameters.serialize()
|
serializedCurrentNetworkParameters = verificationSupport.networkParameters.serialize()
|
||||||
)
|
)
|
||||||
toVerifier.writeCordaSerializable(initialisation)
|
toVerifier.writeCordaSerializable(initialisation)
|
||||||
}
|
}
|
@ -110,6 +110,7 @@ include 'testing:cordapps:dbfailure:dbfworkflows'
|
|||||||
include 'testing:cordapps:missingmigration'
|
include 'testing:cordapps:missingmigration'
|
||||||
include 'testing:cordapps:sleeping'
|
include 'testing:cordapps:sleeping'
|
||||||
include 'testing:cordapps:cashobservers'
|
include 'testing:cordapps:cashobservers'
|
||||||
|
include 'testing:cordapps:4.11-workflows'
|
||||||
|
|
||||||
// Common libraries - start
|
// Common libraries - start
|
||||||
include 'common-validation'
|
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.Closeable
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.NoSuchFileException
|
import java.nio.file.NoSuchFileException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@ -17,7 +18,11 @@ import java.util.jar.Attributes
|
|||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
import java.util.jar.JarOutputStream
|
import java.util.jar.JarOutputStream
|
||||||
import java.util.jar.Manifest
|
import java.util.jar.Manifest
|
||||||
|
import kotlin.io.path.deleteExisting
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +41,6 @@ object JarSignatureTestUtils {
|
|||||||
private fun Path.executeProcess(vararg command: String) {
|
private fun Path.executeProcess(vararg command: String) {
|
||||||
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
|
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
|
||||||
assertEquals(0, ProcessBuilder()
|
assertEquals(0, ProcessBuilder()
|
||||||
.inheritIO()
|
|
||||||
.redirectOutput(shredder)
|
.redirectOutput(shredder)
|
||||||
.redirectError(shredder)
|
.redirectError(shredder)
|
||||||
.directory(this.toFile())
|
.directory(this.toFile())
|
||||||
@ -69,6 +73,16 @@ object JarSignatureTestUtils {
|
|||||||
return ks.getCertificate(alias).publicKey
|
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 {
|
fun Path.getPublicKey(alias: String, storeName: String, storePassword: String) : PublicKey {
|
||||||
val ks = loadKeyStore(this.resolve(storeName), storePassword)
|
val ks = loadKeyStore(this.resolve(storeName), storePassword)
|
||||||
return ks.getCertificate(alias).publicKey
|
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.contracts.TransactionState
|
||||||
import net.corda.core.cordapp.CordappProvider
|
import net.corda.core.cordapp.CordappProvider
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
|
import net.corda.core.internal.AbstractAttachment
|
||||||
import net.corda.core.internal.PLATFORM_VERSION
|
import net.corda.core.internal.PLATFORM_VERSION
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
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.requireSupportedHashType
|
||||||
import net.corda.core.internal.telemetry.TelemetryComponent
|
import net.corda.core.internal.telemetry.TelemetryComponent
|
||||||
import net.corda.core.internal.telemetry.TelemetryServiceImpl
|
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.internal.verification.VerifyingServiceHub
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
import net.corda.core.messaging.FlowHandle
|
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.
|
// Because Kotlin is dumb and makes not publicly visible objects public, thus changing the public API.
|
||||||
private val mockStateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()
|
private val mockStateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()
|
||||||
|
|
||||||
private val dummyAttachment by lazy {
|
private val dummyAttachment: Attachment by lazy {
|
||||||
val inputStream = ByteArrayOutputStream().apply {
|
object : AbstractAttachment(
|
||||||
ZipOutputStream(this).use {
|
{
|
||||||
with(it) {
|
val baos = ByteArrayOutputStream()
|
||||||
putNextEntry(ZipEntry(JarFile.MANIFEST_NAME))
|
ZipOutputStream(baos).use { zip ->
|
||||||
}
|
zip.putNextEntry(ZipEntry(JarFile.MANIFEST_NAME))
|
||||||
}
|
}
|
||||||
}.toByteArray().inputStream()
|
baos.toByteArray()
|
||||||
val attachment = object : Attachment {
|
},
|
||||||
override val id get() = throw UnsupportedOperationException()
|
null
|
||||||
override fun open() = inputStream
|
) {
|
||||||
override val signerKeys get() = throw UnsupportedOperationException()
|
override val id: SecureHash by lazy(attachmentData::sha256)
|
||||||
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 loadState(stateRef: StateRef): TransactionState<*> = mockServices.loadState(stateRef)
|
||||||
|
|
||||||
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = mockServices.loadStates(stateRefs)
|
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(':test-common')
|
||||||
implementation project(':client:rpc')
|
implementation project(':client:rpc')
|
||||||
|
|
||||||
|
implementation "com.google.guava:guava:$guava_version"
|
||||||
implementation "com.typesafe:config:$typesafe_config_version"
|
implementation "com.typesafe:config:$typesafe_config_version"
|
||||||
implementation "org.slf4j:slf4j-api:$slf4j_version"
|
implementation "org.slf4j:slf4j-api:$slf4j_version"
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
package net.corda.smoketesting
|
package net.corda.smoketesting
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
|
||||||
import com.typesafe.config.ConfigFactory.empty
|
import com.typesafe.config.ConfigFactory.empty
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import com.typesafe.config.ConfigValue
|
import com.typesafe.config.ConfigValue
|
||||||
import com.typesafe.config.ConfigValueFactory
|
import com.typesafe.config.ConfigValueFactory
|
||||||
|
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.config.toConfig
|
import net.corda.nodeapi.internal.config.toConfig
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
class NodeConfig(
|
class NodeParams @JvmOverloads constructor(
|
||||||
val legalName: CordaX500Name,
|
val legalName: CordaX500Name,
|
||||||
val p2pPort: Int,
|
val p2pPort: Int,
|
||||||
val rpcPort: Int,
|
val rpcPort: Int,
|
||||||
val rpcAdminPort: Int,
|
val rpcAdminPort: Int,
|
||||||
val isNotary: Boolean,
|
|
||||||
val users: List<User>,
|
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 {
|
companion object {
|
||||||
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
||||||
@ -24,12 +27,7 @@ class NodeConfig(
|
|||||||
|
|
||||||
val commonName: String get() = legalName.organisation
|
val commonName: String get() = legalName.organisation
|
||||||
|
|
||||||
/*
|
fun createNodeConfig(isNotary: Boolean): String {
|
||||||
* The configuration object depends upon the networkMap,
|
|
||||||
* which is mutable.
|
|
||||||
*/
|
|
||||||
//TODO Make use of Any.toConfig
|
|
||||||
private fun toFileConfig(): Config {
|
|
||||||
val config = empty()
|
val config = empty()
|
||||||
.withValue("myLegalName", valueFor(legalName.toString()))
|
.withValue("myLegalName", valueFor(legalName.toString()))
|
||||||
.withValue("p2pAddress", addressValueFor(p2pPort))
|
.withValue("p2pAddress", addressValueFor(p2pPort))
|
||||||
@ -44,11 +42,9 @@ class NodeConfig(
|
|||||||
config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true)))
|
config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true)))
|
||||||
} else {
|
} else {
|
||||||
config
|
config
|
||||||
}
|
}.root().render(renderOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toText(): String = toFileConfig().root().render(renderOptions)
|
|
||||||
|
|
||||||
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
|
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
|
||||||
|
|
||||||
private fun addressValueFor(port: Int) = valueFor("localhost:$port")
|
private fun addressValueFor(port: Int) = valueFor("localhost:$port")
|
@ -1,16 +1,20 @@
|
|||||||
package net.corda.smoketesting
|
package net.corda.smoketesting
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
import net.corda.client.rpc.CordaRPCConnection
|
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.deleteRecursively
|
||||||
import net.corda.core.internal.toPath
|
import net.corda.core.internal.toPath
|
||||||
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.NotaryInfo
|
import net.corda.core.node.NotaryInfo
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
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.nodeapi.internal.rpc.client.AMQPClientSerializationScheme
|
||||||
import net.corda.testing.common.internal.asContextEnv
|
import net.corda.testing.common.internal.asContextEnv
|
||||||
import net.corda.testing.common.internal.checkNotOnClasspath
|
import net.corda.testing.common.internal.checkNotOnClasspath
|
||||||
@ -20,15 +24,18 @@ import java.nio.file.Paths
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId.systemDefault
|
import java.time.ZoneId.systemDefault
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit.SECONDS
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.createDirectory
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
import kotlin.io.path.writeText
|
import kotlin.io.path.writeText
|
||||||
|
|
||||||
class NodeProcess(
|
class NodeProcess(
|
||||||
private val config: NodeConfig,
|
private val config: NodeParams,
|
||||||
private val nodeDir: Path,
|
val nodeDir: Path,
|
||||||
private val node: Process,
|
private val node: Process,
|
||||||
private val client: CordaRPCClient
|
private val client: CordaRPCClient
|
||||||
) : AutoCloseable {
|
) : AutoCloseable {
|
||||||
@ -43,6 +50,7 @@ class NodeProcess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
|
if (!node.isAlive) return
|
||||||
log.info("Stopping node '${config.commonName}'")
|
log.info("Stopping node '${config.commonName}'")
|
||||||
node.destroy()
|
node.destroy()
|
||||||
if (!node.waitFor(60, SECONDS)) {
|
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
|
// 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.
|
// as a CorDapp for the nodes.
|
||||||
class Factory(private val buildDirectory: Path = Paths.get("build")) {
|
class Factory(
|
||||||
val cordaJar: Path by lazy {
|
private val baseNetworkParameters: NetworkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION),
|
||||||
val cordaJarUrl = requireNotNull(this::class.java.getResource("/corda.jar")) {
|
private val buildDirectory: Path = Paths.get("build")
|
||||||
"corda.jar could not be found in classpath"
|
) : AutoCloseable {
|
||||||
}
|
companion object {
|
||||||
cordaJarUrl.toPath()
|
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 {
|
init {
|
||||||
checkNotOnClasspath("net.corda.node.Corda") {
|
checkNotOnClasspath("net.corda.node.Corda") {
|
||||||
"Smoke test has the node in its classpath. Please remove the offending dependency."
|
"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 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) {
|
val notariesParams = Lists.asList(first, rest)
|
||||||
try {
|
val notaryInfos = notariesParams.map { notaryParams ->
|
||||||
networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaries = listOf(notaryInfo)))
|
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) {
|
} catch (_: IllegalStateException) {
|
||||||
// Assuming serialization env not in context.
|
// Assuming serialization env not in context.
|
||||||
AMQPClientSerializationScheme.createSerializationEnv().asContextEnv {
|
AMQPClientSerializationScheme.createSerializationEnv().asContextEnv {
|
||||||
networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaries = listOf(notaryInfo)))
|
NetworkParametersCopier(networkParameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
networkParametersCopier.install(nodeDir)
|
|
||||||
|
return notariesParams.map { createNode(it, isNotary = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun baseDirectory(config: NodeConfig): Path = nodesDirectory / config.commonName
|
fun createNode(params: NodeParams): NodeProcess = createNode(params, isNotary = false)
|
||||||
|
|
||||||
fun create(config: NodeConfig): NodeProcess {
|
private fun createNode(params: NodeParams, isNotary: Boolean): NodeProcess {
|
||||||
val nodeDir = baseDirectory(config).createDirectories()
|
check(::networkParametersCopier.isInitialized) { "Notary not created. Please call `creatNotaries` first." }
|
||||||
|
|
||||||
|
val nodeDir = baseDirectory(params).createDirectories()
|
||||||
log.info("Node directory: {}", nodeDir)
|
log.info("Node directory: {}", nodeDir)
|
||||||
if (config.isNotary) {
|
val cordappsDir = (nodeDir / CORDAPPS_DIR_NAME).createDirectory()
|
||||||
require(notaryParty == null) { "Only one notary can be created." }
|
params.cordappJars.forEach { it.copyToDirectory(cordappsDir) }
|
||||||
notaryParty = DevIdentityGenerator.installKeyStoreWithNodeIdentity(nodeDir, config.legalName)
|
(nodeDir / "node.conf").writeText(params.createNodeConfig(isNotary))
|
||||||
} else {
|
networkParametersCopier.install(nodeDir)
|
||||||
require(notaryParty != null) { "Notary not created. Please call `create` with a notary config first." }
|
nodeInfoFilesCopier.addConfig(nodeDir)
|
||||||
}
|
|
||||||
|
|
||||||
(nodeDir / "node.conf").writeText(config.toText())
|
createSchema(nodeDir, params.version)
|
||||||
createNetworkParameters(NotaryInfo(notaryParty!!, true), nodeDir)
|
val process = startNode(nodeDir, params.version)
|
||||||
|
val client = CordaRPCClient(NetworkHostAndPort("localhost", params.rpcPort), params.clientRpcConfig)
|
||||||
createSchema(nodeDir)
|
waitForNode(process, params, client)
|
||||||
val process = startNode(nodeDir)
|
val nodeProcess = NodeProcess(params, nodeDir, process, client)
|
||||||
val client = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort))
|
nodes!! += nodeProcess
|
||||||
waitForNode(process, config, client)
|
return nodeProcess
|
||||||
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()
|
val executor = Executors.newSingleThreadScheduledExecutor()
|
||||||
try {
|
try {
|
||||||
executor.scheduleWithFixedDelay({
|
executor.scheduleWithFixedDelay({
|
||||||
@ -129,7 +166,7 @@ class NodeProcess(
|
|||||||
// Cancel the "setup" task now that we've created the RPC client.
|
// Cancel the "setup" task now that we've created the RPC client.
|
||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
} catch (e: Exception) {
|
} 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)
|
}, 5, 1, SECONDS)
|
||||||
|
|
||||||
@ -147,10 +184,10 @@ class NodeProcess(
|
|||||||
class SchemaCreationFailedError(nodeDir: Path) : Exception("Creating node schema failed for $nodeDir")
|
class SchemaCreationFailedError(nodeDir: Path) : Exception("Creating node schema failed for $nodeDir")
|
||||||
|
|
||||||
|
|
||||||
private fun createSchema(nodeDir: Path){
|
private fun createSchema(nodeDir: Path, version: String?) {
|
||||||
val process = startNode(nodeDir, "run-migration-scripts", "--core-schemas", "--app-schemas")
|
val process = startNode(nodeDir, version, "run-migration-scripts", "--core-schemas", "--app-schemas")
|
||||||
if (!process.waitFor(schemaCreationTimeOutSeconds, SECONDS)) {
|
if (!process.waitFor(schemaCreationTimeOutSeconds, SECONDS)) {
|
||||||
process.destroy()
|
process.destroyForcibly()
|
||||||
throw SchemaCreationTimedOutError(nodeDir)
|
throw SchemaCreationTimedOutError(nodeDir)
|
||||||
}
|
}
|
||||||
if (process.exitValue() != 0) {
|
if (process.exitValue() != 0) {
|
||||||
@ -158,8 +195,9 @@ class NodeProcess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNode(nodeDir: Path, vararg extraArgs: String): Process {
|
private fun startNode(nodeDir: Path, version: String?, vararg extraArgs: String): Process {
|
||||||
val command = arrayListOf(javaPath.toString(), "-Dcapsule.log=verbose", "-jar", cordaJar.toString())
|
val cordaJar = getCordaJarInfo(version ?: "")
|
||||||
|
val command = arrayListOf("${cordaJar.javaPath}", "-Dcapsule.log=verbose", "-jar", "${cordaJar.jarPath}", "--logging-level=debug")
|
||||||
command += extraArgs
|
command += extraArgs
|
||||||
val now = formatter.format(Instant.now())
|
val now = formatter.format(Instant.now())
|
||||||
val builder = ProcessBuilder()
|
val builder = ProcessBuilder()
|
||||||
@ -171,7 +209,17 @@ class NodeProcess(
|
|||||||
"CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString()
|
"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.*
|
||||||
import net.corda.core.internal.cordapp.CordappProviderInternal
|
import net.corda.core.internal.cordapp.CordappProviderInternal
|
||||||
import net.corda.core.internal.notary.NotaryService
|
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.ServiceHub
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.StatesToRecord
|
import net.corda.core.node.StatesToRecord
|
||||||
@ -139,6 +140,9 @@ data class TestTransactionDSLInterpreter private constructor(
|
|||||||
return ledgerInterpreter.services.loadContractAttachment(stateRef)
|
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 recordUnnotarisedTransaction(txn: SignedTransaction) {}
|
||||||
|
|
||||||
override fun removeUnnotarisedTransaction(id: SecureHash) {}
|
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.internal.readFully
|
||||||
import net.corda.core.node.services.AttachmentId
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
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.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.nodeapi.internal.withContractsInJar
|
import net.corda.nodeapi.internal.withContractsInJar
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.FileAlreadyExistsException
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
|
||||||
import java.util.jar.Attributes
|
import java.util.jar.Attributes
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
@ -33,7 +37,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
|||||||
/** A map of the currently stored files by their [SecureHash] */
|
/** A map of the currently stored files by their [SecureHash] */
|
||||||
val files: Map<SecureHash, Pair<Attachment, ByteArray>> get() = _files
|
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): AttachmentId = importAttachment(jar, UNKNOWN_UPLOADER, null)
|
||||||
|
|
||||||
override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
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)
|
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||||
return try {
|
return try {
|
||||||
importAttachment(jar, UNKNOWN_UPLOADER, null)
|
importAttachment(jar, UNKNOWN_UPLOADER, null)
|
||||||
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
} catch (e: FileAlreadyExistsException) {
|
||||||
AttachmentId.create(e.message!!)
|
AttachmentId.create(e.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +113,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
|||||||
val baseAttachment = MockAttachment({ bytes }, sha256, signers, uploader)
|
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 version = try { Integer.parseInt(baseAttachment.openAsJAR().manifest?.mainAttributes?.getValue(Attributes.Name.IMPLEMENTATION_VERSION)) } catch (e: Exception) { DEFAULT_CORDAPP_VERSION }
|
||||||
val attachment =
|
val attachment =
|
||||||
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment
|
if (contractClassNames.isNullOrEmpty()) baseAttachment
|
||||||
else {
|
else {
|
||||||
contractClassNames.map {contractClassName ->
|
contractClassNames.map {contractClassName ->
|
||||||
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty(), signers, uploader)
|
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty(), signers, uploader)
|
||||||
|
@ -124,9 +124,8 @@ class ExternalVerifier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createAppClassLoader(): ClassLoader {
|
private fun createAppClassLoader(): ClassLoader {
|
||||||
val cordappJarUrls = (baseDirectory / "cordapps").listDirectoryEntries()
|
val cordappJarUrls = (baseDirectory / "cordapps").listDirectoryEntries("*.jar")
|
||||||
.stream()
|
.stream()
|
||||||
.filter { it.toString().endsWith(".jar") }
|
|
||||||
.map { it.toUri().toURL() }
|
.map { it.toUri().toURL() }
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
log.debug { "CorDapps: ${cordappJarUrls?.joinToString()}" }
|
log.debug { "CorDapps: ${cordappJarUrls?.joinToString()}" }
|
||||||
@ -136,7 +135,7 @@ class ExternalVerifier(
|
|||||||
private fun verifyTransaction(request: VerificationRequest) {
|
private fun verifyTransaction(request: VerificationRequest) {
|
||||||
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
|
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
|
||||||
val result: Try<Unit> = try {
|
val result: Try<Unit> = try {
|
||||||
request.stx.verifyInternal(verificationContext, request.checkSufficientSignatures)
|
request.stx.verifyInProcess(verificationContext, request.checkSufficientSignatures)
|
||||||
log.info("${request.stx} verified")
|
log.info("${request.stx} verified")
|
||||||
Try.Success(Unit)
|
Try.Success(Unit)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
@ -10,15 +10,14 @@ import kotlin.io.path.div
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
object Main {
|
object Main {
|
||||||
private val log = loggerFor<Main>()
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val port = args[0].toInt()
|
val port = args[0].toInt()
|
||||||
val loggingLevel = args[0]
|
val loggingLevel = args[1]
|
||||||
val baseDirectory = Path.of("").toAbsolutePath()
|
val baseDirectory = Path.of("").toAbsolutePath()
|
||||||
|
|
||||||
initLogging(baseDirectory, loggingLevel)
|
initLogging(baseDirectory, loggingLevel)
|
||||||
|
val log = loggerFor<Main>()
|
||||||
|
|
||||||
log.info("External verifier started; PID ${ProcessHandle.current().pid()}")
|
log.info("External verifier started; PID ${ProcessHandle.current().pid()}")
|
||||||
log.info("Node base directory: $baseDirectory")
|
log.info("Node base directory: $baseDirectory")
|
||||||
|
Loading…
Reference in New Issue
Block a user