Merge open master

This commit is contained in:
Andras Slemmer 2017-08-29 15:08:04 +01:00
commit a4ba8e4f2f
526 changed files with 7849 additions and 7317 deletions

View File

@ -14,7 +14,7 @@ buildscript {
//
// TODO: Sort this alphabetically.
ext.kotlin_version = constants.getProperty("kotlinVersion")
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
ext.quasar_version = '0.7.9'
// gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3
@ -42,7 +42,6 @@ buildscript {
ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4'
ext.requery_version = '1.3.1'
ext.dokka_version = '0.9.14'
ext.eddsa_version = '0.2.0'
@ -128,6 +127,13 @@ allprojects {
tasks.withType(Test) {
// Prevent the project from creating temporary files outside of the build directory.
systemProperties['java.io.tmpdir'] = buildDir
// Ensures that "net.corda.testing.amqp.enable" is passed correctly from Gradle command line
// down to JVM executing unit test. It looks like we are running unit tests in the forked mode
// and all the "-D" parameters passed to Gradle not making it to unit test level
// TODO: Remove once we fully switched to AMQP
final AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable"
systemProperty(AMQP_ENABLE_PROP_NAME, System.getProperty(AMQP_ENABLE_PROP_NAME))
}
group 'com.r3.corda.enterprise'
@ -250,7 +256,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'
@ -285,7 +291,7 @@ artifactory {
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-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver')
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')
}
}
}

View File

@ -5,7 +5,6 @@ apply plugin: 'com.jfrog.artifactory'
dependencies {
compile project(':core')
compile project(':finance')
testCompile project(':test-utils')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
@ -17,6 +16,7 @@ dependencies {
compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version"
// This adds support for java.time types.
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
compile "com.google.guava:guava:$guava_version"
testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile "junit:junit:$junit_version"

View File

@ -5,11 +5,9 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.*
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.contracts.BusinessCalendar
import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
@ -33,7 +31,6 @@ import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal
import java.security.PublicKey
import java.time.LocalDate
import java.util.*
/**
@ -83,8 +80,6 @@ object JacksonSupport {
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
@ -276,36 +271,6 @@ object JacksonSupport {
}
}
data class BusinessCalendarWrapper(val holidayDates: List<LocalDate>) {
fun toCalendar() = BusinessCalendar(holidayDates)
}
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) {
val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj }
if (calendarName != null) {
generator.writeString(calendarName)
} else {
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
}
}
}
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
return try {
try {
val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
} catch (e: Exception) {
parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar()
}
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
}
}
}
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)

View File

@ -2,7 +2,7 @@ package net.corda.jackson
import com.fasterxml.jackson.databind.SerializationFeature
import net.corda.core.contracts.Amount
import net.corda.core.contracts.USD
import net.corda.finance.USD
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.TransactionSignature

View File

@ -53,7 +53,7 @@ dependencies {
}
task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}

View File

@ -2,15 +2,13 @@ package net.corda.client.jfx
import net.corda.client.jfx.model.NodeMonitorModel
import net.corda.client.jfx.model.ProgressTrackingEvent
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.USD
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate
@ -22,11 +20,13 @@ import net.corda.core.node.services.Vault
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.finance.DOLLARS
import net.corda.finance.USD
import net.corda.finance.flows.CashExitFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.startFlowPermission
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User
import net.corda.testing.*
@ -108,13 +108,10 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue works end to end`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow,
Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity,
notaryNode.notaryIdentity,
anonymous
notaryNode.notaryIdentity
)
vaultUpdates.expectEvents(isStrict = false) {
@ -136,7 +133,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue and move`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow()
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null

View File

@ -4,10 +4,10 @@ import javafx.collections.FXCollections
import javafx.collections.ObservableList
import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.node.services.Vault
import net.corda.finance.contracts.asset.Cash
import rx.Observable
data class Diff<out T : ContractState>(

View File

@ -1,12 +1,14 @@
package net.corda.client.mock
import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD
import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes
import net.corda.flows.CashFlowCommand
import net.corda.finance.GBP
import net.corda.finance.USD
import java.util.*
import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest
import net.corda.finance.flows.CashExitFlow.ExitRequest
import net.corda.finance.flows.CashPaymentFlow.PaymentRequest
/**
* [Generator]s for incoming/outgoing cash flow events between parties. It doesn't necessarily generate correct events!
@ -26,16 +28,16 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
addToMap(ccy, amount)
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary, anonymous = true)
IssueAndPaymentRequest(Amount(amount, ccy), issueRef, to, notary, anonymous = true)
}
protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
addToMap(ccy, -amount)
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
ExitRequest(Amount(amount, ccy), issueRef)
}
open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true)
}
open val issuerGenerator = Generator.frequency(listOf(
@ -54,28 +56,28 @@ class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>,
EXIT_ERROR
}
val errorGenerator = Generator.pickOne(IssuerEvents.values().toList())
private val errorGenerator = Generator.pickOne(IssuerEvents.values().toList())
val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType ->
private val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType ->
when (errorType) {
IssuerEvents.NORMAL_EXIT -> {
println("Normal exit")
if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount)
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care.
ExitRequest(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care.
}
IssuerEvents.EXIT_ERROR -> {
println("Exit error")
CashFlowCommand.ExitCash(Amount(currencyMap[ccy]!! * 2, ccy), issueRef)
ExitRequest(Amount(currencyMap[ccy]!! * 2, ccy), issueRef)
}
}
}
val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
private val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true)
}
val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
private val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
PaymentRequest(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
}
override val moveCashGenerator = Generator.frequency(listOf(

View File

@ -50,6 +50,9 @@ processSmokeTestResources {
from(project(':node:capsule').tasks['buildCordaJAR']) {
rename 'corda-(.*)', 'corda.jar'
}
from(project(':finance').tasks['jar']) {
rename 'finance-(.*)', 'finance.jar'
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -78,12 +81,12 @@ dependencies {
}
task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}
task smokeTest(type: Test) {
testClassesDir = sourceSets.smokeTest.output.classesDir
testClassesDirs = sourceSets.smokeTest.output.classesDirs
classpath = sourceSets.smokeTest.runtimeClasspath
}

View File

@ -1,15 +1,15 @@
package net.corda.client.rpc;
import net.corda.core.concurrent.CordaFuture;
import net.corda.client.rpc.internal.RPCClient;
import net.corda.core.concurrent.CordaFuture;
import net.corda.core.contracts.Amount;
import net.corda.core.messaging.CordaRPCOps;
import net.corda.core.messaging.FlowHandle;
import net.corda.core.node.services.ServiceInfo;
import net.corda.core.utilities.OpaqueBytes;
import net.corda.flows.AbstractCashFlow;
import net.corda.flows.CashIssueFlow;
import net.corda.flows.CashPaymentFlow;
import net.corda.finance.flows.AbstractCashFlow;
import net.corda.finance.flows.CashIssueFlow;
import net.corda.finance.flows.CashPaymentFlow;
import net.corda.node.internal.Node;
import net.corda.node.services.transactions.ValidatingNotaryService;
import net.corda.nodeapi.User;
@ -22,10 +22,14 @@ import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static kotlin.test.AssertionsKt.assertEquals;
import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault;
import static net.corda.contracts.GetBalances.getCashBalance;
import static net.corda.node.services.RPCUserServiceKt.startFlowPermission;
import static net.corda.finance.CurrencyUtils.DOLLARS;
import static net.corda.finance.contracts.GetBalances.getCashBalance;
import static net.corda.node.services.FlowPermissions.startFlowPermission;
import static net.corda.testing.TestConstants.getALICE;
public class CordaRPCJavaClientTest extends NodeBasedTest {
@ -45,10 +49,10 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
@Before
public void setUp() throws ExecutionException, InterruptedException {
Set<ServiceInfo> services = new HashSet<>(Collections.singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, Arrays.asList(rpcUser), Collections.emptyMap());
Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
node = nodeFuture.get();
client = new CordaRPCClient(node.getConfiguration().getRpcAddress(), null, getDefault(), false);
client = new CordaRPCClient(requireNonNull(node.getConfiguration().getRpcAddress()), null, getDefault(), false);
}
@After
@ -65,17 +69,15 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
login(rpcUser.getUsername(), rpcUser.getPassword());
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
dollars123, OpaqueBytes.of("1".getBytes()),
node.info.getLegalIdentity(), node.info.getLegalIdentity());
DOLLARS(123), OpaqueBytes.of("1".getBytes()),
node.info.getLegalIdentity());
System.out.println("Started issuing cash, waiting on result");
flowHandle.getReturnValue().get();
Amount<Currency> balance = getCashBalance(rpcProxy, Currency.getInstance("USD"));
System.out.print("Balance: " + balance + "\n");
assertEquals(dollars123, balance, "matching");
assertEquals(DOLLARS(123), balance, "matching");
}
}

View File

@ -1,20 +1,20 @@
package net.corda.client.rpc
import net.corda.contracts.getCashBalance
import net.corda.contracts.getCashBalances
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.USD
import net.corda.core.crypto.random63BitValue
import net.corda.core.flows.FlowInitiator
import net.corda.core.messaging.*
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashException
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.finance.DOLLARS
import net.corda.finance.USD
import net.corda.finance.contracts.getCashBalance
import net.corda.finance.contracts.getCashBalances
import net.corda.finance.flows.CashException
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.internal.Node
import net.corda.node.services.startFlowPermission
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User
import net.corda.testing.ALICE
@ -77,9 +77,9 @@ class CordaRPCClientTest : NodeBasedTest() {
login(rpcUser.username, rpcUser.password)
println("Creating proxy")
println("Starting flow")
val flowHandle = connection!!.proxy.startTrackedFlow(
::CashIssueFlow,
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
)
println("Started flow, waiting on result")
flowHandle.progress.subscribe {
println("PROGRESS $it")
@ -113,8 +113,7 @@ class CordaRPCClientTest : NodeBasedTest() {
assertTrue(startCash.isEmpty(), "Should not start with any cash")
val flowHandle = proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0),
node.info.legalIdentity, node.info.legalIdentity
123.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
)
println("Started issuing cash, waiting on result")
flowHandle.returnValue.get()
@ -140,14 +139,16 @@ class CordaRPCClientTest : NodeBasedTest() {
}
}
val nodeIdentity = node.info.legalIdentity
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0),
nodeIdentity, nodeIdentity
123.DOLLARS,
OpaqueBytes.of(0),
nodeIdentity
).returnValue.getOrThrow()
proxy.startFlowDynamic(CashIssueFlow::class.java,
1000.DOLLARS, OpaqueBytes.of(0),
nodeIdentity, nodeIdentity).returnValue.getOrThrow()
1000.DOLLARS,
OpaqueBytes.of(0),
nodeIdentity).returnValue.getOrThrow()
assertEquals(2, countRpcFlows)
assertEquals(1, countShellFlows)
}

View File

@ -71,7 +71,7 @@ class CordaRPCClient(
fun initialiseSerialization() {
try {
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme(this))
registerScheme(KryoClientSerializationScheme())
registerScheme(AMQPClientSerializationScheme())
}
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT

View File

@ -5,10 +5,10 @@ import net.corda.core.internal.logElapsedTime
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCApi
@ -110,6 +110,21 @@ class RPCClient<I : RPCOps>(
val proxy: I
/** The RPC protocol version reported by the server */
val serverProtocolVersion: Int
/**
* Closes this client without notifying the server.
* The server will eventually clear out the RPC message queue and disconnect subscribed observers,
* but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose].
* This method is helpful when the node may be shutting down or
* have already shut down and you don't want to block waiting for it to come back.
*/
fun forceClose()
/**
* Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources.
* If the server is not available this method may block for a short period until it's clear the server is not coming back.
*/
fun notifyServerAndClose()
}
/**
@ -168,13 +183,30 @@ class RPCClient<I : RPCOps>(
object : RPCConnection<I> {
override val proxy = ops
override val serverProtocolVersion = serverProtocolVersion
override fun close() {
proxyHandler.close()
private fun close(notify: Boolean) {
if (notify) {
proxyHandler.notifyServerAndClose()
} else {
proxyHandler.forceClose()
}
serverLocator.close()
}
override fun notifyServerAndClose() {
close(true)
}
override fun forceClose() {
close(false)
}
override fun close() {
close(true)
}
}
} catch (exception: Throwable) {
proxyHandler.close()
proxyHandler.notifyServerAndClose()
serverLocator.close()
throw exception
}

View File

@ -10,14 +10,17 @@ import com.google.common.cache.RemovalCause
import com.google.common.cache.RemovalListener
import com.google.common.util.concurrent.SettableFuture
import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.internal.ThreadBox
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.LazyPool
import net.corda.core.internal.LazyStickyPool
import net.corda.core.internal.LifeCycle
import net.corda.core.internal.ThreadBox
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext
import net.corda.core.utilities.*
import net.corda.core.utilities.Try
import net.corda.core.utilities.debug
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.*
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.SimpleString
@ -113,7 +116,8 @@ class RPCClientProxyHandler(
private fun createRpcObservableMap(): RpcObservableMap {
val onObservableRemove = RemovalListener<RPCApi.ObservableId, UnicastSubject<Notification<*>>> {
val rpcCallSite = callSiteMap?.remove(it.key.toLong)
val observableId = it.key!!
val rpcCallSite = callSiteMap?.remove(observableId.toLong)
if (it.cause == RemovalCause.COLLECTED) {
log.warn(listOf(
"A hot observable returned from an RPC was never subscribed to.",
@ -123,7 +127,7 @@ class RPCClientProxyHandler(
"will appear less frequently in future versions of the platform and you can ignore it",
"if you want to.").joinToString(" "), rpcCallSite)
}
observablesToReap.locked { observables.add(it.key) }
observablesToReap.locked { observables.add(observableId) }
}
return CacheBuilder.newBuilder().
weakValues().
@ -159,7 +163,7 @@ class RPCClientProxyHandler(
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build()
)
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
this::reapObservables,
this::reapObservablesAndNotify,
rpcConfiguration.reapInterval.toMillis(),
rpcConfiguration.reapInterval.toMillis(),
TimeUnit.MILLISECONDS
@ -268,13 +272,36 @@ class RPCClientProxyHandler(
}
/**
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
* Closes this handler without notifying observables.
* This method clears up only local resources and as such does not block on any network resources.
*/
fun close() {
fun forceClose() {
close(false)
}
/**
* Closes this handler and sends notifications to all observables, so it can immediately clean up resources.
* Notifications sent to observables are to be acknowledged, therefore this call blocks until all acknowledgements are received.
* If this is not convenient see the [forceClose] method.
* If an observable is not accessible this method may block for a duration of the message broker timeout.
*/
fun notifyServerAndClose() {
close(true)
}
/**
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
* When observables are to be notified (i.e. the [notify] parameter is true),
* the method blocks until all the messages are acknowledged by the observables.
* Note: If any of the observables is inaccessible, the method blocks for the duration of the timeout set on the message broker.
*
* @param notify whether to notify observables or not.
*/
private fun close(notify: Boolean = true) {
sessionAndConsumer?.sessionFactory?.close()
reaperScheduledFuture?.cancel(false)
observableContext.observableMap.invalidateAll()
reapObservables()
reapObservables(notify)
reaperExecutor?.shutdownNow()
sessionAndProducerPool.close().forEach {
it.sessionFactory.close()
@ -315,8 +342,11 @@ class RPCClientProxyHandler(
lifeCycle.transition(State.SERVER_VERSION_NOT_SET, State.STARTED)
}
private fun reapObservables() {
private fun reapObservablesAndNotify() = reapObservables()
private fun reapObservables(notify: Boolean = true) {
observableContext.observableMap.cleanUp()
if (!notify) return
val observableIds = observablesToReap.locked {
if (observables.isNotEmpty()) {
val temporary = observables

View File

@ -3,21 +3,20 @@ package net.corda.client.rpc.serialization
import com.esotericsoftware.kryo.pool.KryoPool
import net.corda.client.rpc.internal.RpcClientObservableSerializer
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory
import net.corda.core.utilities.ByteSequence
import net.corda.nodeapi.RPCKryo
import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer
import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1
class KryoClientSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) {
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
}
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
return KryoPool.Builder {
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, serializationFactory, context)).apply { classLoader = context.deserializationClassLoader }
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader }
}.build()
}

View File

@ -3,13 +3,11 @@ package net.corda.java.rpc;
import net.corda.client.rpc.CordaRPCConnection;
import net.corda.core.contracts.Amount;
import net.corda.core.messaging.CordaRPCOps;
import net.corda.core.messaging.DataFeed;
import net.corda.core.messaging.FlowHandle;
import net.corda.core.node.NodeInfo;
import net.corda.core.node.services.NetworkMapCache;
import net.corda.core.utilities.OpaqueBytes;
import net.corda.flows.AbstractCashFlow;
import net.corda.flows.CashIssueFlow;
import net.corda.finance.flows.AbstractCashFlow;
import net.corda.finance.flows.CashIssueFlow;
import net.corda.nodeapi.User;
import net.corda.smoketesting.NodeConfig;
import net.corda.smoketesting.NodeProcess;
@ -18,12 +16,18 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static kotlin.test.AssertionsKt.assertEquals;
import static net.corda.contracts.GetBalances.getCashBalance;
import static kotlin.test.AssertionsKt.fail;
import static net.corda.finance.contracts.GetBalances.getCashBalance;
public class StandaloneCordaRPCJavaClientTest {
private List<String> perms = Collections.singletonList("ALL");
@ -32,6 +36,7 @@ public class StandaloneCordaRPCJavaClientTest {
private AtomicInteger port = new AtomicInteger(15000);
private NodeProcess.Factory factory;
private NodeProcess notary;
private CordaRPCOps rpcProxy;
private CordaRPCConnection connection;
@ -49,7 +54,9 @@ public class StandaloneCordaRPCJavaClientTest {
@Before
public void setUp() {
notary = new NodeProcess.Factory().create(notaryConfig);
factory = new NodeProcess.Factory();
copyFinanceCordapp();
notary = factory.create(notaryConfig);
connection = notary.connect();
rpcProxy = connection.getProxy();
notaryNode = fetchNotaryIdentity();
@ -60,7 +67,31 @@ public class StandaloneCordaRPCJavaClientTest {
try {
connection.close();
} finally {
notary.close();
if(notary != null) {
notary.close();
}
}
}
private void copyFinanceCordapp() {
Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins"));
try {
Files.createDirectories(pluginsDir);
} catch (IOException ex) {
fail("Failed to create directories");
}
try (Stream<Path> paths = Files.walk(Paths.get("build", "resources", "smokeTest"))) {
paths.forEach(file -> {
if (file.toString().contains("corda-finance")) {
try {
Files.copy(file, pluginsDir.resolve(file.getFileName()));
} catch (IOException ex) {
fail("Failed to copy finance jar");
}
}
});
} catch (IOException e) {
fail("Failed to walk files");
}
}
@ -75,7 +106,7 @@ public class StandaloneCordaRPCJavaClientTest {
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
dollars123, OpaqueBytes.of("1".getBytes()),
notaryNode.getLegalIdentity(), notaryNode.getLegalIdentity());
notaryNode.getLegalIdentity());
System.out.println("Started issuing cash, waiting on result");
flowHandle.getReturnValue().get();

View File

@ -3,13 +3,8 @@ package net.corda.kotlin.rpc
import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream
import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.notUsed
import net.corda.contracts.asset.Cash
import net.corda.contracts.getCashBalance
import net.corda.contracts.getCashBalances
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.InputStreamAndHash
import net.corda.core.internal.*
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.Vault
@ -18,8 +13,15 @@ import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.finance.DOLLARS
import net.corda.finance.POUNDS
import net.corda.finance.SWISS_FRANCS
import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.contracts.getCashBalance
import net.corda.finance.contracts.getCashBalances
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
@ -30,8 +32,11 @@ import org.junit.Before
import org.junit.Test
import java.io.FilterInputStream
import java.io.InputStream
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
import kotlin.streams.toList
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
@ -41,11 +46,12 @@ class StandaloneCordaRPClientTest {
private companion object {
val log = loggerFor<StandaloneCordaRPClientTest>()
val user = User("user1", "test", permissions = setOf("ALL"))
val port = AtomicInteger(15000)
val port = AtomicInteger(15200)
const val attachmentSize = 2116
val timeout = 60.seconds
}
private lateinit var factory: NodeProcess.Factory
private lateinit var notary: NodeProcess
private lateinit var rpcProxy: CordaRPCOps
private lateinit var connection: CordaRPCConnection
@ -62,7 +68,9 @@ class StandaloneCordaRPClientTest {
@Before
fun setUp() {
notary = NodeProcess.Factory().create(notaryConfig)
factory = NodeProcess.Factory()
copyFinanceCordapp()
notary = factory.create(notaryConfig)
connection = notary.connect()
rpcProxy = connection.proxy
notaryNode = fetchNotaryIdentity()
@ -77,6 +85,15 @@ class StandaloneCordaRPClientTest {
}
}
private fun copyFinanceCordapp() {
val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").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)
}
@Test
fun `test attachments`() {
val attachment = InputStreamAndHash.createInMemoryTestZip(attachmentSize, 1)
@ -93,7 +110,7 @@ class StandaloneCordaRPClientTest {
@Test
fun `test starting flow`() {
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout)
}
@ -101,13 +118,16 @@ class StandaloneCordaRPClientTest {
fun `test starting tracked flow`() {
var trackCount = 0
val handle = rpcProxy.startTrackedFlow(
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.notaryIdentity
)
val updateLatch = CountDownLatch(1)
handle.progress.subscribe { msg ->
log.info("Flow>> $msg")
++trackCount
updateLatch.countDown()
}
handle.returnValue.getOrThrow(timeout)
updateLatch.await()
assertNotEquals(0, trackCount)
}
@ -121,17 +141,20 @@ class StandaloneCordaRPClientTest {
val (stateMachines, updates) = rpcProxy.stateMachinesFeed()
assertEquals(0, stateMachines.size)
val updateLatch = CountDownLatch(1)
val updateCount = AtomicInteger(0)
updates.subscribe { update ->
if (update is StateMachineUpdate.Added) {
log.info("StateMachine>> Id=${update.id}")
updateCount.incrementAndGet()
updateLatch.countDown()
}
}
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout)
updateLatch.await()
assertEquals(1, updateCount.get())
}
@ -140,16 +163,16 @@ class StandaloneCordaRPClientTest {
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
assertEquals(0, vault.totalStatesAvailable)
val updateCount = AtomicInteger(0)
val updateLatch = CountDownLatch(1)
vaultUpdates.subscribe { update ->
log.info("Vault>> FlowId=${update.flowId}")
updateCount.incrementAndGet()
updateLatch.countDown()
}
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout)
assertNotEquals(0, updateCount.get())
updateLatch.await()
// Check that this cash exists in the vault
val cashBalance = rpcProxy.getCashBalances()
@ -161,7 +184,7 @@ class StandaloneCordaRPClientTest {
@Test
fun `test vault query by`() {
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout)
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
@ -187,12 +210,10 @@ class StandaloneCordaRPClientTest {
@Test
fun `test cash balances`() {
val startCash = rpcProxy.getCashBalances()
println(startCash)
assertTrue(startCash.isEmpty(), "Should not start with any cash")
val flowHandle = rpcProxy.startFlow(::CashIssueFlow,
629.DOLLARS, OpaqueBytes.of(0),
notaryNode.legalIdentity, notaryNode.legalIdentity
)
val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 629.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity)
println("Started issuing cash, waiting on result")
flowHandle.returnValue.get()

View File

@ -1,5 +1,5 @@
gradlePluginsVersion=0.13.6
kotlinVersion=1.1.1
gradlePluginsVersion=0.15.1
kotlinVersion=1.1.4
guavaVersion=21.0
bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1
typesafeConfigVersion=1.3.1

View File

@ -42,7 +42,7 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}"
// Guava: Google utilities library.
compile "com.google.guava:guava:$guava_version"
testCompile "com.google.guava:guava:$guava_version"
// RxJava: observable streams of events.
compile "io.reactivex:rxjava:$rxjava_version"
@ -63,12 +63,6 @@ dependencies {
// JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
// Requery: SQL based query & persistence for Kotlin
compile "io.requery:requery-kotlin:$requery_version"
// For AMQP serialisation.
compile "org.apache.qpid:proton-j:0.19.0"
}
configurations {

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.utilities.exactAdd
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.exactAdd
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.*
@ -13,6 +13,7 @@ import java.util.*
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
*/
interface TokenizableAssetInfo {
/** The nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. */
val displayTokenSize: BigDecimal
}
@ -28,16 +29,14 @@ interface TokenizableAssetInfo {
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow.
*
* @param quantity the number of tokens as a Long value.
* @param displayTokenSize the nominal display unit size of a single token,
* potentially with trailing decimal display places if the scale parameter is non-zero.
* @param T the type of the token, for example [Currency].
* T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
*
* TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
* @property quantity the number of tokens as a Long value.
* @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero.
* @property token an instance of type T, usually a singleton.
* @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
*/
@CordaSerializable
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
// TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
companion object {
/**
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
@ -73,6 +72,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
* For other possible token types the asset token should implement TokenizableAssetInfo to
* correctly report the designed nominal amount.
*/
@JvmStatic
fun getDisplayTokenSize(token: Any): BigDecimal {
if (token is TokenizableAssetInfo) {
return token.displayTokenSize
@ -86,14 +86,39 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
return BigDecimal.ONE
}
/**
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
* any of the token types are mismatched; if the iterator yields no elements, return null.
*/
@JvmStatic
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
/**
* Sums the amounts yielded by the given iterable, throwing an [IllegalArgumentException] if any of the token
* types are mismatched.
*/
@JvmStatic
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
/**
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
* any of the token types are mismatched; if the iterator yields no elements, return a zero amount of the given
* token type.
*/
@JvmStatic
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
private val currencySymbols: Map<String, Currency> = mapOf(
"$" to USD,
"£" to GBP,
"" to EUR,
"¥" to JPY,
"" to RUB
"$" to Currency.getInstance("USD"),
"£" to Currency.getInstance("GBP"),
"" to Currency.getInstance("EUR"),
"¥" to Currency.getInstance("JPY"),
"" to Currency.getInstance("RUB")
)
private val currencyCodes: Map<String, Currency> by lazy { Currency.getAvailableCurrencies().map { it.currencyCode to it }.toMap() }
private val currencyCodes: Map<String, Currency> by lazy {
Currency.getAvailableCurrencies().associateBy { it.currencyCode }
}
/**
* Returns an amount that is equal to the given currency amount in text. Examples of what is supported:
@ -120,6 +145,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
*
* @throws IllegalArgumentException if the input string was not understood.
*/
@JvmStatic
fun parseCurrency(input: String): Amount<Currency> {
val i = input.filter { it != ',' }
try {
@ -127,7 +153,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
for ((symbol, currency) in currencySymbols) {
if (i.startsWith(symbol)) {
val rest = i.substring(symbol.length)
return fromDecimal(BigDecimal(rest), currency)
return Amount.fromDecimal(BigDecimal(rest), currency)
}
}
// Now check the codes at the end.
@ -136,7 +162,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
val (rest, code) = split
for ((cc, currency) in currencyCodes) {
if (cc == code) {
return fromDecimal(BigDecimal(rest), currency)
return Amount.fromDecimal(BigDecimal(rest), currency)
}
}
}
@ -166,8 +192,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
/**
* A checked addition operator is supported to simplify aggregation of Amounts.
* Mixing non-identical token types will throw [IllegalArgumentException].
*
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
* Mixing non-identical token types will throw [IllegalArgumentException]
*/
operator fun plus(other: Amount<T>): Amount<T> {
checkToken(other)
@ -177,8 +204,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
/**
* A checked addition operator is supported to simplify netting of Amounts.
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
* Mixing non-identical token types will throw [IllegalArgumentException].
*
* @throws ArithmeticException if there is Numeric underflow
* Mixing non-identical token types will throw [IllegalArgumentException]
*/
operator fun minus(other: Amount<T>): Amount<T> {
checkToken(other)
@ -197,6 +225,11 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
*/
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
/**
* The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
* Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
* N.B. Division is not supported as fractional tokens are not representable by an Amount.
*/
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
/**
@ -219,7 +252,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
* of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize,
* which determines the size of a single token and controls the trailing decimal places via it's scale property.
*
* @see Amount.Companion.fromDecimal
* @see Amount.fromDecimal
*/
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
@ -231,29 +264,27 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
* The result of fromDecimal is used to control the numerical formatting and
* the token specifier appended is taken from token.toString.
*
* @see Amount.Companion.fromDecimal
* @see Amount.fromDecimal
*/
override fun toString(): String {
return toDecimal().toPlainString() + " " + token
}
/** @suppress */
override fun compareTo(other: Amount<T>): Int {
checkToken(other)
return quantity.compareTo(other.quantity)
}
}
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
/**
* Simple data class to associate the origin, owner, or holder of a particular Amount object.
* @param source the holder of the Amount.
* @param amount the Amount of asset available.
* @param ref is an optional field used for housekeeping in the caller.
*
* @param P Any class type that can disambiguate where the amount came from.
* @param T The token type of the underlying [Amount].
* @property source the holder of the Amount.
* @property amount the Amount of asset available.
* @property ref is an optional field used for housekeeping in the caller.
* e.g. to point back at the original Vault state objects.
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
* and calculates the resulting Amount distribution as a new list of SourceAndAmount objects.
@ -263,17 +294,17 @@ data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amou
/**
* This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date.
*
* @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
* @property quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
* it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta)
* at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens
* and the destination will lose Math.abs(quantityDelta) tokens.
* Where possible the source and destination should be coded to ensure a positive quantityDelta,
* but in various scenarios it may be more consistent to allow positive and negative values.
* For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination.
* @param token represents the type of asset token as would be used to construct Amount<T> objects.
* @param source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
* @property token represents the type of asset token as would be used to construct Amount<T> objects.
* @property source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
* or the token sink if quantityDelta is negative. The type P should support value equality.
* @param destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
* @property destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
* or the token source if quantityDelta is negative. The type P should support value equality.
*/
@CordaSerializable
@ -305,9 +336,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
return AmountTransfer(deltaTokenCount, token, source, destination)
}
/**
* Helper to make a zero size AmountTransfer
*/
/** Helper to make a zero size AmountTransfer. */
@JvmStatic
fun <T : Any, P : Any> zero(token: T,
source: P,
@ -344,6 +373,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
*/
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
/** @suppress */
fun copy(quantityDelta: Long = this.quantityDelta,
token: T = this.token,
source: P = this.source,
@ -373,7 +403,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
}
/**
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
* This hash code function ensures that reversed source and destination equivalents will hash to the same value.
*/
override fun hashCode(): Int {
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values
@ -382,18 +412,20 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
return result
}
/** @suppress */
override fun toString(): String {
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token"
}
/**
* Novation is a common financial operation in which a bilateral exchange is modified so that the same
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house.
* Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. The net
* total exchange is the same as in the original input. Novation is a common financial operation in which a
* bilateral exchange is modified so that the same relative asset exchange happens, but with each party exchanging
* versus a central counterparty, or clearing house.
*
* @param centralParty The central party to face the exchange against.
* @return Returns a list of two new AmountTransfers each between one of the original parties and the centralParty.
* The net total exchange is the same as in the original input.
*/
@Suppress("UNUSED")
fun novate(centralParty: P): List<AmountTransfer<T, P>> = listOf(copy(destination = centralParty), copy(source = centralParty))
/**
@ -403,7 +435,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
* @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange.
* @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output.
* i.e. To the new payment destination entry and to any residual change output.
* @return The returned list is a copy of the original list, except that funds needed to cover the exchange
* @return A copy of the original list, except that funds needed to cover the exchange
* will have been removed and a new output and possibly residual amount entry will be added at the end of the list.
* @throws ArithmeticException if there is underflow in the summations.
*/

View File

@ -4,7 +4,6 @@ package net.corda.core.contracts
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import java.math.BigDecimal
import java.security.PublicKey
import java.util.*
@ -12,41 +11,10 @@ import java.util.*
* Defines a simple domain specific language for the specification of financial contracts. Currently covers:
*
* - Some utilities for working with commands.
* - Code for working with currencies.
* - An Amount type that represents a positive quantity of a specific currency.
* - An Amount type that represents a positive quantity of a specific token.
* - A simple language extension for specifying requirements in English, along with logic to enforce them.
*
* TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
*/
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
fun currency(code: String) = Currency.getInstance(code)!!
@JvmField val USD = currency("USD")
@JvmField val GBP = currency("GBP")
@JvmField val EUR = currency("EUR")
@JvmField val CHF = currency("CHF")
@JvmField val JPY = currency("JPY")
@JvmField val RUB = currency("RUB")
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
object Requirements {

View File

@ -14,49 +14,28 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
* crude are fungible and countable (oil from two small containers can be poured into one large
* container), shares of the same class in a specific company are fungible and countable, and so on.
*
* See [Cash] for an example contract that implements currency using state objects that implement
* An example usage would be a cash transaction contract that implements currency using state objects that implement
* this interface.
*
* @param T a type that represents the asset in question. This should describe the basic type of the asset
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
*/
interface FungibleAsset<T : Any> : OwnableState {
/**
* Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally
* anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details.
*/
val amount: Amount<Issued<T>>
/**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
* owner to sign, some (i.e. cash) also require the issuer.
*/
val exitKeys: Collection<PublicKey>
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: AbstractParty
fun move(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
// Just for grouping
interface Commands : CommandData {
interface Move : MoveCommand, Commands
/**
* Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction
* has a unique ID even when there are no inputs.
*/
interface Issue : IssueCommand, Commands
/**
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way.
*/
interface Exit<T : Any> : Commands {
val amount: Amount<Issued<T>>
}
}
/**
* Copies the underlying data structure, replacing the amount and owner fields with the new values and leaving the
* rest (exitKeys) alone.
*/
fun withNewOwnerAndAmount(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
}
// Small DSL extensions.
/** Sums the asset states in the list, returning null if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)

View File

@ -128,10 +128,24 @@ infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(new
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, newNotary)
/**
* Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
* quantifiable with integer quantities.
* The [Issued] data class holds the details of an on ledger digital asset.
* In particular it gives the public credentials of the entity that created these digital tokens
* and the particular product represented.
*
* @param P the type of product underlying the definition, for example [java.util.Currency].
* @param P the class type of product underlying the definition, for example [java.util.Currency].
* @property issuer The [AbstractParty] details of the entity which issued the asset
* and a reference blob, which can contain other details related to the token creation e.g. serial number,
* warehouse location, etc.
* The issuer is the gatekeeper for creating, or destroying the tokens on the digital ledger and
* only their [PrivateKey] signature can authorise transactions that do not conserve the total number
* of tokens on the ledger.
* Other identities may own the tokens, but they can only create transactions that conserve the total token count.
* Typically the issuer is also a well know organisation that can convert digital tokens to external assets
* and thus underwrites the digital tokens.
* Different issuer values may coexist for a particular product, but these cannot be merged.
* @property product The details of the specific product represented by these digital tokens. The value
* of product may differentiate different kinds of asset within the same logical class e.g the currency, or
* it may just be a type marker for a single custom asset.
*/
@CordaSerializable
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
@ -156,15 +170,15 @@ data class CommandAndState(val command: CommandData, val ownableState: OwnableSt
* A contract state that can have a single owner.
*/
interface OwnableState : ContractState {
/** There must be a MoveCommand signed by this key to claim the amount */
/** There must be a MoveCommand signed by this key to claim the amount. */
val owner: AbstractParty
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone. */
fun withNewOwner(newOwner: AbstractParty): CommandAndState
}
// DOCEND 3
/** Something which is scheduled to happen at a point in time */
/** Something which is scheduled to happen at a point in time. */
interface Scheduled {
val scheduledAt: Instant
}
@ -284,20 +298,14 @@ data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>)
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
}
/** A common issue command, to enforce that issue commands have a nonce value. */
// TODO: Revisit use of nonce values - should this be part of the TX rather than the command perhaps?
interface IssueCommand : CommandData {
val nonce: Long
}
/** A common move command for contract states which can change owner. */
interface MoveCommand : CommandData {
/**
* Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in
* order to settle an obligation contract's state object(s).
*/
// TODO: Replace SecureHash here with a general contract constraints object
val contractHash: SecureHash?
// TODO: Replace Class here with a general contract constraints object
val contract: Class<out Contract>?
}
/** Indicates that this transaction replaces the inputs contract state to another contract state */
@ -333,15 +341,14 @@ interface Contract {
*/
@Throws(IllegalArgumentException::class)
fun verify(tx: LedgerTransaction)
/**
* Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of
* the contract's contents).
*/
val legalContractReference: SecureHash
}
// DOCEND 5
/** The annotated [Contract] implements the legal prose identified by the given URI. */
@Target(AnnotationTarget.CLASS)
@MustBeDocumented
annotation class LegalProseReference(val uri: String)
/**
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
*
@ -427,7 +434,7 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
* A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot
* use brute force techniques and reveal the content of a Merkle-leaf hashed value.
* Because this salt serves the role of the seed to compute nonces, its size and entropy should be equal to the
* underlying hash function used for Merkle tree generation, currently [SHA256], which has an output of 32 bytes.
* underlying hash function used for Merkle tree generation, currently [SecureHash.SHA256], which has an output of 32 bytes.
* There are two constructors, one that generates a new 32-bytes random salt, and another that takes a [ByteArray] input.
* The latter is required in cases where the salt value needs to be pre-generated (agreed between transacting parties),
* but it is highlighted that one should always ensure it has sufficient entropy.

View File

@ -13,7 +13,7 @@ import java.security.Signature
* This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm.
*/
object ContentSignerBuilder {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
val sigAlgId = signatureScheme.signatureOID
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
if (random != null) {

View File

@ -70,6 +70,7 @@ object Crypto {
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
* Note: Recommended key size >= 3072 bits.
*/
@JvmField
val RSA_SHA256 = SignatureScheme(
1,
"RSA_SHA256",
@ -84,6 +85,7 @@ object Crypto {
)
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
@JvmField
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2,
"ECDSA_SECP256K1_SHA256",
@ -98,6 +100,7 @@ object Crypto {
)
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
@JvmField
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3,
"ECDSA_SECP256R1_SHA256",
@ -112,6 +115,7 @@ object Crypto {
)
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
@JvmField
val EDDSA_ED25519_SHA512 = SignatureScheme(
4,
"EDDSA_ED25519_SHA512",
@ -131,7 +135,10 @@ object Crypto {
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
* at the cost of larger key sizes and loss of compatibility.
*/
@JvmField
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
@JvmField
val SPHINCS256_SHA256 = SignatureScheme(
5,
"SPHINCS-256_SHA512",
@ -146,13 +153,12 @@ 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,
"COMPOSITE",
AlgorithmIdentifier(CordaObjectIdentifier.compositeKey),
AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY),
emptyList(),
CordaSecurityProvider.PROVIDER_NAME,
CompositeKey.KEY_ALGORITHM,
@ -163,13 +169,14 @@ object Crypto {
)
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
@JvmField
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
/**
* Supported digital signature schemes.
* Note: Only the schemes added in this map will be supported (see [Crypto]).
*/
val supportedSignatureSchemes = listOf(
private val signatureSchemeMap: Map<String, SignatureScheme> = listOf(
RSA_SHA256,
ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256,
@ -183,15 +190,15 @@ object Crypto {
* algorithm identifiers.
*/
private val algorithmMap: Map<AlgorithmIdentifier, SignatureScheme>
= (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } }
+ supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) })
= (signatureSchemeMap.values.flatMap { scheme -> scheme.alternativeOIDs.map { Pair(it, scheme) } }
+ signatureSchemeMap.values.map { Pair(it.signatureOID, it) })
.toMap()
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
// that could cause unexpected and suspicious behaviour.
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
// The val is private to avoid any harmful state changes.
val providerMap: Map<String, Provider> = mapOf(
private val providerMap: Map<String, Provider> = mapOf(
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(),
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
@ -201,6 +208,14 @@ object Crypto {
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
}
@JvmStatic
fun supportedSignatureSchemes(): List<SignatureScheme> = ArrayList(signatureSchemeMap.values)
@JvmStatic
fun findProvider(name: String): Provider {
return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name")
}
init {
// This registration is needed for reading back EdDSA key from java keystore.
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
@ -219,8 +234,10 @@ object Crypto {
}
}
@JvmStatic
fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme {
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)]
?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
}
/**
@ -231,8 +248,11 @@ object Crypto {
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(IllegalArgumentException::class)
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
@JvmStatic
fun findSignatureScheme(schemeCodeName: String): SignatureScheme {
return signatureSchemeMap[schemeCodeName]
?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
}
/**
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
@ -242,7 +262,7 @@ object Crypto {
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported.
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
fun findSignatureScheme(key: PublicKey): SignatureScheme {
val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
return findSignatureScheme(keyInfo.algorithm)
@ -256,7 +276,7 @@ object Crypto {
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported.
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
fun findSignatureScheme(key: PrivateKey): SignatureScheme {
val keyInfo = PrivateKeyInfo.getInstance(key.encoded)
return findSignatureScheme(keyInfo.privateKeyAlgorithm)
@ -269,11 +289,12 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm)
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
}
/**
@ -284,8 +305,11 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
@JvmStatic
@Throws(InvalidKeySpecException::class)
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey {
return decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
}
/**
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
@ -295,13 +319,18 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
@JvmStatic
@Throws(InvalidKeySpecException::class)
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse)
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that " +
"it corresponds to the input scheme's code name.", ikse)
}
}
@ -312,11 +341,12 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
}
/**
@ -328,8 +358,11 @@ object Crypto {
* @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
@JvmStatic
@Throws(InvalidKeySpecException::class)
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey {
return decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
}
/**
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
@ -340,19 +373,25 @@ object Crypto {
* @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
@JvmStatic
@Throws(InvalidKeySpecException::class)
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse)
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " +
"that it corresponds to the input scheme's code name.", ikse)
}
}
/**
* Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use
* doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
* @param privateKey the signer's [PrivateKey].
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
* @return the digital signature (in [ByteArray]) on the input message.
@ -360,7 +399,8 @@ object Crypto {
* @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)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData)
/**
@ -373,8 +413,11 @@ object Crypto {
* @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)
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
}
/**
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature].
@ -386,11 +429,14 @@ object Crypto {
* @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)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" }
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
signature.initSign(privateKey)
signature.update(clearData)
return signature.sign()
@ -398,20 +444,24 @@ object Crypto {
/**
* Generic way to sign [SignableData] objects with a [PrivateKey].
* [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as a timestamp or partial and blind signature indicators.
* @param privateKey the signer's [PrivateKey].
* [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as
* a timestamp or partial and blind signature indicators.
* @param keyPair the signer's [KeyPair].
* @param signableData a [SignableData] object that adds extra information to a transaction.
* @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and the signature metadata.
* @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and
* the signature metadata.
* @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(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature {
val sigKey: SignatureScheme = findSignatureScheme(keyPair.private)
val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public)
if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${sigMetaData.schemeCodeName}" +
" is not aligned with the key type: ${sigKey.schemeCodeName}.")
require(sigKey == sigMetaData) {
"Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}."
}
val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes)
return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata)
}
@ -430,8 +480,11 @@ object Crypto {
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
return doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
}
/**
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
@ -448,8 +501,11 @@ object Crypto {
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
return doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
}
/**
* Method to verify a digital signature.
@ -465,9 +521,12 @@ object Crypto {
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
@ -490,14 +549,16 @@ object Crypto {
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
}
/**
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the
* input public key's type.
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
* as it avoids the risk of failing to test the result.
@ -507,14 +568,20 @@ object Crypto {
* the passed-in signatureData is improperly encoded or of the wrong type,
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
*/
@JvmStatic
@Throws(SignatureException::class)
fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
return isValid(findSignatureScheme(transactionSignature.by), transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
return isValid(
findSignatureScheme(transactionSignature.by),
transactionSignature.by,
transactionSignature.bytes,
signableData.serialize().bytes)
}
/**
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the
* input public key's type.
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
* as it avoids the risk of failing to test the result.
@ -527,8 +594,11 @@ object Crypto {
* the passed-in signatureData is improperly encoded or of the wrong type,
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
*/
@JvmStatic
@Throws(SignatureException::class)
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
return isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
}
/**
* Method to verify a digital signature. In comparison to [doVerify] if the key and signature
@ -544,9 +614,12 @@ object Crypto {
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(SignatureException::class, IllegalArgumentException::class)
@JvmStatic
@Throws(SignatureException::class)
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
signature.initVerify(publicKey)
signature.update(clearData)
@ -560,7 +633,7 @@ object Crypto {
* @return a KeyPair for the requested signature scheme code name.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName))
/**
@ -570,10 +643,12 @@ object Crypto {
* @return a new [KeyPair] for the requested [SignatureScheme].
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(IllegalArgumentException::class)
@JvmOverloads
@JvmStatic
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
@ -638,13 +713,17 @@ object Crypto {
* @throws IllegalArgumentException if the requested signature scheme is not supported.
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
*/
@JvmStatic
fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
when (signatureScheme) {
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed)
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
return when (signatureScheme) {
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
EDDSA_ED25519_SHA512 -> deriveKeyPairEdDSA(privateKey, seed)
else -> throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is " +
"not currently implemented for ${signatureScheme.schemeCodeName}")
}
throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}")
}
/**
@ -656,6 +735,7 @@ object Crypto {
* @throws IllegalArgumentException if the requested signature scheme is not supported.
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
*/
@JvmStatic
fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair {
return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed)
}
@ -728,11 +808,13 @@ object Crypto {
* @return a new [KeyPair] from an entropy input.
* @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input.
*/
@JvmStatic
fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
when (signatureScheme) {
EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy)
return when (signatureScheme) {
EDDSA_ED25519_SHA512 -> deriveEdDSAKeyPairFromEntropy(entropy)
else -> throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair " +
"generation: ${signatureScheme.schemeCodeName}")
}
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}")
}
/**
@ -740,6 +822,7 @@ object Crypto {
* @param entropy a [BigInteger] value.
* @return a new [KeyPair] from an entropy input.
*/
@JvmStatic
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
// custom key pair generator from entropy.
@ -766,8 +849,12 @@ object Crypto {
}
private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter {
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? {
return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
}
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? {
return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
}
}
/**
@ -784,22 +871,29 @@ object Crypto {
* @return true if the point lies on the curve or false if it doesn't.
* @throws IllegalArgumentException if the requested signature scheme or the key type is not supported.
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
when (publicKey) {
is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid)
is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve)
require(isSupportedSignatureScheme(signatureScheme)) {
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
}
return when (publicKey) {
is BCECPublicKey -> publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid
is EdDSAPublicKey -> publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve
else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}")
}
}
// 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) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean {
return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
}
/** Check if the requested [SignatureScheme] is supported by the system. */
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme
@JvmStatic
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean {
return signatureScheme.schemeCodeName in signatureSchemeMap
}
// validate a key, by checking its algorithmic params.
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
@ -812,19 +906,19 @@ object Crypto {
// 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 {
when (key) {
is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key)
is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
return when (key) {
is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key)
is BCRSAPublicKey, is BCSphincs256PublicKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
}
}
// check if a private key satisfies algorithm specs.
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
when (key) {
is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec
is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
return when (key) {
is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
is EdDSAPrivateKey -> key.params == signatureScheme.algSpec
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
}
}
@ -838,21 +932,20 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey {
return Crypto.decodePublicKey(key.encoded)
}
@JvmStatic
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey = decodePublicKey(key.encoded)
/**
* Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations.
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default
* return SUN implementations.
* @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
* is inappropriate for a supported key factory to produce a private key.
*/
fun toSupportedPublicKey(key: PublicKey): PublicKey {
return Crypto.decodePublicKey(key.encoded)
}
@JvmStatic
fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded)
/**
* Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
@ -862,7 +955,6 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
return Crypto.decodePrivateKey(key.encoded)
}
@JvmStatic
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded)
}

View File

@ -11,9 +11,9 @@ import java.security.SignatureException
// should be renamed to match.
/** A wrapper around a digital signature. */
@CordaSerializable
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
/** A digital signature that identifies who the public key is owned by. */
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
open class WithKey(val by: PublicKey, bytes: ByteArray) : DigitalSignature(bytes) {
/**
* Utility to simplify the act of verifying a signature.
*

View File

@ -1,6 +1,7 @@
@file:JvmName("X500NameUtils")
package net.corda.core.crypto
import net.corda.core.internal.toX509CertHolder
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
@ -57,7 +58,7 @@ val X500Name.locationOrNull: String? get() = try {
} catch (e: Exception) {
null
}
val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject
val X509Certificate.subject: X500Name get() = toX509CertHolder().subject
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
/**

View File

@ -42,7 +42,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
fun getInstance(asn1: ASN1Primitive): PublicKey {
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.compositeKey)
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY)
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
@ -177,7 +177,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
}
keyVector.add(ASN1Integer(threshold.toLong()))
keyVector.add(DERSequence(childrenVector))
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), DERSequence(keyVector)).encoded
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY), DERSequence(keyVector)).encoded
}
override fun getFormat() = ASN1Encoding.DER
@ -262,7 +262,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
else if (n == 1) {
require(threshold == null || threshold == children.first().weight)
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
children.first().node // We can assume that this node is a correct CompositeKey.
// Returning the only child node which is [PublicKey] itself. We need to avoid single-key [CompositeKey] instances,
// as there are scenarios where developers expected the underlying key and its composite versions to be equivalent.
children.first().node
} else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
}
}

View File

@ -11,7 +11,7 @@ import java.security.spec.AlgorithmParameterSpec
*/
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
companion object {
val SIGNATURE_ALGORITHM = "COMPOSITESIG"
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
}

View File

@ -3,14 +3,13 @@ package net.corda.core.crypto.provider
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.composite.CompositeSignature
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.security.AccessController
import java.security.PrivilegedAction
import java.security.Provider
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
companion object {
val PROVIDER_NAME = "Corda"
const val PROVIDER_NAME = "Corda"
}
init {
@ -21,7 +20,7 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory")
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature")
val compositeKeyOID = CordaObjectIdentifier.compositeKey.id
val compositeKeyOID = CordaObjectIdentifier.COMPOSITE_KEY.id
put("Alg.Alias.KeyFactory.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
@ -32,6 +31,6 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
object CordaObjectIdentifier {
// UUID-based OID
// TODO: Register for an OID space and issue our own shorter OID
val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
@JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
@JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
}

View File

@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
@ -56,12 +57,14 @@ import java.security.PublicKey
* val stx = subFlow(CollectSignaturesFlow(ptx))
*
* @param partiallySignedTx Transaction to collect the remaining signatures for
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
* just in the states. If null, the default well known identity of the node is used.
*/
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
// TODO: Update this flow to handle randomly generated keys when that work is complete.
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
val myOptionalKeys: Iterable<PublicKey>?,
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
companion object {
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
}
@Suspendable override fun call(): SignedTransaction {
// TODO: Revisit when key management is properly fleshed out.
// This will break if a party uses anything other than their legalIdentityKey.
// Check the signatures which have already been provided and that the transaction is valid.
// Usually just the Initiator and possibly an oracle would have signed at this point.
val myKey = serviceHub.myInfo.legalIdentity.owningKey
val myKeys: Iterable<PublicKey> = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey)
val signed = partiallySignedTx.sigs.map { it.by }
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
// One of the signatures collected so far MUST be from the initiator of this flow.
require(partiallySignedTx.sigs.any { it.by == myKey }) {
require(partiallySignedTx.sigs.any { it.by in myKeys }) {
"The Initiator of CollectSignaturesFlow must have signed the transaction."
}
@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
if (unsigned.isEmpty()) return partiallySignedTx
// Collect signatures from all counter-parties and append them to the partially signed transaction.
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) }
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) }
val stx = partiallySignedTx + counterpartySignatures
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
@ -112,23 +113,32 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
/**
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
*
* @return a pair of the well known identity to contact for a signature, and the public key that party should sign
* with (this may belong to a confidential identity).
*/
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Party> = keys.map {
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key.
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
partyNode.legalIdentity
Pair(party, it)
}
// DOCSTART 1
/**
* Get and check the required signature.
*
* @param counterparty the party to request a signature from.
* @param signingKey the key the party should use to sign the transaction.
*/
@Suspendable private fun collectSignature(counterparty: Party): TransactionSignature {
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
@Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
// Send the key we expect the counterparty to sign with - this is important where they may have several
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
// for us to check we have the expected signature returned.
send(counterparty, signingKey)
return receive<TransactionSignature>(counterparty).unwrap {
require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." }
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
it
}
}
@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party,
progressTracker.currentStep = RECEIVING
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
// keys we own.
val signingKey = receive<PublicKey>(otherParty).unwrap {
// TODO: We should have a faster way of verifying we own a single key
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
}
progressTracker.currentStep = VERIFYING
// Check that the Responder actually needs to sign.
checkMySignatureRequired(stx)
checkMySignatureRequired(stx, signingKey)
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(stx)
stx.tx.toLedgerTransaction(serviceHub).verify()
@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
}
// Sign and send back our signature to the Initiator.
progressTracker.currentStep = SIGNING
val mySignature = serviceHub.createSignature(stx)
val mySignature = serviceHub.createSignature(stx, signingKey)
send(otherParty, mySignature)
// Return the fully signed transaction once it has been committed.
@ -214,8 +231,10 @@ abstract class SignTransactionFlow(val otherParty: Party,
}
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
require(stx.sigs.any { it.by == otherParty.owningKey }) {
"The Initiator of CollectSignaturesFlow must have signed the transaction."
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
require(otherParty in signingWellKnownIdentities) {
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
}
val signed = stx.sigs.map { it.by }
val allSigners = stx.tx.requiredSigningKeys
@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party,
*/
@Suspendable abstract protected fun checkTransaction(stx: SignedTransaction)
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) {
// TODO: Revisit when key management is properly fleshed out.
val myKey = serviceHub.myInfo.legalIdentity.owningKey
require(myKey in stx.tx.requiredSigningKeys) {
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
require(signingKey in stx.tx.requiredSigningKeys) {
"Party is not a participant for any of the input states of transaction ${stx.id}"
}
}

View File

@ -140,7 +140,7 @@ abstract class FlowLogic<out T> {
* network's event horizon time.
*/
@Suspendable
open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions)
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
/**
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
@ -239,7 +239,7 @@ abstract class FlowLogic<out T> {
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
* Note: This logic is only available during tests and is not meant to be used during the production deployment.
* Therefore the default implementationdoes nothing.
* Therefore the default implementation does nothing.
*/
@Suspendable
fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java)
@ -256,7 +256,7 @@ abstract class FlowLogic<out T> {
* Therefore the default implementation does nothing.
*/
@Suspendable
fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java)
fun persistFlowStackSnapshot() = stateMachine.persistFlowStackSnapshot(this::class.java)
////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1,69 +1,21 @@
package net.corda.core.flows
import net.corda.core.utilities.loggerFor
import java.nio.file.Path
import java.util.*
interface FlowStackSnapshotFactory {
private object Holder {
val INSTANCE: FlowStackSnapshotFactory
init {
val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull()
INSTANCE = serviceFactory ?: FlowStackSnapshotDefaultFactory()
}
}
companion object {
val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE }
}
/**
* Returns flow stack data snapshot extracted from Quasar stack.
* It is designed to be used in the debug mode of the flow execution.
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
* Therefore the default implementation does nothing.
*/
fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
/** Stores flow stack snapshot as a json file. The stored shapshot is only partial and consists
* only data (i.e. stack traces and local variables values) relevant to the flow. It does not
* persist corda internal data (e.g. FlowStateMachine). Instead it uses [StackFrameDataToken] to indicate
* the class of the element on the stack.
* The flow stack snapshot is stored in a file located in
* {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
* Therefore the default implementation does nothing.
*/
fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String): Unit
}
private class FlowStackSnapshotDefaultFactory : FlowStackSnapshotFactory {
val log = loggerFor<FlowStackSnapshotDefaultFactory>()
override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
return null
}
override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) {
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
}
}
import java.time.Instant
/**
* Main data object representing snapshot of the flow stack, extracted from the Quasar stack.
*/
data class FlowStackSnapshot constructor(
val timestamp: Long = System.currentTimeMillis(),
val flowClass: String? = null,
val stackFrames: List<Frame> = listOf()
data class FlowStackSnapshot(
val time: Instant,
val flowClass: String,
val stackFrames: List<Frame>
) {
data class Frame(
val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects]
val stackObjects: List<Any?> = listOf()
)
val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects]
val stackObjects: List<Any?>
) {
override fun toString(): String = stackTraceElement.toString()
}
}
/**

View File

@ -0,0 +1,96 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
object IdentitySyncFlow {
/**
* Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential
* identities used in the transaction. This is intended for use as a subflow of another flow, typically between
* transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants
* to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that
* party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that
* identity.
*
* @return a mapping of well known identities to the confidential identities used in the transaction.
*/
// TODO: Can this be triggered automatically from [SendTransactionFlow]
class Send(val otherSides: Set<Party>,
val tx: WireTransaction,
override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
constructor(otherSide: Party, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
companion object {
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
fun tracker() = ProgressTracker(SYNCING_IDENTITIES)
}
@Suspendable
override fun call() {
progressTracker.currentStep = SYNCING_IDENTITIES
val states: List<ContractState> = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data })
val identities: Set<AbstractParty> = states.flatMap { it.participants }.toSet()
// Filter participants down to the set of those not in the network map (are not well known)
val confidentialIdentities = identities
.filter { serviceHub.networkMapCache.getNodeByLegalIdentityKey(it.owningKey) == null }
.toList()
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
otherSides.forEach { otherSide ->
val requestedIdentities: List<AbstractParty> = sendAndReceive<List<AbstractParty>>(otherSide, confidentialIdentities).unwrap { req ->
require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" }
req
}
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
val identityCertificate = identityCertificates[it]
if (identityCertificate != null)
identityCertificate
else
throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}")
}
send(otherSide, sendIdentities)
}
}
}
/**
* Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which
* we do not yet know about.
*/
class Receive(val otherSide: Party) : FlowLogic<Unit>() {
companion object {
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
}
override val progressTracker: ProgressTracker = ProgressTracker(RECEIVING_IDENTITIES, RECEIVING_CERTIFICATES)
@Suspendable
override fun call(): Unit {
progressTracker.currentStep = RECEIVING_IDENTITIES
val allIdentities = receive<List<AbstractParty>>(otherSide).unwrap { it }
val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null }
progressTracker.currentStep = RECEIVING_CERTIFICATES
val missingIdentities = sendAndReceive<List<PartyAndCertificate>>(otherSide, unknownIdentities)
// Batch verify the identities we've received, so we know they're all correct before we start storing them in
// the identity service
missingIdentities.unwrap { identities ->
identities.forEach { it.verify(serviceHub.identityService.trustAnchor) }
identities
}.forEach { identity ->
// Store the received confidential identities in the identity service so we have a record of which well known identity they map to.
serviceHub.identityService.verifyAndRegisterIdentity(identity)
}
}
}
}

View File

@ -13,8 +13,7 @@ import java.security.PublicKey
@CordaSerializable
abstract class AbstractParty(val owningKey: PublicKey) {
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
override fun hashCode(): Int = owningKey.hashCode()
abstract fun nameOrNull(): X500Name?

View File

@ -1,7 +1,6 @@
package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.toBase58String
import net.corda.core.crypto.toStringShort
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
@ -12,11 +11,7 @@ import java.security.PublicKey
* information such as name. It is intended to represent a party on the distributed ledger.
*/
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
// Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party]
// can put in the key and actual name
override fun toString() = "${owningKey.toStringShort()} <Anonymous>"
override fun nameOrNull(): X500Name? = null
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
}

View File

@ -1,9 +1,11 @@
package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.CertificateAndKeyPair
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey
/**
@ -26,10 +28,9 @@ import java.security.PublicKey
* @see CompositeKey
*/
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public)
override fun toString() = name.toString()
override fun nameOrNull(): X500Name? = name
constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo))
override fun nameOrNull(): X500Name = name
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
override fun toString() = name.toString()
}

