mirror of
https://github.com/corda/corda.git
synced 2025-02-20 01:16:42 +00:00
Merge branch 'master' into m4ksio_gradle_no_o_fix
# Conflicts: # gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy
This commit is contained in:
commit
3f04b45010
@ -1576,24 +1576,8 @@ public interface net.corda.core.node.services.KeyManagementService
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SignableData, java.security.PublicKey)
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.DigitalSignature$WithKey sign(byte[], java.security.PublicKey)
|
||||
##
|
||||
public interface net.corda.core.node.services.NetworkMapCache
|
||||
public abstract void clearNetworkMapCache()
|
||||
@org.jetbrains.annotations.NotNull public abstract List getAllNodes()
|
||||
@org.jetbrains.annotations.NotNull public abstract rx.Observable getChanged()
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByAddress(net.corda.core.utilities.NetworkHostAndPort)
|
||||
public interface net.corda.core.node.services.NetworkMapCache extends net.corda.core.node.services.NetworkMapCacheBase
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalIdentity(net.corda.core.identity.AbstractParty)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady()
|
||||
@org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey)
|
||||
@org.jetbrains.annotations.NotNull public abstract List getNodesByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities()
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate getPeerCertificateByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
public abstract boolean isNotary(net.corda.core.identity.Party)
|
||||
public abstract boolean isValidatingNotary(net.corda.core.identity.Party)
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track()
|
||||
##
|
||||
public abstract static class net.corda.core.node.services.NetworkMapCache$MapChange extends java.lang.Object
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo getNode()
|
||||
@ -1627,6 +1611,24 @@ public static final class net.corda.core.node.services.NetworkMapCache$MapChange
|
||||
public int hashCode()
|
||||
public String toString()
|
||||
##
|
||||
public interface net.corda.core.node.services.NetworkMapCacheBase
|
||||
public abstract void clearNetworkMapCache()
|
||||
@org.jetbrains.annotations.NotNull public abstract List getAllNodes()
|
||||
@org.jetbrains.annotations.NotNull public abstract rx.Observable getChanged()
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByAddress(net.corda.core.utilities.NetworkHostAndPort)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady()
|
||||
@org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey)
|
||||
@org.jetbrains.annotations.NotNull public abstract List getNodesByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities()
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
@org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate getPeerCertificateByLegalName(net.corda.core.identity.CordaX500Name)
|
||||
public abstract boolean isNotary(net.corda.core.identity.Party)
|
||||
public abstract boolean isValidatingNotary(net.corda.core.identity.Party)
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track()
|
||||
##
|
||||
public abstract class net.corda.core.node.services.NotaryService extends net.corda.core.serialization.SingletonSerializeAsToken
|
||||
public <init>()
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowLogic createServiceFlow(net.corda.core.flows.FlowSession)
|
||||
@ -2435,6 +2437,17 @@ public final class net.corda.core.serialization.MissingAttachmentsException exte
|
||||
public <init>(List)
|
||||
@org.jetbrains.annotations.NotNull public final List getIds()
|
||||
##
|
||||
public final class net.corda.core.serialization.ObjectWithCompatibleContext extends java.lang.Object
|
||||
public <init>(Object, net.corda.core.serialization.SerializationContext)
|
||||
@org.jetbrains.annotations.NotNull public final Object component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.serialization.ObjectWithCompatibleContext copy(Object, net.corda.core.serialization.SerializationContext)
|
||||
public boolean equals(Object)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getContext()
|
||||
@org.jetbrains.annotations.NotNull public final Object getObj()
|
||||
public int hashCode()
|
||||
public String toString()
|
||||
##
|
||||
public final class net.corda.core.serialization.SerializationAPIKt extends java.lang.Object
|
||||
@org.jetbrains.annotations.NotNull public static final net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationFactory, net.corda.core.serialization.SerializationContext)
|
||||
##
|
||||
@ -2476,6 +2489,7 @@ public abstract class net.corda.core.serialization.SerializationFactory extends
|
||||
public <init>()
|
||||
public final Object asCurrent(kotlin.jvm.functions.Function1)
|
||||
@org.jetbrains.annotations.NotNull public abstract Object deserialize(net.corda.core.utilities.ByteSequence, Class, net.corda.core.serialization.SerializationContext)
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.ObjectWithCompatibleContext deserializeWithCompatibleContext(net.corda.core.utilities.ByteSequence, Class, net.corda.core.serialization.SerializationContext)
|
||||
@org.jetbrains.annotations.Nullable public final net.corda.core.serialization.SerializationContext getCurrentContext()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getDefaultContext()
|
||||
@org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationContext)
|
||||
@ -2923,7 +2937,7 @@ public static final class net.corda.core.utilities.NonEmptySet$iterator$1 extend
|
||||
##
|
||||
public class net.corda.core.utilities.OpaqueBytes extends net.corda.core.utilities.ByteSequence
|
||||
public <init>(byte[])
|
||||
@org.jetbrains.annotations.NotNull public byte[] getBytes()
|
||||
@org.jetbrains.annotations.NotNull public final byte[] getBytes()
|
||||
public int getOffset()
|
||||
public int getSize()
|
||||
public static final net.corda.core.utilities.OpaqueBytes$Companion Companion
|
||||
|
@ -10,13 +10,14 @@ if [ ! -f $apiCurrent ]; then
|
||||
exit -1
|
||||
fi
|
||||
|
||||
diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt`
|
||||
echo "Diff contents:"
|
||||
# Remove the two header lines from the diff output.
|
||||
diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail --lines=+3`
|
||||
echo "Diff contents:"
|
||||
echo "$diffContents"
|
||||
echo
|
||||
|
||||
# A removed line means that an API was either deleted or modified.
|
||||
removals=$(echo "$diffContents" | grep "^-\s")
|
||||
removals=$(echo "$diffContents" | grep "^-")
|
||||
removalCount=`grep -v "^$" <<EOF | wc -l
|
||||
$removals
|
||||
EOF
|
||||
@ -29,7 +30,7 @@ if [ $removalCount -gt 0 ]; then
|
||||
fi
|
||||
|
||||
# Adding new abstract methods could also break the API.
|
||||
newAbstracts=$(echo "$diffContents" | grep "^+\s" | grep "\(public\|protected\) abstract")
|
||||
newAbstracts=$(echo "$diffContents" | grep "^+" | grep "\(public\|protected\) abstract")
|
||||
abstractCount=`grep -v "^$" <<EOF | wc -l
|
||||
$newAbstracts
|
||||
EOF
|
||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -60,6 +60,8 @@
|
||||
<module name="jfx_integrationTest" target="1.8" />
|
||||
<module name="jfx_main" target="1.8" />
|
||||
<module name="jfx_test" target="1.8" />
|
||||
<module name="kryo-hook_main" target="1.8" />
|
||||
<module name="kryo-hook_test" target="1.8" />
|
||||
<module name="loadtest_main" target="1.8" />
|
||||
<module name="loadtest_test" target="1.8" />
|
||||
<module name="mock_main" target="1.8" />
|
||||
|
11
build.gradle
11
build.gradle
@ -4,7 +4,7 @@ buildscript {
|
||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_release_version = "1.1-SNAPSHOT"
|
||||
ext.corda_release_version = "2.0-SNAPSHOT"
|
||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
||||
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
|
||||
ext.corda_platform_version = 1
|
||||
@ -231,7 +231,6 @@ tasks.withType(Test) {
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "O=Controller,OU=corda,L=London,C=GB"
|
||||
node {
|
||||
name "O=Controller,OU=corda,L=London,C=GB"
|
||||
notary = [validating : true]
|
||||
@ -294,12 +293,16 @@ artifactory {
|
||||
publish {
|
||||
contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory'
|
||||
repository {
|
||||
repoKey = 'corda-releases'
|
||||
repoKey = 'corda-dev'
|
||||
username = 'teamcity'
|
||||
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
||||
}
|
||||
|
||||
defaults {
|
||||
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities')
|
||||
// Root project applies the plugin (for this block) but does not need to be published
|
||||
if(project != rootProject) {
|
||||
publications(project.extensions.publish.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,11 +28,13 @@ import static kotlin.test.AssertionsKt.assertEquals;
|
||||
import static net.corda.finance.Currencies.DOLLARS;
|
||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||
import static net.corda.node.services.FlowPermissions.startFlowPermission;
|
||||
import static net.corda.testing.CoreTestUtils.setCordappPackages;
|
||||
import static net.corda.testing.CoreTestUtils.unsetCordappPackages;
|
||||
import static net.corda.testing.TestConstants.getALICE;
|
||||
|
||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
public CordaRPCJavaClientTest() {
|
||||
super(Arrays.asList("net.corda.finance.contracts", CashSchemaV1.class.getPackage().getName()));
|
||||
}
|
||||
|
||||
private List<String> perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class));
|
||||
private Set<String> permSet = new HashSet<>(perms);
|
||||
private User rpcUser = new User("user1", "test", permSet);
|
||||
@ -49,17 +51,14 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws ExecutionException, InterruptedException {
|
||||
setCordappPackages("net.corda.finance.contracts");
|
||||
CordaFuture<StartedNode<Node>> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true);
|
||||
node = nodeFuture.get();
|
||||
node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE));
|
||||
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()));
|
||||
}
|
||||
|
||||
@After
|
||||
public void done() throws IOException {
|
||||
connection.close();
|
||||
unsetCordappPackages();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -23,7 +23,7 @@ import org.junit.rules.ExpectedException
|
||||
@CordaSerializable
|
||||
data class Packet(val x: () -> Long)
|
||||
|
||||
class BlacklistKotlinClosureTest : NodeBasedTest() {
|
||||
class BlacklistKotlinClosureTest : NodeBasedTest(listOf("net.corda.client.rpc")) {
|
||||
companion object {
|
||||
@Suppress("UNUSED") val logger = loggerFor<BlacklistKotlinClosureTest>()
|
||||
const val EVIL: Long = 666
|
||||
@ -66,7 +66,6 @@ class BlacklistKotlinClosureTest : NodeBasedTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setCordappPackages("net.corda.client.rpc")
|
||||
aliceNode = startNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
bobNode = startNode(BOB.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
bobNode.registerInitiatedFlow(RemoteFlowC::class.java)
|
||||
@ -78,7 +77,6 @@ class BlacklistKotlinClosureTest : NodeBasedTest() {
|
||||
connection?.close()
|
||||
bobNode.internals.stop()
|
||||
aliceNode.internals.stop()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -2,6 +2,7 @@ package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.messaging.FlowProgressHandle
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
@ -23,8 +24,6 @@ import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
@ -34,7 +33,7 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CordaRPCClientTest : NodeBasedTest() {
|
||||
class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) {
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
@ -49,16 +48,13 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setCordappPackages("net.corda.finance.contracts")
|
||||
node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
node.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
connection?.close()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -92,6 +92,7 @@ class RPCStabilityTests {
|
||||
startAndStop()
|
||||
}
|
||||
val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor)
|
||||
|
||||
assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter)
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
@ -74,9 +74,9 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
}
|
||||
|
||||
private void copyFinanceCordapp() {
|
||||
Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins"));
|
||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps"));
|
||||
try {
|
||||
Files.createDirectories(pluginsDir);
|
||||
Files.createDirectories(cordappsDir);
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to create directories");
|
||||
}
|
||||
@ -84,7 +84,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
paths.forEach(file -> {
|
||||
if (file.toString().contains("corda-finance")) {
|
||||
try {
|
||||
Files.copy(file, pluginsDir.resolve(file.getFileName()));
|
||||
Files.copy(file, cordappsDir.resolve(file.getFileName()));
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to copy finance jar");
|
||||
}
|
||||
|
@ -89,12 +89,12 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
|
||||
private fun copyFinanceCordapp() {
|
||||
val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories()
|
||||
val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories()
|
||||
// Find the finance jar file for the smoke tests of this module
|
||||
val financeJar = Paths.get("build", "resources", "smokeTest").list {
|
||||
it.filter { "corda-finance" in it.toString() }.toList().single()
|
||||
}
|
||||
financeJar.copyToDirectory(pluginsDir)
|
||||
financeJar.copyToDirectory(cordappsDir)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -28,26 +28,24 @@ class IdentitySyncFlowTests {
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
setCordappPackages("net.corda.finance.contracts.asset")
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset"))
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync confidential identities`() {
|
||||
// Set up values we'll need
|
||||
mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val notary = aliceNode.services.getDefaultNotary()
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
val alice: Party = aliceNode.info.singleIdentity()
|
||||
val bob: Party = bobNode.info.singleIdentity()
|
||||
val notary = notaryNode.services.getDefaultNotary()
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||
@ -73,12 +71,12 @@ class IdentitySyncFlowTests {
|
||||
fun `don't offer other's identities confidential identities`() {
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val charlieNode = mockNet.createPartyNode(CHARLIE.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val charlie: Party = charlieNode.services.myInfo.chooseIdentity()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
val charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
|
||||
val alice: Party = aliceNode.info.singleIdentity()
|
||||
val bob: Party = bobNode.info.singleIdentity()
|
||||
val charlie: Party = charlieNode.info.singleIdentity()
|
||||
val notary = notaryNode.services.getDefaultNotary()
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
|
@ -13,14 +13,14 @@ class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
fun `issue key`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
val mockNet = MockNetwork(threadPerNode = true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bob = bobNode.services.myInfo.singleIdentity()
|
||||
|
||||
// Run the flows
|
||||
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob))
|
||||
@ -53,13 +53,13 @@ class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
fun `verifies identity name`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
val mockNet = MockNetwork(threadPerNode = true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.singleIdentity()
|
||||
val notBob = notaryNode.database.transaction {
|
||||
notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}
|
||||
@ -78,13 +78,13 @@ class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
fun `verifies signature`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
val mockNet = MockNetwork(threadPerNode = true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.singleIdentity()
|
||||
// Check that the wrong signature is rejected
|
||||
notaryNode.database.transaction {
|
||||
notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
|
@ -1,4 +1,4 @@
|
||||
gradlePluginsVersion=2.0.1
|
||||
gradlePluginsVersion=2.0.5
|
||||
kotlinVersion=1.1.50
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.extractFile
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@ -17,6 +18,7 @@ import java.util.jar.JarInputStream
|
||||
* - Legal documents
|
||||
* - Facts generated by oracles which might be reused a lot
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface Attachment : NamedByHash {
|
||||
fun open(): InputStream
|
||||
fun openAsJAR(): JarInputStream {
|
||||
|
@ -10,5 +10,6 @@ enum class ComponentGroupEnum {
|
||||
COMMANDS_GROUP, // ordinal = 2.
|
||||
ATTACHMENTS_GROUP, // ordinal = 3.
|
||||
NOTARY_GROUP, // ordinal = 4.
|
||||
TIMEWINDOW_GROUP // ordinal = 5.
|
||||
TIMEWINDOW_GROUP, // ordinal = 5.
|
||||
SIGNERS_GROUP // ordinal = 6.
|
||||
}
|
||||
|
@ -64,6 +64,17 @@ abstract class TimeWindow {
|
||||
*/
|
||||
abstract val midpoint: Instant?
|
||||
|
||||
/**
|
||||
* Returns the duration between [fromTime] and [untilTime] if both are non-null. Otherwise returns null.
|
||||
*/
|
||||
val length: Duration? get() {
|
||||
return if (fromTime == null || untilTime == null) {
|
||||
null
|
||||
} else {
|
||||
Duration.between(fromTime, untilTime)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */
|
||||
abstract operator fun contains(instant: Instant): Boolean
|
||||
|
||||
|
@ -19,7 +19,7 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
||||
class ContractConstraintRejection(txId: SecureHash, contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
||||
|
||||
class MissingAttachmentRejection(txId: SecureHash, contractClass: String)
|
||||
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
|
||||
|
||||
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)
|
||||
|
@ -2,7 +2,10 @@ package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.X509EdDSAEngine
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.*
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
import net.i2p.crypto.eddsa.math.GroupElement
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
@ -39,8 +42,6 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
@ -148,7 +149,7 @@ object Crypto {
|
||||
"at the cost of larger key sizes and loss of compatibility."
|
||||
)
|
||||
|
||||
/** Corda composite key type */
|
||||
/** Corda composite key type. */
|
||||
@JvmField
|
||||
val COMPOSITE_KEY = SignatureScheme(
|
||||
6,
|
||||
@ -823,7 +824,7 @@ object Crypto {
|
||||
@JvmStatic
|
||||
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
|
||||
|
||||
// custom key pair generator from entropy.
|
||||
// Custom key pair generator from entropy.
|
||||
private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair {
|
||||
val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
|
||||
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length.
|
||||
@ -882,7 +883,7 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// return true if EdDSA publicKey is point at infinity.
|
||||
// Return true if EdDSA publicKey is point at infinity.
|
||||
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean {
|
||||
return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
@ -894,7 +895,7 @@ object Crypto {
|
||||
return signatureScheme.schemeCodeName in signatureSchemeMap
|
||||
}
|
||||
|
||||
// validate a key, by checking its algorithmic params.
|
||||
// Validate a key, by checking its algorithmic params.
|
||||
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
|
||||
return when (key) {
|
||||
is PublicKey -> validatePublicKey(signatureScheme, key)
|
||||
@ -903,7 +904,7 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
// Check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
|
||||
return when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key)
|
||||
@ -912,7 +913,7 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// check if a private key satisfies algorithm specs.
|
||||
// Check if a private key satisfies algorithm specs.
|
||||
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
|
||||
return when (key) {
|
||||
is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
|
||||
@ -924,7 +925,6 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Convert a public key to a supported implementation.
|
||||
*
|
||||
* @param key a public key.
|
||||
* @return a supported implementation of the input public key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
|
@ -20,9 +20,19 @@ import java.security.*
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign))
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of signing a byte array and return a [DigitalSignature.WithKey] object.
|
||||
* Note that there is no check if the public key matches with the signing private key.
|
||||
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the [DigitalSignature.WithKey] object on the input message [bytesToSign] and [publicKey].
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes)
|
||||
|
||||
/**
|
||||
@ -33,10 +43,13 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
|
||||
|
||||
/** Helper function to sign the bytes of [bytesToSign] with a key pair. */
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes)
|
||||
|
||||
/**
|
||||
* Helper function for signing a [SignableData] object.
|
||||
* @param signableData the object to be signed.
|
||||
@ -56,8 +69,8 @@ fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSi
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`,
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class)
|
||||
// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`.
|
||||
@Throws(SignatureException::class, InvalidKeyException::class)
|
||||
fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content)
|
||||
|
||||
/**
|
||||
@ -70,9 +83,10 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d
|
||||
* signature).
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged).
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
* @throws IllegalStateException if this is a [CompositeKey], because verification of composite key signatures is not supported.
|
||||
* @return whether the signature is correct for this key.
|
||||
*/
|
||||
@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@Throws(SignatureException::class, InvalidKeyException::class)
|
||||
fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean {
|
||||
if (this is CompositeKey)
|
||||
throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification.
|
||||
@ -82,9 +96,12 @@ fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean
|
||||
/** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */
|
||||
fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58()
|
||||
|
||||
/** Return a [Set] of the contained keys if this is a [CompositeKey]; otherwise, return a [Set] with a single element (this [PublicKey]). */
|
||||
val PublicKey.keys: Set<PublicKey> get() = (this as? CompositeKey)?.leafKeys ?: setOf(this)
|
||||
|
||||
/** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */
|
||||
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
|
||||
/** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */
|
||||
fun PublicKey.isFulfilledBy(otherKeys: Iterable<PublicKey>): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys)
|
||||
|
||||
/** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */
|
||||
@ -98,8 +115,9 @@ fun Iterable<TransactionSignature>.byKeys() = map { it.by }.toSet()
|
||||
|
||||
// Allow Kotlin destructuring:
|
||||
// val (private, public) = keyPair
|
||||
/* The [PrivateKey] of this [KeyPair] .*/
|
||||
operator fun KeyPair.component1(): PrivateKey = this.private
|
||||
|
||||
/* The [PublicKey] of this [KeyPair] .*/
|
||||
operator fun KeyPair.component2(): PublicKey = this.public
|
||||
|
||||
/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future. */
|
||||
@ -122,7 +140,7 @@ fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEnt
|
||||
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData)
|
||||
|
||||
/**
|
||||
@ -135,7 +153,7 @@ fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean =
|
||||
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData)
|
||||
|
||||
/**
|
||||
|
@ -158,4 +158,41 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
return false
|
||||
return (verifyRoot == merkleRootHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return the index of the input leaf in the partial Merkle tree structure.
|
||||
* @param leaf the component hash to check.
|
||||
* @return leaf-index of this component (starting from zero).
|
||||
* @throws MerkleTreeException if the provided hash is not in the tree.
|
||||
*/
|
||||
@Throws(MerkleTreeException::class)
|
||||
internal fun leafIndex(leaf: SecureHash): Int {
|
||||
// Special handling if the tree consists of one node only.
|
||||
if (root is PartialTree.IncludedLeaf && root.hash == leaf) return 0
|
||||
val flagPath = mutableListOf<Boolean>()
|
||||
if (!leafIndexHelper(leaf, this.root, flagPath)) throw MerkleTreeException("The provided hash $leaf is not in the tree.")
|
||||
return indexFromFlagPath(flagPath)
|
||||
}
|
||||
|
||||
// Helper function to compute the path. False means go to the left and True to the right.
|
||||
// Because the path is updated recursively, the path is returned in reverse order.
|
||||
private fun leafIndexHelper(leaf: SecureHash, node: PartialTree, path: MutableList<Boolean>): Boolean {
|
||||
if (node is PartialTree.IncludedLeaf) {
|
||||
return node.hash == leaf
|
||||
} else if (node is PartialTree.Node) {
|
||||
if (leafIndexHelper(leaf, node.left, path)) {
|
||||
path.add(false)
|
||||
return true
|
||||
}
|
||||
if (leafIndexHelper(leaf, node.right, path)) {
|
||||
path.add(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Return the leaf index from the path boolean list.
|
||||
private fun indexFromFlagPath(pathList: List<Boolean>) =
|
||||
pathList.mapIndexed { index, value -> if (value) (1 shl index) else 0 }.sum()
|
||||
}
|
||||
|
@ -19,39 +19,87 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the hash value to an uppercase hexadecimal [String].
|
||||
*/
|
||||
override fun toString(): String = bytes.toHexString()
|
||||
|
||||
/**
|
||||
* Returns the first [prefixLen] hexadecimal digits of the [SecureHash] value.
|
||||
* @param prefixLen The number of characters in the prefix.
|
||||
*/
|
||||
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
|
||||
|
||||
/**
|
||||
* Append a second hash value to this hash value, and then compute the SHA-256 hash of the result.
|
||||
* @param other The hash to append to this one.
|
||||
*/
|
||||
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
|
||||
|
||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||
companion object {
|
||||
/**
|
||||
* Converts a SHA-256 hash value represented as a hexadecimal [String] into a [SecureHash].
|
||||
* @param str A sequence of 64 hexadecimal digits that represents a SHA-256 hash value.
|
||||
* @throws IllegalArgumentException The input string does not contain 64 hexadecimal digits, or it contains incorrectly-encoded characters.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parse(str: String) = str.toUpperCase().parseAsHex().let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
fun parse(str: String): SHA256 {
|
||||
return str.toUpperCase().parseAsHex().let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash value of the [ByteArray].
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes))
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash.
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes)
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash of the [String]'s UTF-8 byte contents.
|
||||
* @param str [String] whose UTF-8 contents will be hashed.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256(str: String) = sha256(str.toByteArray())
|
||||
|
||||
/**
|
||||
* Generates a random SHA-256 value.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0x00 bytes.
|
||||
*/
|
||||
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0xFF bytes.
|
||||
*/
|
||||
val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() }))
|
||||
}
|
||||
|
||||
// In future, maybe SHA3, truncated hashes etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the SHA-256 hash for the contents of the [ByteArray].
|
||||
*/
|
||||
fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this)
|
||||
|
||||
/**
|
||||
* Compute the SHA-256 hash for the contents of the [OpaqueBytes].
|
||||
*/
|
||||
fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bytes)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
@ -16,6 +17,8 @@ import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.debug
|
||||
import org.slf4j.Logger
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A sub-class of [FlowLogic<T>] implements a flow using direct, straight line blocking code. Thus you
|
||||
@ -43,6 +46,34 @@ abstract class FlowLogic<out T> {
|
||||
/** This is where you should log things to. */
|
||||
val logger: Logger get() = stateMachine.logger
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Return the outermost [FlowLogic] instance, or null if not in a flow.
|
||||
*/
|
||||
@JvmStatic
|
||||
val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic
|
||||
|
||||
/**
|
||||
* If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise,
|
||||
* just sleep for [duration]. This sleep function is not designed to aid scheduling, for which you should
|
||||
* consider using [SchedulableState]. It is designed to aid with managing contention for which you have not
|
||||
* managed via another means.
|
||||
*
|
||||
* Warning: long sleeps and in general long running flows are highly discouraged, as there is currently no
|
||||
* support for flow migration! This method will throw an exception if you attempt to sleep for longer than
|
||||
* 5 minutes.
|
||||
*/
|
||||
@Suspendable
|
||||
@JvmStatic
|
||||
@Throws(FlowException::class)
|
||||
fun sleep(duration: Duration) {
|
||||
if (duration.compareTo(Duration.ofMinutes(5)) > 0) {
|
||||
throw FlowException("Attempt to sleep for longer than 5 minutes is not supported. Consider using SchedulableState.")
|
||||
}
|
||||
(Strand.currentStrand() as? FlowStateMachine<*>)?.sleepUntil(Instant.now() + duration) ?: Strand.sleep(duration.toMillis())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapped [java.util.UUID] object that identifies this state machine run (i.e. subflows have the same
|
||||
* identifier as their parents).
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
import java.time.Instant
|
||||
|
||||
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
||||
interface FlowStateMachine<R> {
|
||||
@ -35,6 +36,9 @@ interface FlowStateMachine<R> {
|
||||
@Suspendable
|
||||
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction
|
||||
|
||||
@Suspendable
|
||||
fun sleepUntil(until: Instant)
|
||||
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>)
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>)
|
||||
@ -45,6 +49,7 @@ interface FlowStateMachine<R> {
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>)
|
||||
|
||||
val logic: FlowLogic<R>
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
val id: StateMachineRunId
|
||||
|
@ -300,3 +300,6 @@ fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serial
|
||||
* @suppress
|
||||
*/
|
||||
fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext)
|
||||
|
||||
/** Convenience method to get the package name of a class literal. */
|
||||
val KClass<*>.packageName get() = java.`package`.name
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -42,4 +43,9 @@ data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
||||
|
||||
/** Returns true if [party] is one of the identities of this node, else false. */
|
||||
fun isLegalIdentity(party: Party): Boolean = party in legalIdentities
|
||||
|
||||
fun identityFromX500Name(name: CordaX500Name): Party {
|
||||
val identity = legalIdentitiesAndCerts.singleOrNull { it.name == name } ?: throw IllegalArgumentException("Node does not have an identity \"$name\"")
|
||||
return identity.party
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,7 @@ import java.security.PublicKey
|
||||
* from an authoritative service, and adds easy lookup of the data stored within it. Generally it would be initialised
|
||||
* with a specified network map service, which it fetches data from and then subscribes to updates of.
|
||||
*/
|
||||
interface NetworkMapCache {
|
||||
|
||||
interface NetworkMapCache : NetworkMapCacheBase {
|
||||
@CordaSerializable
|
||||
sealed class MapChange {
|
||||
abstract val node: NodeInfo
|
||||
@ -29,6 +28,22 @@ interface NetworkMapCache {
|
||||
data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party
|
||||
* is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this
|
||||
* returns null.
|
||||
* Notice that when there are more than one node for a given party (in case of distributed services) first service node
|
||||
* found will be returned. See also: [NetworkMapCache.getNodesByLegalIdentityKey].
|
||||
*
|
||||
* @param party party to retrieve node information for.
|
||||
* @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is
|
||||
* no node for the party, only that this cache is unaware of it.
|
||||
*/
|
||||
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
|
||||
}
|
||||
|
||||
/** Subset of [NetworkMapCache] that doesn't depend on an [IdentityService]. */
|
||||
interface NetworkMapCacheBase {
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* A list of notary services available on the network.
|
||||
@ -40,7 +55,7 @@ interface NetworkMapCache {
|
||||
// DOCEND 1
|
||||
|
||||
/** Tracks changes to the network map cache. */
|
||||
val changed: Observable<MapChange>
|
||||
val changed: Observable<NetworkMapCache.MapChange>
|
||||
/** Future to track completion of the NetworkMapService registration. */
|
||||
val nodeReady: CordaFuture<Void?>
|
||||
|
||||
@ -48,20 +63,7 @@ interface NetworkMapCache {
|
||||
* Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the
|
||||
* first subscriber is registered so as to avoid racing with early updates.
|
||||
*/
|
||||
fun track(): DataFeed<List<NodeInfo>, MapChange>
|
||||
|
||||
/**
|
||||
* Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party
|
||||
* is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this
|
||||
* returns null.
|
||||
* Notice that when there are more than one node for a given party (in case of distributed services) first service node
|
||||
* found will be returned. See also: [getNodesByLegalIdentityKey].
|
||||
*
|
||||
* @param party party to retrieve node information for.
|
||||
* @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is
|
||||
* no node for the party, only that this cache is unaware of it.
|
||||
*/
|
||||
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
|
||||
fun track(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
|
||||
|
||||
/**
|
||||
* Look up the node info for a legal name.
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import com.google.common.primitives.Booleans
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.*
|
||||
@ -15,12 +16,13 @@ import java.security.PublicKey
|
||||
abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
const val ID_PREFIX = "corda.notary."
|
||||
fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false): String {
|
||||
require(!raft || !bft)
|
||||
fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String {
|
||||
require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" }
|
||||
return StringBuffer(ID_PREFIX).apply {
|
||||
append(if (validating) "validating" else "simple")
|
||||
if (raft) append(".raft")
|
||||
if (bft) append(".bft")
|
||||
if (custom) append(".custom")
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
@file:JvmName("SerializationAPI")
|
||||
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -8,6 +6,9 @@ import net.corda.core.internal.WriteOnceProperty
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.sequence
|
||||
import java.sql.Blob
|
||||
|
||||
data class ObjectWithCompatibleContext<out T : Any>(val obj: T, val context: SerializationContext)
|
||||
|
||||
/**
|
||||
* An abstraction for serializing and deserializing objects, with support for versioning of the wire format via
|
||||
@ -23,6 +24,16 @@ abstract class SerializationFactory {
|
||||
*/
|
||||
abstract fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T
|
||||
|
||||
/**
|
||||
* Deserialize the bytes in to an object, using the prefixed bytes to determine the format.
|
||||
*
|
||||
* @param byteSequence The bytes to deserialize, including a format header prefix.
|
||||
* @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown.
|
||||
* @param context A context that configures various parameters to deserialization.
|
||||
* @return deserialized object along with [SerializationContext] to identify encoding used.
|
||||
*/
|
||||
abstract fun <T : Any> deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): ObjectWithCompatibleContext<T>
|
||||
|
||||
/**
|
||||
* Serialize an object to bytes using the preferred serialization format version from the context.
|
||||
*
|
||||
@ -88,6 +99,8 @@ abstract class SerializationFactory {
|
||||
}
|
||||
}
|
||||
|
||||
typealias VersionHeader = ByteSequence
|
||||
|
||||
/**
|
||||
* Parameters to serialization and deserialization.
|
||||
*/
|
||||
@ -95,7 +108,7 @@ interface SerializationContext {
|
||||
/**
|
||||
* When serializing, use the format this header sequence represents.
|
||||
*/
|
||||
val preferredSerializationVersion: ByteSequence
|
||||
val preferredSerializationVersion: VersionHeader
|
||||
/**
|
||||
* The class loader to use for deserialization.
|
||||
*/
|
||||
@ -148,7 +161,7 @@ interface SerializationContext {
|
||||
/**
|
||||
* Helper method to return a new context based on this context but with serialization using the format this header sequence represents.
|
||||
*/
|
||||
fun withPreferredSerializationVersion(versionHeader: ByteSequence): SerializationContext
|
||||
fun withPreferredSerializationVersion(versionHeader: VersionHeader): SerializationContext
|
||||
|
||||
/**
|
||||
* The use case that we are serializing for, since it influences the implementations chosen.
|
||||
@ -175,6 +188,15 @@ inline fun <reified T : Any> ByteSequence.deserialize(serializationFactory: Seri
|
||||
return serializationFactory.deserialize(this, T::class.java, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionally returns [SerializationContext] which was used for encoding.
|
||||
* It might be helpful to know [SerializationContext] to use the same encoding in the reply.
|
||||
*/
|
||||
inline fun <reified T : Any> ByteSequence.deserializeWithCompatibleContext(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext<T> {
|
||||
return serializationFactory.deserializeWithCompatibleContext(this, T::class.java, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience extension method for deserializing SerializedBytes with type matching, utilising the defaults.
|
||||
*/
|
||||
@ -187,6 +209,11 @@ inline fun <reified T : Any> SerializedBytes<T>.deserialize(serializationFactory
|
||||
*/
|
||||
inline fun <reified T : Any> ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context)
|
||||
|
||||
/**
|
||||
* Convenience extension method for deserializing a JDBC Blob, utilising the defaults.
|
||||
*/
|
||||
inline fun <reified T : Any> Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context)
|
||||
|
||||
/**
|
||||
* Convenience extension method for serializing an object of type T, utilising the defaults.
|
||||
*/
|
||||
|
@ -25,7 +25,7 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
|
||||
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
|
||||
val commands: List<Command<*>> = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<Command<*>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
val commands: List<Command<*>> = deserialiseCommands()
|
||||
|
||||
override val notary: Party? = let {
|
||||
val notaries: List<Party> = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes<Party>(it).deserialize() })
|
||||
@ -74,6 +74,31 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// Method to deserialise Commands from its two groups:
|
||||
// COMMANDS_GROUP which contains the CommandData part
|
||||
// and SIGNERS_GROUP which contains the Signers part.
|
||||
private fun deserialiseCommands(): List<Command<*>> {
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
// and it will throw if any of the signers objects is not List of public keys).
|
||||
val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes<List<PublicKey>>(it).deserialize() })
|
||||
val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<CommandData>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
||||
if (group is FilteredComponentGroup) {
|
||||
check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" }
|
||||
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
|
||||
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
// or a FilteredTransaction with no Commands (in which case group is null).
|
||||
check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" }
|
||||
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,11 +136,12 @@ class FilteredTransaction private constructor(
|
||||
val filteredSerialisedComponents: MutableMap<Int, MutableList<OpaqueBytes>> = hashMapOf()
|
||||
val filteredComponentNonces: MutableMap<Int, MutableList<SecureHash>> = hashMapOf()
|
||||
val filteredComponentHashes: MutableMap<Int, MutableList<SecureHash>> = hashMapOf() // Required for partial Merkle tree generation.
|
||||
var signersIncluded = false
|
||||
|
||||
fun <T : Any> filter(t: T, componentGroupIndex: Int, internalIndex: Int) {
|
||||
if (filtering.test(t)) {
|
||||
val group = filteredSerialisedComponents[componentGroupIndex]
|
||||
// Because the filter passed, we know there is a match. We also use first vs single as the init function
|
||||
// Because the filter passed, we know there is a match. We also use first Vs single as the init function
|
||||
// of WireTransaction ensures there are no duplicated groups.
|
||||
val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex]
|
||||
if (group == null) {
|
||||
@ -132,6 +158,17 @@ class FilteredTransaction private constructor(
|
||||
filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex])
|
||||
filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex])
|
||||
}
|
||||
// If at least one command is visible, then all command-signers should be visible as well.
|
||||
// This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details.
|
||||
if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) {
|
||||
signersIncluded = true
|
||||
val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal
|
||||
// There exist commands, thus the signers group is not empty.
|
||||
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
|
||||
filteredSerialisedComponents.put(signersGroupIndex, signersGroupComponents.components.toMutableList())
|
||||
filteredComponentNonces.put(signersGroupIndex, wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList())
|
||||
filteredComponentHashes.put(signersGroupIndex, wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,6 +179,10 @@ class FilteredTransaction private constructor(
|
||||
wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) }
|
||||
if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0)
|
||||
if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0)
|
||||
// It is highlighted that because there is no a signers property in TraversableTransaction,
|
||||
// one cannot specifically filter them in or out.
|
||||
// The above is very important to ensure someone won't filter out the signers component group if at least one
|
||||
// command is included in a FilteredTransaction.
|
||||
|
||||
// It's sometimes possible that when we receive a WireTransaction for which there is a new or more unknown component groups,
|
||||
// we decide to filter and attach this field to a FilteredTransaction.
|
||||
@ -207,7 +248,9 @@ class FilteredTransaction private constructor(
|
||||
/**
|
||||
* Function that checks if all of the components in a particular group are visible.
|
||||
* This functionality is required on non-Validating Notaries to check that all inputs are visible.
|
||||
* It might also be applied in Oracles, where an Oracle should know it can see all commands.
|
||||
* It might also be applied in Oracles or any other entity requiring [Command] visibility, but because this method
|
||||
* cannot distinguish between related and unrelated to the signer [Command]s, one should use the
|
||||
* [checkCommandVisibility] method, which is specifically designed for [Command] visibility purposes.
|
||||
* The logic behind this algorithm is that we check that the root of the provided group partialMerkleTree matches with the
|
||||
* root of a fullMerkleTree if computed using all visible components.
|
||||
* Note that this method is usually called after or before [verify], to also ensure that the provided partial Merkle
|
||||
@ -229,18 +272,54 @@ class FilteredTransaction private constructor(
|
||||
visibilityCheck(group.groupIndex < groupHashes.size) { "There is no matching component group hash for group ${group.groupIndex}" }
|
||||
val groupPartialRoot = groupHashes[group.groupIndex]
|
||||
val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash
|
||||
visibilityCheck(groupPartialRoot == groupFullRoot) { "The partial Merkle tree root does not match with the received root for group ${group.groupIndex}" }
|
||||
visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" }
|
||||
// Verify the top level Merkle tree from groupHashes.
|
||||
visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" }
|
||||
}
|
||||
}
|
||||
|
||||
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any): Unit {
|
||||
/**
|
||||
* Function that checks if all of the commands that should be signed by the input public key are visible.
|
||||
* This functionality is required from Oracles to check that all of the commands they should sign are visible.
|
||||
* This algorithm uses the [ComponentGroupEnum.SIGNERS_GROUP] to count how many commands should be signed by the
|
||||
* input [PublicKey] and it then matches it with the size of received [commands].
|
||||
* Note that this method does not throw if there are no commands for this key to sign in the original [WireTransaction].
|
||||
* @param publicKey signer's [PublicKey]
|
||||
* @throws ComponentVisibilityException if not all of the related commands are visible.
|
||||
*/
|
||||
@Throws(ComponentVisibilityException::class)
|
||||
fun checkCommandVisibility(publicKey: PublicKey) {
|
||||
val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal }
|
||||
val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners)
|
||||
val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size
|
||||
visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" }
|
||||
}
|
||||
|
||||
// Function to return number of expected commands to sign.
|
||||
private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int {
|
||||
checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP)
|
||||
if (commandSigners == null) return 0
|
||||
fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List<PublicKey> {
|
||||
try {
|
||||
return SerializedBytes<List<PublicKey>>(opaqueBytes.bytes).deserialize()
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Malformed transaction, signers at index $internalIndex cannot be deserialised", e)
|
||||
}
|
||||
}
|
||||
|
||||
return commandSigners.components
|
||||
.mapIndexed { internalIndex, opaqueBytes -> signersKeys(internalIndex, opaqueBytes) }
|
||||
.filter { signers -> publicKey in signers }.size
|
||||
}
|
||||
|
||||
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
if (!value) {
|
||||
val message = lazyMessage()
|
||||
throw FilteredTransactionVerificationException(id, message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any): Unit {
|
||||
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
if (!value) {
|
||||
val message = lazyMessage()
|
||||
throw ComponentVisibilityException(id, message.toString())
|
||||
|
@ -1,8 +1,6 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.transactions.SignedTransaction.SignaturesMissingException
|
||||
|
@ -6,9 +6,10 @@ import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
@ -213,10 +214,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
|
||||
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }))
|
||||
if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }))
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }))
|
||||
// Adding commandData only to the commands group. Signers are added in their own group.
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }))
|
||||
if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }))
|
||||
if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())))
|
||||
if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())))
|
||||
// Adding signers to their own group. This is required for command visibility purposes: a party receiving
|
||||
// a FilteredTransaction can now verify it sees all the commands it should sign.
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }))
|
||||
return componentGroupMap
|
||||
}
|
||||
}
|
||||
|
@ -118,8 +118,11 @@ sealed class ByteSequence : Comparable<ByteSequence> {
|
||||
* In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such
|
||||
* functionality to Java, but it won't arrive for a few years yet!
|
||||
*/
|
||||
open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() {
|
||||
open class OpaqueBytes(bytes: ByteArray) : ByteSequence() {
|
||||
companion object {
|
||||
/**
|
||||
* Create [OpaqueBytes] from a sequence of [Byte] values.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
|
||||
}
|
||||
@ -128,13 +131,35 @@ open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() {
|
||||
require(bytes.isNotEmpty())
|
||||
}
|
||||
|
||||
override val size: Int get() = bytes.size
|
||||
override val offset: Int get() = 0
|
||||
/**
|
||||
* The bytes are always cloned so that this object becomes immutable. This has been done
|
||||
* to prevent tampering with entities such as [SecureHash] and [PrivacySalt], as well as
|
||||
* preserve the integrity of our hash constants [zeroHash] and [allOnesHash].
|
||||
*
|
||||
* Cloning like this may become a performance issue, depending on whether or not the JIT
|
||||
* compiler is ever able to optimise away the clone. In which case we may need to revisit
|
||||
* this later.
|
||||
*/
|
||||
override final val bytes: ByteArray = bytes
|
||||
get() = field.clone()
|
||||
override val size: Int = bytes.size
|
||||
override val offset: Int = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy [size] bytes from this [ByteArray] starting from [offset] into a new [ByteArray].
|
||||
*/
|
||||
fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of(this, offset, size)
|
||||
|
||||
/**
|
||||
* Converts this [ByteArray] into a [String] of hexadecimal digits.
|
||||
*/
|
||||
fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this)
|
||||
|
||||
/**
|
||||
* Converts this [String] of hexadecimal digits into a [ByteArray].
|
||||
* @throws IllegalArgumentException if the [String] contains incorrectly-encoded characters.
|
||||
*/
|
||||
fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
|
||||
/**
|
||||
|
@ -3,9 +3,8 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.crypto.Base58
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.nio.charset.Charset
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -15,11 +14,13 @@ import javax.xml.bind.DatatypeConverter
|
||||
|
||||
// [ByteArray] encoders
|
||||
|
||||
/** Convert a byte array to a Base58 encoded [String]. */
|
||||
fun ByteArray.toBase58(): String = Base58.encode(this)
|
||||
|
||||
/** Convert a byte array to a Base64 encoded [String]. */
|
||||
fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this)
|
||||
|
||||
/** Convert a byte array to a hex (base 16) capitalized encoded string.*/
|
||||
/** Convert a byte array to a hex (Base16) capitalized encoded [String]. */
|
||||
fun ByteArray.toHex(): String = DatatypeConverter.printHexBinary(this)
|
||||
|
||||
|
||||
@ -65,7 +66,15 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64()
|
||||
// TODO We use for both CompositeKeys and EdDSAPublicKey custom serializers and deserializers. We need to specify encoding.
|
||||
// TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition
|
||||
// structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
|
||||
fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>()
|
||||
/**
|
||||
* Method to return the [PublicKey] object given its Base58-[String] representation.
|
||||
* @param base58String the Base58 encoded format of the serialised [PublicKey].
|
||||
* @return the resulted [PublicKey] after decoding the [base58String] input and then deserialising to a [PublicKey] object.
|
||||
*/
|
||||
fun parsePublicKeyBase58(base58String: String): PublicKey = Crypto.decodePublicKey(base58String.base58ToByteArray())
|
||||
|
||||
fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58()
|
||||
fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised).
|
||||
/** Return the Base58 representation of the serialised public key. */
|
||||
fun PublicKey.toBase58String(): String = this.encoded.toBase58()
|
||||
|
||||
/** Return the bytes of the SHA-256 output for this public key. */
|
||||
fun PublicKey.toSHA256Bytes(): ByteArray = this.encoded.sha256().bytes
|
||||
|
@ -1,5 +1,3 @@
|
||||
@file:JvmName("KotlinUtils")
|
||||
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.internal.concurrent.get
|
||||
|
@ -1,13 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.secureRandomBytes
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.ComponentGroup
|
||||
import net.corda.core.transactions.ComponentVisibilityException
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -34,22 +30,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
|
||||
private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) }
|
||||
private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) }
|
||||
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }) }
|
||||
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) }
|
||||
private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty.
|
||||
private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) }
|
||||
private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) }
|
||||
private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) }
|
||||
|
||||
private val newUnknownComponentGroup = ComponentGroup(20, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
|
||||
private val newUnknownComponentEmptyGroup = ComponentGroup(21, emptyList())
|
||||
private val newUnknownComponentGroup = ComponentGroup(100, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
|
||||
private val newUnknownComponentEmptyGroup = ComponentGroup(101, emptyList())
|
||||
|
||||
// Do not add attachments (empty list).
|
||||
private val componentGroupsA by lazy {
|
||||
listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
}
|
||||
private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) }
|
||||
@ -74,7 +72,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup,
|
||||
attachmentGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroups = componentGroupsEmptyAttachment, privacySalt = privacySalt) }
|
||||
|
||||
@ -86,7 +85,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
val wireTransaction1ShuffledInputs = WireTransaction(componentGroups = componentGroupsB, privacySalt = privacySalt)
|
||||
// The ID has changed due to change of the internal ordering in inputs.
|
||||
@ -106,7 +106,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
inputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertEquals(wireTransactionA, WireTransaction(componentGroups = shuffledComponentGroupsA, privacySalt = privacySalt))
|
||||
}
|
||||
@ -123,7 +124,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup,
|
||||
ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components),
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsB, privacySalt) }
|
||||
|
||||
@ -134,7 +136,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup, // First commandsGroup.
|
||||
commandGroup, // Second commandsGroup.
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsDuplicatedCommands, privacySalt) }
|
||||
|
||||
@ -144,7 +147,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsC, privacySalt) }
|
||||
|
||||
@ -154,23 +158,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 20 that we cannot process.
|
||||
signersGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
// The old client (receiving more component types than expected) is still compatible.
|
||||
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
|
||||
assertEquals(wireTransactionCompatibleA.availableComponentGroups, wireTransactionA.availableComponentGroups) // The known components are the same.
|
||||
assertNotEquals(wireTransactionCompatibleA, wireTransactionA) // But obviously, its Merkle root has changed Vs wireTransactionA (which doesn't include this extra component).
|
||||
assertEquals(6, wireTransactionCompatibleA.componentGroups.size)
|
||||
|
||||
// The old client will trhow if receiving an empty component (even if this unknown).
|
||||
// The old client will throw if receiving an empty component (even if this is unknown).
|
||||
val componentGroupsCompatibleEmptyNew = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
newUnknownComponentEmptyGroup // A new unknown component with ordinal 21 that we cannot process.
|
||||
signersGroup,
|
||||
newUnknownComponentEmptyGroup // A new unknown component with ordinal 101 that we cannot process.
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsCompatibleEmptyNew, privacySalt) }
|
||||
}
|
||||
@ -179,7 +184,9 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
fun `FilteredTransaction constructors and compatibility`() {
|
||||
// Filter out all of the components.
|
||||
val ftxNothing = wireTransactionA.buildFilteredTransaction(Predicate { false }) // Nothing filtered.
|
||||
assertEquals(6, ftxNothing.groupHashes.size) // Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
|
||||
// Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
|
||||
// Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes.
|
||||
assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size)
|
||||
ftxNothing.verify()
|
||||
|
||||
// Include all of the components.
|
||||
@ -191,6 +198,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP)
|
||||
ftxAll.checkAllComponentsVisible(NOTARY_GROUP)
|
||||
ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP)
|
||||
ftxAll.checkAllComponentsVisible(SIGNERS_GROUP)
|
||||
|
||||
// Filter inputs only.
|
||||
fun filtering(elem: Any): Boolean {
|
||||
@ -222,12 +230,14 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
|
||||
|
||||
// The old client (receiving more component types than expected) is still compatible.
|
||||
val componentGroupsCompatibleA = listOf(inputGroup,
|
||||
val componentGroupsCompatibleA = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 10,000 that we cannot process.
|
||||
signersGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
|
||||
val ftxCompatible = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filtering))
|
||||
@ -245,9 +255,288 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
ftxCompatibleAll.verify()
|
||||
assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id)
|
||||
|
||||
// Check we received the last (6th) element that we cannot process (backwards compatibility).
|
||||
assertEquals(6, ftxCompatibleAll.filteredComponentGroups.size)
|
||||
// Check we received the last element that we cannot process (backwards compatibility).
|
||||
assertEquals(wireTransactionCompatibleA.componentGroups.size, ftxCompatibleAll.filteredComponentGroups.size)
|
||||
|
||||
// Hide one component group only.
|
||||
// Filter inputs only.
|
||||
fun filterOutInputs(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is StateRef -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
val ftxCompatibleNoInputs = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filterOutInputs))
|
||||
ftxCompatibleNoInputs.verify()
|
||||
assertFailsWith<ComponentVisibilityException> { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) }
|
||||
assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size)
|
||||
assertEquals(wireTransactionCompatibleA.componentGroups.map { it.groupIndex }.max()!!, ftxCompatibleNoInputs.groupHashes.size - 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Command visibility tests`() {
|
||||
// 1st and 3rd commands require a signature from KEY_1.
|
||||
val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public))
|
||||
val componentGroups = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }),
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
|
||||
|
||||
// Filter all commands.
|
||||
fun filterCommandsOnly(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> true // Even if one Command is filtered, all signers are automatically filtered as well
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out commands only.
|
||||
fun filterOutCommands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
// Filter KEY_1 commands.
|
||||
fun filterKEY1Commands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> DUMMY_KEY_1.public in elem.signers
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter only one KEY_1 command.
|
||||
fun filterTwoSignersCommands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> elem.signers.size == 2 // dummyCommand(DUMMY_KEY_1.public) is filtered out.
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Again filter only one KEY_1 command.
|
||||
fun filterSingleSignersCommands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> elem.signers.size == 1 // dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public) is filtered out.
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val allCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterCommandsOnly))
|
||||
val noCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterOutCommands))
|
||||
val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands))
|
||||
val oneKey1CommandFtxA = wtx.buildFilteredTransaction(Predicate(::filterTwoSignersCommands))
|
||||
val oneKey1CommandFtxB = wtx.buildFilteredTransaction(Predicate(::filterSingleSignersCommands))
|
||||
|
||||
allCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public)
|
||||
assertFailsWith<ComponentVisibilityException> { noCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
key1CommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public)
|
||||
assertFailsWith<ComponentVisibilityException> { oneKey1CommandFtxA.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
assertFailsWith<ComponentVisibilityException> { oneKey1CommandFtxB.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
allCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP)
|
||||
assertFailsWith<ComponentVisibilityException> { noCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) } // If we filter out all commands, signers are not sent as well.
|
||||
key1CommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
|
||||
oneKey1CommandFtxA.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
|
||||
oneKey1CommandFtxB.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
|
||||
|
||||
// We don't send a list of signers.
|
||||
val componentGroupsCompatible = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
// ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }),
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
// Invalid Transaction. Sizes of CommandData and Signers (empty) do not match.
|
||||
assertFailsWith<IllegalStateException> { WireTransaction(componentGroups = componentGroupsCompatible, privacySalt = PrivacySalt()) }
|
||||
|
||||
// We send smaller list of signers.
|
||||
val componentGroupsLessSigners = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }.subList(0, 1)), // Send first signer only.
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
// Invalid Transaction. Sizes of CommandData and Signers (empty) do not match.
|
||||
assertFailsWith<IllegalStateException> { WireTransaction(componentGroups = componentGroupsLessSigners, privacySalt = PrivacySalt()) }
|
||||
|
||||
// Test if there is no command to sign.
|
||||
val commandsNoKey1= listOf(dummyCommand(DUMMY_KEY_2.public))
|
||||
|
||||
val componentGroupsNoKey1ToSign = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, commandsNoKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, commandsNoKey1.map { it.signers.serialize() }),
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
val wtxNoKey1 = WireTransaction(componentGroups = componentGroupsNoKey1ToSign, privacySalt = PrivacySalt())
|
||||
val allCommandsNoKey1Ftx= wtxNoKey1.buildFilteredTransaction(Predicate(::filterCommandsOnly))
|
||||
allCommandsNoKey1Ftx.checkCommandVisibility(DUMMY_KEY_1.public) // This will pass, because there are indeed no commands to sign in the original transaction.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilteredTransaction signer manipulation tests`() {
|
||||
// Required to call the private constructor.
|
||||
val ftxConstructor = FilteredTransaction::class.java.declaredConstructors[1]
|
||||
ftxConstructor.isAccessible = true
|
||||
|
||||
// 1st and 3rd commands require a signature from KEY_1.
|
||||
val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public))
|
||||
val componentGroups = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() })
|
||||
)
|
||||
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
|
||||
|
||||
// Filter KEY_1 commands (commands 1 and 3).
|
||||
fun filterKEY1Commands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> DUMMY_KEY_1.public in elem.signers
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter KEY_2 commands (commands 1 and 2).
|
||||
fun filterKEY2Commands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> DUMMY_KEY_2.public in elem.signers
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands))
|
||||
val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands))
|
||||
|
||||
// val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components
|
||||
val commandDataHashes = wtx.availableComponentHashes[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!!
|
||||
val noLastCommandDataPMT = PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(commandDataHashes),
|
||||
commandDataHashes.subList(0, 1)
|
||||
)
|
||||
val noLastCommandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components.subList(0, 1)
|
||||
val noLastCommandDataNonces = key1CommandsFtx.filteredComponentGroups[0].nonces.subList(0, 1)
|
||||
val noLastCommandDataGroup = FilteredComponentGroup(
|
||||
ComponentGroupEnum.COMMANDS_GROUP.ordinal,
|
||||
noLastCommandDataComponents,
|
||||
noLastCommandDataNonces,
|
||||
noLastCommandDataPMT
|
||||
)
|
||||
|
||||
val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components
|
||||
val signerHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!
|
||||
val noLastSignerPMT = PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(signerHashes),
|
||||
signerHashes.subList(0, 2)
|
||||
)
|
||||
val noLastSignerComponents = key1CommandsFtx.filteredComponentGroups[1].components.subList(0, 2)
|
||||
val noLastSignerNonces = key1CommandsFtx.filteredComponentGroups[1].nonces.subList(0, 2)
|
||||
val noLastSignerGroup = FilteredComponentGroup(
|
||||
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
|
||||
noLastSignerComponents,
|
||||
noLastSignerNonces,
|
||||
noLastSignerPMT
|
||||
)
|
||||
val noLastSignerGroupSamePartialTree = FilteredComponentGroup(
|
||||
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
|
||||
noLastSignerComponents,
|
||||
noLastSignerNonces,
|
||||
key1CommandsFtx.filteredComponentGroups[1].partialMerkleTree) // We don't update that, so we can catch the index mismatch.
|
||||
|
||||
val updatedFilteredComponentsNoSignersKey2 = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroup)
|
||||
val updatedFilteredComponentsNoSignersKey2SamePMT = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree)
|
||||
|
||||
// There are only two components in key1CommandsFtx (commandData and signers).
|
||||
assertEquals(2, key1CommandsFtx.componentGroups.size)
|
||||
|
||||
// Remove last signer for which there is a pointer from a visible commandData. This is the case of Key1.
|
||||
// This will result to an invalid transaction.
|
||||
// A command with no corresponding signer detected
|
||||
// because the pointer of CommandData (3rd leaf) cannot find a corresponding (3rd) signer.
|
||||
val updatedFilteredComponentsNoSignersKey1SamePMT = listOf(key1CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree)
|
||||
assertFails { ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoSignersKey1SamePMT, key1CommandsFtx.groupHashes) }
|
||||
|
||||
// Remove both last signer (KEY1) and related command.
|
||||
// Update partial Merkle tree for signers.
|
||||
val updatedFilteredComponentsNoLastCommandAndSigners = listOf(noLastCommandDataGroup, noLastSignerGroup)
|
||||
val ftxNoLastCommandAndSigners = ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoLastCommandAndSigners, key1CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// verify() will pass as the transaction is well-formed.
|
||||
ftxNoLastCommandAndSigners.verify()
|
||||
// checkCommandVisibility() will not pass, because checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) will fail.
|
||||
assertFailsWith<ComponentVisibilityException> { ftxNoLastCommandAndSigners.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
// Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2.
|
||||
// Do not change partial Merkle tree for signers.
|
||||
// This time the object can be constructed as there is no pointer mismatch.
|
||||
val ftxNoLastSigner = ftxConstructor.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2SamePMT, key2CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// verify() will fail as we didn't change the partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxNoLastSigner.verify() }
|
||||
// checkCommandVisibility() will not pass.
|
||||
assertFailsWith<ComponentVisibilityException> { ftxNoLastSigner.checkCommandVisibility(DUMMY_KEY_2.public) }
|
||||
|
||||
// Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2.
|
||||
// Update partial Merkle tree for signers.
|
||||
val ftxNoLastSignerB = ftxConstructor.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2, key2CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// verify() will pass, the transaction is well-formed.
|
||||
ftxNoLastSignerB.verify()
|
||||
// But, checkAllComponentsVisible() will not pass.
|
||||
assertFailsWith<ComponentVisibilityException> { ftxNoLastSignerB.checkCommandVisibility(DUMMY_KEY_2.public) }
|
||||
|
||||
// Modify last signer (we have a pointer from commandData).
|
||||
// Update partial Merkle tree for signers.
|
||||
val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice.
|
||||
val alterSignersHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
|
||||
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes)
|
||||
val alterSignerPMTK = PartialMerkleTree.build(
|
||||
alterMTree,
|
||||
alterSignersHashes
|
||||
)
|
||||
|
||||
val alterSignerGroup = FilteredComponentGroup(
|
||||
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
|
||||
alterSignerComponents,
|
||||
key1CommandsFtx.filteredComponentGroups[1].nonces,
|
||||
alterSignerPMTK
|
||||
)
|
||||
val alterFilteredComponents = listOf(key1CommandsFtx.filteredComponentGroups[0], alterSignerGroup)
|
||||
|
||||
// Do not update groupHashes.
|
||||
val ftxAlterSigner = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// Visible components in signers group cannot be verified against their partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxAlterSigner.verify() }
|
||||
// Also, checkAllComponentsVisible() will not pass (groupHash matching will fail).
|
||||
assertFailsWith<ComponentVisibilityException> { ftxAlterSigner.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
// Update groupHashes.
|
||||
val ftxAlterSignerB = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes.subList(0, 6) + alterMTree.hash) as FilteredTransaction
|
||||
// Visible components in signers group cannot be verified against their partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxAlterSignerB.verify() }
|
||||
// Also, checkAllComponentsVisible() will not pass (top level Merkle tree cannot be verified against transaction's id).
|
||||
assertFailsWith<ComponentVisibilityException> { ftxAlterSignerB.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
ftxConstructor.isAccessible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.times
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.core.utilities.minutes
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset.UTC
|
||||
@ -17,6 +20,7 @@ class TimeWindowTest {
|
||||
assertThat(timeWindow.fromTime).isEqualTo(now)
|
||||
assertThat(timeWindow.untilTime).isNull()
|
||||
assertThat(timeWindow.midpoint).isNull()
|
||||
assertThat(timeWindow.length).isNull()
|
||||
assertThat(timeWindow.contains(now - 1.millis)).isFalse()
|
||||
assertThat(timeWindow.contains(now)).isTrue()
|
||||
assertThat(timeWindow.contains(now + 1.millis)).isTrue()
|
||||
@ -28,6 +32,7 @@ class TimeWindowTest {
|
||||
assertThat(timeWindow.fromTime).isNull()
|
||||
assertThat(timeWindow.untilTime).isEqualTo(now)
|
||||
assertThat(timeWindow.midpoint).isNull()
|
||||
assertThat(timeWindow.length).isNull()
|
||||
assertThat(timeWindow.contains(now - 1.millis)).isTrue()
|
||||
assertThat(timeWindow.contains(now)).isFalse()
|
||||
assertThat(timeWindow.contains(now + 1.millis)).isFalse()
|
||||
@ -42,6 +47,7 @@ class TimeWindowTest {
|
||||
assertThat(timeWindow.fromTime).isEqualTo(fromTime)
|
||||
assertThat(timeWindow.untilTime).isEqualTo(untilTime)
|
||||
assertThat(timeWindow.midpoint).isEqualTo(today.atTime(12, 15).toInstant(UTC))
|
||||
assertThat(timeWindow.length).isEqualTo(Duration.between(fromTime, untilTime))
|
||||
assertThat(timeWindow.contains(fromTime - 1.millis)).isFalse()
|
||||
assertThat(timeWindow.contains(fromTime)).isTrue()
|
||||
assertThat(timeWindow.contains(fromTime + 1.millis)).isTrue()
|
||||
@ -51,17 +57,21 @@ class TimeWindowTest {
|
||||
|
||||
@Test
|
||||
fun fromStartAndDuration() {
|
||||
val timeWindow = TimeWindow.fromStartAndDuration(now, 10.minutes)
|
||||
val duration = 10.minutes
|
||||
val timeWindow = TimeWindow.fromStartAndDuration(now, duration)
|
||||
assertThat(timeWindow.fromTime).isEqualTo(now)
|
||||
assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes)
|
||||
assertThat(timeWindow.midpoint).isEqualTo(now + 5.minutes)
|
||||
assertThat(timeWindow.untilTime).isEqualTo(now + duration)
|
||||
assertThat(timeWindow.midpoint).isEqualTo(now + duration / 2)
|
||||
assertThat(timeWindow.length).isEqualTo(duration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withTolerance() {
|
||||
val timeWindow = TimeWindow.withTolerance(now, 10.minutes)
|
||||
assertThat(timeWindow.fromTime).isEqualTo(now - 10.minutes)
|
||||
assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes)
|
||||
val tolerance = 10.minutes
|
||||
val timeWindow = TimeWindow.withTolerance(now, tolerance)
|
||||
assertThat(timeWindow.fromTime).isEqualTo(now - tolerance)
|
||||
assertThat(timeWindow.untilTime).isEqualTo(now + tolerance)
|
||||
assertThat(timeWindow.midpoint).isEqualTo(now)
|
||||
assertThat(timeWindow.length).isEqualTo(tolerance * 2)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.identity.Party
|
||||
@ -14,10 +13,12 @@ import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import java.util.stream.IntStream
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.*
|
||||
|
||||
class PartialMerkleTreeTest : TestDependencyInjectionBase() {
|
||||
val nodes = "abcdef"
|
||||
private val nodes = "abcdef"
|
||||
private val hashed = nodes.map {
|
||||
initialiseTestSerialization()
|
||||
try {
|
||||
@ -115,16 +116,18 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
|
||||
val d = testTx.serialize().deserialize()
|
||||
assertEquals(testTx.id, d.id)
|
||||
|
||||
val mt = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
|
||||
assertEquals(4, mt.filteredComponentGroups.size)
|
||||
assertEquals(1, mt.inputs.size)
|
||||
assertEquals(0, mt.attachments.size)
|
||||
assertEquals(1, mt.outputs.size)
|
||||
assertEquals(1, mt.commands.size)
|
||||
assertNull(mt.notary)
|
||||
assertNotNull(mt.timeWindow)
|
||||
mt.verify()
|
||||
// We expect 5 and not 4 component groups, because there is at least one command in the ftx and thus,
|
||||
// the signers component is also sent (required for visibility purposes).
|
||||
assertEquals(5, ftx.filteredComponentGroups.size)
|
||||
assertEquals(1, ftx.inputs.size)
|
||||
assertEquals(0, ftx.attachments.size)
|
||||
assertEquals(1, ftx.outputs.size)
|
||||
assertEquals(1, ftx.commands.size)
|
||||
assertNull(ftx.notary)
|
||||
assertNotNull(ftx.timeWindow)
|
||||
ftx.verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -246,4 +249,50 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
|
||||
privacySalt = privacySalt
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find leaf index`() {
|
||||
// A Merkle tree with 20 leaves.
|
||||
val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { SecureHash.sha256(it.toString()) }
|
||||
val merkleTree = MerkleTree.getMerkleTree(sampleLeaves)
|
||||
|
||||
// Provided hashes are not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("20"))) }
|
||||
// One of the provided hashes is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("20"), SecureHash.sha256("1"), SecureHash.sha256("5"))) }
|
||||
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("1"), SecureHash.sha256("5"), SecureHash.sha256("0"), SecureHash.sha256("19")))
|
||||
// First leaf.
|
||||
assertEquals(0, pmt.leafIndex(SecureHash.sha256("0")))
|
||||
// Second leaf.
|
||||
assertEquals(1, pmt.leafIndex(SecureHash.sha256("1")))
|
||||
// A random leaf.
|
||||
assertEquals(5, pmt.leafIndex(SecureHash.sha256("5")))
|
||||
// The last leaf.
|
||||
assertEquals(19, pmt.leafIndex(SecureHash.sha256("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("10")) }
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("30")) }
|
||||
|
||||
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("0")))
|
||||
assertEquals(0, pmtFirstElementOnly.leafIndex(SecureHash.sha256("0")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(SecureHash.sha256("10")) }
|
||||
|
||||
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("19")))
|
||||
assertEquals(19, pmtLastElementOnly.leafIndex(SecureHash.sha256("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(SecureHash.sha256("10")) }
|
||||
|
||||
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("5")))
|
||||
assertEquals(5, pmtOneElement.leafIndex(SecureHash.sha256("5")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(SecureHash.sha256("10")) }
|
||||
|
||||
val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves)
|
||||
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(SecureHash.sha256(i.toString())))
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmtAllIncluded.leafIndex(SecureHash.sha256("30")) }
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,12 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.nodeapi.internal.ServiceInfo
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.singleIdentity
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -55,13 +55,13 @@ class AttachmentTests {
|
||||
|
||||
@Test
|
||||
fun `download and store`() {
|
||||
mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
|
||||
// Ensure that registration was successful before progressing any further
|
||||
mockNet.runNetwork()
|
||||
aliceNode.internals.ensureRegistered()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
|
||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
@ -73,7 +73,7 @@ class AttachmentTests {
|
||||
|
||||
// Get node one to run a flow to fetch it and insert it.
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity())
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
|
||||
mockNet.runNetwork()
|
||||
assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size)
|
||||
|
||||
@ -87,13 +87,12 @@ class AttachmentTests {
|
||||
// Shut down node zero and ensure node one can still resolve the attachment.
|
||||
aliceNode.dispose()
|
||||
|
||||
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()).resultFuture.getOrThrow()
|
||||
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow()
|
||||
assertEquals(attachment, response.fromDisk[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing`() {
|
||||
mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
|
||||
@ -107,7 +106,8 @@ class AttachmentTests {
|
||||
// Get node one to fetch a non-existent attachment.
|
||||
val hash = SecureHash.randomSHA256()
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), aliceNode.info.chooseIdentity())
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice)
|
||||
mockNet.runNetwork()
|
||||
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.resultFuture.getOrThrow() }
|
||||
assertEquals(hash, e.requested)
|
||||
@ -130,6 +130,7 @@ class AttachmentTests {
|
||||
// Ensure that registration was successful before progressing any further
|
||||
mockNet.runNetwork()
|
||||
aliceNode.internals.ensureRegistered()
|
||||
val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||
|
||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
@ -146,12 +147,12 @@ class AttachmentTests {
|
||||
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
|
||||
aliceNode.database.transaction {
|
||||
DatabaseTransactionManager.current().session.update(corruptAttachment)
|
||||
session.update(corruptAttachment)
|
||||
}
|
||||
|
||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity())
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.resultFuture.getOrThrow() }
|
||||
}
|
||||
|
@ -23,29 +23,37 @@ import kotlin.reflect.KClass
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CollectSignaturesFlowTests {
|
||||
companion object {
|
||||
private val cordappPackages = listOf("net.corda.testing.contracts")
|
||||
}
|
||||
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var aliceNode: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var bobNode: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var charlieNode: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var alice: Party
|
||||
lateinit var bob: Party
|
||||
lateinit var charlie: Party
|
||||
lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.testing.contracts")
|
||||
mockNet = MockNetwork()
|
||||
mockNet = MockNetwork(cordappPackages = cordappPackages)
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
bobNode = mockNet.createPartyNode(BOB.name)
|
||||
charlieNode = mockNet.createPartyNode(CHARLIE.name)
|
||||
mockNet.runNetwork()
|
||||
notary = notaryNode.services.getDefaultNotary()
|
||||
aliceNode.internals.ensureRegistered()
|
||||
alice = aliceNode.info.singleIdentity()
|
||||
bob = bobNode.info.singleIdentity()
|
||||
charlie = charlieNode.info.singleIdentity()
|
||||
notary = notaryNode.services.getDefaultNotary()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
|
||||
@ -141,7 +149,8 @@ class CollectSignaturesFlowTests {
|
||||
@Test
|
||||
fun `successfully collects two signatures`() {
|
||||
val bConfidentialIdentity = bobNode.database.transaction {
|
||||
bobNode.services.keyManagementService.freshKeyAndCert(bobNode.info.chooseIdentityAndCert(), false)
|
||||
val bobCert = bobNode.services.myInfo.legalIdentitiesAndCerts.single { it.name == bob.name }
|
||||
bobNode.services.keyManagementService.freshKeyAndCert(bobCert, false)
|
||||
}
|
||||
aliceNode.database.transaction {
|
||||
// Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity
|
||||
@ -149,7 +158,7 @@ class CollectSignaturesFlowTests {
|
||||
}
|
||||
registerFlowOnAllNodes(TestFlowTwo.Responder::class)
|
||||
val magicNumber = 1337
|
||||
val parties = listOf(aliceNode.info.chooseIdentity(), bConfidentialIdentity.party, charlieNode.info.chooseIdentity())
|
||||
val parties = listOf(alice, bConfidentialIdentity.party, charlie)
|
||||
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
||||
val flow = aliceNode.services.startFlow(TestFlowTwo.Initiator(state))
|
||||
mockNet.runNetwork()
|
||||
@ -161,7 +170,7 @@ class CollectSignaturesFlowTests {
|
||||
|
||||
@Test
|
||||
fun `no need to collect any signatures`() {
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1))
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
|
||||
val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
@ -173,8 +182,8 @@ class CollectSignaturesFlowTests {
|
||||
|
||||
@Test
|
||||
fun `fails when not signed by initiator`() {
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1))
|
||||
val miniCorpServices = MockServices(MINI_CORP_KEY)
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
|
||||
val miniCorpServices = MockServices(cordappPackages, MINI_CORP_KEY)
|
||||
val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
@ -186,9 +195,9 @@ class CollectSignaturesFlowTests {
|
||||
@Test
|
||||
fun `passes with multiple initial signatures`() {
|
||||
val twoPartyDummyContract = DummyContract.generateInitial(1337, notary,
|
||||
aliceNode.info.chooseIdentity().ref(1),
|
||||
bobNode.info.chooseIdentity().ref(2),
|
||||
bobNode.info.chooseIdentity().ref(3))
|
||||
alice.ref(1),
|
||||
bob.ref(2),
|
||||
bob.ref(3))
|
||||
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
||||
val signedByBoth = bobNode.services.addSignature(signedByA)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet()))
|
||||
|
@ -40,8 +40,7 @@ class ContractUpgradeFlowTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows")
|
||||
mockNet = MockNetwork()
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows"))
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
bobNode = mockNet.createPartyNode(BOB.name)
|
||||
@ -56,7 +55,6 @@ class ContractUpgradeFlowTest {
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -121,7 +119,7 @@ class ContractUpgradeFlowTest {
|
||||
return startRpcClient<CordaRPCOps>(
|
||||
rpcAddress = startRpcServer(
|
||||
rpcUser = user,
|
||||
ops = CordaRPCOpsImpl(node.services, node.smm, node.database)
|
||||
ops = CordaRPCOpsImpl(node.services, node.smm, node.database, node.services)
|
||||
).get().broker.hostAndPort!!,
|
||||
username = user.username,
|
||||
password = user.password
|
||||
|
@ -6,7 +6,7 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
@ -16,53 +16,57 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class FinalityFlowTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var aliceNode: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var bobNode: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var notary: Party
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var aliceServices: StartedNodeServices
|
||||
private lateinit var bobServices: StartedNodeServices
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bob: Party
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.asset")
|
||||
mockNet = MockNetwork()
|
||||
mockNet.createNotaryNode()
|
||||
aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
bobNode = mockNet.createPartyNode(BOB.name)
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
mockNet.runNetwork()
|
||||
aliceNode.internals.ensureRegistered()
|
||||
notary = aliceNode.services.getDefaultNotary()
|
||||
aliceServices = aliceNode.services
|
||||
bobServices = bobNode.services
|
||||
alice = aliceNode.info.singleIdentity()
|
||||
bob = bobNode.info.singleIdentity()
|
||||
notary = notaryNode.services.getDefaultNotary()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `finalise a simple transaction`() {
|
||||
val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0))
|
||||
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, bobNode.info.chooseIdentity(), notary)
|
||||
val stx = aliceNode.services.signInitialTransaction(builder)
|
||||
val flow = aliceNode.services.startFlow(FinalityFlow(stx))
|
||||
Cash().generateIssue(builder, amount, bob, notary)
|
||||
val stx = aliceServices.signInitialTransaction(builder)
|
||||
val flow = aliceServices.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
val notarisedTx = flow.resultFuture.getOrThrow()
|
||||
notarisedTx.verifyRequiredSignatures()
|
||||
val transactionSeenByB = bobNode.services.database.transaction {
|
||||
bobNode.services.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
val transactionSeenByB = bobServices.database.transaction {
|
||||
bobServices.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
}
|
||||
assertEquals(notarisedTx, transactionSeenByB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reject a transaction with unknown parties`() {
|
||||
val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0))
|
||||
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
||||
val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, fakeIdentity, notary)
|
||||
val stx = aliceNode.services.signInitialTransaction(builder)
|
||||
val flow = aliceNode.services.startFlow(FinalityFlow(stx))
|
||||
val stx = aliceServices.signInitialTransaction(builder)
|
||||
val flow = aliceServices.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
flow.resultFuture.getOrThrow()
|
||||
|
@ -61,6 +61,7 @@ class InternalUtilsTest {
|
||||
assertArrayEquals(intArrayOf(1, 2, 3, 4), (1 until 5).stream().toArray())
|
||||
assertArrayEquals(intArrayOf(1, 3), (1..4 step 2).stream().toArray())
|
||||
assertArrayEquals(intArrayOf(1, 3), (1..3 step 2).stream().toArray())
|
||||
@Suppress("EmptyRange") // It's supposed to be empty.
|
||||
assertArrayEquals(intArrayOf(), (1..0).stream().toArray())
|
||||
assertArrayEquals(intArrayOf(1, 0), (1 downTo 0).stream().toArray())
|
||||
assertArrayEquals(intArrayOf(3, 1), (3 downTo 0 step 2).stream().toArray())
|
||||
|
@ -36,8 +36,7 @@ class ResolveTransactionsFlowTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.testing.contracts")
|
||||
mockNet = MockNetwork()
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
notaryNode = mockNet.createNotaryNode()
|
||||
megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name)
|
||||
miniCorpNode = mockNet.createPartyNode(MINI_CORP.name)
|
||||
@ -52,7 +51,6 @@ class ResolveTransactionsFlowTest {
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
|
@ -16,7 +16,7 @@ import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.node.utilities.currentDBSession
|
||||
import net.corda.nodeapi.internal.ServiceInfo
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
@ -54,7 +54,7 @@ private fun StartedNode<*>.hackAttachment(attachmentId: SecureHash, content: Str
|
||||
* @see NodeAttachmentService.importAttachment
|
||||
*/
|
||||
private fun updateAttachment(attachmentId: SecureHash, data: ByteArray) {
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val session = currentDBSession()
|
||||
val attachment = session.get<NodeAttachmentService.DBAttachment>(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString())
|
||||
attachment?.let {
|
||||
attachment.content = data
|
||||
|
@ -12,11 +12,12 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class TransactionSerializationTests : TestDependencyInjectionBase() {
|
||||
val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash"
|
||||
private val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash"
|
||||
|
||||
class TestCash : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
@ -65,7 +66,10 @@ class TransactionSerializationTests : TestDependencyInjectionBase() {
|
||||
stx.verifyRequiredSignatures()
|
||||
|
||||
// Corrupt the data and ensure the signature catches the problem.
|
||||
stx.id.bytes[5] = stx.id.bytes[5].inc()
|
||||
val bytesField = stx.id::bytes.javaField?.apply { setAccessible(true) }
|
||||
val bytes = bytesField?.get(stx.id) as ByteArray
|
||||
bytes[5] = bytes[5].inc()
|
||||
|
||||
assertFailsWith(SignatureException::class) {
|
||||
stx.verifyRequiredSignatures()
|
||||
}
|
||||
|
@ -7,6 +7,8 @@
|
||||
API: Persistence
|
||||
================
|
||||
|
||||
.. contents::
|
||||
|
||||
Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping*
|
||||
(ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing
|
||||
persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins
|
||||
@ -79,10 +81,10 @@ Custom schema registration
|
||||
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan
|
||||
for schemas (any class that extends the ``MappedSchema`` interface) in the `plugins` configuration directory in your CorDapp jar.
|
||||
|
||||
For testing purposes it is necessary to manually register custom schemas as follows:
|
||||
For testing purposes it is necessary to manually register the packages containing custom schemas as follows:
|
||||
|
||||
- Tests using ``MockNetwork`` and ``MockNode`` must explicitly register custom schemas using the `registerCustomSchemas()` method of ``MockNode``
|
||||
- Tests using ``MockServices`` must explicitly register schemas using `customSchemas` attribute of the ``MockServices`` `makeTestDatabaseAndMockServices()` helper method.
|
||||
- Tests using ``MockNetwork`` and ``MockNode`` must explicitly register packages using the `cordappPackages` parameter of ``MockNetwork``
|
||||
- Tests using ``MockServices`` must explicitly register packages using the `cordappPackages` parameter of the ``MockServices`` `makeTestDatabaseAndMockServices()` helper method.
|
||||
|
||||
.. note:: Tests using the `DriverDSL` will automatically register your custom schemas if they are in the same project structure as the driver call.
|
||||
|
||||
|
@ -1,33 +1,42 @@
|
||||
API: Vault Query
|
||||
================
|
||||
|
||||
Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault).
|
||||
.. contents::
|
||||
|
||||
Overview
|
||||
--------
|
||||
Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and
|
||||
libraries for accessing RDBMS backed transactional stores (including the Vault).
|
||||
|
||||
Corda provides a number of flexible query mechanisms for accessing the Vault:
|
||||
|
||||
- Vault Query API
|
||||
- using a JDBC session (as described in :ref:`Persistence <jdbc_session_ref>`)
|
||||
- custom JPA_/JPQL_ queries
|
||||
- custom 3rd party Data Access frameworks such as `Spring Data <http://projects.spring.io/spring-data>`_
|
||||
- Using a JDBC session (as described in :ref:`Persistence <jdbc_session_ref>`)
|
||||
- Custom JPA_/JPQL_ queries
|
||||
- Custom 3rd party Data Access frameworks such as `Spring Data <http://projects.spring.io/spring-data>`_
|
||||
|
||||
The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows:
|
||||
The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the
|
||||
``VaultService`` for use directly by flows:
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultService.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryAPI
|
||||
:end-before: DOCEND VaultQueryAPI
|
||||
:dedent: 4
|
||||
|
||||
and via ``CordaRPCOps`` for use by RPC client applications:
|
||||
And via ``CordaRPCOps`` for use by RPC client applications:
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryByAPI
|
||||
:end-before: DOCEND VaultQueryByAPI
|
||||
:dedent: 4
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultTrackByAPI
|
||||
:end-before: DOCEND VaultTrackByAPI
|
||||
:dedent: 4
|
||||
|
||||
Helper methods are also provided with default values for arguments:
|
||||
|
||||
@ -35,51 +44,84 @@ Helper methods are also provided with default values for arguments:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryAPIHelpers
|
||||
:end-before: DOCEND VaultQueryAPIHelpers
|
||||
:dedent: 4
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultTrackAPIHelpers
|
||||
:end-before: DOCEND VaultTrackAPIHelpers
|
||||
:dedent: 4
|
||||
|
||||
The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of filter criteria.
|
||||
The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of
|
||||
filter criteria:
|
||||
|
||||
- Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``)
|
||||
- Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``)
|
||||
|
||||
.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL)
|
||||
|
||||
Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable.
|
||||
Defaults are defined for Paging (pageNumber = 1, pageSize = 200) and Sorting (direction = ASC).
|
||||
Simple pagination (page number and size) and sorting (directional ordering using standard or custom property
|
||||
attributes) is also specifiable. Defaults are defined for paging (pageNumber = 1, pageSize = 200) and sorting
|
||||
(direction = ASC).
|
||||
|
||||
The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported.
|
||||
The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including
|
||||
and/or composition and a rich set of operators to include:
|
||||
|
||||
* Binary logical (AND, OR)
|
||||
* Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL)
|
||||
* Equality (EQUAL, NOT_EQUAL)
|
||||
* Likeness (LIKE, NOT_LIKE)
|
||||
* Nullability (IS_NULL, NOT_NULL)
|
||||
* Collection based (IN, NOT_IN)
|
||||
* Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT)
|
||||
|
||||
There are four implementations of this interface which can be chained together to define advanced filters.
|
||||
|
||||
1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED).
|
||||
1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED,
|
||||
CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED).
|
||||
|
||||
.. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft locked states).
|
||||
.. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft
|
||||
locked states).
|
||||
|
||||
2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s).
|
||||
2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core
|
||||
``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a
|
||||
specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable
|
||||
attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s).
|
||||
|
||||
.. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table.
|
||||
.. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common
|
||||
state attributes to the **vault_fungible_states** table.
|
||||
|
||||
3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s).
|
||||
3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState``
|
||||
and ``DealState`` contract state interfaces, used to represent entities that continuously supersede themselves, all
|
||||
of which share the same ``linearId`` (e.g. trade entity states such as the ``IRSState`` defined in the SIMM
|
||||
valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s).
|
||||
|
||||
.. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table.
|
||||
.. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those
|
||||
interfaces common state attributes to the **vault_linear_states** table.
|
||||
|
||||
4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence </api-persistence>` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL.
|
||||
4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined
|
||||
by a custom contract state that implements its own schema as described in the :doc:`Persistence </api-persistence>`
|
||||
documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe
|
||||
``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The
|
||||
``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator
|
||||
types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg,
|
||||
max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple
|
||||
construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in
|
||||
``QueryCriteriaUtils`` for a complete specification of the DSL.
|
||||
|
||||
.. note:: custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to
|
||||
:doc:`Persistence </api-persistence>` for mechanisms of registering custom schemas for different testing purposes.
|
||||
.. note:: Custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to
|
||||
:doc:`Persistence </api-persistence>` for mechanisms of registering custom schemas for different testing
|
||||
purposes.
|
||||
|
||||
All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators.
|
||||
|
||||
All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes:
|
||||
|
||||
1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states.
|
||||
When chaining several criterias using AND / OR, the last value of this attribute will override any previous.
|
||||
2. Contract state types (``<Set<Class<out ContractState>>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all state types).
|
||||
When chaining several criteria using ``and`` and ``or`` operators, all specified contract state types are combined into a single set.
|
||||
When chaining several criterias using AND / OR, the last value of this attribute will override any previous
|
||||
2. Contract state types (``<Set<Class<out ContractState>>``), which will contain at minimum one type (by default this
|
||||
will be ``ContractState`` which resolves to all state types). When chaining several criteria using ``and`` and
|
||||
``or`` operators, all specified contract state types are combined into a single set
|
||||
|
||||
An example of a custom query is illustrated here:
|
||||
|
||||
@ -87,20 +129,30 @@ An example of a custom query is illustrated here:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample20
|
||||
:end-before: DOCEND VaultQueryExample20
|
||||
:dedent: 12
|
||||
|
||||
.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples.
|
||||
.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types
|
||||
``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root
|
||||
``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See
|
||||
``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples.
|
||||
|
||||
Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java.
|
||||
|
||||
.. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States).
|
||||
.. note:: When specifying the ``ContractType`` as a parameterised type to the ``QueryCriteria`` in Kotlin, queries now
|
||||
include all concrete implementations of that type if this is an interface. Previously, it was only possible to query
|
||||
on concrete types (or the universe of all ``ContractState``).
|
||||
|
||||
The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based :doc:`Persistence </api-persistence>` framework adopted by Corda.
|
||||
The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based
|
||||
:doc:`Persistence </api-persistence>` framework adopted by Corda.
|
||||
|
||||
.. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/
|
||||
|
||||
.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables.
|
||||
.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based,
|
||||
read-only access to underlying Corda tables.
|
||||
|
||||
.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute types.
|
||||
.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that
|
||||
Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute
|
||||
types.
|
||||
|
||||
An example of a custom query in Java is illustrated here:
|
||||
|
||||
@ -108,17 +160,24 @@ An example of a custom query in Java is illustrated here:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample3
|
||||
:end-before: DOCEND VaultJavaQueryExample3
|
||||
:dedent: 16
|
||||
|
||||
.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not resolve to an X500Name via the IdentityService, no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria.
|
||||
.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case,
|
||||
where an anonymous party does not resolve to an X500 name via the ``IdentityService``, no query results will ever be
|
||||
produced. For performance reasons, queries do not use ``PublicKey`` as search criteria.
|
||||
|
||||
Pagination
|
||||
----------
|
||||
The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 results).
|
||||
Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer from worrying about pagination where
|
||||
result sets are expected to be constrained to 200 or fewer entries. Where large result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to define a ``PageSpecification`` to correctly process results with efficient memory utilistion. A fail-fast mode is in place to alert API users to the need for pagination where a single query returns more than 200 results and no ``PageSpecification``
|
||||
has been supplied.
|
||||
The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200
|
||||
results). Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer
|
||||
from worrying about pagination where result sets are expected to be constrained to 200 or fewer entries. Where large
|
||||
result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to
|
||||
define a ``PageSpecification`` to correctly process results with efficient memory utilisation. A fail-fast mode is in
|
||||
place to alert API users to the need for pagination where a single query returns more than 200 results and no
|
||||
``PageSpecification`` has been supplied.
|
||||
|
||||
.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme caution as results returned may exceed your JVM's memory footprint.
|
||||
.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme
|
||||
caution as results returned may exceed your JVM's memory footprint.
|
||||
|
||||
Example usage
|
||||
-------------
|
||||
@ -126,7 +185,7 @@ Example usage
|
||||
Kotlin
|
||||
^^^^^^
|
||||
|
||||
**General snapshot queries using** ``VaultQueryCriteria``
|
||||
**General snapshot queries using** ``VaultQueryCriteria``:
|
||||
|
||||
Query for all unconsumed states (simplest query possible):
|
||||
|
||||
@ -134,6 +193,7 @@ Query for all unconsumed states (simplest query possible):
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample1
|
||||
:end-before: DOCEND VaultQueryExample1
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed states for some state references:
|
||||
|
||||
@ -141,6 +201,7 @@ Query for unconsumed states for some state references:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample2
|
||||
:end-before: DOCEND VaultQueryExample2
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed states for several contract state types:
|
||||
|
||||
@ -148,6 +209,7 @@ Query for unconsumed states for several contract state types:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample3
|
||||
:end-before: DOCEND VaultQueryExample3
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed states for a given notary:
|
||||
|
||||
@ -155,6 +217,7 @@ Query for unconsumed states for a given notary:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample4
|
||||
:end-before: DOCEND VaultQueryExample4
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed states for a given set of participants:
|
||||
|
||||
@ -162,6 +225,7 @@ Query for unconsumed states for a given set of participants:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample5
|
||||
:end-before: DOCEND VaultQueryExample5
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed states recorded between two time intervals:
|
||||
|
||||
@ -169,6 +233,7 @@ Query for unconsumed states recorded between two time intervals:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample6
|
||||
:end-before: DOCEND VaultQueryExample6
|
||||
:dedent: 12
|
||||
|
||||
.. note:: This example illustrates usage of a ``Between`` ``ColumnPredicate``.
|
||||
|
||||
@ -178,17 +243,21 @@ Query for all states with pagination specification (10 results per page):
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample7
|
||||
:end-before: DOCEND VaultQueryExample7
|
||||
:dedent: 12
|
||||
|
||||
.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as demonstrated in the following example.
|
||||
.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as
|
||||
demonstrated in the following example.
|
||||
|
||||
Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further pages available:
|
||||
Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further
|
||||
pages available:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExamplePaging
|
||||
:end-before: DOCEND VaultQueryExamplePaging
|
||||
:dedent: 8
|
||||
|
||||
**LinearState and DealState queries using** ``LinearStateQueryCriteria``
|
||||
**LinearState and DealState queries using** ``LinearStateQueryCriteria``:
|
||||
|
||||
Query for unconsumed linear states for given linear ids:
|
||||
|
||||
@ -196,6 +265,7 @@ Query for unconsumed linear states for given linear ids:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample8
|
||||
:end-before: DOCEND VaultQueryExample8
|
||||
:dedent: 12
|
||||
|
||||
Query for all linear states associated with a linear id:
|
||||
|
||||
@ -203,6 +273,7 @@ Query for all linear states associated with a linear id:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample9
|
||||
:end-before: DOCEND VaultQueryExample9
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed deal states with deals references:
|
||||
|
||||
@ -210,6 +281,7 @@ Query for unconsumed deal states with deals references:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample10
|
||||
:end-before: DOCEND VaultQueryExample10
|
||||
:dedent: 12
|
||||
|
||||
Query for unconsumed deal states with deals parties:
|
||||
|
||||
@ -217,8 +289,9 @@ Query for unconsumed deal states with deals parties:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample11
|
||||
:end-before: DOCEND VaultQueryExample11
|
||||
:dedent: 12
|
||||
|
||||
**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria``
|
||||
**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria``:
|
||||
|
||||
Query for fungible assets for a given currency:
|
||||
|
||||
@ -226,6 +299,7 @@ Query for fungible assets for a given currency:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample12
|
||||
:end-before: DOCEND VaultQueryExample12
|
||||
:dedent: 12
|
||||
|
||||
Query for fungible assets for a minimum quantity:
|
||||
|
||||
@ -233,19 +307,21 @@ Query for fungible assets for a minimum quantity:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample13
|
||||
:end-before: DOCEND VaultQueryExample13
|
||||
:dedent: 12
|
||||
|
||||
.. note:: This example uses the builder DSL.
|
||||
|
||||
Query for fungible assets for a specifc issuer party:
|
||||
Query for fungible assets for a specific issuer party:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample14
|
||||
:end-before: DOCEND VaultQueryExample14
|
||||
:dedent: 12
|
||||
|
||||
**Aggregate Function queries using** ``VaultCustomQueryCriteria``
|
||||
**Aggregate Function queries using** ``VaultCustomQueryCriteria``:
|
||||
|
||||
.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page.
|
||||
.. note:: Query results for aggregate functions are contained in the ``otherResults`` attribute of a results Page.
|
||||
|
||||
Aggregations on cash using various functions:
|
||||
|
||||
@ -253,8 +329,9 @@ Aggregations on cash using various functions:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample21
|
||||
:end-before: DOCEND VaultQueryExample21
|
||||
:dedent: 12
|
||||
|
||||
.. note:: `otherResults` will contain 5 items, one per calculated aggregate function.
|
||||
.. note:: ``otherResults`` will contain 5 items, one per calculated aggregate function.
|
||||
|
||||
Aggregations on cash grouped by currency for various functions:
|
||||
|
||||
@ -262,8 +339,10 @@ Aggregations on cash grouped by currency for various functions:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample22
|
||||
:end-before: DOCEND VaultQueryExample22
|
||||
:dedent: 12
|
||||
|
||||
.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result).
|
||||
.. note:: ``otherResults`` will contain 24 items, one result per calculated aggregate function per currency (the
|
||||
grouping attribute - currency in this case - is returned per aggregate result).
|
||||
|
||||
Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
|
||||
|
||||
@ -271,10 +350,15 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample23
|
||||
:end-before: DOCEND VaultQueryExample23
|
||||
:dedent: 12
|
||||
|
||||
.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result).
|
||||
.. note:: ``otherResults`` will contain 12 items sorted from largest summed cash amount to smallest, one result per
|
||||
calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result).
|
||||
|
||||
**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable<Vault.Update>``. Refer to `ReactiveX Observable <http://reactivex.io/documentation/observable.html>`_ for a detailed understanding and usage of this type.
|
||||
Dynamic queries (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an
|
||||
additional ``QueryResults`` return type in the form of an ``Observable<Vault.Update>``. Refer to
|
||||
`ReactiveX Observable <http://reactivex.io/documentation/observable.html>`_ for a detailed understanding and usage of
|
||||
this type.
|
||||
|
||||
Track unconsumed cash states:
|
||||
|
||||
@ -282,6 +366,7 @@ Track unconsumed cash states:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample15
|
||||
:end-before: DOCEND VaultQueryExample15
|
||||
:dedent: 20
|
||||
|
||||
Track unconsumed linear states:
|
||||
|
||||
@ -289,8 +374,9 @@ Track unconsumed linear states:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample16
|
||||
:end-before: DOCEND VaultQueryExample16
|
||||
:dedent: 20
|
||||
|
||||
.. note:: This will return both Deal and Linear states.
|
||||
.. note:: This will return both ``DealState`` and ``LinearState`` states.
|
||||
|
||||
Track unconsumed deal states:
|
||||
|
||||
@ -298,8 +384,9 @@ Track unconsumed deal states:
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample17
|
||||
:end-before: DOCEND VaultQueryExample17
|
||||
:dedent: 20
|
||||
|
||||
.. note:: This will return only Deal states.
|
||||
.. note:: This will return only ``DealState`` states.
|
||||
|
||||
Java examples
|
||||
^^^^^^^^^^^^^
|
||||
@ -310,6 +397,7 @@ Query for all unconsumed linear states:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample0
|
||||
:end-before: DOCEND VaultJavaQueryExample0
|
||||
:dedent: 12
|
||||
|
||||
Query for all consumed cash states:
|
||||
|
||||
@ -317,6 +405,7 @@ Query for all consumed cash states:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample1
|
||||
:end-before: DOCEND VaultJavaQueryExample1
|
||||
:dedent: 12
|
||||
|
||||
Query for consumed deal states or linear ids, specify a paging specification and sort by unique identifier:
|
||||
|
||||
@ -324,8 +413,9 @@ Query for consumed deal states or linear ids, specify a paging specification and
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample2
|
||||
:end-before: DOCEND VaultJavaQueryExample2
|
||||
:dedent: 12
|
||||
|
||||
**Aggregate Function queries using** ``VaultCustomQueryCriteria``
|
||||
**Aggregate Function queries using** ``VaultCustomQueryCriteria``:
|
||||
|
||||
Aggregations on cash using various functions:
|
||||
|
||||
@ -333,6 +423,7 @@ Aggregations on cash using various functions:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample21
|
||||
:end-before: DOCEND VaultJavaQueryExample21
|
||||
:dedent: 16
|
||||
|
||||
Aggregations on cash grouped by currency for various functions:
|
||||
|
||||
@ -340,6 +431,7 @@ Aggregations on cash grouped by currency for various functions:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample22
|
||||
:end-before: DOCEND VaultJavaQueryExample22
|
||||
:dedent: 16
|
||||
|
||||
Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
|
||||
|
||||
@ -347,6 +439,7 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample23
|
||||
:end-before: DOCEND VaultJavaQueryExample23
|
||||
:dedent: 16
|
||||
|
||||
Track unconsumed cash states:
|
||||
|
||||
@ -354,13 +447,16 @@ Track unconsumed cash states:
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample4
|
||||
:end-before: DOCEND VaultJavaQueryExample4
|
||||
:dedent: 12
|
||||
|
||||
Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique identifier):
|
||||
Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique
|
||||
identifier):
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample4
|
||||
:end-before: DOCEND VaultJavaQueryExample4
|
||||
:dedent: 12
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
@ -373,24 +469,27 @@ If the results your were expecting do not match actual returned query results we
|
||||
|
||||
Behavioural notes
|
||||
-----------------
|
||||
1. **TrackBy** updates do not take into account the full criteria specification due to different and more restrictive syntax
|
||||
in `observables <https://github.com/ReactiveX/RxJava/wiki>`_ filtering (vs full SQL-92 JDBC filtering as used in snapshot views).
|
||||
Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` (UNCONSUMED, CONSUMED, ALL) only.
|
||||
2. **QueryBy** and **TrackBy snapshot views** using pagination may return different result sets as each paging request is a
|
||||
separate SQL query on the underlying database, and it is entirely conceivable that state modifications are taking
|
||||
place in between and/or in parallel to paging requests.
|
||||
When using pagination, always check the value of the ``totalStatesAvailable`` (from the ``Vault.Page`` result) and
|
||||
adjust further paging requests appropriately.
|
||||
1. ``TrackBy`` updates do not take into account the full criteria specification due to different and more restrictive
|
||||
syntax in `observables <https://github.com/ReactiveX/RxJava/wiki>`_ filtering (vs full SQL-92 JDBC filtering as used
|
||||
in snapshot views). Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType``
|
||||
(UNCONSUMED, CONSUMED, ALL) only
|
||||
2. ``QueryBy`` and ``TrackBy`` snapshot views using pagination may return different result sets as each paging request
|
||||
is a separate SQL query on the underlying database, and it is entirely conceivable that state modifications are
|
||||
taking place in between and/or in parallel to paging requests. When using pagination, always check the value of the
|
||||
``totalStatesAvailable`` (from the ``Vault.Page`` result) and adjust further paging requests appropriately.
|
||||
|
||||
Other use case scenarios
|
||||
------------------------
|
||||
|
||||
For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of the box. Namely, implementations of JPQL (JPA Query Language) such as **Hibernate** for advanced SQL access, and **Spring Data** for advanced pagination and ordering constructs.
|
||||
For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is
|
||||
recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of
|
||||
the box. Namely, implementations of JPQL (JPA Query Language) such as Hibernate for advanced SQL access, and
|
||||
Spring Data for advanced pagination and ordering constructs.
|
||||
|
||||
The Corda Tutorials provide examples satisfying these additional Use Cases:
|
||||
|
||||
1. Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State
|
||||
2. Template / Tutorial CorDapp service query extension executing Named Queries via JPQL_
|
||||
1. Example CorDapp service using Vault API Custom Query to access attributes of IOU State
|
||||
2. Example CorDapp service query extension executing Named Queries via JPQL_
|
||||
3. `Advanced pagination <https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html>`_ queries using Spring Data JPA_
|
||||
|
||||
.. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql
|
||||
|
@ -97,7 +97,7 @@ Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages t
|
||||
|
||||
* **Loading the Yo! CorDapp onto your nodes**
|
||||
|
||||
The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory.
|
||||
The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their cordapps directory.
|
||||
|
||||
Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command:
|
||||
|
||||
@ -105,14 +105,14 @@ For Corda nodes running release M10
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
cd /opt/corda/plugins
|
||||
cd /opt/corda/cordapps
|
||||
wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar
|
||||
|
||||
For Corda nodes running release M11
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
cd /opt/corda/plugins
|
||||
cd /opt/corda/cordapps
|
||||
wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar
|
||||
|
||||
Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal:
|
||||
|
@ -6,14 +6,19 @@ from the previous milestone release.
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
* ``OpaqueBytes.bytes`` now returns a clone of its underlying ``ByteArray``, and has been redeclared as ``final``.
|
||||
This is a minor change to the public API, but is required to ensure that classes like ``SecureHash`` are immutable.
|
||||
|
||||
* ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions.
|
||||
|
||||
* Renamed "plugins" directory on nodes to "cordapps"
|
||||
|
||||
* The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and
|
||||
deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in
|
||||
the standard CorDapp format.
|
||||
|
||||
* ``Cordform`` and node identity generation
|
||||
* Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens:
|
||||
* ``Cordform`` and node identity generation:
|
||||
* Removed the parameter ``NetworkMap`` from Cordform. Now at the end of the deployment the following happens:
|
||||
1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory.
|
||||
2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder.
|
||||
|
||||
@ -21,7 +26,7 @@ UNRELEASED
|
||||
|
||||
* ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup.
|
||||
|
||||
* Enums now respsect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't
|
||||
* Enums now respect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't
|
||||
either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is
|
||||
thrown.
|
||||
|
||||
@ -36,6 +41,28 @@ UNRELEASED
|
||||
* Cordformation node building DSL can have an additional parameter `configFile` with the path to a properties file
|
||||
to be appended to node.conf.
|
||||
|
||||
* ``FlowLogic`` now has a static method called ``sleep`` which can be used in certain circumstances to help with resolving
|
||||
contention over states in flows. This should be used in place of any other sleep primitive since these are not compatible
|
||||
with flows and their use will be prevented at some point in the future. Pay attention to the warnings and limitations
|
||||
described in the documentation for this method. This helps resolve a bug in ``Cash`` coin selection.
|
||||
A new static property `currentTopLevel` returns the top most `FlowLogic` instance, or null if not in a flow.
|
||||
|
||||
* ``CordaService`` annotated classes should be upgraded to take a constructor parameter of type ``AppServiceHub`` which
|
||||
allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability
|
||||
service classes with only ``ServiceHub`` constructors will still work.
|
||||
|
||||
* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the
|
||||
time-window is open-ended.
|
||||
|
||||
* A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command``
|
||||
signers.
|
||||
|
||||
* ``PartialMerkleTree`` is equipped with a ``leafIndex`` function that returns the index of a hash (leaf) in the
|
||||
partial Merkle tree structure.
|
||||
|
||||
* A new function ``checkCommandVisibility(publicKey: PublicKey)`` has been added to ``FilteredTransaction`` to check
|
||||
if every command that a signer should receive (e.g. an Oracle) is indeed visible.
|
||||
|
||||
.. _changelog_v1:
|
||||
|
||||
Release 1.0
|
||||
|
@ -74,7 +74,7 @@ The following modules are available but we do not commit to their stability or c
|
||||
* **net.corda.samples.demos.bankofcorda**: simulates the role of an asset issuing authority (eg. central bank for cash)
|
||||
* **net.corda.samples.demos.irs**: demonstrates an Interest Rate Swap agreement between two banks
|
||||
* **net.corda.samples.demos.notary**: a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft or BFT SMaRt) notary
|
||||
* **net.corda.samples.demos.simmvaluation**: See our [main documentation site](https://docs.corda.net/initial-margin-agreement.html) regarding the SIMM valuation and agreement on a distributed ledger
|
||||
* **net.corda.samples.demos.simmvaluation**: A demo of SIMM valuation and agreement on a distributed ledger
|
||||
* **net.corda.samples.demos.trader**: demonstrates four nodes, a notary, an issuer of cash (Bank of Corda), and two parties trading with each other, exchanging cash for a commercial paper
|
||||
* **net.corda.node.smoke.test.utils**: test utilities for smoke testing
|
||||
* **net.corda.node.test.common**: common test functionality
|
||||
@ -90,4 +90,4 @@ The following modules are available but we do not commit to their stability or c
|
||||
.. warning:: Code inside any package in the ``net.corda`` namespace which contains ``.internal`` or in ``net.corda.node`` for internal use only.
|
||||
Future releases will reject any CorDapps that use types from these packages.
|
||||
|
||||
.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow.
|
||||
.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow.
|
||||
|
@ -94,7 +94,7 @@ path to the node's base directory.
|
||||
.. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field
|
||||
is present the web server will start.
|
||||
|
||||
:notary: Optional config object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
|
||||
:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
|
||||
cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both.
|
||||
|
||||
:validating: Boolean to determine whether the notary is a validating or non-validating one.
|
||||
@ -108,11 +108,15 @@ path to the node's base directory.
|
||||
members must be active and be able to communicate with the cluster leader for joining. If empty, a new
|
||||
cluster will be bootstrapped.
|
||||
|
||||
:bftSMaRt: If part of a distributed BFT SMaRt cluster specify this config object, with the following settings:
|
||||
:bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings:
|
||||
|
||||
:replicaId:
|
||||
:replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id.
|
||||
|
||||
:clusterAddresses:
|
||||
:clusterAddresses: List of all BFT-SMaRt cluster member addresses.
|
||||
|
||||
:custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`.
|
||||
|
||||
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
|
||||
|
||||
:networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is
|
||||
a config object with the details of the network map service:
|
||||
|
@ -78,11 +78,11 @@ For further information about managing dependencies, see
|
||||
Installing CorDapps
|
||||
-------------------
|
||||
|
||||
At runtime, nodes will load any plugins present in their ``plugins`` folder. Therefore in order to install a cordapp to
|
||||
a node the cordapp JAR must be added to the ``<node_dir>/plugins/`` folder, where ``node_dir`` is the folder in which the
|
||||
At runtime, nodes will load any CorDapp JARs present in their ``cordapps`` folder. Therefore in order to install a CorDapp to
|
||||
a node the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which the
|
||||
node's JAR and configuration files are stored).
|
||||
|
||||
The ``deployNodes`` gradle task, if correctly configured, will automatically place your cordapp JAR as well as any
|
||||
The ``deployNodes`` gradle task, if correctly configured, will automatically place your CorDapp JAR as well as any
|
||||
dependent cordapp JARs specified into the directory automatically.
|
||||
|
||||
Example
|
||||
|
@ -37,13 +37,13 @@ Profiles
|
||||
|
||||
notary/
|
||||
node.conf
|
||||
plugins/
|
||||
cordapps/
|
||||
banka/
|
||||
node.conf
|
||||
plugins/
|
||||
cordapps/
|
||||
bankb/
|
||||
node.conf
|
||||
plugins/
|
||||
cordapps/
|
||||
example-cordapp.jar
|
||||
...
|
||||
|
||||
@ -133,7 +133,7 @@ current working directory of the JVM):
|
||||
corda-webserver.jar
|
||||
explorer/
|
||||
node-explorer.jar
|
||||
plugins/
|
||||
cordapps/
|
||||
bank-of-corda.jar
|
||||
|
||||
..
|
||||
|
@ -11,13 +11,12 @@ Cordform is the local node deployment system for CorDapps. The nodes generated a
|
||||
debugging, and testing node configurations, but not for production or testnet deployment.
|
||||
|
||||
Here is an example Gradle task called ``deployNodes`` that uses the Cordform plugin to deploy three nodes, plus a
|
||||
notary/network map node:
|
||||
notary node:
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "O=Controller,OU=corda,L=London,C=UK"
|
||||
node {
|
||||
name "O=Controller,OU=corda,L=London,C=UK"
|
||||
notary = [validating : true]
|
||||
@ -52,9 +51,7 @@ notary/network map node:
|
||||
}
|
||||
}
|
||||
|
||||
You can extend ``deployNodes`` to generate any number of nodes you like. The only requirement is that you must specify
|
||||
one node as running the network map service, by putting their name in the ``networkMap`` field. In our example, the
|
||||
``Controller`` is set as the network map service.
|
||||
You can extend ``deployNodes`` to generate any number of nodes you like.
|
||||
|
||||
.. warning:: When adding nodes, make sure that there are no port clashes!
|
||||
|
||||
@ -85,9 +82,14 @@ run all the nodes at once. Each node in the ``nodes`` folder has the following s
|
||||
.. sourcecode:: none
|
||||
|
||||
. nodeName
|
||||
├── corda.jar // The Corda runtime
|
||||
├── node.conf // The node's configuration
|
||||
└── plugins // Any installed CorDapps
|
||||
├── corda.jar // The Corda runtime
|
||||
├── node.conf // The node's configuration
|
||||
├── cordapps // Any installed CorDapps
|
||||
└── additional-node-infos // Directory containing all the addresses and certificates of the other nodes.
|
||||
|
||||
.. note:: During the build process each node generates a NodeInfo file which is written in its own root directory,
|
||||
the plug-in proceeds and copies each node NodeInfo to every other node ``additional-node-infos`` directory.
|
||||
The NodeInfo file contains a node hostname and port, legal name and security certificate.
|
||||
|
||||
.. note:: Outside of development environments, do not store your node directories in the build folder.
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
|
||||
repositories {
|
||||
@ -73,7 +72,6 @@ task integrationTest(type: Test) {
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "O=Notary Service,OU=corda,L=London,C=GB"
|
||||
node {
|
||||
name "O=Notary Service,OU=corda,L=London,C=GB"
|
||||
notary = [validating : true]
|
||||
|
@ -0,0 +1,46 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
// DOCSTART 01
|
||||
import net.corda.core.contracts.CommandData;
|
||||
import net.corda.core.contracts.CommandWithParties;
|
||||
import net.corda.core.contracts.Contract;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
public class IOUContract implements Contract {
|
||||
// Our Create command.
|
||||
public static class Create implements CommandData {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) {
|
||||
final CommandWithParties<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1);
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState out = tx.outputsOfType(IOUState.class).get(0);
|
||||
final Party lender = out.getLender();
|
||||
final Party borrower = out.getBorrower();
|
||||
check.using("The IOU's value must be non-negative.", out.getValue() > 0);
|
||||
check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
|
||||
|
||||
// Constraints on the signers.
|
||||
final List<PublicKey> signers = command.getSigners();
|
||||
check.using("There must only be one signer.", signers.size() == 1);
|
||||
check.using("The signer must be the lender.", signers.contains(lender.getOwningKey()));
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,68 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.contracts.StateAndContract;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
public class IOUFlow extends FlowLogic<Void> {
|
||||
private final Integer iouValue;
|
||||
private final Party otherParty;
|
||||
|
||||
/**
|
||||
* The progress tracker provides checkpoints indicating the progress of the flow to observers.
|
||||
*/
|
||||
private final ProgressTracker progressTracker = new ProgressTracker();
|
||||
|
||||
public IOUFlow(Integer iouValue, Party otherParty) {
|
||||
this.iouValue = iouValue;
|
||||
this.otherParty = otherParty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressTracker getProgressTracker() {
|
||||
return progressTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flow logic is encapsulated within the call() method.
|
||||
*/
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// We retrieve the notary identity from the network map.
|
||||
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||
|
||||
// We create a transaction builder.
|
||||
final TransactionBuilder txBuilder = new TransactionBuilder();
|
||||
txBuilder.setNotary(notary);
|
||||
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
String outputContract = IOUContract.class.getName();
|
||||
StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract);
|
||||
Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey());
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd);
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(getServiceHub());
|
||||
|
||||
// Signing the transaction.
|
||||
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(signedTx));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,39 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
// DOCSTART 01
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
private final int value;
|
||||
private final Party lender;
|
||||
private final Party borrower;
|
||||
|
||||
public IOUState(int value, Party lender, Party borrower) {
|
||||
this.value = value;
|
||||
this.lender = lender;
|
||||
this.borrower = borrower;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getLender() {
|
||||
return lender;
|
||||
}
|
||||
|
||||
public Party getBorrower() {
|
||||
return borrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(lender, borrower);
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,50 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
// DOCSTART 01
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.CommandData;
|
||||
import net.corda.core.contracts.CommandWithParties;
|
||||
import net.corda.core.contracts.Contract;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
// DOCEND 01
|
||||
|
||||
public class IOUContract implements Contract {
|
||||
// Our Create command.
|
||||
public static class Create implements CommandData {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) {
|
||||
final CommandWithParties<net.corda.docs.java.tutorial.helloworld.IOUContract.Create> command = requireSingleCommand(tx.getCommands(), net.corda.docs.java.tutorial.helloworld.IOUContract.Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1);
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState out = tx.outputsOfType(IOUState.class).get(0);
|
||||
final Party lender = out.getLender();
|
||||
final Party borrower = out.getBorrower();
|
||||
check.using("The IOU's value must be non-negative.", out.getValue() > 0);
|
||||
check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
|
||||
|
||||
// DOCSTART 02
|
||||
// Constraints on the signers.
|
||||
final List<PublicKey> signers = command.getSigners();
|
||||
check.using("There must be two signers.", signers.size() == 2);
|
||||
check.using("The borrower and lender must be signers.", signers.containsAll(
|
||||
ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey())));
|
||||
// DOCEND 02
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.contracts.StateAndContract;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
// DOCEND 01
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
public class IOUFlow extends FlowLogic<Void> {
|
||||
private final Integer iouValue;
|
||||
private final Party otherParty;
|
||||
|
||||
/**
|
||||
* The progress tracker provides checkpoints indicating the progress of the flow to observers.
|
||||
*/
|
||||
private final ProgressTracker progressTracker = new ProgressTracker();
|
||||
|
||||
public IOUFlow(Integer iouValue, Party otherParty) {
|
||||
this.iouValue = iouValue;
|
||||
this.otherParty = otherParty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressTracker getProgressTracker() {
|
||||
return progressTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flow logic is encapsulated within the call() method.
|
||||
*/
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// We retrieve the notary identity from the network map.
|
||||
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||
|
||||
// We create a transaction builder.
|
||||
final TransactionBuilder txBuilder = new TransactionBuilder();
|
||||
txBuilder.setNotary(notary);
|
||||
|
||||
// DOCSTART 02
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
String outputContract = IOUContract.class.getName();
|
||||
StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract);
|
||||
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd);
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(getServiceHub());
|
||||
|
||||
// Signing the transaction.
|
||||
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Creating a session with the other party.
|
||||
FlowSession otherpartySession = initiateFlow(otherParty);
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
||||
signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker()));
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(fullySignedTx));
|
||||
|
||||
return null;
|
||||
// DOCEND 02
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
import net.corda.docs.java.tutorial.helloworld.IOUFlow;
|
||||
import net.corda.docs.java.tutorial.helloworld.IOUState;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
@InitiatedBy(IOUFlow.class)
|
||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
private final FlowSession otherPartySession;
|
||||
|
||||
public IOUFlowResponder(FlowSession otherPartySession) {
|
||||
this.otherPartySession = otherPartySession;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
class SignTxFlow extends SignTransactionFlow {
|
||||
private SignTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) {
|
||||
super(otherPartySession, progressTracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTransaction(SignedTransaction stx) {
|
||||
requireThat(require -> {
|
||||
ContractState output = stx.getTx().getOutputs().get(0).getData();
|
||||
require.using("This must be an IOU transaction.", output instanceof IOUState);
|
||||
IOUState iou = (IOUState) output;
|
||||
require.using("The IOU's value can't be too high.", iou.getValue() < 100);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker()));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,37 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
private final int value;
|
||||
private final Party lender;
|
||||
private final Party borrower;
|
||||
|
||||
public IOUState(int value, Party lender, Party borrower) {
|
||||
this.value = value;
|
||||
this.lender = lender;
|
||||
this.borrower = borrower;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getLender() {
|
||||
return lender;
|
||||
}
|
||||
|
||||
public Party getBorrower() {
|
||||
return borrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(lender, borrower);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -23,7 +23,7 @@ import java.util.*
|
||||
object CustomVaultQuery {
|
||||
|
||||
@CordaService
|
||||
class Service(val services: ServiceHub) : SingletonSerializeAsToken() {
|
||||
class Service(val services: AppServiceHub) : SingletonSerializeAsToken() {
|
||||
private companion object {
|
||||
val log = loggerFor<Service>()
|
||||
}
|
||||
@ -49,7 +49,7 @@ object CustomVaultQuery {
|
||||
val session = services.jdbcSession()
|
||||
val prepStatement = session.prepareStatement(nativeQuery)
|
||||
val rs = prepStatement.executeQuery()
|
||||
var topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
||||
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
||||
while (rs.next()) {
|
||||
val currencyStr = rs.getString(1)
|
||||
val amount = rs.getLong(2)
|
||||
|
@ -0,0 +1,33 @@
|
||||
package net.corda.docs.tutorial.helloworld
|
||||
|
||||
// DOCSTART 01
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.requireSingleCommand
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
class IOUContract : Contract {
|
||||
// Our Create command.
|
||||
class Create : CommandData
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputsOfType<IOUState>().single()
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
||||
|
||||
// Constraints on the signers.
|
||||
"There must only be one signer." using (command.signers.toSet().size == 1)
|
||||
"The signer must be the lender." using (command.signers.contains(out.lender.owningKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,52 @@
|
||||
package net.corda.docs.tutorial.helloworld
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class IOUFlow(val iouValue: Int,
|
||||
val otherParty: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
|
||||
override val progressTracker = ProgressTracker()
|
||||
|
||||
/** The flow logic is encapsulated within the call() method. */
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// We retrieve the notary identity from the network map.
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
|
||||
// We create a transaction builder
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val outputContract = IOUContract::class.jvmName
|
||||
val outputContractAndState = StateAndContract(outputState, outputContract)
|
||||
val cmd = Command(IOUContract.Create(), ourIdentity.owningKey)
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd)
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(serviceHub)
|
||||
|
||||
// Signing the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(FinalityFlow(signedTx))
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,12 @@
|
||||
package net.corda.docs.tutorial.helloworld
|
||||
|
||||
// DOCSTART 01
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,36 @@
|
||||
package net.corda.docs.tutorial.twoparty
|
||||
|
||||
// DOCSTART 01
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.requireSingleCommand
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
// DOCEND 01
|
||||
|
||||
class IOUContract : Contract {
|
||||
// Our Create command.
|
||||
class Create : CommandData
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputsOfType<net.corda.docs.tutorial.helloworld.IOUState>().single()
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
||||
|
||||
// DOCSTART 02
|
||||
// Constraints on the signers.
|
||||
"There must be two signers." using (command.signers.toSet().size == 2)
|
||||
"The borrower and lender must be signers." using (command.signers.containsAll(listOf(
|
||||
out.borrower.owningKey, out.lender.owningKey)))
|
||||
// DOCEND 02
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.corda.docs.tutorial.twoparty
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
// DOCEND 01
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class IOUFlow(val iouValue: Int,
|
||||
val otherParty: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
|
||||
override val progressTracker = ProgressTracker()
|
||||
|
||||
/** The flow logic is encapsulated within the call() method. */
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// We retrieve the notary identity from the network map.
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
|
||||
// We create a transaction builder
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
|
||||
// DOCSTART 02
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val outputContract = IOUContract::class.jvmName
|
||||
val outputContractAndState = StateAndContract(outputState, outputContract)
|
||||
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd)
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(serviceHub)
|
||||
|
||||
// Signing the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// Creating a session with the other party.
|
||||
val otherpartySession = initiateFlow(otherParty)
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker()))
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(FinalityFlow(fullySignedTx))
|
||||
// DOCEND 02
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.corda.docs.tutorial.twoparty
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.SignTransactionFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.docs.tutorial.helloworld.IOUFlow
|
||||
import net.corda.docs.tutorial.helloworld.IOUState
|
||||
|
||||
@InitiatedBy(IOUFlow::class)
|
||||
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
val output = stx.tx.outputs.single().data
|
||||
"This must be an IOU transaction." using (output is IOUState)
|
||||
val iou = output as IOUState
|
||||
"The IOU's value can't be too high." using (iou.value < 100)
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(signTransactionFlow)
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,10 @@
|
||||
package net.corda.docs.tutorial.twoparty
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.docs
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.*
|
||||
@ -26,23 +27,18 @@ class CustomVaultQueryTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.asset")
|
||||
mockNet = MockNetwork(threadPerNode = true)
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName))
|
||||
mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name)
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
|
||||
nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
|
||||
nodeA.internals.installCordaService(CustomVaultQuery.Service::class.java)
|
||||
nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
nodeA.installCordaService(CustomVaultQuery.Service::class.java)
|
||||
notary = nodeA.services.getDefaultNotary()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
@ -24,13 +25,10 @@ class FxTransactionBuildTutorialTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.asset")
|
||||
mockNet = MockNetwork(threadPerNode = true)
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName))
|
||||
mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name)
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
||||
notary = nodeA.services.getDefaultNotary()
|
||||
}
|
||||
@ -38,7 +36,6 @@ class FxTransactionBuildTutorialTest {
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -3,17 +3,15 @@ package net.corda.docs
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -21,8 +19,10 @@ import kotlin.test.assertEquals
|
||||
|
||||
class WorkflowTransactionBuildTutorialTest {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var nodeA: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var nodeB: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var aliceServices: StartedNodeServices
|
||||
lateinit var bobServices: StartedNodeServices
|
||||
lateinit var alice: Party
|
||||
lateinit var bob: Party
|
||||
|
||||
// Helper method to locate the latest Vault version of a LinearState
|
||||
private inline fun <reified T : LinearState> ServiceHub.latest(ref: UniqueIdentifier): StateAndRef<T> {
|
||||
@ -32,67 +32,70 @@ class WorkflowTransactionBuildTutorialTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.docs")
|
||||
mockNet = MockNetwork(threadPerNode = true)
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
|
||||
// While we don't use the notary, we need there to be one on the network
|
||||
mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name)
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
||||
aliceServices = aliceNode.services
|
||||
bobServices = bobNode.services
|
||||
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||
bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Run workflow to completion`() {
|
||||
// Setup a vault subscriber to wait for successful upload of the proposal to NodeB
|
||||
val nodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture()
|
||||
val nodeBVaultUpdate = bobServices.vaultService.updates.toFuture()
|
||||
// Kick of the proposal flow
|
||||
val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.chooseIdentity()))
|
||||
val flow1 = aliceServices.startFlow(SubmitTradeApprovalFlow("1234", bob))
|
||||
// Wait for the flow to finish
|
||||
val proposalRef = flow1.resultFuture.getOrThrow()
|
||||
val proposalLinearId = proposalRef.state.data.linearId
|
||||
// Wait for NodeB to include it's copy in the vault
|
||||
nodeBVaultUpdate.get()
|
||||
// Fetch the latest copy of the state from both nodes
|
||||
val latestFromA = nodeA.database.transaction {
|
||||
nodeA.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
val latestFromA = aliceServices.database.transaction {
|
||||
aliceServices.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
val latestFromB = nodeB.database.transaction {
|
||||
nodeB.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
val latestFromB = bobServices.database.transaction {
|
||||
bobServices.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
// Confirm the state as as expected
|
||||
assertEquals(WorkflowState.NEW, proposalRef.state.data.state)
|
||||
assertEquals("1234", proposalRef.state.data.tradeId)
|
||||
assertEquals(nodeA.info.chooseIdentity(), proposalRef.state.data.source)
|
||||
assertEquals(nodeB.info.chooseIdentity(), proposalRef.state.data.counterparty)
|
||||
assertEquals(alice, proposalRef.state.data.source)
|
||||
assertEquals(bob, proposalRef.state.data.counterparty)
|
||||
assertEquals(proposalRef, latestFromA)
|
||||
assertEquals(proposalRef, latestFromB)
|
||||
// Setup a vault subscriber to pause until the final update is in NodeA and NodeB
|
||||
val nodeAVaultUpdate = nodeA.services.vaultService.updates.toFuture()
|
||||
val secondNodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture()
|
||||
val nodeAVaultUpdate = aliceServices.vaultService.updates.toFuture()
|
||||
val secondNodeBVaultUpdate = bobServices.vaultService.updates.toFuture()
|
||||
// Run the manual completion flow from NodeB
|
||||
val flow2 = nodeB.services.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED))
|
||||
val flow2 = bobServices.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED))
|
||||
// wait for the flow to end
|
||||
val completedRef = flow2.resultFuture.getOrThrow()
|
||||
// wait for the vault updates to stabilise
|
||||
nodeAVaultUpdate.get()
|
||||
secondNodeBVaultUpdate.get()
|
||||
// Fetch the latest copies from the vault
|
||||
val finalFromA = nodeA.database.transaction {
|
||||
nodeA.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
val finalFromA = aliceServices.database.transaction {
|
||||
aliceServices.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
val finalFromB = nodeB.database.transaction {
|
||||
nodeB.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
val finalFromB = bobServices.database.transaction {
|
||||
bobServices.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
// Confirm the state is as expected
|
||||
assertEquals(WorkflowState.APPROVED, completedRef.state.data.state)
|
||||
assertEquals("1234", completedRef.state.data.tradeId)
|
||||
assertEquals(nodeA.info.chooseIdentity(), completedRef.state.data.source)
|
||||
assertEquals(nodeB.info.chooseIdentity(), completedRef.state.data.counterparty)
|
||||
assertEquals(alice, completedRef.state.data.source)
|
||||
assertEquals(bob, completedRef.state.data.counterparty)
|
||||
assertEquals(completedRef, finalFromA)
|
||||
assertEquals(completedRef, finalFromB)
|
||||
}
|
||||
|
@ -78,80 +78,15 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
...
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
|
||||
...
|
||||
|
||||
class IOUContract : Contract {
|
||||
// Our Create command.
|
||||
class Create : CommandData
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputs.single().data as IOUState
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
||||
|
||||
// Constraints on the signers.
|
||||
"There must only be one signer." using (command.signers.toSet().size == 1)
|
||||
"The signer must be the lender." using (command.signers.contains(out.lender.owningKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template.contract;
|
||||
|
||||
import com.template.state.IOUState;
|
||||
import net.corda.core.contracts.CommandWithParties;
|
||||
import net.corda.core.contracts.CommandData;
|
||||
import net.corda.core.contracts.Contract;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
public class IOUContract implements Contract {
|
||||
// Our Create command.
|
||||
public static class Create implements CommandData {}
|
||||
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) {
|
||||
final CommandWithParties<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1);
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState out = (IOUState) tx.getOutputs().get(0).getData();
|
||||
final Party lender = out.getLender();
|
||||
final Party borrower = out.getBorrower();
|
||||
check.using("The IOU's value must be non-negative.",out.getValue() > 0);
|
||||
check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
|
||||
|
||||
// Constraints on the signers.
|
||||
check.using("There must only be one signer.", command.getSigners().size() == 1);
|
||||
check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey()));
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``.
|
||||
|
||||
|
@ -33,128 +33,20 @@ FlowLogic
|
||||
Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding
|
||||
``FlowLogic.call``.
|
||||
|
||||
We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Overwrite both the existing flows in the template
|
||||
with the following:
|
||||
We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the template, and
|
||||
replace them with the following:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
...
|
||||
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.flows.*
|
||||
|
||||
...
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class IOUFlow(val iouValue: Int,
|
||||
val otherParty: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
|
||||
override val progressTracker = ProgressTracker()
|
||||
|
||||
/** The flow logic is encapsulated within the call() method. */
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// We retrieve the notary identity from the network map.
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
|
||||
// We create a transaction builder
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val outputContract = IOUContract::class.jvmName
|
||||
val outputContractAndState = StateAndContract(outputState, outputContract)
|
||||
val cmd = Command(IOUContract.Create(), ourIdentity.owningKey)
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd)
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(serviceHub)
|
||||
|
||||
// Signing the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(FinalityFlow(signedTx))
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template.flow;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.template.contract.IOUContract;
|
||||
import com.template.state.IOUState;
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.contracts.StateAndContract;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
public class IOUFlow extends FlowLogic<Void> {
|
||||
private final Integer iouValue;
|
||||
private final Party otherParty;
|
||||
|
||||
/**
|
||||
* The progress tracker provides checkpoints indicating the progress of the flow to observers.
|
||||
*/
|
||||
private final ProgressTracker progressTracker = new ProgressTracker();
|
||||
|
||||
public IOUFlow(Integer iouValue, Party otherParty) {
|
||||
this.iouValue = iouValue;
|
||||
this.otherParty = otherParty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressTracker getProgressTracker() {
|
||||
return progressTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flow logic is encapsulated within the call() method.
|
||||
*/
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// We retrieve the notary identity from the network map.
|
||||
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||
|
||||
// We create a transaction builder.
|
||||
final TransactionBuilder txBuilder = new TransactionBuilder();
|
||||
txBuilder.setNotary(notary);
|
||||
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
String outputContract = IOUContract.class.getName();
|
||||
StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract);
|
||||
Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey());
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd);
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(getServiceHub());
|
||||
|
||||
// Signing the transaction.
|
||||
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(signedTx));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
If you're following along in Java, you'll also need to rename ``TemplateFlow.java`` to ``IOUFlow.java``.
|
||||
|
||||
|
@ -5,7 +5,7 @@ By this point, :doc:`your dev environment should be set up <getting-set-up>`, yo
|
||||
:doc:`your first CorDapp <tutorial-cordapp>`, and you're familiar with Corda's :doc:`key concepts <key-concepts>`. What
|
||||
comes next?
|
||||
|
||||
If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a plugin that is
|
||||
If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a JAR that is
|
||||
installed on one or more Corda nodes, and gives them the ability to conduct some new process - anything from
|
||||
issuing a debt instrument to making a restaurant booking.
|
||||
|
||||
|
@ -17,39 +17,36 @@ Kotlin) file. We won't be using it, and it will cause build errors unless we rem
|
||||
Deploying our CorDapp
|
||||
---------------------
|
||||
Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the
|
||||
``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB:
|
||||
``task deployNodes`` section. This section defines three nodes - the Controller, PartyA, and PartyB:
|
||||
|
||||
.. container:: codeset
|
||||
.. code:: bash
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "O=Controller,L=London,C=GB"
|
||||
node {
|
||||
name "O=Controller,L=London,C=GB"
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
rpcPort 10003
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
}
|
||||
node {
|
||||
name "O=PartyA,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcPort 10006
|
||||
webPort 10007
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
|
||||
}
|
||||
node {
|
||||
name "O=PartyB,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcPort 10009
|
||||
webPort 10010
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
|
||||
}
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
node {
|
||||
name "O=Controller,L=London,C=GB"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
p2pPort 10002
|
||||
rpcPort 10003
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
}
|
||||
node {
|
||||
name "O=PartyA,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcPort 10006
|
||||
webPort 10007
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
|
||||
}
|
||||
node {
|
||||
name "O=PartyB,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcPort 10009
|
||||
webPort 10010
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
|
||||
}
|
||||
}
|
||||
|
||||
We have three standard nodes, plus a special Controller node that is running the network map service, and is also
|
||||
advertising a validating notary service. Feel free to add additional node definitions here to expand the size of the
|
||||
@ -62,7 +59,7 @@ We can run this ``deployNodes`` task using Gradle. For each node definition, Gra
|
||||
|
||||
We can do that now by running the following commands from the root of the project:
|
||||
|
||||
.. code:: python
|
||||
.. code:: bash
|
||||
|
||||
// On Windows
|
||||
gradlew clean deployNodes
|
||||
@ -75,19 +72,18 @@ Running the nodes
|
||||
Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see
|
||||
the three node folders. Each node folder has the following structure:
|
||||
|
||||
.. code:: python
|
||||
.. code:: bash
|
||||
|
||||
.
|
||||
|____corda.jar // The runnable node
|
||||
|____corda-webserver.jar // The node's webserver
|
||||
|____dependencies
|
||||
|____node.conf // The node's configuration file
|
||||
|____plugins
|
||||
|____java/kotlin-source-0.1.jar // Our IOU CorDapp
|
||||
|____cordapps
|
||||
|____java/kotlin-source-0.1.jar // Our IOU CorDapp
|
||||
|
||||
Let's start the nodes by running the following commands from the root of the project:
|
||||
|
||||
.. code:: python
|
||||
.. code:: bash
|
||||
|
||||
// On Windows
|
||||
build/nodes/runnodes.bat
|
||||
@ -136,7 +132,7 @@ will display a list of the available commands. We can examine the contents of a
|
||||
|
||||
The vaults of PartyA and PartyB should both display the following output:
|
||||
|
||||
.. code:: python
|
||||
.. code:: bash
|
||||
|
||||
states:
|
||||
- state:
|
||||
@ -192,11 +188,9 @@ There are a number of improvements we could make to this CorDapp:
|
||||
* We could add an API, to make it easier to interact with the CorDapp
|
||||
|
||||
We will explore some of these improvements in future tutorials. But you should now be ready to develop your own
|
||||
CorDapps. There's `a more fleshed-out version of the IOU CorDapp <https://github.com/corda/cordapp-example>`_ with an
|
||||
API and web front-end, and a set of example CorDapps in `the main Corda repo <https://github.com/corda/corda>`_, under
|
||||
``samples``. An explanation of how to run these samples :doc:`here <running-the-demos>`.
|
||||
CorDapps. You can find a list of sample CorDapps `here <https://www.corda.net/samples/>`_.
|
||||
|
||||
As you write CorDapps, you can learn more about the API available :doc:`here <api>`.
|
||||
As you write CorDapps, you can learn more about the Corda API :doc:`here <corda-api>`.
|
||||
|
||||
If you get stuck at any point, please reach out on `Slack <https://slack.corda.net/>`_,
|
||||
`Discourse <https://discourse.corda.net/>`_, or `Stack Overflow <https://stackoverflow.com/questions/tagged/corda>`_.
|
||||
|
@ -63,53 +63,15 @@ define an ``IOUState``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template.state;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
private final int value;
|
||||
private final Party lender;
|
||||
private final Party borrower;
|
||||
|
||||
public IOUState(int value, Party lender, Party borrower) {
|
||||
this.value = value;
|
||||
this.lender = lender;
|
||||
this.borrower = borrower;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getLender() {
|
||||
return lender;
|
||||
}
|
||||
|
||||
public Party getBorrower() {
|
||||
return borrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(lender, borrower);
|
||||
}
|
||||
}
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
If you're following along in Java, you'll also need to rename ``TemplateState.java`` to ``IOUState.java``.
|
||||
|
||||
|
@ -42,18 +42,23 @@ implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin,
|
||||
.. code-block:: java
|
||||
|
||||
// 1. The state
|
||||
src/main/java/com/template/state/TemplateState.java
|
||||
src/main/java/com/template/TemplateState.java
|
||||
|
||||
// 2. The contract
|
||||
src/main/java/com/template/contract/TemplateContract.java
|
||||
src/main/java/com/template/TemplateContract.java
|
||||
|
||||
// 3. The flow
|
||||
src/main/java/com/template/flow/TemplateFlow.java
|
||||
src/main/java/com/template/TemplateFlow.java
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
src/main/kotlin/com/template/App.kt
|
||||
|
||||
To prevent build errors later on, you should delete the following file:
|
||||
|
||||
* Java: ``src/test/java/com/template/FlowTests.java``
|
||||
* Kotlin: ``src/test/kotlin/com/template/FlowTests.kt``
|
||||
|
||||
Progress so far
|
||||
---------------
|
||||
We now have a template that we can build upon to define our IOU CorDapp.
|
||||
|
@ -95,9 +95,9 @@ to specify JAR URLs in the case that the CorDapp(s) involved in testing already
|
||||
MockNetwork/MockNode
|
||||
********************
|
||||
|
||||
The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to make a call
|
||||
to ``setCordappPackages`` before the MockNetwork/Node are created and then ``unsetCordappPackages`` after the test
|
||||
has finished. These calls will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files
|
||||
The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the
|
||||
``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java)
|
||||
when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files
|
||||
within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the
|
||||
``CordappLoader``. An example of this usage would be:
|
||||
|
||||
@ -108,17 +108,7 @@ within those packages will be zipped into a JAR and added to the attachment stor
|
||||
|
||||
@Before
|
||||
void setup() {
|
||||
// The ordering of the two below lines is important - if the MockNetwork is created before the nodes and network
|
||||
// are created the CorDapps will not be loaded into the MockNodes correctly.
|
||||
setCordappPackages(Arrays.asList("com.domain.cordapp"))
|
||||
network = new MockNetwork()
|
||||
}
|
||||
|
||||
@After
|
||||
void teardown() {
|
||||
// This must be called at the end otherwise the global state set by setCordappPackages may leak into future
|
||||
// tests in the same test runner environment.
|
||||
unsetCordappPackages()
|
||||
network = new MockNetwork(new MockNetworkParameters().setCordappPackages(Arrays.asList("com.domain.cordapp")))
|
||||
}
|
||||
|
||||
... // Your tests go here
|
||||
@ -146,4 +136,4 @@ The driver takes a parameter called ``extraCordappPackagesToScan`` which is a li
|
||||
Full Nodes
|
||||
**********
|
||||
|
||||
When testing against full nodes simply place your CorDapp into the plugins directory of the node.
|
||||
When testing against full nodes simply place your CorDapp into the cordapps directory of the node.
|
||||
|
@ -36,7 +36,7 @@ The core elements of the architecture are:
|
||||
* A network interface for interacting with other nodes
|
||||
* An RPC interface for interacting with the node's owner
|
||||
* A service hub for allowing the node's flows to call upon the node's other services
|
||||
* A plugin registry for extending the node by installing CorDapps
|
||||
* A cordapp interface and provider for extending the node by installing CorDapps
|
||||
|
||||
Persistence layer
|
||||
-----------------
|
||||
@ -68,11 +68,11 @@ updates. The key services provided are:
|
||||
* Information about the node itself
|
||||
* The current time, as tracked by the node
|
||||
|
||||
The plugin registry
|
||||
-------------------
|
||||
The plugin registry is where new CorDapps are installed to extend the behavior of the node.
|
||||
The CorDapp provider
|
||||
--------------------
|
||||
The CorDapp provider is where new CorDapps are installed to extend the behavior of the node.
|
||||
|
||||
The node also has several plugins installed by default to handle common tasks such as:
|
||||
The node also has several CorDapps installed by default to handle common tasks such as:
|
||||
|
||||
* Retrieving transactions and attachments from counterparties
|
||||
* Upgrading contracts
|
||||
|
@ -209,10 +209,8 @@ NodeAttachmentService
|
||||
The ``NodeAttachmentService`` provides an implementation of the
|
||||
``AttachmentStorage`` interface exposed on the ``ServiceHub`` allowing
|
||||
transactions to add documents, copies of the contract code and binary
|
||||
data to transactions. The data is persisted to the local file system
|
||||
inside the attachments subfolder of the node workspace. The service is
|
||||
also interfaced to by the web server, which allows files to be uploaded
|
||||
via an HTTP post request.
|
||||
data to transactions. The service is also interfaced to by the web server,
|
||||
which allows files to be uploaded via an HTTP post request.
|
||||
|
||||
Flow framework and event scheduling services
|
||||
--------------------------------------------
|
||||
@ -320,7 +318,7 @@ does this by tracking update notifications from the
|
||||
``TransactionStorage`` service and processing relevant updates to delete
|
||||
consumed states and insert new states. The resulting update is then
|
||||
persisted to the database. The ``VaultService`` then exposes query and
|
||||
event notification APIs to flows and CorDapp plugins to allow them
|
||||
event notification APIs to flows and CorDapp services to allow them
|
||||
to respond to updates, or query for states meeting various conditions to
|
||||
begin the formation of new transactions consuming them. The equivalent
|
||||
services are also forwarded to RPC clients, so that they may show
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 282 KiB |
@ -10,7 +10,7 @@ already installed. You run each node by navigating to ``<node_dir>`` in a termin
|
||||
|
||||
java -jar corda.jar
|
||||
|
||||
.. warning:: If your working directory is not ``<node_dir>`` your plugins and configuration will not be used.
|
||||
.. warning:: If your working directory is not ``<node_dir>`` your cordapps and configuration will not be used.
|
||||
|
||||
The configuration file and workspace paths can be overridden on the command line. For example:
|
||||
|
||||
|
@ -35,7 +35,7 @@ To run from the command line in Unix:
|
||||
2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes
|
||||
3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively.
|
||||
4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch``
|
||||
|
||||
|
||||
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
|
||||
to your terminal.
|
||||
|
||||
@ -45,7 +45,7 @@ To run from the command line in Windows:
|
||||
2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes
|
||||
3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node
|
||||
4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch``
|
||||
|
||||
|
||||
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
|
||||
to your terminal.
|
||||
|
||||
@ -112,8 +112,11 @@ Notary demo
|
||||
This demo shows a party getting transactions notarised by either a single-node or a distributed notary service.
|
||||
All versions of the demo start two counterparty nodes.
|
||||
One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit them for notarisation.
|
||||
The `Raft <https://raft.github.io/>`_ version of the demo will start three distributed notary nodes.
|
||||
The `BFT SMaRt <https://bft-smart.github.io/library/>`_ version of the demo will start four distributed notary nodes.
|
||||
|
||||
* The `Raft <https://raft.github.io/>`_ version of the demo will start three distributed notary nodes.
|
||||
* The `BFT SMaRt <https://bft-smart.github.io/library/>`_ version of the demo will start four distributed notary nodes.
|
||||
* The Single version of the demo will start a single-node validating notary service.
|
||||
* The Custom version of the demo will load and start a custom single-node notary service that is defined the demo CorDapp.
|
||||
|
||||
The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary,
|
||||
every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement.
|
||||
@ -122,9 +125,9 @@ You will notice that successive transactions get signed by different members of
|
||||
|
||||
To run the Raft version of the demo from the command line in Unix:
|
||||
|
||||
1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and
|
||||
Single notaries).
|
||||
1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create node directories for all versions of the demo,
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for
|
||||
BFT, Single and Custom notaries respectively).
|
||||
2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs.
|
||||
Wait until a "Node started up and registered in ..." message appears on each of the terminals
|
||||
3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests
|
||||
@ -133,8 +136,8 @@ To run the Raft version of the demo from the command line in Unix:
|
||||
To run from the command line in Windows:
|
||||
|
||||
1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and
|
||||
Single notaries).
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for
|
||||
BFT, Single and Custom notaries respectively).
|
||||
2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs.
|
||||
Wait until a "Node started up and registered in ..." message appears on each of the terminals
|
||||
3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests
|
||||
@ -142,6 +145,7 @@ To run from the command line in Windows:
|
||||
|
||||
To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes
|
||||
trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``.
|
||||
For the custom notary service use ``nodesCustom`.
|
||||
|
||||
Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node.
|
||||
You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores
|
||||
|
@ -63,7 +63,7 @@ The long term goal is to migrate the current serialization format for everything
|
||||
#. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time
|
||||
and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states.
|
||||
#. Increased security from deserialized objects being constructed through supported constructors rather than having
|
||||
data poked directy into their fields without an opportunity to validate consistency or intercept attempts to manipulate
|
||||
data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
|
||||
supposed invariants.
|
||||
|
||||
Documentation on that format, and how JVM classes are translated to AMQP, will be linked here when it is available.
|
||||
@ -259,9 +259,10 @@ Kotlin Objects
|
||||
``````````````
|
||||
|
||||
#. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties
|
||||
and deserialize back to the singleton instance.
|
||||
|
||||
Currently, the same is not true of Java singletons, and they will deserialize to new instances of the class.
|
||||
and deserialize back to the singleton instance. Currently, the same is not true of Java singletons,
|
||||
and they will deserialize to new instances of the class.
|
||||
#. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like:
|
||||
``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration.
|
||||
|
||||
The Carpenter
|
||||
`````````````
|
||||
|
@ -11,31 +11,37 @@ Remember that each state references a contract. The contract imposes constraints
|
||||
If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid
|
||||
ledger update.
|
||||
|
||||
We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. This will
|
||||
only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final two lines of
|
||||
the ``requireThat`` block as follows:
|
||||
We need to modify our contract so that the borrower's signature is required in any IOU creation transaction.
|
||||
|
||||
In ``IOUContract.java``/``IOUContract.kt``, change the imports block to the following:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
// Constraints on the signers.
|
||||
"There must be two signers." using (command.signers.toSet().size == 2)
|
||||
"The borrower and lender must be signers." using (command.signers.containsAll(listOf(
|
||||
out.borrower.owningKey, out.lender.owningKey)))
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
.. code-block:: java
|
||||
And update the final block of constraints in the ``requireThat`` block as follows:
|
||||
|
||||
...
|
||||
.. container:: codeset
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 02
|
||||
:end-before: DOCEND 02
|
||||
:dedent: 12
|
||||
|
||||
...
|
||||
|
||||
// Constraints on the signers.
|
||||
check.using("There must be two signers.", command.getSigners().size() == 2);
|
||||
check.using("The borrower and lender must be signers.", command.getSigners().containsAll(
|
||||
ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey())));
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 02
|
||||
:end-before: DOCEND 02
|
||||
:dedent: 12
|
||||
|
||||
Progress so far
|
||||
---------------
|
||||
|
@ -21,76 +21,35 @@ by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to
|
||||
We also need to add the borrower's public key to the transaction's command, making the borrower one of the required
|
||||
signers on the transaction.
|
||||
|
||||
In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows:
|
||||
In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
...
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val outputContract = IOUContract::class.jvmName
|
||||
val outputContractAndState = StateAndContract(outputState, outputContract)
|
||||
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||
And update ``IOUFlow.call`` by changing the code following the creation of the ``TransactionBuilder`` as follows:
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd)
|
||||
.. container:: codeset
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(serviceHub)
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 02
|
||||
:end-before: DOCEND 02
|
||||
:dedent: 8
|
||||
|
||||
// Signing the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// Creating a session with the other party.
|
||||
val otherpartySession = initiateFlow(otherParty)
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker()))
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(FinalityFlow(fullySignedTx))
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
...
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
...
|
||||
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
String outputContract = IOUContract.class.getName();
|
||||
StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract);
|
||||
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd);
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(getServiceHub());
|
||||
|
||||
// Signing the transaction.
|
||||
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Creating a session with the other party.
|
||||
FlowSession otherpartySession = initiateFlow(otherParty);
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
||||
signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker()));
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(signedTx));
|
||||
|
||||
return null;
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 02
|
||||
:end-before: DOCEND 02
|
||||
:dedent: 8
|
||||
|
||||
To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command.
|
||||
|
||||
@ -116,81 +75,15 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
...
|
||||
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
...
|
||||
|
||||
@InitiatedBy(IOUFlow::class)
|
||||
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
val output = stx.tx.outputs.single().data
|
||||
"This must be an IOU transaction." using (output is IOUState)
|
||||
val iou = output as IOUState
|
||||
"The IOU's value can't be too high." using (iou.value < 100)
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(signTransactionFlow)
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template.flow;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.template.state.IOUState;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.flows.FlowException;
|
||||
import net.corda.core.flows.FlowLogic;
|
||||
import net.corda.core.flows.FlowSession;
|
||||
import net.corda.core.flows.InitiatedBy;
|
||||
import net.corda.core.flows.SignTransactionFlow;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
@InitiatedBy(IOUFlow.class)
|
||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
private final FlowSession otherPartySession;
|
||||
|
||||
public IOUFlowResponder(FlowSession otherPartySession) {
|
||||
this.otherPartySession = otherPartySession;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
class SignTxFlow extends SignTransactionFlow {
|
||||
private signTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) {
|
||||
super(otherPartySession, progressTracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTransaction(SignedTransaction stx) {
|
||||
requireThat(require -> {
|
||||
ContractState output = stx.getTx().getOutputs().get(0).getData();
|
||||
require.using("This must be an IOU transaction.", output instanceof IOUState);
|
||||
IOUState iou = (IOUState) output;
|
||||
require.using("The IOU's value can't be too high.", iou.getValue() < 100);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker()));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden
|
||||
``FlowLogic.call``.
|
||||
|
@ -20,4 +20,4 @@ IOU onto the ledger. We'll need to make two changes:
|
||||
signature (as well as the lender's) to become valid ledger updates
|
||||
* The ``IOUFlow`` will need to be updated to allow for the gathering of the borrower's signature
|
||||
|
||||
We'll start by updating the contract.
|
||||
We'll start by updating the contract.
|
@ -17,7 +17,7 @@ if:
|
||||
|
||||
We will deploy the CorDapp on 4 test nodes:
|
||||
|
||||
* **Controller**, which hosts the network map service and a validating notary service
|
||||
* **Controller**, which hosts a validating notary service
|
||||
* **PartyA**
|
||||
* **PartyB**
|
||||
* **PartyC**
|
||||
@ -210,9 +210,9 @@ Building the example CorDapp
|
||||
. nodeName
|
||||
├── corda.jar
|
||||
├── node.conf
|
||||
└── plugins
|
||||
└── cordapps
|
||||
|
||||
``corda.jar`` is the Corda runtime, ``plugins`` contains our node's CorDapps, and the node's configuration is
|
||||
``corda.jar`` is the Corda runtime, ``cordapps`` contains our node's CorDapps, and the node's configuration is
|
||||
given by ``node.conf``
|
||||
|
||||
Running the example CorDapp
|
||||
@ -276,7 +276,7 @@ IntelliJ
|
||||
|
||||
The node driver defined in ``/src/test/kotlin/com/example/Main.kt`` allows you to specify how many nodes you would like
|
||||
to run and the configuration settings for each node. For the example CorDapp, the driver starts up four nodes
|
||||
and adds an RPC user for all but the "Controller" node (which serves as the notary and network map service):
|
||||
and adds an RPC user for all but the "Controller" node (which serves as the notary):
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@ -489,9 +489,6 @@ You must now edit the configuration file for each node, including the controller
|
||||
and make the following changes:
|
||||
|
||||
* Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``)
|
||||
* Change the network map service's address to the IP address of the machine where the controller node is running
|
||||
(e.g. ``networkMapService { address="10.18.0.166:10002" legalName="O=Controller,L=London,C=GB" ``). The controller
|
||||
will not have the ``networkMapService`` configuration entry
|
||||
|
||||
After starting each node, the nodes will be able to see one another and agree IOUs among themselves.
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
.. highlight:: kotlin
|
||||
|
||||
Writing a custom notary service
|
||||
===============================
|
||||
Writing a custom notary service (experimental)
|
||||
==============================================
|
||||
|
||||
.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently,
|
||||
.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently,
|
||||
customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to
|
||||
implement a custom database connector (or use a separate database for the notary), and use a custom configuration file.
|
||||
|
||||
Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it
|
||||
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement
|
||||
is that the class provide a constructor with a single parameter of type ``ServiceHub``.
|
||||
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary
|
||||
service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``.
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt
|
||||
.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
|
||||
:language: kotlin
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
@ -20,7 +20,16 @@ The next step is to write a notary service flow. You are free to copy and modify
|
||||
as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the
|
||||
``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt
|
||||
.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
|
||||
:language: kotlin
|
||||
:start-after: START 2
|
||||
:end-before: END 2
|
||||
|
||||
To enable the service, add the following to the node configuration:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
notary : {
|
||||
validating : true # Set to false if your service is non-validating
|
||||
custom : true
|
||||
}
|
@ -16,6 +16,7 @@ Tutorials
|
||||
flow-testing
|
||||
running-a-notary
|
||||
oracles
|
||||
tutorial-custom-notary
|
||||
tutorial-tear-offs
|
||||
tutorial-attachments
|
||||
event-scheduling
|
@ -25,6 +25,19 @@ versions you are currently using are still in force.
|
||||
|
||||
We also strongly recommend cross referencing with the :doc:`changelog` to confirm changes.
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
|
||||
Testing
|
||||
^^^^^^^
|
||||
|
||||
* The registration mechanism for CorDapps in ``MockNetwork`` unit tests has changed.
|
||||
|
||||
It is now done via the ``cordappPackages`` constructor parameter of MockNetwork.
|
||||
This takes a list of `String` values which should be the
|
||||
package names of the CorDapps containing the contract verification code you wish to load.
|
||||
The ``unsetCordappPackages`` method is now redundant and has been removed.
|
||||
|
||||
:ref:`Milestone 14 <changelog_m14>`
|
||||
------------
|
||||
|
||||
@ -220,6 +233,8 @@ Miscellaneous
|
||||
apps would not typically select random, unknown counterparties from the network map based on self-declared capabilities.
|
||||
We will introduce a replacement for this functionality, business networks, in a future release.
|
||||
|
||||
For now, your should retrieve the service by legal name using ``NetworkMapCache.getNodeByLegalName``.
|
||||
|
||||
Gotchas
|
||||
^^^^^^^
|
||||
|
||||
|
53
experimental/kryo-hook/build.gradle
Normal file
53
experimental/kryo-hook/build.gradle
Normal file
@ -0,0 +1,53 @@
|
||||
buildscript {
|
||||
// For sharing constants between builds
|
||||
Properties constants = new Properties()
|
||||
file("$projectDir/../../constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
ext.javaassist_version = "3.12.1.GA"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'idea'
|
||||
|
||||
description 'A javaagent to allow hooking into Kryo'
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "javassist:javassist:$javaassist_version"
|
||||
compile "com.esotericsoftware:kryo:4.0.0"
|
||||
compile "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveName = "${project.name}.jar"
|
||||
manifest {
|
||||
attributes(
|
||||
'Premain-Class': 'net.corda.kryohook.KryoHookAgent',
|
||||
'Can-Redefine-Classes': 'true',
|
||||
'Can-Retransform-Classes': 'true',
|
||||
'Can-Set-Native-Method-Prefix': 'true',
|
||||
'Implementation-Title': "KryoHook",
|
||||
'Implementation-Version': rootProject.version
|
||||
)
|
||||
}
|
||||
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
package net.corda.kryohook
|
||||
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import javassist.ClassPool
|
||||
import javassist.CtClass
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.lang.StringBuilder
|
||||
import java.lang.instrument.ClassFileTransformer
|
||||
import java.lang.instrument.Instrumentation
|
||||
import java.security.ProtectionDomain
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class KryoHookAgent {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun premain(argumentsString: String?, instrumentation: Instrumentation) {
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
val statsTrees = KryoHook.events.values.flatMap {
|
||||
readTrees(it, 0).second
|
||||
}
|
||||
val builder = StringBuilder()
|
||||
statsTrees.forEach {
|
||||
prettyStatsTree(0, it, builder)
|
||||
}
|
||||
print(builder.toString())
|
||||
})
|
||||
instrumentation.addTransformer(KryoHook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) {
|
||||
when (statsTree) {
|
||||
is StatsTree.Object -> {
|
||||
builder.append(kotlin.CharArray(indent) { ' ' })
|
||||
builder.append(statsTree.className)
|
||||
builder.append(" ")
|
||||
builder.append(statsTree.size)
|
||||
builder.append("\n")
|
||||
for (child in statsTree.children) {
|
||||
prettyStatsTree(indent + 2, child, builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hook simply records the write() entries and exits together with the output offset at the time of the call.
|
||||
* This is recorded in a StrandID -> List<StatsEvent> map.
|
||||
*
|
||||
* Later we "parse" these lists into a tree.
|
||||
*/
|
||||
object KryoHook : ClassFileTransformer {
|
||||
val classPool = ClassPool.getDefault()
|
||||
|
||||
val hookClassName = javaClass.name
|
||||
|
||||
override fun transform(
|
||||
loader: ClassLoader?,
|
||||
className: String,
|
||||
classBeingRedefined: Class<*>?,
|
||||
protectionDomain: ProtectionDomain?,
|
||||
classfileBuffer: ByteArray
|
||||
): ByteArray? {
|
||||
if (className.startsWith("java") || className.startsWith("javassist") || className.startsWith("kotlin")) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val clazz = classPool.makeClass(ByteArrayInputStream(classfileBuffer))
|
||||
instrumentClass(clazz)?.toBytecode()
|
||||
} catch (throwable: Throwable) {
|
||||
println("SOMETHING WENT WRONG")
|
||||
throwable.printStackTrace(System.out)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun instrumentClass(clazz: CtClass): CtClass? {
|
||||
for (method in clazz.declaredBehaviors) {
|
||||
if (method.name == "write") {
|
||||
val parameterTypeNames = method.parameterTypes.map { it.name }
|
||||
if (parameterTypeNames == listOf("com.esotericsoftware.kryo.Kryo", "com.esotericsoftware.kryo.io.Output", "java.lang.Object")) {
|
||||
if (method.isEmpty) continue
|
||||
println("Instrumenting ${clazz.name}")
|
||||
method.insertBefore("$hookClassName.${this::writeEnter.name}($1, $2, $3);")
|
||||
method.insertAfter("$hookClassName.${this::writeExit.name}($1, $2, $3);")
|
||||
return clazz
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// StrandID -> StatsEvent map
|
||||
val events = ConcurrentHashMap<Long, ArrayList<StatsEvent>>()
|
||||
|
||||
@JvmStatic
|
||||
fun writeEnter(kryo: Kryo, output: Output, obj: Any) {
|
||||
events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add(
|
||||
StatsEvent.Enter(obj.javaClass.name, output.total())
|
||||
)
|
||||
}
|
||||
@JvmStatic
|
||||
fun writeExit(kryo: Kryo, output: Output, obj: Any) {
|
||||
events.get(Strand.currentStrand().id)!!.add(
|
||||
StatsEvent.Exit(obj.javaClass.name, output.total())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO we could add events on entries/exits to field serializers to get more info on what's being serialised.
|
||||
*/
|
||||
sealed class StatsEvent {
|
||||
data class Enter(val className: String, val offset: Long) : StatsEvent()
|
||||
data class Exit(val className: String, val offset: Long) : StatsEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO add Field constructor.
|
||||
*/
|
||||
sealed class StatsTree {
|
||||
data class Object(
|
||||
val className: String,
|
||||
val size: Long,
|
||||
val children: List<StatsTree>
|
||||
) : StatsTree()
|
||||
}
|
||||
|
||||
fun readTree(events: List<StatsEvent>, index: Int): Pair<Int, StatsTree> {
|
||||
val event = events[index]
|
||||
when (event) {
|
||||
is StatsEvent.Enter -> {
|
||||
val (nextIndex, children) = readTrees(events, index + 1)
|
||||
val exit = events[nextIndex] as StatsEvent.Exit
|
||||
require(event.className == exit.className)
|
||||
return Pair(nextIndex + 1, StatsTree.Object(event.className, exit.offset - event.offset, children))
|
||||
}
|
||||
is StatsEvent.Exit -> {
|
||||
throw IllegalStateException("Wasn't expecting Exit")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readTrees(events: List<StatsEvent>, index: Int): Pair<Int, List<StatsTree>> {
|
||||
val trees = ArrayList<StatsTree>()
|
||||
var i = index
|
||||
while (true) {
|
||||
val event = events.getOrNull(i)
|
||||
when (event) {
|
||||
is StatsEvent.Enter -> {
|
||||
val (nextIndex, tree) = readTree(events, i)
|
||||
trees.add(tree)
|
||||
i = nextIndex
|
||||
}
|
||||
is StatsEvent.Exit -> {
|
||||
return Pair(i, trees)
|
||||
}
|
||||
null -> {
|
||||
return Pair(i, trees)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
What is this
|
||||
------------
|
||||
|
||||
This is a javaagent that hooks into Kryo serializers to record a breakdown of how many bytes objects take in the output.
|
||||
|
||||
The dump is quite ugly now, but the in-memory representation is a simple tree so we could put some nice visualisation on
|
||||
top if we want.
|
||||
|
||||
How do I run it
|
||||
---------------
|
||||
|
||||
Build the agent:
|
||||
```
|
||||
./gradlew experimental:kryo-hook:jar
|
||||
```
|
||||
|
||||
Add this JVM flag to what you're running:
|
||||
|
||||
```
|
||||
-javaagent:<PROJECT>/experimental/kryo-hook/build/libs/kryo-hook.jar
|
||||
```
|
||||
|
||||
The agent will dump the output when the JVM shuts down.
|
@ -5,16 +5,18 @@ import net.corda.finance.contracts.FixOf
|
||||
import net.corda.finance.contracts.Frequency
|
||||
import net.corda.finance.contracts.Tenor
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.transaction
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import net.corda.testing.EnforceVerifyOrFail
|
||||
import net.corda.testing.TransactionDSL
|
||||
import net.corda.testing.TransactionDSLInterpreter
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
|
||||
net.corda.testing.transaction(cordappPackages = listOf("net.corda.finance.contracts.universal"), dsl = script)
|
||||
}
|
||||
|
||||
class Cap {
|
||||
|
||||
val TEST_TX_TIME_1: Instant get() = Instant.parse("2017-09-02T12:00:00.00Z")
|
||||
@ -167,16 +169,6 @@ class Cap {
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.universal")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
|
@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal
|
||||
import net.corda.finance.contracts.FixOf
|
||||
import net.corda.finance.contracts.Tenor
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.transaction
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -53,17 +48,6 @@ class Caplet {
|
||||
val stateFixed = UniversalContract.State(listOf(DUMMY_NOTARY), contractFixed)
|
||||
|
||||
val stateFinal = UniversalContract.State(listOf(DUMMY_NOTARY), contractFinal)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.universal")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
|
@ -1,11 +1,6 @@
|
||||
package net.corda.finance.contracts.universal
|
||||
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.transaction
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -50,17 +45,6 @@ class FXFwdTimeOption
|
||||
val inState = UniversalContract.State(listOf(DUMMY_NOTARY), initialContract)
|
||||
val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract1)
|
||||
val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract2)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.universal")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue - signature`() {
|
||||
transaction {
|
||||
|
@ -1,11 +1,6 @@
|
||||
package net.corda.finance.contracts.universal
|
||||
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.transaction
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -41,17 +36,6 @@ class FXSwap {
|
||||
val outStateBad3 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad3)
|
||||
|
||||
val inState = UniversalContract.State(listOf(DUMMY_NOTARY), contract)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setCordappPackages("net.corda.finance.contracts.universal")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue - signature`() {
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user