View File

@ -1,49 +1,42 @@
package net.corda.core.identity
import net.corda.core.serialization.CordaSerializable
import net.corda.core.internal.toX509CertHolder
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey
import java.security.cert.*
import java.util.*
/**
* A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of
* [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party,
* not part of the identifier themselves. While party and certificate can both be derived from the certificate path,
* this class exists in order to ensure the implementation classes of certificates and party public keys are kept stable.
* not part of the identifier themselves.
*/
@CordaSerializable
data class PartyAndCertificate(val party: Party,
val certificate: X509CertificateHolder,
val certPath: CertPath) {
constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath)
val name: X500Name
get() = party.name
val owningKey: PublicKey
get() = party.owningKey
override fun equals(other: Any?): Boolean {
return if (other is PartyAndCertificate)
party == other.party
else
false
//TODO Is VerifiableIdentity a better name?
class PartyAndCertificate(val certPath: CertPath) {
@Transient val certificate: X509CertificateHolder
init {
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
val certs = certPath.certificates
require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
certificate = certs[0].toX509CertHolder()
}
@Transient val party: Party = Party(certificate)
val owningKey: PublicKey get() = party.owningKey
val name: X500Name get() = party.name
operator fun component1(): Party = party
operator fun component2(): X509CertificateHolder = certificate
override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party
override fun hashCode(): Int = party.hashCode()
override fun toString(): String = party.toString()
/**
* Verify that the given certificate path is valid and leads to the owning key of the party.
*/
/** Verify the certificate path is valid. */
fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult {
require(certPath.certificates.first() is X509Certificate) { "Subject certificate must be an X.509 certificate" }
require(Arrays.equals(party.owningKey.encoded, certificate.subjectPublicKeyInfo.encoded)) { "Certificate public key must match party owning key" }
require(Arrays.equals(certPath.certificates.first().encoded, certificate.encoded)) { "Certificate path must link to certificate" }
val validatorParameters = PKIXParameters(setOf(trustAnchor))
val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false }
val validator = CertPathValidator.getInstance("PKIX")
validatorParameters.isRevocationEnabled = false
return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult
return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult
}
}

View File

@ -3,11 +3,7 @@ package net.corda.core.internal
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowContext
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowStackSnapshot
import net.corda.core.flows.StateMachineRunId
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
@ -39,24 +35,11 @@ interface FlowStateMachine<R> {
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit
/**
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
*/
@Suspendable
fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
/**
* Persists a shallow copy of the Quasar stack frames at the time of call to [persistFlowStackSnapshot].
* Use this to track the monitor evolution of the quasar stack values during the flow execution.
* The flow stack snapshot is stored in a file located in {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
*
* Note: With respect to the [flowStackSnapshot], the snapshot being persisted by this method is partial,
* meaning that only flow relevant traces and local variables are persisted.
*/
@Suspendable
fun persistFlowStackSnapshot(flowClass: Class<*>): Unit
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
val serviceHub: ServiceHub
val logger: Logger

View File

@ -2,6 +2,7 @@ package net.corda.core.internal
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import org.bouncycastle.cert.X509CertificateHolder
import org.slf4j.Logger
import rx.Observable
import rx.Observer
@ -165,6 +166,9 @@ fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T
}
}
fun java.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
fun javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
val bytes = toByteArray()

View File

@ -92,7 +92,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
// half way through, it's no big deal, although it might result in us attempting to re-download data
// redundantly next time we attempt verification.
it.verify(serviceHub)
serviceHub.recordTransactions(it)
serviceHub.recordTransactions(false, it)
}
return signedTransaction?.let {

View File

@ -1,6 +1,6 @@
package net.corda.core.internal.concurrent
import com.google.common.annotations.VisibleForTesting
import net.corda.core.internal.VisibleForTesting
import net.corda.core.concurrent.CordaFuture
import net.corda.core.concurrent.match
import net.corda.core.utilities.getOrThrow

View File

@ -421,6 +421,29 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
arg3: D
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
@Suppress("unused")
inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D, E) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
@Suppress("unused")
inline fun <T : Any, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D, E, F) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E,
arg5: F
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
/**
* The Data feed contains a snapshot of the requested data and an [Observable] of future updates.
*/

View File

@ -1,7 +1,10 @@
package net.corda.core.messaging
import net.corda.core.serialization.CordaSerializable
/** The interface for a group of message recipients (which may contain only one recipient) */
@CordaSerializable
interface MessageRecipients
/** A base class for the case of point-to-point messages */

View File

@ -24,16 +24,17 @@ data class NodeInfo(val addresses: List<NetworkHostAndPort>,
val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services.
val legalIdentitiesAndCerts: NonEmptySet<PartyAndCertificate>,
val platformVersion: Int,
var advertisedServices: List<ServiceEntry> = emptyList(),
val advertisedServices: List<ServiceEntry> = emptyList(),
val worldMapLocation: WorldMapLocation? = null) {
init {
require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" }
require(advertisedServices.none { it.identity == legalIdentityAndCert }) {
"Service identities must be different from node legal identity"
}
}
val legalIdentity: Party
get() = legalIdentityAndCert.party
val notaryIdentity: Party
get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
val legalIdentity: Party get() = legalIdentityAndCert.party
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
fun serviceIdentities(type: ServiceType): List<Party> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party }
return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null }
}
}

View File

@ -68,18 +68,35 @@ interface ServiceHub : ServicesForResolution {
/**
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* further processing. This is expected to be run within a database transaction.
* further processing if [notifyVault] is true. This is expected to be run within a database transaction.
*
* @param txs The transactions to record.
* @param notifyVault indicate if the vault should be notified for the update.
*/
fun recordTransactions(txs: Iterable<SignedTransaction>)
fun recordTransactions(notifyVault: Boolean, txs: Iterable<SignedTransaction>)
/**
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* further processing if [notifyVault] is true. This is expected to be run within a database transaction.
*/
fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) {
recordTransactions(notifyVault, listOf(first, *remaining))
}
/**
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* further processing. This is expected to be run within a database transaction.
*/
fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) {
recordTransactions(listOf(first, *remaining))
recordTransactions(true, first, *remaining)
}
/**
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* further processing. This is expected to be run within a database transaction.
*/
fun recordTransactions(txs: Iterable<SignedTransaction>) {
recordTransactions(true, txs)
}
/**
@ -92,8 +109,7 @@ interface ServiceHub : ServicesForResolution {
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index]
}
else stx.tx.outputs[stateRef.index]
} else stx.tx.outputs[stateRef.index]
}
/**
@ -106,8 +122,7 @@ interface ServiceHub : ServicesForResolution {
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
}
else {
} else {
stx.tx.outRef<T>(stateRef.index)
}
}

View File

@ -1,7 +1,10 @@
package net.corda.core.node.services
import net.corda.core.contracts.PartyAndReference
import net.corda.core.identity.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.InvalidAlgorithmParameterException
@ -114,6 +117,6 @@ interface IdentityService {
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
*/
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
}
class UnknownAnonymousPartyException(msg: String) : Exception(msg)

View File

@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture
@ -135,8 +136,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val recordedTime: Instant,
val consumedTime: Instant?,
val status: Vault.StateStatus,
val notaryName: String,
val notaryKey: String,
val notary: AbstractParty?,
val lockId: String?,
val lockUpdateTime: Instant?)
}

View File

@ -4,12 +4,12 @@ package net.corda.core.node.services.vault
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.Vault
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import java.time.Instant
import java.util.*
import javax.persistence.criteria.Predicate
@ -40,6 +40,7 @@ sealed class QueryCriteria {
abstract class CommonQueryCriteria : QueryCriteria() {
abstract val status: Vault.StateStatus
abstract val contractStateTypes: Set<Class<out ContractState>>?
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this)
}
@ -49,13 +50,14 @@ sealed class QueryCriteria {
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
*/
data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
val contractStateTypes: Set<Class<out ContractState>>? = null,
override val contractStateTypes: Set<Class<out ContractState>>? = null,
val stateRefs: List<StateRef>? = null,
val notaryName: List<X500Name>? = null,
val notary: List<AbstractParty>? = null,
val softLockingCondition: SoftLockingCondition? = null,
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
super.visit(parser)
return parser.parseCriteria(this)
}
}
@ -65,9 +67,16 @@ sealed class QueryCriteria {
data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val uuid: List<UUID>? = null,
val externalId: List<String>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
constructor(participants: List<AbstractParty>? = null,
linearId: List<UniqueIdentifier>? = null,
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
contractStateTypes: Set<Class<out ContractState>>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
super.visit(parser)
return parser.parseCriteria(this)
}
}
@ -81,11 +90,13 @@ sealed class QueryCriteria {
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
val issuerPartyName: List<AbstractParty>? = null,
val issuer: List<AbstractParty>? = null,
val issuerRef: List<OpaqueBytes>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
super.visit(parser)
return parser.parseCriteria(this)
}
}
@ -101,9 +112,11 @@ sealed class QueryCriteria {
*/
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
(val expression: CriteriaExpression<L, Boolean>,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
super.visit(parser)
return parser.parseCriteria(this)
}
}

View File

@ -9,39 +9,36 @@ import kotlin.reflect.KProperty1
import kotlin.reflect.jvm.javaGetter
@CordaSerializable
enum class BinaryLogicalOperator {
interface Operator
enum class BinaryLogicalOperator : Operator {
AND,
OR
}
@CordaSerializable
enum class EqualityComparisonOperator {
enum class EqualityComparisonOperator : Operator {
EQUAL,
NOT_EQUAL
}
@CordaSerializable
enum class BinaryComparisonOperator {
enum class BinaryComparisonOperator : Operator {
LESS_THAN,
LESS_THAN_OR_EQUAL,
GREATER_THAN,
GREATER_THAN_OR_EQUAL,
}
@CordaSerializable
enum class NullOperator {
enum class NullOperator : Operator {
IS_NULL,
NOT_NULL
}
@CordaSerializable
enum class LikenessOperator {
enum class LikenessOperator : Operator {
LIKE,
NOT_LIKE
}
@CordaSerializable
enum class CollectionOperator {
enum class CollectionOperator : Operator {
IN,
NOT_IN
}
@ -151,7 +148,7 @@ data class Sort(val columns: Collection<SortColumn>) {
enum class VaultStateAttribute(val attributeName: String) : Attribute {
/** Vault States */
NOTARY_NAME("notaryName"),
NOTARY_NAME("notary"),
CONTRACT_TYPE("contractStateClassName"),
STATE_STATUS("stateStatus"),
RECORDED_TIME("recordedTime"),

View File

@ -17,10 +17,17 @@ object CommonSchema
/**
* First version of the Vault ORM schema
*/
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) {
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = emptyList()) {
@MappedSuperclass
open class LinearState(
/** [ContractState] attributes */
/** X500Name of participant parties **/
@ElementCollection
@Column(name = "participants")
var participants: MutableSet<AbstractParty>? = null,
/**
* Represents a [LinearState] [UniqueIdentifier]
*/
@ -31,18 +38,26 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
var uuid: UUID
) : PersistentState() {
constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id)
constructor(uid: UniqueIdentifier, _participants: Set<AbstractParty>)
: this(participants = _participants.toMutableSet(),
externalId = uid.externalId,
uuid = uid.id)
}
@MappedSuperclass
open class FungibleState(
/** [ContractState] attributes */
@OneToMany(cascade = arrayOf(CascadeType.ALL))
var participants: Set<CommonSchemaV1.Party>,
/** X500Name of participant parties **/
@ElementCollection
@Column(name = "participants")
var participants: MutableSet<AbstractParty>? = null,
/** [OwnableState] attributes */
@OneToOne(cascade = arrayOf(CascadeType.ALL))
var ownerKey: CommonSchemaV1.Party,
/** X500Name of owner party **/
@Column(name = "owner_name")
var owner: AbstractParty,
/** [FungibleAsset] attributes
*
@ -55,42 +70,12 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
var quantity: Long,
/** Issuer attributes */
@OneToOne(cascade = arrayOf(CascadeType.ALL))
var issuerParty: CommonSchemaV1.Party,
/** X500Name of issuer party **/
@Column(name = "issuer_name")
var issuer: AbstractParty,
@Column(name = "issuer_reference")
var issuerRef: ByteArray
) : PersistentState() {
constructor(_participants: Set<AbstractParty>, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray)
: this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(),
ownerKey = CommonSchemaV1.Party(_ownerKey),
quantity = _quantity,
issuerParty = CommonSchemaV1.Party(_issuerParty),
issuerRef = _issuerRef)
}
/**
* Party entity (to be replaced by referencing final Identity Schema)
*/
@Entity
@Table(name = "vault_party",
indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name")))
class Party(
@Id
@GeneratedValue
@Column(name = "party_id")
var id: Int,
/**
* [Party] attributes
*/
@Column(name = "party_name")
var name: String,
@Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?)
var key: String
) {
constructor(party: AbstractParty)
: this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String())
}
) : PersistentState()
}

View File

@ -1,6 +1,5 @@
package net.corda.core.schemas
import io.requery.Persistable
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.serialization.CordaSerializable
@ -69,4 +68,4 @@ data class PersistentStateRef(
/**
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
*/
interface StatePersistable : Persistable
interface StatePersistable

View File

@ -2,6 +2,7 @@ package net.corda.core.schemas.converters
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.IdentityService
import net.corda.core.utilities.loggerFor
import org.bouncycastle.asn1.x500.X500Name
import javax.persistence.AttributeConverter
import javax.persistence.Converter
@ -17,9 +18,15 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
identitySvc()
}
companion object {
val log = loggerFor<AbstractPartyToX500NameAsStringConverter>()
}
override fun convertToDatabaseColumn(party: AbstractParty?): String? {
party?.let {
return identityService.partyFromAnonymous(party)?.toString()
val partyName = identityService.partyFromAnonymous(party)?.toString()
if (partyName != null) return partyName
else log.warn ("Identity service unable to resolve AbstractParty: $party")
}
return null // non resolvable anonymous parties
}
@ -27,7 +34,8 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
override fun convertToEntityAttribute(dbData: String?): AbstractParty? {
dbData?.let {
val party = identityService.partyFromX500Name(X500Name(dbData))
return party as AbstractParty
if (party != null) return party
else log.warn ("Identity service unable to resolve X500name: $dbData")
}
return null // non resolvable anonymous parties are stored as nulls
}

View File

@ -1,27 +0,0 @@
package net.corda.core.schemas.requery
import io.requery.Key
import io.requery.Persistable
import io.requery.Superclass
import net.corda.core.contracts.StateRef
import net.corda.core.schemas.StatePersistable
import javax.persistence.Column
object Requery {
/**
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
*/
// TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt
// once we cut-over all existing Hibernate ContractState persistence to Requery
@Superclass interface PersistentState : StatePersistable {
@get:Key
@get:Column(name = "transaction_id", length = 64)
var txId: String
@get:Key
@get:Column(name = "output_index")
var index: Int
}
}

View File

@ -1,28 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import java.sql.Blob
import javax.sql.rowset.serial.SerialBlob
/**
* Converts from a [ByteArray] to a [Blob].
*/
class BlobConverter : Converter<ByteArray, Blob> {
override fun getMappedType(): Class<ByteArray> = ByteArray::class.java
override fun getPersistedType(): Class<Blob> = Blob::class.java
/**
* creates BLOB(INT.MAX) = 2 GB
*/
override fun getPersistedSize(): Int? = null
override fun convertToPersisted(value: ByteArray?): Blob? {
return value?.let { SerialBlob(value) }
}
override fun convertToMapped(type: Class<out ByteArray>?, value: Blob?): ByteArray? {
return value?.getBytes(1, value.length().toInt())
}
}

View File

@ -1,22 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import java.sql.Timestamp
import java.time.Instant
/**
* Converts from a [Instant] to a [java.sql.Timestamp] for Java 8. Note that
* when converting between the time type and the database type all times will be converted to the
* UTC zone offset.
*/
class InstantConverter : Converter<Instant, Timestamp> {
override fun getMappedType() = Instant::class.java
override fun getPersistedType() = Timestamp::class.java
override fun getPersistedSize() = null
override fun convertToPersisted(value: Instant?) = value?.let { Timestamp.from(it) }
override fun convertToMapped(type: Class<out Instant>, value: Timestamp?) = value?.toInstant()
}

View File

@ -1,28 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import net.corda.core.crypto.SecureHash
/**
* Convert from a [SecureHash] to a [String]
*/
class SecureHashConverter : Converter<SecureHash, String> {
override fun getMappedType(): Class<SecureHash> = SecureHash::class.java
override fun getPersistedType(): Class<String> = String::class.java
/**
* SecureHash consists of 32 bytes which need VARCHAR(64) in hex
* TODO: think about other hash widths
*/
override fun getPersistedSize(): Int? = 64
override fun convertToPersisted(value: SecureHash?): String? {
return value?.toString()
}
override fun convertToMapped(type: Class<out SecureHash>, value: String?): SecureHash? {
return value?.let { SecureHash.parse(value) }
}
}

View File

@ -1,21 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
/**
* Converts from a [StateRef] to a Composite Key defined by a [String] txnHash and an [Int] index
*/
class StateRefConverter : Converter<StateRef, Pair<String, Int>> {
override fun getMappedType() = StateRef::class.java
@Suppress("UNCHECKED_CAST")
override fun getPersistedType() = Pair::class.java as Class<Pair<String, Int>>
override fun getPersistedSize() = null
override fun convertToPersisted(value: StateRef?) = value?.let { Pair(it.txhash.toString(), it.index) }
override fun convertToMapped(type: Class<out StateRef>, value: Pair<String, Int>?) = value?.let { StateRef(SecureHash.parse(it.first), it.second) }
}

View File

@ -1,9 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.converter.EnumOrdinalConverter
import net.corda.core.node.services.Vault
/**
* Converter which persists a [Vault.StateStatus] enum using its enum ordinal representation
*/
class VaultStateStatusConverter : EnumOrdinalConverter<Vault.StateStatus>(Vault.StateStatus::class.java)

View File

@ -0,0 +1,14 @@
package net.corda.core.serialization
/**
* This annotation is a marker to indicate which secondary constructors should be considered, and in which
* order, for evolving objects during their deserialisation.
*
* Versions will be considered in descending order, currently duplicate versions will result in
* non deterministic behaviour when deserialising objects
*/
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class DeprecatedConstructorForDeserialization(val version: Int)

View File

@ -39,7 +39,7 @@ interface SerializationContext {
/**
* When serializing, use the format this header sequence represents.
*/
val preferedSerializationVersion: ByteSequence
val preferredSerializationVersion: ByteSequence
/**
* The class loader to use for deserialization.
*/

View File

@ -8,6 +8,7 @@ import net.corda.core.crypto.keys
import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
import java.security.SignatureException
import java.util.function.Predicate
@ -17,6 +18,7 @@ import java.util.function.Predicate
* by a [SignedTransaction] that carries the signatures over this payload.
* The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]).
*/
@CordaSerializable
data class WireTransaction(
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef>,

View File

@ -139,7 +139,10 @@ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of
fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this)
fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
private class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() {
/**
* Class is public for serialization purposes
*/
class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() {
init {
require(offset >= 0 && offset < bytes.size)
require(size >= 0 && size <= bytes.size)

View File

@ -43,31 +43,31 @@ inline fun Logger.debug(msg: () -> String) {
* Extension method for easier construction of [Duration]s in terms of integer days: `val twoDays = 2.days`.
* @see Duration.ofDays
*/
inline val Int.days: Duration get() = Duration.ofDays(toLong())
val Int.days: Duration get() = Duration.ofDays(toLong())
/**
* Extension method for easier construction of [Duration]s in terms of integer hours: `val twoHours = 2.hours`.
* @see Duration.ofHours
*/
inline val Int.hours: Duration get() = Duration.ofHours(toLong())
val Int.hours: Duration get() = Duration.ofHours(toLong())
/**
* Extension method for easier construction of [Duration]s in terms of integer minutes: `val twoMinutes = 2.minutes`.
* @see Duration.ofMinutes
*/
inline val Int.minutes: Duration get() = Duration.ofMinutes(toLong())
val Int.minutes: Duration get() = Duration.ofMinutes(toLong())
/**
* Extension method for easier construction of [Duration]s in terms of integer seconds: `val twoSeconds = 2.seconds`.
* @see Duration.ofSeconds
*/
inline val Int.seconds: Duration get() = Duration.ofSeconds(toLong())
val Int.seconds: Duration get() = Duration.ofSeconds(toLong())
/**
* Extension method for easier construction of [Duration]s in terms of integer milliseconds: `val twoMillis = 2.millis`.
* @see Duration.ofMillis
*/
inline val Int.millis: Duration get() = Duration.ofMillis(toLong())
val Int.millis: Duration get() = Duration.ofMillis(toLong())
/**
* A simple wrapper that enables the use of Kotlin's `val x by transient { ... }` syntax. Such a property

View File

@ -1,5 +1,6 @@
package net.corda.core.utilities
import net.corda.core.serialization.CordaSerializable
import java.net.URI
/**
@ -7,6 +8,7 @@ import java.net.URI
* @param host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets.
* @param port a valid port number.
*/
@CordaSerializable
data class NetworkHostAndPort(val host: String, val port: Int) {
companion object {
internal val invalidPortFormat = "Invalid port: %s"

View File

@ -1,5 +1,7 @@
package net.corda.core.contracts
import net.corda.finance.*
import net.corda.core.contracts.Amount.Companion.sumOrZero
import org.junit.Test
import java.math.BigDecimal
import java.util.*
@ -13,13 +15,6 @@ import kotlin.test.assertTrue
* Tests of the [Amount] class.
*/
class AmountTests {
@Test
fun basicCurrency() {
val expected = 1000L
val amount = Amount(expected, GBP)
assertEquals(expected, amount.quantity)
}
@Test
fun `make sure Amount has decimal places`() {
val x = Amount(1, Currency.getInstance("USD"))
@ -27,7 +22,7 @@ class AmountTests {
}
@Test
fun decimalConversion() {
fun `decimal conversion`() {
val quantity = 1234L
val amountGBP = Amount(quantity, GBP)
val expectedGBP = BigDecimal("12.34")
@ -49,22 +44,6 @@ class AmountTests {
override fun toString(): String = name
}
@Test
fun parsing() {
assertEquals(Amount(1234L, GBP), Amount.parseCurrency("£12.34"))
assertEquals(Amount(1200L, GBP), Amount.parseCurrency("£12"))
assertEquals(Amount(1000L, USD), Amount.parseCurrency("$10"))
assertEquals(Amount(5000L, JPY), Amount.parseCurrency("¥5000"))
assertEquals(Amount(500000L, RUB), Amount.parseCurrency("₽5000"))
assertEquals(Amount(1500000000L, CHF), Amount.parseCurrency("15,000,000 CHF"))
}
@Test
fun rendering() {
assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString())
assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString())
}
@Test
fun split() {
for (baseQuantity in 0..1000) {
@ -81,7 +60,7 @@ class AmountTests {
}
@Test
fun amountTransfersEquality() {
fun `amount transfers equality`() {
val partyA = "A"
val partyB = "B"
val partyC = "C"
@ -106,7 +85,7 @@ class AmountTests {
}
@Test
fun amountTransferAggregation() {
fun `amount transfer aggregation`() {
val partyA = "A"
val partyB = "B"
val partyC = "C"
@ -137,7 +116,7 @@ class AmountTests {
}
@Test
fun amountTransferApply() {
fun `amount transfer apply`() {
val partyA = "A"
val partyB = "B"
val partyC = "C"
@ -182,6 +161,5 @@ class AmountTests {
assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)])
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)])
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, GBP)])
}
}

View File

@ -7,6 +7,7 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.contracts.DummyContract
import net.corda.testing.dummyCommand
import net.corda.testing.node.MockServices
import org.junit.Before
import org.junit.Test
@ -27,6 +28,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
interface Commands {
data class Cmd1(val id: Int) : CommandData, Commands
data class Cmd2(val id: Int) : CommandData, Commands
data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks.
}
@ -50,7 +52,11 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
private fun makeDummyStateAndRef(data: Any): StateAndRef<*> {
val dummyState = makeDummyState(data)
val fakeIssueTx = services.signInitialTransaction(TransactionBuilder(notary = DUMMY_NOTARY).addOutputState(dummyState))
val fakeIssueTx = services.signInitialTransaction(
TransactionBuilder(notary = DUMMY_NOTARY)
.addOutputState(dummyState)
.addCommand(dummyCommand())
)
services.recordTransactions(fakeIssueTx)
val dummyStateRef = StateRef(fakeIssueTx.id, 0)
return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef)
@ -182,7 +188,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
val intCmd2 = ltx.commandsOfType<Commands.Cmd2>()
assertEquals(5, intCmd2.size)
assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { it.value.id })
val notPresentQuery = ltx.commandsOfType(FungibleAsset.Commands.Exit::class.java)
val notPresentQuery = ltx.commandsOfType(Commands.Cmd3::class.java)
assertEquals(emptyList(), notPresentQuery)
}

View File

@ -1,9 +1,10 @@
package net.corda.core.contracts
import net.corda.contracts.asset.Cash
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.testing.MEGA_CORP
import net.corda.testing.MINI_CORP
import net.corda.testing.ledger
@ -27,7 +28,6 @@ class TransactionEncumbranceTests {
val timeLock = DummyTimeLock.State(FIVE_PM)
class DummyTimeLock : Contract {
override val legalContractReference = SecureHash.sha256("DummyTimeLock")
override fun verify(tx: LedgerTransaction) {
val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return
val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window")

View File

@ -34,14 +34,15 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() {
val notaryServices = MockServices(DUMMY_NOTARY_KEY)
val originBuilder = TransactionBuilder(DUMMY_NOTARY)
originBuilder.addOutputState(DummyState(random31BitValue()))
originBuilder.addCommand(command, MEGA_CORP_PUBKEY)
.addOutputState(DummyState(random31BitValue()))
.addCommand(command, MEGA_CORP_PUBKEY)
val originPtx = megaCorpServices.signInitialTransaction(originBuilder)
val originTx = notaryServices.addSignature(originPtx)
val inputBuilder = TransactionBuilder(DUMMY_NOTARY)
inputBuilder.addInputState(originTx.tx.outRef<DummyState>(0))
.addInputState(originTx.tx.outRef<DummyState>(0))
.addCommand(dummyCommand(MEGA_CORP_PUBKEY))
val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder)
val inputTx = megaCorpServices.addSignature(inputPtx)

View File

@ -1,12 +1,12 @@
package net.corda.core.contracts
import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import org.junit.Test

View File

@ -25,16 +25,16 @@ class CompositeKeyTests : TestDependencyInjectionBase() {
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
val aliceKey = generateKeyPair()
val bobKey = generateKeyPair()
val charlieKey = generateKeyPair()
private val aliceKey = generateKeyPair()
private val bobKey = generateKeyPair()
private val charlieKey = generateKeyPair()
val alicePublicKey: PublicKey = aliceKey.public
val bobPublicKey: PublicKey = bobKey.public
val charliePublicKey: PublicKey = charlieKey.public
private val alicePublicKey: PublicKey = aliceKey.public
private val bobPublicKey: PublicKey = bobKey.public
private val charliePublicKey: PublicKey = charlieKey.public
val message = OpaqueBytes("Transaction".toByteArray())
val secureHash = message.sha256()
private val message = OpaqueBytes("Transaction".toByteArray())
private val secureHash = message.sha256()
// By lazy is required so that the serialisers are configured before vals initialisation takes place (they internally invoke serialise).
val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) }
@ -43,7 +43,6 @@ class CompositeKeyTests : TestDependencyInjectionBase() {
@Test
fun `(Alice) fulfilled by Alice signature`() {
println(aliceKey.serialize().hash)
assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) }
assertFalse { alicePublicKey.isFulfilledBy(charlieSignature.by) }
}

View File

@ -343,7 +343,7 @@ class CryptoUtilsTest {
// test list of supported algorithms
@Test
fun `Check supported algorithms`() {
val algList: List<String> = Crypto.supportedSignatureSchemes.keys.toList()
val algList: List<String> = Crypto.supportedSignatureSchemes().map { it.schemeCodeName }
val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE")
assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); }
}

View File

@ -1,13 +1,15 @@
package net.corda.core.crypto
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash.Companion.zeroHash
import net.corda.core.identity.Party
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.WireTransaction
import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.testing.*
import org.junit.Test
import java.security.PublicKey

View File

@ -4,20 +4,18 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseProperties
import org.junit.After
import org.junit.Before
import org.junit.Test
@ -32,13 +30,10 @@ import kotlin.test.assertFailsWith
class AttachmentTests {
lateinit var mockNet: MockNetwork
lateinit var configuration: RequeryConfiguration
@Before
fun setUp() {
mockNet = MockNetwork()
val dataSourceProperties = makeTestDataSourceProperties()
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
}
@After
@ -137,11 +132,9 @@ class AttachmentTests {
val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
val corruptAttachment = AttachmentEntity()
corruptAttachment.attId = id
corruptAttachment.content = attachment
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
n0.database.transaction {
n0.attachments.session.update(corruptAttachment)
DatabaseTransactionManager.current().session.update(corruptAttachment)
}
// Get n1 to fetch the attachment. Should receive corrupted bytes.

View File

@ -3,13 +3,14 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command
import net.corda.core.contracts.requireThat
import net.corda.testing.contracts.DummyContract
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.testing.MINI_CORP_KEY
import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices
import org.junit.After
@ -77,16 +78,18 @@ class CollectSignaturesFlowTests {
}
@InitiatedBy(TestFlow.Initiator::class)
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
val myInputKeys = state.participants.map { it.owningKey }
val myKeys = myInputKeys + (identities[serviceHub.myInfo.legalIdentity] ?: serviceHub.myInfo.legalIdentity).owningKey
val command = Command(DummyContract.Commands.Create(), myInputKeys)
val builder = TransactionBuilder(notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx))
val stx = subFlow(CollectSignaturesFlow(ptx, myKeys))
val ftx = subFlow(FinalityFlow(stx)).single()
return ftx
@ -103,10 +106,11 @@ class CollectSignaturesFlowTests {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
val myInputKeys = state.participants.map { it.owningKey }
val command = Command(DummyContract.Commands.Create(), myInputKeys)
val builder = TransactionBuilder(notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx))
val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys))
val ftx = subFlow(FinalityFlow(stx)).single()
return ftx
@ -136,9 +140,12 @@ class CollectSignaturesFlowTests {
@Test
fun `successfully collects two signatures`() {
val bConfidentialIdentity = b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false)
// Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity
a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity)
registerFlowOnAllNodes(TestFlowTwo.Responder::class)
val magicNumber = 1337
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
val parties = listOf(a.info.legalIdentity, bConfidentialIdentity.party, c.info.legalIdentity)
val state = DummyContract.MultiOwnerState(magicNumber, parties)
val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
mockNet.runNetwork()

View File

@ -1,22 +1,23 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.queryBy
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.internal.Emoji
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashIssueFlow
import net.corda.finance.USD
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.internal.CordaRPCOpsImpl
import net.corda.node.services.startFlowPermission
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
import net.corda.nodeapi.User
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.contracts.DummyContract
@ -42,11 +43,14 @@ class ContractUpgradeFlowTest {
@Before
fun setup() {
mockNet = MockNetwork()
val nodes = mockNet.createSomeNodes()
val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override
a = nodes.partyNodes[0]
b = nodes.partyNodes[1]
notary = nodes.notaryNode.info.notaryIdentity
mockNet.runNetwork()
val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity }
a.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
b.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
}
@After
@ -173,8 +177,7 @@ class ContractUpgradeFlowTest {
@Test
fun `upgrade Cash to v2`() {
// Create some cash.
val anonymous = false
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture
mockNet.runNetwork()
val stx = result.getOrThrow().stx
val stateAndRef = stx.tx.outRef<Cash.State>(0)
@ -200,7 +203,7 @@ class ContractUpgradeFlowTest {
override val contract = CashV2()
override val participants = owners
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)"
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
}
@ -208,9 +211,6 @@ class ContractUpgradeFlowTest {
override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
override fun verify(tx: LedgerTransaction) {}
// Dummy Cash contract for testing.
override val legalContractReference = SecureHash.sha256("")
}
@StartableByRPC

View File

@ -1,12 +1,12 @@
package net.corda.core.flows
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.Issued
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.finance.GBP
import net.corda.finance.contracts.asset.Cash
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices
import org.junit.After

View File

@ -0,0 +1,87 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.identity.Party
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class IdentitySyncFlowTests {
lateinit var mockNet: MockNetwork
@Before
fun before() {
// We run this in parallel threads to help catch any race conditions that may exist.
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `sync confidential identities`() {
// Set up values we'll need
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
val alice: Party = aliceNode.services.myInfo.legalIdentity
val bob: Party = bobNode.services.myInfo.legalIdentity
aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert)
aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert)
bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert)
bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert)
bobNode.registerInitiatedFlow(Receive::class.java)
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
val anonymous = true
val ref = OpaqueBytes.of(0x01)
val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notaryNode.services.myInfo.notaryIdentity))
val issueTx = issueFlow.resultFuture.getOrThrow().stx
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
assertNull(bobNode.services.identityService.partyFromAnonymous(confidentialIdentity))
// Run the flow to sync up the identities
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow()
val expected = aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity)
val actual = bobNode.services.identityService.partyFromAnonymous(confidentialIdentity)
assertEquals(expected, actual)
}
/**
* Very lightweight wrapping flow to trigger the counterparty flow that receives the identities.
*/
@InitiatingFlow
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
@Suspendable
override fun call(): Boolean {
subFlow(IdentitySyncFlow.Send(otherSide, tx))
// Wait for the counterparty to indicate they're done
return receive<Boolean>(otherSide).unwrap { it }
}
}
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
class Receive(val otherSide: Party): FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(IdentitySyncFlow.Receive(otherSide))
// Notify the initiator that we've finished syncing
send(otherSide, true)
}
}
}

View File

@ -1,12 +1,12 @@
package net.corda.core.flows
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.Issued
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.finance.GBP
import net.corda.finance.contracts.asset.Cash
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices
import org.junit.After

View File

@ -0,0 +1,26 @@
package net.corda.core.identity
import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.testing.getTestPartyAndCertificate
import net.corda.testing.withTestSerialization
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
import java.math.BigInteger
class PartyAndCertificateTest {
@Test
fun `kryo serialisation`() {
withTestSerialization {
val original = getTestPartyAndCertificate(Party(
X500Name("CN=Test Corp,O=Test Corp,L=Madrid,C=ES"),
entropyToKeyPair(BigInteger.valueOf(83)).public))
val copy = original.serialize().deserialize()
assertThat(copy).isEqualTo(original).isNotSameAs(original)
assertThat(copy.certPath).isEqualTo(original.certPath)
assertThat(copy.certificate).isEqualTo(original.certificate)
}
}
}

View File

@ -17,8 +17,6 @@ class VaultUpdateTests {
override fun verify(tx: LedgerTransaction) {
}
override val legalContractReference: SecureHash = SecureHash.sha256("")
}
private class DummyState : ContractState {

View File

@ -18,7 +18,7 @@ import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Before
@ -53,13 +53,11 @@ private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, conten
* @see NodeAttachmentService.importAttachment
*/
private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) {
with(session) {
withTransaction {
update(AttachmentEntity().apply {
attId = attachmentId
content = data
})
}
val session = DatabaseTransactionManager.current().session
val attachment = session.get<NodeAttachmentService.DBAttachment>(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString())
attachment?.let {
attachment.content = data
session.save(attachment)
}
}

View File

@ -0,0 +1,18 @@
package net.corda.core.serialization
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.asset.Cash
import net.corda.testing.TestDependencyInjectionBase
import org.junit.Test
import kotlin.test.assertEquals
class CommandsSerializationTests : TestDependencyInjectionBase() {
@Test
fun `test cash move serialization`() {
val command = Cash.Commands.Move(CommercialPaper::class.java)
val copiedCommand = command.serialize().deserialize()
assertEquals(command, copiedCommand)
}
}

View File

@ -1,11 +1,11 @@
package net.corda.core.serialization
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds
import net.corda.finance.POUNDS
import net.corda.testing.*
import net.corda.testing.node.MockServices
import org.junit.Before
@ -19,8 +19,6 @@ val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash()
class TransactionSerializationTests : TestDependencyInjectionBase() {
class TestCash : Contract {
override val legalContractReference = SecureHash.sha256("TestCash")
override fun verify(tx: LedgerTransaction) {
}

View File

@ -18,6 +18,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc")
processConfigurations = ['compile']
sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin')
includes = ['packages.md']
}
task buildDocs(dependsOn: ['apidocs', 'makeDocs'])

View File

@ -7,11 +7,15 @@ set -xeo pipefail
# Install the virtualenv
if [ ! -d "virtualenv" ]
then
# If the canonical working directory contains whitespace, virtualenv installs broken scripts.
# But if we pass in an absolute path that uses symlinks to avoid whitespace, that fixes the problem.
# If you run this script manually (not via gradle) from such a path alias, it's available in PWD:
absolutevirtualenv="$PWD/virtualenv"
# Check if python2.7 is installed explicitly otherwise fall back to the default python
if type "python2.7" > /dev/null; then
virtualenv -p python2.7 virtualenv
virtualenv -p python2.7 "$absolutevirtualenv"
else
virtualenv virtualenv
virtualenv "$absolutevirtualenv"
fi
fi
@ -29,4 +33,4 @@ if [ ! -d "virtualenv/lib/python2.7/site-packages/sphinx" ]
then
echo "Installing pip dependencies ... "
pip install -r requirements.txt
fi
fi

3
docs/packages.md Normal file
View File

@ -0,0 +1,3 @@
# Package net.corda.finance.utils
A collection of utilities for summing financial states, for example, summing obligations to get total debts.

View File

@ -22,9 +22,7 @@ The ``Contract`` interface is defined as follows:
Where:
* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this
contract type are valid
* ``legalContractReference`` is the hash of the legal prose contract that ``verify`` seeks to express in code
* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this contract type are valid
verify()
--------
@ -187,8 +185,6 @@ execution of ``verify()``:
}
}
}
override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash")
}
.. sourcecode:: java
@ -209,9 +205,6 @@ execution of ``verify()``:
// Transfer verification logic.
}
}
private final SecureHash legalContractReference = SecureHash.sha256("X contract hash");
@Override public final SecureHash getLegalContractReference() { return legalContractReference; }
}
Grouping states
@ -297,13 +290,5 @@ We can now verify these groups individually:
Legal prose
-----------
Current, ``legalContractReference`` is simply the SHA-256 hash of a contract:
.. container:: codeset
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt
:language: kotlin
:start-after: DOCSTART 2
:end-before: DOCEND 2
In the future, a contract's legal prose will be included as an attachment instead.
Currently, a ``Contract`` subtype may refer to the legal prose it implements via a ``LegalProseReference`` annotation.
In the future, a contract's legal prose will be included as an attachment.

View File

@ -90,7 +90,7 @@ in order to initialise the ORM layer.
Several examples of entities and mappings are provided in the codebase, including ``Cash.State`` and
``CommercialPaper.State``. For example, here's the first version of the cash schema.
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt
:language: kotlin
Identity mapping

View File

@ -129,7 +129,7 @@ For example, here is a relatively complex state definition, for a state represen
.. container:: codeset
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt
:language: kotlin
:start-after: DOCSTART 1
:end-before: DOCEND 1

View File

@ -11,12 +11,11 @@ API: Transactions
Transaction workflow
--------------------
There are four states the transaction can occupy:
At any time, a transaction can occupy one of three states:
* ``TransactionBuilder``, a builder for a transaction in construction
* ``WireTransaction``, an immutable transaction
* ``TransactionBuilder``, a builder for an in-construction transaction
* ``SignedTransaction``, an immutable transaction with 1+ associated signatures
* ``LedgerTransaction``, a transaction that can be checked for validity
* ``LedgerTransaction``, an immutable transaction that can be checked for validity
Here are the possible transitions between transaction states:
@ -26,8 +25,8 @@ TransactionBuilder
------------------
Creating a builder
^^^^^^^^^^^^^^^^^^
The first step when creating a transaction is to instantiate a ``TransactionBuilder``. We can create a builder for each
transaction type as follows:
The first step when creating a new transaction is to instantiate a ``TransactionBuilder``. We create a builder for a
transaction as follows:
.. container:: codeset
@ -342,7 +341,7 @@ SignedTransaction
-----------------
A ``SignedTransaction`` is a combination of:
* An immutable ``WireTransaction``
* An immutable transaction
* A list of signatures over that transaction
.. container:: codeset
@ -357,45 +356,9 @@ transaction's signatures.
Verifying the transaction's contents
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't
currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow
called ``ReceiveTransactionFlow``. See :doc:`api-flows` for more details.
When verifying a ``SignedTransaction``, we don't verify the ``SignedTransaction`` *per se*, but rather the
``WireTransaction`` it contains. We extract this ``WireTransaction`` as follows:
.. container:: codeset
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
:language: kotlin
:start-after: DOCSTART 31
:end-before: DOCEND 31
:dedent: 12
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
:language: java
:start-after: DOCSTART 31
:end-before: DOCEND 31
:dedent: 12
However, this still isn't enough. The ``WireTransaction`` holds its inputs as ``StateRef`` instances, and its
attachments as hashes. These do not provide enough information to properly validate the transaction's contents. To
resolve these into actual ``ContractState`` and ``Attachment`` instances, we need to use the ``ServiceHub`` to convert
the ``WireTransaction`` into a ``LedgerTransaction``:
.. container:: codeset
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
:language: kotlin
:start-after: DOCSTART 32
:end-before: DOCEND 32
:dedent: 12
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
:language: java
:start-after: DOCSTART 32
:end-before: DOCEND 32
:dedent: 12
To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't currently have
in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow called
``ReceiveTransactionFlow``. See :doc:`api-flows` for more details.
We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output
states:
@ -414,8 +377,27 @@ states:
:end-before: DOCEND 33
:dedent: 12
We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in
the contract. Here's an example of how we might do this:
We can also conduct additional validation of the transaction, beyond what is performed by its contracts. However, the
``SignedTransaction`` holds its inputs as ``StateRef`` instances, and its attachments as hashes. These do not provide
enough information to properly validate the transaction's contents. To resolve these into actual ``ContractState`` and
``Attachment`` instances, we need to use the ``ServiceHub`` to convert the ``SignedTransaction`` into a
``LedgerTransaction``:
.. container:: codeset
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
:language: kotlin
:start-after: DOCSTART 32
:end-before: DOCEND 32
:dedent: 12
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
:language: java
:start-after: DOCSTART 32
:end-before: DOCEND 32
:dedent: 12
We can now perform additional verification. Here's a simple example:
.. container:: codeset
@ -558,8 +540,8 @@ Or using another one of our public keys, as follows:
Notarising and recording
^^^^^^^^^^^^^^^^^^^^^^^^
Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See
:doc:`api-flows` for more details.
Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See :doc:`api-flows` for
more details.
Notary-change transactions
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -70,6 +70,15 @@ There are four implementations of this interface which can be chained together t
.. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`)
All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above.
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 types).
When chaining several criteria using AND / OR, all specified contract state types are combined into a single set.
An example of a custom query is illustrated here:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
@ -77,10 +86,6 @@ An example of a custom query is illustrated here:
:start-after: DOCSTART VaultQueryExample20
:end-before: DOCEND VaultQueryExample20
All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above.
All ``QueryCriteria`` implementations provide an explicitly specifiable ``StateStatus`` attribute which defaults to filtering on UNCONSUMED states.
.. 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.
@ -317,22 +322,22 @@ Query for consumed deal states or linear ids, specify a paging specification and
Aggregations on cash using various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
:language: kotlin
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
:language: java
:start-after: DOCSTART VaultJavaQueryExample21
:end-before: DOCEND VaultJavaQueryExample21
Aggregations on cash grouped by currency for various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
:language: kotlin
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
:language: java
:start-after: DOCSTART VaultJavaQueryExample22
:end-before: DOCEND VaultJavaQueryExample22
Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
:language: kotlin
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
:language: java
:start-after: DOCSTART VaultJavaQueryExample23
:end-before: DOCEND VaultJavaQueryExample23

View File

@ -6,6 +6,15 @@ from the previous milestone release.
UNRELEASED
----------
* Vault query common attributes (state status and contract state types) are now handled correctly when using composite
criteria specifications. State status is overridable. Contract states types are aggregatable.
* Cash selection algorithm is now pluggable (with H2 being the default implementation)
* Removed usage of Requery ORM library (repalced with JPA/Hibernate)
* Vault Query performance improvement (replaced expensive per query SQL statement to obtain concrete state types
with single query on start-up followed by dynamic updates using vault state observable))
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
@ -40,6 +49,25 @@ UNRELEASED
If you specifically need well known identities, use the network map, which is the authoritative source of current well
known identities.
* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`.
* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use
`CashIssueFlow` instead.
* Some utility/extension functions (``sumOrThrow``, ``sumOrNull``, ``sumOrZero`` on ``Amount`` and ``Commodity``)
have moved to be static methods on the classes themselves. This improves the API for Java users who no longer
have to see or known about file-level FooKt style classes generated by the Kotlin compile, but means that IntelliJ
no longer auto-suggests these extension functions in completion unless you add import lines for them yourself
(this is Kotlin IDE bug KT-15286).
* ``:finance`` module now acting as a CorDapp with regard to flow registration, schemas and serializable types.
* ``WebServerPluginRegistry`` now has a ``customizeJSONSerialization`` which can be overridden to extend the REST JSON
serializers. In particular the IRS demos must now register the ``BusinessCalendar`` serializers.
* Moved ``:finance`` gradle project files into a ``net.corda.finance`` package namespace.
This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files.
Milestone 14
------------

View File

@ -60,7 +60,7 @@ Currently, users need special permissions to start flows via RPC. These permissi
]
.. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without
logging in. This will be changed in a future release.
logging in. This will be changed in a future release.
Observables
-----------

View File

@ -23,6 +23,11 @@ Cash shares a common superclass, ``OnLedgerAsset``, with the Commodity contract.
assets which can be issued, moved and exited on chain, with the subclasses handling asset-specific data types and
behaviour.
.. note:: Corda supports a pluggable cash selection algorithm by implementing the ``CashSelection`` interface.
The default implementation uses an H2 specific query that can be overridden for different database providers.
Please see ``CashSelectionH2Impl`` and its associated declaration in
``META-INF\services\net.corda.finance.contracts.asset.CashSelection``
Commodity
---------

View File

@ -20,7 +20,6 @@ The Corda repository comprises the following folders:
* **lib** contains some dependencies
* **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence)
* **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC
* **node-schemas** contains entity classes used to represent relational database tables
* **samples** contains all our Corda demos and code samples
* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the
mock network) implementation

View File

@ -64,7 +64,7 @@ applicationDistribution.into("bin") {
}
task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}
@ -89,7 +89,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
rpcUsers = [
['username' : "user",
'password' : "password",
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
'permissions' : ["StartFlow.net.corda.finance.flows.CashFlow"]]
]
}
}

View File

@ -1,30 +1,22 @@
package net.corda.docs
import net.corda.contracts.asset.Cash
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.DOLLARS
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultTrackBy
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.node.services.startFlowPermission
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User
import net.corda.testing.*
import net.corda.testing.driver.driver
import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.testing.parallel
import net.corda.testing.sequence
import org.junit.Test
import java.util.*
import kotlin.concurrent.thread
import kotlin.test.assertEquals
class IntegrationTestingTutorial {
@ -33,7 +25,8 @@ class IntegrationTestingTutorial {
// START 1
driver {
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
startFlowPermission<CashIssueFlow>()
startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashPaymentFlow>()
))
val bobUser = User("bobUser", "testPassword2", permissions = setOf(
startFlowPermission<CashPaymentFlow>()
@ -63,18 +56,21 @@ class IntegrationTestingTutorial {
// START 4
val issueRef = OpaqueBytes.of(0)
val futures = Stack<CordaFuture<*>>()
(1..10).map { i ->
thread {
futures.push(aliceProxy.startFlow(::CashIssueFlow,
i.DOLLARS,
issueRef,
bob.nodeInfo.legalIdentity,
notary.nodeInfo.notaryIdentity
).returnValue)
}
}.forEach(Thread::join) // Ensure the stack of futures is populated.
futures.forEach { it.getOrThrow() }
aliceProxy.startFlow(::CashIssueFlow,
i.DOLLARS,
issueRef,
notary.nodeInfo.notaryIdentity
).returnValue
}.transpose().getOrThrow()
// We wait for all of the issuances to run before we start making payments
(1..10).map { i ->
aliceProxy.startFlow(::CashPaymentFlow,
i.DOLLARS,
bob.nodeInfo.legalIdentity,
true
).returnValue
}.transpose().getOrThrow()
bobVaultUpdates.expectEvents {
parallel(

View File

@ -3,7 +3,6 @@ package net.corda.docs;
import co.paralleluniverse.fibers.Suspendable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import net.corda.contracts.asset.Cash;
import net.corda.core.contracts.*;
import net.corda.core.crypto.SecureHash;
import net.corda.core.crypto.TransactionSignature;
@ -21,6 +20,7 @@ import net.corda.core.transactions.WireTransaction;
import net.corda.core.utilities.ProgressTracker;
import net.corda.core.utilities.ProgressTracker.Step;
import net.corda.core.utilities.UntrustworthyData;
import net.corda.finance.contracts.asset.Cash;
import net.corda.testing.contracts.DummyContract;
import net.corda.testing.contracts.DummyState;
import org.bouncycastle.asn1.x500.X500Name;
@ -28,6 +28,7 @@ import org.jetbrains.annotations.NotNull;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@ -280,7 +281,7 @@ public class FlowCookbookJava {
TypeOnlyCommandData typeOnlyCommandData = new DummyContract.Commands.Create();
// 2. Include additional data which can be used by the contract
// during verification, alongside fulfilling the roles above
CommandData commandDataWithData = new Cash.Commands.Issue(12345678);
CommandData commandDataWithData = new Cash.Commands.Issue();
// Attachments are identified by their hash.
// The attachment with the corresponding hash must have been
@ -394,15 +395,18 @@ public class FlowCookbookJava {
----------------------------*/
progressTracker.setCurrentStep(TX_VERIFICATION);
// Verifying a transaction will also verify every transaction in the transaction's dependency chain, which will require
// transaction data access on counterparty's node. The ``SendTransactionFlow`` can be used to automate the sending
// and data vending process. The ``SendTransactionFlow`` will listen for data request until the transaction
// is resolved and verified on the other side:
// Verifying a transaction will also verify every transaction in
// the transaction's dependency chain, which will require
// transaction data access on counterparty's node. The
// ``SendTransactionFlow`` can be used to automate the sending and
// data vending process. The ``SendTransactionFlow`` will listen
// for data request until the transaction is resolved and verified
// on the other side:
// DOCSTART 12
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx));
// Optional request verification to further restrict data access.
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx){
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx) {
@Override
protected void verifyDataRequest(@NotNull FetchDataFlow.Request.Data dataRequest) {
// Extra request verification.
@ -425,41 +429,43 @@ public class FlowCookbookJava {
List<StateAndRef<DummyState>> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow<DummyState>(counterparty));
// DOCEND 14
// A ``SignedTransaction`` is a pairing of a ``WireTransaction``
// with signatures over this ``WireTransaction``. We don't verify
// a signed transaction per se, but rather the ``WireTransaction``
// it contains.
// DOCSTART 31
WireTransaction wireTx = twiceSignedTx.getTx();
// DOCEND 31
// Before we can verify the transaction, we need the
// ``ServiceHub`` to use our node's local storage to resolve the
// transaction's inputs and attachments into actual objects,
// rather than just references. We do this by converting the
// ``WireTransaction`` into a ``LedgerTransaction``.
// DOCSTART 32
LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub());
// DOCEND 32
// We can now verify the transaction.
// DOCSTART 33
ledgerTx.verify();
// DOCEND 33
try {
// We'll often want to perform our own additional verification
// too. Just because a transaction is valid based on the contract
// rules and requires our signature doesn't mean we have to
// sign it! We need to make sure the transaction represents an
// agreement we actually want to enter into.
// DOCSTART 34
DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData();
if (outputState.getMagicNumber() != 777) {
// ``FlowException`` is a special exception type. It will be
// propagated back to any counterparty flows waiting for a
// message from this flow, notifying them that the flow has
// failed.
throw new FlowException("We expected a magic number of 777.");
// We can now verify the transaction to ensure that it satisfies
// the contracts of all the transaction's input and output states.
// DOCSTART 33
twiceSignedTx.verify(getServiceHub());
// DOCEND 33
// We'll often want to perform our own additional verification
// too. Just because a transaction is valid based on the contract
// rules and requires our signature doesn't mean we have to
// sign it! We need to make sure the transaction represents an
// agreement we actually want to enter into.
// To do this, we need to convert our ``SignedTransaction``
// into a ``LedgerTransaction``. This will use our ServiceHub
// to resolve the transaction's inputs and attachments into
// actual objects, rather than just references.
// DOCSTART 32
LedgerTransaction ledgerTx = twiceSignedTx.toLedgerTransaction(getServiceHub());
// DOCEND 32
// We can now perform our additional verification.
// DOCSTART 34
DummyState outputState = ledgerTx.outputsOfType(DummyState.class).get(0);
if (outputState.getMagicNumber() != 777) {
// ``FlowException`` is a special exception type. It will be
// propagated back to any counterparty flows waiting for a
// message from this flow, notifying them that the flow has
// failed.
throw new FlowException("We expected a magic number of 777.");
}
// DOCEND 34
} catch (GeneralSecurityException e) {
// Handle this as required.
}
// DOCEND 34
// Of course, if you are not a required signer on the transaction,
// you have no power to decide whether it is valid or not. If it

View File

@ -1,9 +1,6 @@
package net.corda.docs
import net.corda.client.rpc.notUsed
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.USD
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultQueryBy
@ -13,10 +10,12 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationCustomization
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.node.services.startFlowPermission
import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashExitFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User
import net.corda.testing.ALICE
@ -128,7 +127,7 @@ fun generateTransactions(proxy: CordaRPCOps) {
proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me)
} else {
val quantity = Math.abs(random.nextLong() % 1000)
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, me, notary)
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, notary)
ownedQuantity += quantity
}
}

Some files were not shown because too many files have changed in this diff Show More