Merge pull request #28 from corda/aslemmer-enterprise-merge-august-29

Aslemmer enterprise merge august 29
This commit is contained in:
Andras Slemmer 2017-08-30 15:37:52 +01:00 committed by GitHub
commit 4e761ebd2e
526 changed files with 7849 additions and 7317 deletions

View File

@ -14,7 +14,7 @@ buildscript {
// //
// TODO: Sort this alphabetically. // TODO: Sort this alphabetically.
ext.kotlin_version = constants.getProperty("kotlinVersion") 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 // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 // 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.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194' ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4' ext.rxjava_version = '1.2.4'
ext.requery_version = '1.3.1'
ext.dokka_version = '0.9.14' ext.dokka_version = '0.9.14'
ext.eddsa_version = '0.2.0' ext.eddsa_version = '0.2.0'
@ -128,6 +127,13 @@ allprojects {
tasks.withType(Test) { tasks.withType(Test) {
// Prevent the project from creating temporary files outside of the build directory. // Prevent the project from creating temporary files outside of the build directory.
systemProperties['java.io.tmpdir'] = buildDir 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' group 'com.r3.corda.enterprise'
@ -250,7 +256,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda' projectUrl = 'https://github.com/corda/corda'
gpgSign = true gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') 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 { license {
name = 'Apache-2.0' name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0'
@ -285,7 +291,7 @@ artifactory {
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
} }
defaults { 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 { dependencies {
compile project(':core') compile project(':core')
compile project(':finance')
testCompile project(':test-utils') testCompile project(':test-utils')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
@ -17,6 +16,7 @@ dependencies {
compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version"
// This adds support for java.time types. // This adds support for java.time types.
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
compile "com.google.guava:guava:$guava_version"
testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile "junit:junit:$junit_version" 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.core.*
import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers 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.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.contracts.BusinessCalendar
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
@ -33,7 +31,6 @@ import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal import java.math.BigDecimal
import java.security.PublicKey import java.security.PublicKey
import java.time.LocalDate
import java.util.* import java.util.*
/** /**
@ -83,8 +80,6 @@ object JacksonSupport {
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer) addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys // For ed25519 pubkeys
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer) 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>() { object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) { override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) 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 com.fasterxml.jackson.databind.SerializationFeature
import net.corda.core.contracts.Amount 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.Crypto
import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature

View File

@ -53,7 +53,7 @@ dependencies {
} }
task integrationTest(type: Test) { task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath 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.NodeMonitorModel
import net.corda.client.jfx.model.ProgressTrackingEvent import net.corda.client.jfx.model.ProgressTrackingEvent
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState 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.isFulfilledBy
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate 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.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashExitFlow import net.corda.finance.DOLLARS
import net.corda.flows.CashIssueFlow import net.corda.finance.USD
import net.corda.flows.CashPaymentFlow 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.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.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.* import net.corda.testing.*
@ -108,13 +108,10 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue works end to end`() { fun `cash issue works end to end`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow, rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })), OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity, notaryNode.notaryIdentity
notaryNode.notaryIdentity,
anonymous
) )
vaultUpdates.expectEvents(isStrict = false) { vaultUpdates.expectEvents(isStrict = false) {
@ -136,7 +133,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue and move`() { fun `cash issue and move`() {
val anonymous = false 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() rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null

View File

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

View File

@ -1,12 +1,14 @@
package net.corda.client.mock package net.corda.client.mock
import net.corda.core.contracts.Amount 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.identity.Party
import net.corda.core.utilities.OpaqueBytes 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 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! * [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 -> protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
addToMap(ccy, amount) 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 -> protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
addToMap(ccy, -amount) addToMap(ccy, -amount)
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef) ExitRequest(Amount(amount, ccy), issueRef)
} }
open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> 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( open val issuerGenerator = Generator.frequency(listOf(
@ -54,28 +56,28 @@ class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>,
EXIT_ERROR 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) { when (errorType) {
IssuerEvents.NORMAL_EXIT -> { IssuerEvents.NORMAL_EXIT -> {
println("Normal exit") println("Normal exit")
if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount) 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 -> { IssuerEvents.EXIT_ERROR -> {
println("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 -> private val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true) PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true)
} }
val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency -> private val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true) PaymentRequest(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
} }
override val moveCashGenerator = Generator.frequency(listOf( override val moveCashGenerator = Generator.frequency(listOf(

View File

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

View File

@ -1,15 +1,15 @@
package net.corda.client.rpc; package net.corda.client.rpc;
import net.corda.core.concurrent.CordaFuture;
import net.corda.client.rpc.internal.RPCClient; import net.corda.client.rpc.internal.RPCClient;
import net.corda.core.concurrent.CordaFuture;
import net.corda.core.contracts.Amount; import net.corda.core.contracts.Amount;
import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.CordaRPCOps;
import net.corda.core.messaging.FlowHandle; import net.corda.core.messaging.FlowHandle;
import net.corda.core.node.services.ServiceInfo; import net.corda.core.node.services.ServiceInfo;
import net.corda.core.utilities.OpaqueBytes; import net.corda.core.utilities.OpaqueBytes;
import net.corda.flows.AbstractCashFlow; import net.corda.finance.flows.AbstractCashFlow;
import net.corda.flows.CashIssueFlow; import net.corda.finance.flows.CashIssueFlow;
import net.corda.flows.CashPaymentFlow; import net.corda.finance.flows.CashPaymentFlow;
import net.corda.node.internal.Node; import net.corda.node.internal.Node;
import net.corda.node.services.transactions.ValidatingNotaryService; import net.corda.node.services.transactions.ValidatingNotaryService;
import net.corda.nodeapi.User; import net.corda.nodeapi.User;
@ -22,10 +22,14 @@ import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException; 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 kotlin.test.AssertionsKt.assertEquals;
import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault; import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault;
import static net.corda.contracts.GetBalances.getCashBalance; import static net.corda.finance.CurrencyUtils.DOLLARS;
import static net.corda.node.services.RPCUserServiceKt.startFlowPermission; import static net.corda.finance.contracts.GetBalances.getCashBalance;
import static net.corda.node.services.FlowPermissions.startFlowPermission;
import static net.corda.testing.TestConstants.getALICE; import static net.corda.testing.TestConstants.getALICE;
public class CordaRPCJavaClientTest extends NodeBasedTest { public class CordaRPCJavaClientTest extends NodeBasedTest {
@ -45,10 +49,10 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
@Before @Before
public void setUp() throws ExecutionException, InterruptedException { public void setUp() throws ExecutionException, InterruptedException {
Set<ServiceInfo> services = new HashSet<>(Collections.singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, Arrays.asList(rpcUser), Collections.emptyMap()); CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
node = nodeFuture.get(); node = nodeFuture.get();
client = new CordaRPCClient(node.getConfiguration().getRpcAddress(), null, getDefault(), false); client = new CordaRPCClient(requireNonNull(node.getConfiguration().getRpcAddress()), null, getDefault(), false);
} }
@After @After
@ -65,17 +69,15 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
login(rpcUser.getUsername(), rpcUser.getPassword()); login(rpcUser.getUsername(), rpcUser.getPassword());
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
dollars123, OpaqueBytes.of("1".getBytes()), DOLLARS(123), OpaqueBytes.of("1".getBytes()),
node.info.getLegalIdentity(), node.info.getLegalIdentity()); node.info.getLegalIdentity());
System.out.println("Started issuing cash, waiting on result"); System.out.println("Started issuing cash, waiting on result");
flowHandle.getReturnValue().get(); flowHandle.getReturnValue().get();
Amount<Currency> balance = getCashBalance(rpcProxy, Currency.getInstance("USD")); Amount<Currency> balance = getCashBalance(rpcProxy, Currency.getInstance("USD"));
System.out.print("Balance: " + balance + "\n"); 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 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.crypto.random63BitValue
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowInitiator
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashException import net.corda.finance.DOLLARS
import net.corda.flows.CashIssueFlow import net.corda.finance.USD
import net.corda.flows.CashPaymentFlow 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.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.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.ALICE import net.corda.testing.ALICE
@ -77,9 +77,9 @@ class CordaRPCClientTest : NodeBasedTest() {
login(rpcUser.username, rpcUser.password) login(rpcUser.username, rpcUser.password)
println("Creating proxy") println("Creating proxy")
println("Starting flow") println("Starting flow")
val flowHandle = connection!!.proxy.startTrackedFlow( val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
::CashIssueFlow, 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity) )
println("Started flow, waiting on result") println("Started flow, waiting on result")
flowHandle.progress.subscribe { flowHandle.progress.subscribe {
println("PROGRESS $it") println("PROGRESS $it")
@ -113,8 +113,7 @@ class CordaRPCClientTest : NodeBasedTest() {
assertTrue(startCash.isEmpty(), "Should not start with any cash") assertTrue(startCash.isEmpty(), "Should not start with any cash")
val flowHandle = proxy.startFlow(::CashIssueFlow, val flowHandle = proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0), 123.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
node.info.legalIdentity, node.info.legalIdentity
) )
println("Started issuing cash, waiting on result") println("Started issuing cash, waiting on result")
flowHandle.returnValue.get() flowHandle.returnValue.get()
@ -140,14 +139,16 @@ class CordaRPCClientTest : NodeBasedTest() {
} }
} }
val nodeIdentity = node.info.legalIdentity 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, proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0), 123.DOLLARS,
nodeIdentity, nodeIdentity OpaqueBytes.of(0),
nodeIdentity
).returnValue.getOrThrow() ).returnValue.getOrThrow()
proxy.startFlowDynamic(CashIssueFlow::class.java, proxy.startFlowDynamic(CashIssueFlow::class.java,
1000.DOLLARS, OpaqueBytes.of(0), 1000.DOLLARS,
nodeIdentity, nodeIdentity).returnValue.getOrThrow() OpaqueBytes.of(0),
nodeIdentity).returnValue.getOrThrow()
assertEquals(2, countRpcFlows) assertEquals(2, countRpcFlows)
assertEquals(1, countShellFlows) assertEquals(1, countShellFlows)
} }

View File

@ -71,7 +71,7 @@ class CordaRPCClient(
fun initialiseSerialization() { fun initialiseSerialization() {
try { try {
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme(this)) registerScheme(KryoClientSerializationScheme())
registerScheme(AMQPClientSerializationScheme()) registerScheme(AMQPClientSerializationScheme())
} }
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT 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.messaging.RPCOps
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults 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.NetworkHostAndPort
import net.corda.core.utilities.loggerFor 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.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
@ -110,6 +110,21 @@ class RPCClient<I : RPCOps>(
val proxy: I val proxy: I
/** The RPC protocol version reported by the server */ /** The RPC protocol version reported by the server */
val serverProtocolVersion: Int 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> { object : RPCConnection<I> {
override val proxy = ops override val proxy = ops
override val serverProtocolVersion = serverProtocolVersion override val serverProtocolVersion = serverProtocolVersion
override fun close() {
proxyHandler.close() private fun close(notify: Boolean) {
if (notify) {
proxyHandler.notifyServerAndClose()
} else {
proxyHandler.forceClose()
}
serverLocator.close() serverLocator.close()
} }
override fun notifyServerAndClose() {
close(true)
}
override fun forceClose() {
close(false)
}
override fun close() {
close(true)
}
} }
} catch (exception: Throwable) { } catch (exception: Throwable) {
proxyHandler.close() proxyHandler.notifyServerAndClose()
serverLocator.close() serverLocator.close()
throw exception throw exception
} }

View File

@ -10,14 +10,17 @@ import com.google.common.cache.RemovalCause
import com.google.common.cache.RemovalListener import com.google.common.cache.RemovalListener
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import com.google.common.util.concurrent.ThreadFactoryBuilder import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.internal.ThreadBox
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.LazyPool import net.corda.core.internal.LazyPool
import net.corda.core.internal.LazyStickyPool import net.corda.core.internal.LazyStickyPool
import net.corda.core.internal.LifeCycle import net.corda.core.internal.LifeCycle
import net.corda.core.internal.ThreadBox
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext 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 net.corda.nodeapi.*
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
@ -113,7 +116,8 @@ class RPCClientProxyHandler(
private fun createRpcObservableMap(): RpcObservableMap { private fun createRpcObservableMap(): RpcObservableMap {
val onObservableRemove = RemovalListener<RPCApi.ObservableId, UnicastSubject<Notification<*>>> { 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) { if (it.cause == RemovalCause.COLLECTED) {
log.warn(listOf( log.warn(listOf(
"A hot observable returned from an RPC was never subscribed to.", "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", "will appear less frequently in future versions of the platform and you can ignore it",
"if you want to.").joinToString(" "), rpcCallSite) "if you want to.").joinToString(" "), rpcCallSite)
} }
observablesToReap.locked { observables.add(it.key) } observablesToReap.locked { observables.add(observableId) }
} }
return CacheBuilder.newBuilder(). return CacheBuilder.newBuilder().
weakValues(). weakValues().
@ -159,7 +163,7 @@ class RPCClientProxyHandler(
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build() ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build()
) )
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
this::reapObservables, this::reapObservablesAndNotify,
rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(),
rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(),
TimeUnit.MILLISECONDS 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() sessionAndConsumer?.sessionFactory?.close()
reaperScheduledFuture?.cancel(false) reaperScheduledFuture?.cancel(false)
observableContext.observableMap.invalidateAll() observableContext.observableMap.invalidateAll()
reapObservables() reapObservables(notify)
reaperExecutor?.shutdownNow() reaperExecutor?.shutdownNow()
sessionAndProducerPool.close().forEach { sessionAndProducerPool.close().forEach {
it.sessionFactory.close() it.sessionFactory.close()
@ -315,8 +342,11 @@ class RPCClientProxyHandler(
lifeCycle.transition(State.SERVER_VERSION_NOT_SET, State.STARTED) 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() observableContext.observableMap.cleanUp()
if (!notify) return
val observableIds = observablesToReap.locked { val observableIds = observablesToReap.locked {
if (observables.isNotEmpty()) { if (observables.isNotEmpty()) {
val temporary = observables val temporary = observables

View File

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

View File

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

View File

@ -1,5 +1,5 @@
gradlePluginsVersion=0.13.6 gradlePluginsVersion=0.15.1
kotlinVersion=1.1.1 kotlinVersion=1.1.4
guavaVersion=21.0 guavaVersion=21.0
bouncycastleVersion=1.57 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}" testCompile "org.assertj:assertj-core:${assertj_version}"
// Guava: Google utilities library. // Guava: Google utilities library.
compile "com.google.guava:guava:$guava_version" testCompile "com.google.guava:guava:$guava_version"
// RxJava: observable streams of events. // RxJava: observable streams of events.
compile "io.reactivex:rxjava:$rxjava_version" compile "io.reactivex:rxjava:$rxjava_version"
@ -63,12 +63,6 @@ dependencies {
// JPA 2.1 annotations. // JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" 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 { configurations {

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.utilities.exactAdd
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.exactAdd
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.util.* import java.util.*
@ -13,6 +13,7 @@ import java.util.*
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects. * indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
*/ */
interface TokenizableAssetInfo { 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 val displayTokenSize: BigDecimal
} }
@ -28,16 +29,14 @@ interface TokenizableAssetInfo {
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow. * overflow.
* *
* @param quantity the number of tokens as a Long value. * @property quantity the number of tokens as a Long value.
* @param displayTokenSize the nominal display unit size of a single token, * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero.
* 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]. * @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
* 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.
*/ */
@CordaSerializable @CordaSerializable
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> { 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 { companion object {
/** /**
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP", * 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 * For other possible token types the asset token should implement TokenizableAssetInfo to
* correctly report the designed nominal amount. * correctly report the designed nominal amount.
*/ */
@JvmStatic
fun getDisplayTokenSize(token: Any): BigDecimal { fun getDisplayTokenSize(token: Any): BigDecimal {
if (token is TokenizableAssetInfo) { if (token is TokenizableAssetInfo) {
return token.displayTokenSize return token.displayTokenSize
@ -86,14 +86,39 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
return BigDecimal.ONE 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( private val currencySymbols: Map<String, Currency> = mapOf(
"$" to USD, "$" to Currency.getInstance("USD"),
"£" to GBP, "£" to Currency.getInstance("GBP"),
"" to EUR, "" to Currency.getInstance("EUR"),
"¥" to JPY, "¥" to Currency.getInstance("JPY"),
"" to RUB "" 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: * 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. * @throws IllegalArgumentException if the input string was not understood.
*/ */
@JvmStatic
fun parseCurrency(input: String): Amount<Currency> { fun parseCurrency(input: String): Amount<Currency> {
val i = input.filter { it != ',' } val i = input.filter { it != ',' }
try { try {
@ -127,7 +153,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
for ((symbol, currency) in currencySymbols) { for ((symbol, currency) in currencySymbols) {
if (i.startsWith(symbol)) { if (i.startsWith(symbol)) {
val rest = i.substring(symbol.length) val rest = i.substring(symbol.length)
return fromDecimal(BigDecimal(rest), currency) return Amount.fromDecimal(BigDecimal(rest), currency)
} }
} }
// Now check the codes at the end. // 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 val (rest, code) = split
for ((cc, currency) in currencyCodes) { for ((cc, currency) in currencyCodes) {
if (cc == code) { 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. * 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 * @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> { operator fun plus(other: Amount<T>): Amount<T> {
checkToken(other) 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. * A checked addition operator is supported to simplify netting of Amounts.
* If this leads to the Amount going negative this will throw [IllegalArgumentException]. * 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 * @throws ArithmeticException if there is Numeric underflow
* Mixing non-identical token types will throw [IllegalArgumentException]
*/ */
operator fun minus(other: Amount<T>): Amount<T> { operator fun minus(other: Amount<T>): Amount<T> {
checkToken(other) 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) 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) 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, * 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. * 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 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 result of fromDecimal is used to control the numerical formatting and
* the token specifier appended is taken from token.toString. * the token specifier appended is taken from token.toString.
* *
* @see Amount.Companion.fromDecimal * @see Amount.fromDecimal
*/ */
override fun toString(): String { override fun toString(): String {
return toDecimal().toPlainString() + " " + token return toDecimal().toPlainString() + " " + token
} }
/** @suppress */
override fun compareTo(other: Amount<T>): Int { override fun compareTo(other: Amount<T>): Int {
checkToken(other) checkToken(other)
return quantity.compareTo(other.quantity) 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. * 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 P Any class type that can disambiguate where the amount came from.
* @param ref is an optional field used for housekeeping in the caller. * @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. * e.g. to point back at the original Vault state objects.
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects * @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
* and calculates the resulting Amount distribution as a new 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. * 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) * 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 * 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. * and the destination will lose Math.abs(quantityDelta) tokens.
* Where possible the source and destination should be coded to ensure a positive quantityDelta, * 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. * 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. * 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. * @property 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 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. * 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. * or the token source if quantityDelta is negative. The type P should support value equality.
*/ */
@CordaSerializable @CordaSerializable
@ -305,9 +336,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
return AmountTransfer(deltaTokenCount, token, source, destination) return AmountTransfer(deltaTokenCount, token, source, destination)
} }
/** /** Helper to make a zero size AmountTransfer. */
* Helper to make a zero size AmountTransfer
*/
@JvmStatic @JvmStatic
fun <T : Any, P : Any> zero(token: T, fun <T : Any, P : Any> zero(token: T,
source: P, 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) fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
/** @suppress */
fun copy(quantityDelta: Long = this.quantityDelta, fun copy(quantityDelta: Long = this.quantityDelta,
token: T = this.token, token: T = this.token,
source: P = this.source, 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 { override fun hashCode(): Int {
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values 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 return result
} }
/** @suppress */
override fun toString(): String { override fun toString(): String {
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token" 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 * Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. The net
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house. * 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. * @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)) 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 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. * @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. * 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. * 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. * @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.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import java.math.BigDecimal
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -12,41 +11,10 @@ import java.util.*
* Defines a simple domain specific language for the specification of financial contracts. Currently covers: * Defines a simple domain specific language for the specification of financial contracts. Currently covers:
* *
* - Some utilities for working with commands. * - Some utilities for working with commands.
* - Code for working with currencies. * - An Amount type that represents a positive quantity of a specific token.
* - An Amount type that represents a positive quantity of a specific currency.
* - A simple language extension for specifying requirements in English, along with logic to enforce them. * - 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 ///////////////////////////////////////////////////////////////////////////////////////////////////// //// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
object 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 * 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. * 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. * this interface.
* *
* @param T a type that represents the asset in question. This should describe the basic type of the asset * @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.). * (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
*/ */
interface FungibleAsset<T : Any> : OwnableState { 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>> val amount: Amount<Issued<T>>
/** /**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their * 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. * owner to sign, some (i.e. cash) also require the issuer.
*/ */
val exitKeys: Collection<PublicKey> 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> /**
* Copies the underlying data structure, replacing the amount and owner fields with the new values and leaving the
// Just for grouping * rest (exitKeys) alone.
interface Commands : CommandData { */
interface Move : MoveCommand, Commands fun withNewOwnerAndAmount(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
/**
* 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>>
}
}
} }
// 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) 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 * The [Issued] data class holds the details of an on ledger digital asset.
* quantifiable with integer quantities. * 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 @CordaSerializable
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) { 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. * A contract state that can have a single owner.
*/ */
interface OwnableState : ContractState { 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 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 fun withNewOwner(newOwner: AbstractParty): CommandAndState
} }
// DOCEND 3 // 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 { interface Scheduled {
val scheduledAt: Instant 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()}" 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. */ /** A common move command for contract states which can change owner. */
interface MoveCommand : CommandData { interface MoveCommand : CommandData {
/** /**
* Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in * 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). * order to settle an obligation contract's state object(s).
*/ */
// TODO: Replace SecureHash here with a general contract constraints object // TODO: Replace Class here with a general contract constraints object
val contractHash: SecureHash? val contract: Class<out Contract>?
} }
/** Indicates that this transaction replaces the inputs contract state to another contract state */ /** Indicates that this transaction replaces the inputs contract state to another contract state */
@ -333,15 +341,14 @@ interface Contract {
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun verify(tx: LedgerTransaction) 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 // 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. * 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 * 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. * 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 * 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. * 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), * 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. * 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. * This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm.
*/ */
object ContentSignerBuilder { 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 sigAlgId = signatureScheme.signatureOID
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply { val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
if (random != null) { 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. * RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
* Note: Recommended key size >= 3072 bits. * Note: Recommended key size >= 3072 bits.
*/ */
@JvmField
val RSA_SHA256 = SignatureScheme( val RSA_SHA256 = SignatureScheme(
1, 1,
"RSA_SHA256", "RSA_SHA256",
@ -84,6 +85,7 @@ object Crypto {
) )
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */ /** ECDSA signature scheme using the secp256k1 Koblitz curve. */
@JvmField
val ECDSA_SECP256K1_SHA256 = SignatureScheme( val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2, 2,
"ECDSA_SECP256K1_SHA256", "ECDSA_SECP256K1_SHA256",
@ -98,6 +100,7 @@ object Crypto {
) )
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */ /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
@JvmField
val ECDSA_SECP256R1_SHA256 = SignatureScheme( val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3, 3,
"ECDSA_SECP256R1_SHA256", "ECDSA_SECP256R1_SHA256",
@ -112,6 +115,7 @@ object Crypto {
) )
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */ /** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
@JvmField
val EDDSA_ED25519_SHA512 = SignatureScheme( val EDDSA_ED25519_SHA512 = SignatureScheme(
4, 4,
"EDDSA_ED25519_SHA512", "EDDSA_ED25519_SHA512",
@ -131,7 +135,10 @@ object Crypto {
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers * 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. * at the cost of larger key sizes and loss of compatibility.
*/ */
@JvmField
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256)) val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
@JvmField
val SPHINCS256_SHA256 = SignatureScheme( val SPHINCS256_SHA256 = SignatureScheme(
5, 5,
"SPHINCS-256_SHA512", "SPHINCS-256_SHA512",
@ -146,13 +153,12 @@ object Crypto {
"at the cost of larger key sizes and loss of compatibility." "at the cost of larger key sizes and loss of compatibility."
) )
/** /** Corda composite key type */
* Corda composite key type @JvmField
*/
val COMPOSITE_KEY = SignatureScheme( val COMPOSITE_KEY = SignatureScheme(
6, 6,
"COMPOSITE", "COMPOSITE",
AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY),
emptyList(), emptyList(),
CordaSecurityProvider.PROVIDER_NAME, CordaSecurityProvider.PROVIDER_NAME,
CompositeKey.KEY_ALGORITHM, CompositeKey.KEY_ALGORITHM,
@ -163,13 +169,14 @@ object Crypto {
) )
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
@JvmField
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
/** /**
* Supported digital signature schemes. * Supported digital signature schemes.
* Note: Only the schemes added in this map will be supported (see [Crypto]). * 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, RSA_SHA256,
ECDSA_SECP256K1_SHA256, ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256, ECDSA_SECP256R1_SHA256,
@ -183,15 +190,15 @@ object Crypto {
* algorithm identifiers. * algorithm identifiers.
*/ */
private val algorithmMap: Map<AlgorithmIdentifier, SignatureScheme> private val algorithmMap: Map<AlgorithmIdentifier, SignatureScheme>
= (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } } = (signatureSchemeMap.values.flatMap { scheme -> scheme.alternativeOIDs.map { Pair(it, scheme) } }
+ supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) }) + signatureSchemeMap.values.map { Pair(it.signatureOID, it) })
.toMap() .toMap()
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
// that could cause unexpected and suspicious behaviour. // 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. // 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. // 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(), BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(), CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(),
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. "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)) 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 { init {
// This registration is needed for reading back EdDSA key from java keystore. // 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. // 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 { 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. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws IllegalArgumentException if the requested signature scheme is not supported.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") 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]. * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
@ -242,7 +262,7 @@ object Crypto {
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported. * @throws IllegalArgumentException if the requested key type is not supported.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun findSignatureScheme(key: PublicKey): SignatureScheme { fun findSignatureScheme(key: PublicKey): SignatureScheme {
val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded) val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
return findSignatureScheme(keyInfo.algorithm) return findSignatureScheme(keyInfo.algorithm)
@ -256,7 +276,7 @@ object Crypto {
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported. * @throws IllegalArgumentException if the requested key type is not supported.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun findSignatureScheme(key: PrivateKey): SignatureScheme { fun findSignatureScheme(key: PrivateKey): SignatureScheme {
val keyInfo = PrivateKeyInfo.getInstance(key.encoded) val keyInfo = PrivateKeyInfo.getInstance(key.encoded)
return findSignatureScheme(keyInfo.privateKeyAlgorithm) return findSignatureScheme(keyInfo.privateKeyAlgorithm)
@ -269,11 +289,12 @@ object Crypto {
* @throws IllegalArgumentException on not supported scheme or if the given key specification * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * is inappropriate for this key factory to produce a private key.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
val keyInfo = PrivateKeyInfo.getInstance(encodedKey) val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm) 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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * is inappropriate for this key factory to produce a private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class) @JvmStatic
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey) @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. * 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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * 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 { 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 { 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) { } 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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * is inappropriate for this key factory to produce a private key.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun decodePublicKey(encodedKey: ByteArray): PublicKey { fun decodePublicKey(encodedKey: ByteArray): PublicKey {
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey) val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm) 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 * @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key. * is inappropriate for this key factory to produce a public key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class) @JvmStatic
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey) @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. * 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 * @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key. * 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 { 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 { 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) { } 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 * 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 privateKey the signer's [PrivateKey].
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). * @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. * @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 InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key. * @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) 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 InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key. * @throws SignatureException if signing is not possible due to malformed data or private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @JvmStatic
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) @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]. * 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 InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key. * @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 { 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]) 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.initSign(privateKey)
signature.update(clearData) signature.update(clearData)
return signature.sign() return signature.sign()
@ -398,20 +444,24 @@ object Crypto {
/** /**
* Generic way to sign [SignableData] objects with a [PrivateKey]. * 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. * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as
* @param privateKey the signer's [PrivateKey]. * 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. * @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 IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid. * @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key. * @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 { fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature {
val sigKey: SignatureScheme = findSignatureScheme(keyPair.private) val sigKey: SignatureScheme = findSignatureScheme(keyPair.private)
val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public) val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public)
if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${sigMetaData.schemeCodeName}" + require(sigKey == sigMetaData) {
" is not aligned with the key type: ${sigKey.schemeCodeName}.") "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}."
}
val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes) val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes)
return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata) 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. * 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 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) @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. * 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. * 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 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
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData) @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. * 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. * 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 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 { 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 (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData) 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. * 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 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 { fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
val signableData = SignableData(txId, transactionSignature.signatureMetadata) val signableData = SignableData(txId, transactionSignature.signatureMetadata)
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes) 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 * 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, * 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. * 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, * 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. * if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
*/ */
@JvmStatic
@Throws(SignatureException::class) @Throws(SignatureException::class)
fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
val signableData = SignableData(txId, transactionSignature.signatureMetadata) 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 * 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, * 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. * 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, * 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. * if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
*/ */
@JvmStatic
@Throws(SignatureException::class) @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 * 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. * 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 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 { 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]) val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
signature.initVerify(publicKey) signature.initVerify(publicKey)
signature.update(clearData) signature.update(clearData)
@ -560,7 +633,7 @@ object Crypto {
* @return a KeyPair for the requested signature scheme code name. * @return a KeyPair for the requested signature scheme code name.
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws IllegalArgumentException if the requested signature scheme is not supported.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName)) fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName))
/** /**
@ -570,10 +643,12 @@ object Crypto {
* @return a new [KeyPair] for the requested [SignatureScheme]. * @return a new [KeyPair] for the requested [SignatureScheme].
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws IllegalArgumentException if the requested signature scheme is not supported.
*/ */
@Throws(IllegalArgumentException::class)
@JvmOverloads @JvmOverloads
@JvmStatic
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { 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]) val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
if (signatureScheme.algSpec != null) if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
@ -638,13 +713,17 @@ object Crypto {
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws IllegalArgumentException if the requested signature scheme is not supported.
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
*/ */
@JvmStatic
fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } require(isSupportedSignatureScheme(signatureScheme)) {
when (signatureScheme) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) }
EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed) 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 IllegalArgumentException if the requested signature scheme is not supported.
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
*/ */
@JvmStatic
fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair {
return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed) return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed)
} }
@ -728,11 +808,13 @@ object Crypto {
* @return a new [KeyPair] from an entropy input. * @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. * @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 { fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
when (signatureScheme) { return when (signatureScheme) {
EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy) 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. * @param entropy a [BigInteger] value.
* @return a new [KeyPair] from an entropy input. * @return a new [KeyPair] from an entropy input.
*/ */
@JvmStatic
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
// custom key pair generator from entropy. // custom key pair generator from entropy.
@ -766,8 +849,12 @@ object Crypto {
} }
private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter { private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter {
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? {
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } 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. * @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 if the requested signature scheme or the key type is not supported.
*/ */
@Throws(IllegalArgumentException::class) @JvmStatic
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean { fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } require(isSupportedSignatureScheme(signatureScheme)) {
when (publicKey) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
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) 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}") else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}")
} }
} }
// return true if EdDSA publicKey is point at infinity. // return true if EdDSA publicKey is point at infinity.
// For EdDSA a custom function is required as it is not supported by the I2P implementation. // 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. */ /** 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. // validate a key, by checking its algorithmic params.
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { 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). // 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 { private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
when (key) { return when (key) {
is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key) is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key)
is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). 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}") else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
} }
} }
// check if a private key satisfies algorithm specs. // check if a private key satisfies algorithm specs.
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
when (key) { return when (key) {
is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec is EdDSAPrivateKey -> 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). 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}") 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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key. * is inappropriate for a supported key factory to produce a private key.
*/ */
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey { @JvmStatic
return Crypto.decodePublicKey(key.encoded) 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. * 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. * @param key a public key.
* @return a supported implementation of the input public key. * @return a supported implementation of the input public key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key. * is inappropriate for a supported key factory to produce a private key.
*/ */
fun toSupportedPublicKey(key: PublicKey): PublicKey { @JvmStatic
return Crypto.decodePublicKey(key.encoded) 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. * 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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key. * is inappropriate for a supported key factory to produce a private key.
*/ */
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { @JvmStatic
return Crypto.decodePrivateKey(key.encoded) fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded)
}
} }

View File

@ -11,9 +11,9 @@ import java.security.SignatureException
// should be renamed to match. // should be renamed to match.
/** A wrapper around a digital signature. */ /** A wrapper around a digital signature. */
@CordaSerializable @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. */ /** 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. * Utility to simplify the act of verifying a signature.
* *

View File

@ -1,6 +1,7 @@
@file:JvmName("X500NameUtils") @file:JvmName("X500NameUtils")
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.internal.toX509CertHolder
import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder import org.bouncycastle.asn1.x500.X500NameBuilder
@ -57,7 +58,7 @@ val X500Name.locationOrNull: String? get() = try {
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject val X509Certificate.subject: X500Name get() = toX509CertHolder().subject
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) 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 { fun getInstance(asn1: ASN1Primitive): PublicKey {
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1) 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 sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt() val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1)) 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(ASN1Integer(threshold.toLong()))
keyVector.add(DERSequence(childrenVector)) 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 override fun getFormat() = ASN1Encoding.DER
@ -262,7 +262,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
else if (n == 1) { else if (n == 1) {
require(threshold == null || threshold == children.first().weight) require(threshold == null || threshold == children.first().weight)
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." } { "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.") } 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) { class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
companion object { 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()) 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.CompositeKey
import net.corda.core.crypto.composite.CompositeSignature import net.corda.core.crypto.composite.CompositeSignature
import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.security.AccessController import java.security.AccessController
import java.security.PrivilegedAction import java.security.PrivilegedAction
import java.security.Provider import java.security.Provider
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") { class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
companion object { companion object {
val PROVIDER_NAME = "Corda" const val PROVIDER_NAME = "Corda"
} }
init { 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("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory")
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature") 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.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM) put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_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 { object CordaObjectIdentifier {
// UUID-based OID // UUID-based OID
// TODO: Register for an OID space and issue our own shorter OID // TODO: Register for an OID space and issue our own shorter OID
val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") @JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") @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.TransactionSignature
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -56,12 +57,14 @@ import java.security.PublicKey
* val stx = subFlow(CollectSignaturesFlow(ptx)) * val stx = subFlow(CollectSignaturesFlow(ptx))
* *
* @param partiallySignedTx Transaction to collect the remaining signatures for * @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: AbstractStateReplacementFlow needs updating to use this flow.
// TODO: Update this flow to handle randomly generated keys when that work is complete. class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, val myOptionalKeys: Iterable<PublicKey>?,
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() { override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
companion object { companion object {
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
} }
@Suspendable override fun call(): 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. // 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. // 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 signed = partiallySignedTx.sigs.map { it.by }
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
// One of the signatures collected so far MUST be from the initiator of this flow. // 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." "The Initiator of CollectSignaturesFlow must have signed the transaction."
} }
@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
if (unsigned.isEmpty()) return partiallySignedTx if (unsigned.isEmpty()) return partiallySignedTx
// Collect signatures from all counter-parties and append them to the partially signed transaction. // 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 val stx = partiallySignedTx + counterpartySignatures
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures. // 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]. * 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 { @Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key. val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.") ?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
partyNode.legalIdentity Pair(party, it)
} }
// DOCSTART 1 // DOCSTART 1
/** /**
* Get and check the required signature. * 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 { @Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
// SendTransactionFlow allows otherParty to access our data to resolve the transaction. // SendTransactionFlow allows counterparty to access our data to resolve the transaction.
subFlow(SendTransactionFlow(counterparty, partiallySignedTx)) 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 { 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 it
} }
} }
@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party,
progressTracker.currentStep = RECEIVING progressTracker.currentStep = RECEIVING
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures. // Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false)) 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 progressTracker.currentStep = VERIFYING
// Check that the Responder actually needs to sign. // 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. // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(stx) checkSignatures(stx)
stx.tx.toLedgerTransaction(serviceHub).verify() stx.tx.toLedgerTransaction(serviceHub).verify()
@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
} }
// Sign and send back our signature to the Initiator. // Sign and send back our signature to the Initiator.
progressTracker.currentStep = SIGNING progressTracker.currentStep = SIGNING
val mySignature = serviceHub.createSignature(stx) val mySignature = serviceHub.createSignature(stx, signingKey)
send(otherParty, mySignature) send(otherParty, mySignature)
// Return the fully signed transaction once it has been committed. // 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) { @Suspendable private fun checkSignatures(stx: SignedTransaction) {
require(stx.sigs.any { it.by == otherParty.owningKey }) { val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
"The Initiator of CollectSignaturesFlow must have signed the transaction." 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 signed = stx.sigs.map { it.by }
val allSigners = stx.tx.requiredSigningKeys val allSigners = stx.tx.requiredSigningKeys
@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party,
*/ */
@Suspendable abstract protected fun checkTransaction(stx: SignedTransaction) @Suspendable abstract protected fun checkTransaction(stx: SignedTransaction)
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) { @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
// TODO: Revisit when key management is properly fleshed out. require(signingKey in stx.tx.requiredSigningKeys) {
val myKey = serviceHub.myInfo.legalIdentity.owningKey
require(myKey in stx.tx.requiredSigningKeys) {
"Party is not a participant for any of the input states of transaction ${stx.id}" "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. * network's event horizon time.
*/ */
@Suspendable @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 * 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 * 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). * 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. * 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 @Suspendable
fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java) fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java)
@ -256,7 +256,7 @@ abstract class FlowLogic<out T> {
* Therefore the default implementation does nothing. * Therefore the default implementation does nothing.
*/ */
@Suspendable @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 package net.corda.core.flows
import net.corda.core.utilities.loggerFor import java.time.Instant
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")
}
}
/** /**
* Main data object representing snapshot of the flow stack, extracted from the Quasar stack. * Main data object representing snapshot of the flow stack, extracted from the Quasar stack.
*/ */
data class FlowStackSnapshot constructor( data class FlowStackSnapshot(
val timestamp: Long = System.currentTimeMillis(), val time: Instant,
val flowClass: String? = null, val flowClass: String,
val stackFrames: List<Frame> = listOf() val stackFrames: List<Frame>
) { ) {
data class Frame( data class Frame(
val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects] val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects]
val stackObjects: List<Any?> = listOf() 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 @CordaSerializable
abstract class AbstractParty(val owningKey: PublicKey) { 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 */ /** 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() override fun hashCode(): Int = owningKey.hashCode()
abstract fun nameOrNull(): X500Name? abstract fun nameOrNull(): X500Name?

View File

@ -1,7 +1,6 @@
package net.corda.core.identity package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.toBase58String
import net.corda.core.crypto.toStringShort import net.corda.core.crypto.toStringShort
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name 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. * information such as name. It is intended to represent a party on the distributed ledger.
*/ */
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) { 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 nameOrNull(): X500Name? = null
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) 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 package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference 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 net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -26,10 +28,9 @@ import java.security.PublicKey
* @see CompositeKey * @see CompositeKey
*/ */
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public) constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo))
override fun toString() = name.toString() override fun nameOrNull(): X500Name = name
override fun nameOrNull(): X500Name? = name
fun anonymise(): AnonymousParty = AnonymousParty(owningKey) fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) 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 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.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.* 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 * 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, * [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, * not part of the identifier themselves.
* this class exists in order to ensure the implementation classes of certificates and party public keys are kept stable.
*/ */
@CordaSerializable //TODO Is VerifiableIdentity a better name?
data class PartyAndCertificate(val party: Party, class PartyAndCertificate(val certPath: CertPath) {
val certificate: X509CertificateHolder, @Transient val certificate: X509CertificateHolder
val certPath: CertPath) { init {
constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath) require(certPath.type == "X.509") { "Only X.509 certificates supported" }
val name: X500Name val certs = certPath.certificates
get() = party.name require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
val owningKey: PublicKey certificate = certs[0].toX509CertHolder()
get() = party.owningKey
override fun equals(other: Any?): Boolean {
return if (other is PartyAndCertificate)
party == other.party
else
false
} }
@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 hashCode(): Int = party.hashCode()
override fun toString(): String = party.toString() override fun toString(): String = party.toString()
/** /** Verify the certificate path is valid. */
* Verify that the given certificate path is valid and leads to the owning key of the party.
*/
fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult { fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult {
require(certPath.certificates.first() is X509Certificate) { "Subject certificate must be an X.509 certificate" } val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false }
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 validator = CertPathValidator.getInstance("PKIX") val validator = CertPathValidator.getInstance("PKIX")
validatorParameters.isRevocationEnabled = false return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult
return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult
} }
} }

View File

@ -3,11 +3,7 @@ package net.corda.core.internal
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowContext import net.corda.core.flows.*
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.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -39,24 +35,11 @@ interface FlowStateMachine<R> {
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit 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 @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 @Suspendable
fun persistFlowStackSnapshot(flowClass: Class<*>): Unit fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
val serviceHub: ServiceHub val serviceHub: ServiceHub
val logger: Logger 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.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import org.bouncycastle.cert.X509CertificateHolder
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable import rx.Observable
import rx.Observer 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]. */ /** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash { fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
val bytes = toByteArray() 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 // 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. // redundantly next time we attempt verification.
it.verify(serviceHub) it.verify(serviceHub)
serviceHub.recordTransactions(it) serviceHub.recordTransactions(false, it)
} }
return signedTransaction?.let { return signedTransaction?.let {

View File

@ -1,6 +1,6 @@
package net.corda.core.internal.concurrent 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.CordaFuture
import net.corda.core.concurrent.match import net.corda.core.concurrent.match
import net.corda.core.utilities.getOrThrow 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 arg3: D
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) ): 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. * 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 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) */ /** The interface for a group of message recipients (which may contain only one recipient) */
@CordaSerializable
interface MessageRecipients interface MessageRecipients
/** A base class for the case of point-to-point messages */ /** 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 legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services.
val legalIdentitiesAndCerts: NonEmptySet<PartyAndCertificate>, val legalIdentitiesAndCerts: NonEmptySet<PartyAndCertificate>,
val platformVersion: Int, val platformVersion: Int,
var advertisedServices: List<ServiceEntry> = emptyList(), val advertisedServices: List<ServiceEntry> = emptyList(),
val worldMapLocation: WorldMapLocation? = null) { val worldMapLocation: WorldMapLocation? = null) {
init { 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 legalIdentity: Party get() = legalIdentityAndCert.party
val notaryIdentity: Party val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
fun serviceIdentities(type: ServiceType): List<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 * 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 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 * 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. This is expected to be run within a database transaction.
*/ */
fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) { 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) val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return if (stx.isNotaryChangeTransaction()) { return if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] 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) val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return if (stx.isNotaryChangeTransaction()) { return if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index) stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
} } else {
else {
stx.tx.outRef<T>(stateRef.index) stx.tx.outRef<T>(stateRef.index)
} }
} }

View File

@ -1,7 +1,10 @@
package net.corda.core.node.services package net.corda.core.node.services
import net.corda.core.contracts.PartyAndReference 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.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import java.security.InvalidAlgorithmParameterException 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. * @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> 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.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture import net.corda.core.toFuture
@ -135,8 +136,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val recordedTime: Instant, val recordedTime: Instant,
val consumedTime: Instant?, val consumedTime: Instant?,
val status: Vault.StateStatus, val status: Vault.StateStatus,
val notaryName: String, val notary: AbstractParty?,
val notaryKey: String,
val lockId: String?, val lockId: String?,
val lockUpdateTime: Instant?) 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.ContractState
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import javax.persistence.criteria.Predicate import javax.persistence.criteria.Predicate
@ -40,6 +40,7 @@ sealed class QueryCriteria {
abstract class CommonQueryCriteria : QueryCriteria() { abstract class CommonQueryCriteria : QueryCriteria() {
abstract val status: Vault.StateStatus abstract val status: Vault.StateStatus
abstract val contractStateTypes: Set<Class<out ContractState>>?
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this) return parser.parseCriteria(this)
} }
@ -49,13 +50,14 @@ sealed class QueryCriteria {
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
*/ */
data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, 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 stateRefs: List<StateRef>? = null,
val notaryName: List<X500Name>? = null, val notary: List<AbstractParty>? = null,
val softLockingCondition: SoftLockingCondition? = null, val softLockingCondition: SoftLockingCondition? = null,
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { 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, data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val uuid: List<UUID>? = null, val uuid: List<UUID>? = null,
val externalId: List<String>? = 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> { 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, data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null, val owner: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null, val quantity: ColumnPredicate<Long>? = null,
val issuerPartyName: List<AbstractParty>? = null, val issuer: List<AbstractParty>? = null,
val issuerRef: List<OpaqueBytes>? = 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> { 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 data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
(val expression: CriteriaExpression<L, Boolean>, (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> { 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 import kotlin.reflect.jvm.javaGetter
@CordaSerializable @CordaSerializable
enum class BinaryLogicalOperator { interface Operator
enum class BinaryLogicalOperator : Operator {
AND, AND,
OR OR
} }
@CordaSerializable enum class EqualityComparisonOperator : Operator {
enum class EqualityComparisonOperator {
EQUAL, EQUAL,
NOT_EQUAL NOT_EQUAL
} }
@CordaSerializable enum class BinaryComparisonOperator : Operator {
enum class BinaryComparisonOperator {
LESS_THAN, LESS_THAN,
LESS_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL,
GREATER_THAN, GREATER_THAN,
GREATER_THAN_OR_EQUAL, GREATER_THAN_OR_EQUAL,
} }
@CordaSerializable enum class NullOperator : Operator {
enum class NullOperator {
IS_NULL, IS_NULL,
NOT_NULL NOT_NULL
} }
@CordaSerializable enum class LikenessOperator : Operator {
enum class LikenessOperator {
LIKE, LIKE,
NOT_LIKE NOT_LIKE
} }
@CordaSerializable enum class CollectionOperator : Operator {
enum class CollectionOperator {
IN, IN,
NOT_IN NOT_IN
} }
@ -151,7 +148,7 @@ data class Sort(val columns: Collection<SortColumn>) {
enum class VaultStateAttribute(val attributeName: String) : Attribute { enum class VaultStateAttribute(val attributeName: String) : Attribute {
/** Vault States */ /** Vault States */
NOTARY_NAME("notaryName"), NOTARY_NAME("notary"),
CONTRACT_TYPE("contractStateClassName"), CONTRACT_TYPE("contractStateClassName"),
STATE_STATUS("stateStatus"), STATE_STATUS("stateStatus"),
RECORDED_TIME("recordedTime"), RECORDED_TIME("recordedTime"),

View File

@ -17,10 +17,17 @@ object CommonSchema
/** /**
* First version of the Vault ORM schema * 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 @MappedSuperclass
open class LinearState( open class LinearState(
/** [ContractState] attributes */
/** X500Name of participant parties **/
@ElementCollection
@Column(name = "participants")
var participants: MutableSet<AbstractParty>? = null,
/** /**
* Represents a [LinearState] [UniqueIdentifier] * Represents a [LinearState] [UniqueIdentifier]
*/ */
@ -31,18 +38,26 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
var uuid: UUID var uuid: UUID
) : PersistentState() { ) : 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 @MappedSuperclass
open class FungibleState( open class FungibleState(
/** [ContractState] attributes */ /** [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 */ /** [OwnableState] attributes */
@OneToOne(cascade = arrayOf(CascadeType.ALL))
var ownerKey: CommonSchemaV1.Party, /** X500Name of owner party **/
@Column(name = "owner_name")
var owner: AbstractParty,
/** [FungibleAsset] attributes /** [FungibleAsset] attributes
* *
@ -55,42 +70,12 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
var quantity: Long, var quantity: Long,
/** Issuer attributes */ /** 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") @Column(name = "issuer_reference")
var issuerRef: ByteArray var issuerRef: ByteArray
) : PersistentState() { ) : 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())
}
} }

View File

@ -1,6 +1,5 @@
package net.corda.core.schemas package net.corda.core.schemas
import io.requery.Persistable
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.serialization.CordaSerializable 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 * 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.identity.AbstractParty
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.core.utilities.loggerFor
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import javax.persistence.AttributeConverter import javax.persistence.AttributeConverter
import javax.persistence.Converter import javax.persistence.Converter
@ -17,9 +18,15 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
identitySvc() identitySvc()
} }
companion object {
val log = loggerFor<AbstractPartyToX500NameAsStringConverter>()
}
override fun convertToDatabaseColumn(party: AbstractParty?): String? { override fun convertToDatabaseColumn(party: AbstractParty?): String? {
party?.let { 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 return null // non resolvable anonymous parties
} }
@ -27,7 +34,8 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
override fun convertToEntityAttribute(dbData: String?): AbstractParty? { override fun convertToEntityAttribute(dbData: String?): AbstractParty? {
dbData?.let { dbData?.let {
val party = identityService.partyFromX500Name(X500Name(dbData)) 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 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. * When serializing, use the format this header sequence represents.
*/ */
val preferedSerializationVersion: ByteSequence val preferredSerializationVersion: ByteSequence
/** /**
* The class loader to use for deserialization. * 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.identity.Party
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.function.Predicate import java.util.function.Predicate
@ -17,6 +18,7 @@ import java.util.function.Predicate
* by a [SignedTransaction] that carries the signatures over this payload. * 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]). * The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]).
*/ */
@CordaSerializable
data class WireTransaction( data class WireTransaction(
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef>, 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 ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this)
fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(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 { init {
require(offset >= 0 && offset < bytes.size) require(offset >= 0 && offset < bytes.size)
require(size >= 0 && size <= 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`. * Extension method for easier construction of [Duration]s in terms of integer days: `val twoDays = 2.days`.
* @see Duration.ofDays * @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`. * Extension method for easier construction of [Duration]s in terms of integer hours: `val twoHours = 2.hours`.
* @see Duration.ofHours * @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`. * Extension method for easier construction of [Duration]s in terms of integer minutes: `val twoMinutes = 2.minutes`.
* @see Duration.ofMinutes * @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`. * Extension method for easier construction of [Duration]s in terms of integer seconds: `val twoSeconds = 2.seconds`.
* @see Duration.ofSeconds * @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`. * Extension method for easier construction of [Duration]s in terms of integer milliseconds: `val twoMillis = 2.millis`.
* @see Duration.ofMillis * @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 * 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 package net.corda.core.utilities
import net.corda.core.serialization.CordaSerializable
import java.net.URI 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 host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets.
* @param port a valid port number. * @param port a valid port number.
*/ */
@CordaSerializable
data class NetworkHostAndPort(val host: String, val port: Int) { data class NetworkHostAndPort(val host: String, val port: Int) {
companion object { companion object {
internal val invalidPortFormat = "Invalid port: %s" internal val invalidPortFormat = "Invalid port: %s"

View File

@ -1,5 +1,7 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.finance.*
import net.corda.core.contracts.Amount.Companion.sumOrZero
import org.junit.Test import org.junit.Test
import java.math.BigDecimal import java.math.BigDecimal
import java.util.* import java.util.*
@ -13,13 +15,6 @@ import kotlin.test.assertTrue
* Tests of the [Amount] class. * Tests of the [Amount] class.
*/ */
class AmountTests { class AmountTests {
@Test
fun basicCurrency() {
val expected = 1000L
val amount = Amount(expected, GBP)
assertEquals(expected, amount.quantity)
}
@Test @Test
fun `make sure Amount has decimal places`() { fun `make sure Amount has decimal places`() {
val x = Amount(1, Currency.getInstance("USD")) val x = Amount(1, Currency.getInstance("USD"))
@ -27,7 +22,7 @@ class AmountTests {
} }
@Test @Test
fun decimalConversion() { fun `decimal conversion`() {
val quantity = 1234L val quantity = 1234L
val amountGBP = Amount(quantity, GBP) val amountGBP = Amount(quantity, GBP)
val expectedGBP = BigDecimal("12.34") val expectedGBP = BigDecimal("12.34")
@ -49,22 +44,6 @@ class AmountTests {
override fun toString(): String = name 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 @Test
fun split() { fun split() {
for (baseQuantity in 0..1000) { for (baseQuantity in 0..1000) {
@ -81,7 +60,7 @@ class AmountTests {
} }
@Test @Test
fun amountTransfersEquality() { fun `amount transfers equality`() {
val partyA = "A" val partyA = "A"
val partyB = "B" val partyB = "B"
val partyC = "C" val partyC = "C"
@ -106,7 +85,7 @@ class AmountTests {
} }
@Test @Test
fun amountTransferAggregation() { fun `amount transfer aggregation`() {
val partyA = "A" val partyA = "A"
val partyB = "B" val partyB = "B"
val partyC = "C" val partyC = "C"
@ -137,7 +116,7 @@ class AmountTests {
} }
@Test @Test
fun amountTransferApply() { fun `amount transfer apply`() {
val partyA = "A" val partyA = "A"
val partyB = "B" val partyB = "B"
val partyC = "C" val partyC = "C"
@ -182,6 +161,5 @@ class AmountTests {
assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)]) assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)])
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)]) assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)])
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, 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.DUMMY_NOTARY
import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.dummyCommand
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -27,6 +28,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
interface Commands { interface Commands {
data class Cmd1(val id: Int) : CommandData, Commands data class Cmd1(val id: Int) : CommandData, Commands
data class Cmd2(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<*> { private fun makeDummyStateAndRef(data: Any): StateAndRef<*> {
val dummyState = makeDummyState(data) 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) services.recordTransactions(fakeIssueTx)
val dummyStateRef = StateRef(fakeIssueTx.id, 0) val dummyStateRef = StateRef(fakeIssueTx.id, 0)
return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef) return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef)
@ -182,7 +188,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
val intCmd2 = ltx.commandsOfType<Commands.Cmd2>() val intCmd2 = ltx.commandsOfType<Commands.Cmd2>()
assertEquals(5, intCmd2.size) assertEquals(5, intCmd2.size)
assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { it.value.id }) 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) assertEquals(emptyList(), notPresentQuery)
} }

View File

@ -1,9 +1,10 @@
package net.corda.core.contracts 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.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction 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.MEGA_CORP
import net.corda.testing.MINI_CORP import net.corda.testing.MINI_CORP
import net.corda.testing.ledger import net.corda.testing.ledger
@ -27,7 +28,6 @@ class TransactionEncumbranceTests {
val timeLock = DummyTimeLock.State(FIVE_PM) val timeLock = DummyTimeLock.State(FIVE_PM)
class DummyTimeLock : Contract { class DummyTimeLock : Contract {
override val legalContractReference = SecureHash.sha256("DummyTimeLock")
override fun verify(tx: LedgerTransaction) { override fun verify(tx: LedgerTransaction) {
val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return
val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window") 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 notaryServices = MockServices(DUMMY_NOTARY_KEY)
val originBuilder = TransactionBuilder(DUMMY_NOTARY) val originBuilder = TransactionBuilder(DUMMY_NOTARY)
originBuilder.addOutputState(DummyState(random31BitValue())) .addOutputState(DummyState(random31BitValue()))
originBuilder.addCommand(command, MEGA_CORP_PUBKEY) .addCommand(command, MEGA_CORP_PUBKEY)
val originPtx = megaCorpServices.signInitialTransaction(originBuilder) val originPtx = megaCorpServices.signInitialTransaction(originBuilder)
val originTx = notaryServices.addSignature(originPtx) val originTx = notaryServices.addSignature(originPtx)
val inputBuilder = TransactionBuilder(DUMMY_NOTARY) 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 inputPtx = megaCorpServices.signInitialTransaction(inputBuilder)
val inputTx = megaCorpServices.addSignature(inputPtx) val inputTx = megaCorpServices.addSignature(inputPtx)

View File

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

View File

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

View File

@ -343,7 +343,7 @@ class CryptoUtilsTest {
// test list of supported algorithms // test list of supported algorithms
@Test @Test
fun `Check supported algorithms`() { 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") 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(); } assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); }
} }

View File

@ -1,13 +1,15 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.crypto.SecureHash.Companion.zeroHash
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.WireTransaction 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 net.corda.testing.*
import org.junit.Test import org.junit.Test
import java.security.PublicKey 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.contracts.Attachment
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 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.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow 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.config.NodeConfiguration
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.network.NetworkMapService 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.services.transactions.SimpleNotaryService
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.testing.node.MockNetwork 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.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -32,13 +30,10 @@ import kotlin.test.assertFailsWith
class AttachmentTests { class AttachmentTests {
lateinit var mockNet: MockNetwork lateinit var mockNet: MockNetwork
lateinit var configuration: RequeryConfiguration
@Before @Before
fun setUp() { fun setUp() {
mockNet = MockNetwork() mockNet = MockNetwork()
val dataSourceProperties = makeTestDataSourceProperties()
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
} }
@After @After
@ -137,11 +132,9 @@ class AttachmentTests {
val corruptBytes = "arggghhhh".toByteArray() val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size) System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
val corruptAttachment = AttachmentEntity() val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
corruptAttachment.attId = id
corruptAttachment.content = attachment
n0.database.transaction { n0.database.transaction {
n0.attachments.session.update(corruptAttachment) DatabaseTransactionManager.current().session.update(corruptAttachment)
} }
// Get n1 to fetch the attachment. Should receive corrupted bytes. // 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 co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.contracts.requireThat 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.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.testing.MINI_CORP_KEY import net.corda.testing.MINI_CORP_KEY
import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.junit.After import org.junit.After
@ -77,16 +78,18 @@ class CollectSignaturesFlowTests {
} }
@InitiatedBy(TestFlow.Initiator::class) @InitiatedBy(TestFlow.Initiator::class)
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() { class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it } val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity 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 builder = TransactionBuilder(notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder) val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx)) val stx = subFlow(CollectSignaturesFlow(ptx, myKeys))
val ftx = subFlow(FinalityFlow(stx)).single() val ftx = subFlow(FinalityFlow(stx)).single()
return ftx return ftx
@ -103,10 +106,11 @@ class CollectSignaturesFlowTests {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity 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 builder = TransactionBuilder(notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder) val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx)) val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys))
val ftx = subFlow(FinalityFlow(stx)).single() val ftx = subFlow(FinalityFlow(stx)).single()
return ftx return ftx
@ -136,9 +140,12 @@ class CollectSignaturesFlowTests {
@Test @Test
fun `successfully collects two signatures`() { 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) registerFlowOnAllNodes(TestFlowTwo.Responder::class)
val magicNumber = 1337 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 state = DummyContract.MultiOwnerState(magicNumber, parties)
val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
mockNet.runNetwork() mockNet.runNetwork()

View File

@ -1,22 +1,23 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction 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.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.internal.CordaRPCOpsImpl
import net.corda.node.services.startFlowPermission import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.RPCDriverExposedDSLInterface import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
@ -42,11 +43,14 @@ class ContractUpgradeFlowTest {
@Before @Before
fun setup() { fun setup() {
mockNet = MockNetwork() mockNet = MockNetwork()
val nodes = mockNet.createSomeNodes() val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override
a = nodes.partyNodes[0] a = nodes.partyNodes[0]
b = nodes.partyNodes[1] b = nodes.partyNodes[1]
notary = nodes.notaryNode.info.notaryIdentity 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 @After
@ -173,8 +177,7 @@ class ContractUpgradeFlowTest {
@Test @Test
fun `upgrade Cash to v2`() { fun `upgrade Cash to v2`() {
// Create some cash. // Create some cash.
val anonymous = false val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val stx = result.getOrThrow().stx val stx = result.getOrThrow().stx
val stateAndRef = stx.tx.outRef<Cash.State>(0) val stateAndRef = stx.tx.outRef<Cash.State>(0)
@ -200,7 +203,7 @@ class ContractUpgradeFlowTest {
override val contract = CashV2() override val contract = CashV2()
override val participants = owners 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 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))) 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 upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
override fun verify(tx: LedgerTransaction) {} override fun verify(tx: LedgerTransaction) {}
// Dummy Cash contract for testing.
override val legalContractReference = SecureHash.sha256("")
} }
@StartableByRPC @StartableByRPC

View File

@ -1,12 +1,12 @@
package net.corda.core.flows package net.corda.core.flows
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.Issued import net.corda.core.contracts.Issued
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow 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.MockNetwork
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.junit.After 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 package net.corda.core.flows
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.Issued import net.corda.core.contracts.Issued
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow 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.MockNetwork
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.junit.After 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 fun verify(tx: LedgerTransaction) {
} }
override val legalContractReference: SecureHash = SecureHash.sha256("")
} }
private class DummyState : ContractState { 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.config.NodeConfiguration
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.persistence.NodeAttachmentService 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 net.corda.testing.node.MockNetwork
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -53,13 +53,11 @@ private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, conten
* @see NodeAttachmentService.importAttachment * @see NodeAttachmentService.importAttachment
*/ */
private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) { private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) {
with(session) { val session = DatabaseTransactionManager.current().session
withTransaction { val attachment = session.get<NodeAttachmentService.DBAttachment>(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString())
update(AttachmentEntity().apply { attachment?.let {
attId = attachmentId attachment.content = data
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 package net.corda.core.serialization
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.finance.POUNDS
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.junit.Before import org.junit.Before
@ -19,8 +19,6 @@ val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash()
class TransactionSerializationTests : TestDependencyInjectionBase() { class TransactionSerializationTests : TestDependencyInjectionBase() {
class TestCash : Contract { class TestCash : Contract {
override val legalContractReference = SecureHash.sha256("TestCash")
override fun verify(tx: LedgerTransaction) { 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") outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc")
processConfigurations = ['compile'] 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') 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']) task buildDocs(dependsOn: ['apidocs', 'makeDocs'])

View File

@ -7,11 +7,15 @@ set -xeo pipefail
# Install the virtualenv # Install the virtualenv
if [ ! -d "virtualenv" ] if [ ! -d "virtualenv" ]
then 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 # Check if python2.7 is installed explicitly otherwise fall back to the default python
if type "python2.7" > /dev/null; then if type "python2.7" > /dev/null; then
virtualenv -p python2.7 virtualenv virtualenv -p python2.7 "$absolutevirtualenv"
else else
virtualenv virtualenv virtualenv "$absolutevirtualenv"
fi fi
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: Where:
* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this * ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this contract type are valid
contract type are valid
* ``legalContractReference`` is the hash of the legal prose contract that ``verify`` seeks to express in code
verify() verify()
-------- --------
@ -187,8 +185,6 @@ execution of ``verify()``:
} }
} }
} }
override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash")
} }
.. sourcecode:: java .. sourcecode:: java
@ -209,9 +205,6 @@ execution of ``verify()``:
// Transfer verification logic. // Transfer verification logic.
} }
} }
private final SecureHash legalContractReference = SecureHash.sha256("X contract hash");
@Override public final SecureHash getLegalContractReference() { return legalContractReference; }
} }
Grouping states Grouping states
@ -297,13 +290,5 @@ We can now verify these groups individually:
Legal prose Legal prose
----------- -----------
Current, ``legalContractReference`` is simply the SHA-256 hash of a contract: 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.
.. 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.

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 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. ``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 :language: kotlin
Identity mapping Identity mapping

View File

@ -129,7 +129,7 @@ For example, here is a relatively complex state definition, for a state represen
.. container:: codeset .. 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 :language: kotlin
:start-after: DOCSTART 1 :start-after: DOCSTART 1
:end-before: DOCEND 1 :end-before: DOCEND 1

View File

@ -11,12 +11,11 @@ API: Transactions
Transaction workflow 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 * ``TransactionBuilder``, a builder for an in-construction transaction
* ``WireTransaction``, an immutable transaction
* ``SignedTransaction``, an immutable transaction with 1+ associated signatures * ``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: Here are the possible transitions between transaction states:
@ -26,8 +25,8 @@ TransactionBuilder
------------------ ------------------
Creating a builder Creating a builder
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
The first step when creating a transaction is to instantiate a ``TransactionBuilder``. We can create a builder for each The first step when creating a new transaction is to instantiate a ``TransactionBuilder``. We create a builder for a
transaction type as follows: transaction as follows:
.. container:: codeset .. container:: codeset
@ -342,7 +341,7 @@ SignedTransaction
----------------- -----------------
A ``SignedTransaction`` is a combination of: A ``SignedTransaction`` is a combination of:
* An immutable ``WireTransaction`` * An immutable transaction
* A list of signatures over that transaction * A list of signatures over that transaction
.. container:: codeset .. container:: codeset
@ -357,45 +356,9 @@ transaction's signatures.
Verifying the transaction's contents Verifying the transaction's contents
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't currently have
currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow called
called ``ReceiveTransactionFlow``. See :doc:`api-flows` for more details. ``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
We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output
states: states:
@ -414,8 +377,27 @@ states:
:end-before: DOCEND 33 :end-before: DOCEND 33
:dedent: 12 :dedent: 12
We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in We can also conduct additional validation of the transaction, beyond what is performed by its contracts. However, the
the contract. Here's an example of how we might do this: ``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 .. container:: codeset
@ -558,8 +540,8 @@ Or using another one of our public keys, as follows:
Notarising and recording Notarising and recording
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See :doc:`api-flows` for
:doc:`api-flows` for more details. more details.
Notary-change transactions 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`) .. 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: An example of a custom query is illustrated here:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt .. 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 :start-after: DOCSTART VaultQueryExample20
:end-before: DOCEND 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. .. 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. 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: Aggregations on cash using various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
:language: kotlin :language: java
:start-after: DOCSTART VaultJavaQueryExample21 :start-after: DOCSTART VaultJavaQueryExample21
:end-before: DOCEND VaultJavaQueryExample21 :end-before: DOCEND VaultJavaQueryExample21
Aggregations on cash grouped by currency for various functions: Aggregations on cash grouped by currency for various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
:language: kotlin :language: java
:start-after: DOCSTART VaultJavaQueryExample22 :start-after: DOCSTART VaultJavaQueryExample22
:end-before: DOCEND VaultJavaQueryExample22 :end-before: DOCEND VaultJavaQueryExample22
Sum aggregation on cash grouped by issuer party and currency and sorted by sum: 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 .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
:language: kotlin :language: java
:start-after: DOCSTART VaultJavaQueryExample23 :start-after: DOCSTART VaultJavaQueryExample23
:end-before: DOCEND VaultJavaQueryExample23 :end-before: DOCEND VaultJavaQueryExample23

View File

@ -6,6 +6,15 @@ from the previous milestone release.
UNRELEASED 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`` * 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 If you specifically need well known identities, use the network map, which is the authoritative source of current well
known identities. 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 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 .. 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 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 assets which can be issued, moved and exited on chain, with the subclasses handling asset-specific data types and
behaviour. 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 Commodity
--------- ---------

View File

@ -20,7 +20,6 @@ The Corda repository comprises the following folders:
* **lib** contains some dependencies * **lib** contains some dependencies
* **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence) * **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-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 * **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 * **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the
mock network) implementation mock network) implementation

View File

@ -64,7 +64,7 @@ applicationDistribution.into("bin") {
} }
task integrationTest(type: Test) { task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath classpath = sourceSets.integrationTest.runtimeClasspath
} }
@ -89,7 +89,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
rpcUsers = [ rpcUsers = [
['username' : "user", ['username' : "user",
'password' : "password", '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 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.internal.concurrent.transpose
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultTrackBy import net.corda.core.messaging.vaultTrackBy
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault 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.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashIssueFlow import net.corda.finance.DOLLARS
import net.corda.flows.CashPaymentFlow import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.startFlowPermission 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.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.driver.driver 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 org.junit.Test
import java.util.*
import kotlin.concurrent.thread
import kotlin.test.assertEquals import kotlin.test.assertEquals
class IntegrationTestingTutorial { class IntegrationTestingTutorial {
@ -33,7 +25,8 @@ class IntegrationTestingTutorial {
// START 1 // START 1
driver { driver {
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
startFlowPermission<CashIssueFlow>() startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashPaymentFlow>()
)) ))
val bobUser = User("bobUser", "testPassword2", permissions = setOf( val bobUser = User("bobUser", "testPassword2", permissions = setOf(
startFlowPermission<CashPaymentFlow>() startFlowPermission<CashPaymentFlow>()
@ -63,18 +56,21 @@ class IntegrationTestingTutorial {
// START 4 // START 4
val issueRef = OpaqueBytes.of(0) val issueRef = OpaqueBytes.of(0)
val futures = Stack<CordaFuture<*>>()
(1..10).map { i -> (1..10).map { i ->
thread { aliceProxy.startFlow(::CashIssueFlow,
futures.push(aliceProxy.startFlow(::CashIssueFlow, i.DOLLARS,
i.DOLLARS, issueRef,
issueRef, notary.nodeInfo.notaryIdentity
bob.nodeInfo.legalIdentity, ).returnValue
notary.nodeInfo.notaryIdentity }.transpose().getOrThrow()
).returnValue) // We wait for all of the issuances to run before we start making payments
} (1..10).map { i ->
}.forEach(Thread::join) // Ensure the stack of futures is populated. aliceProxy.startFlow(::CashPaymentFlow,
futures.forEach { it.getOrThrow() } i.DOLLARS,
bob.nodeInfo.legalIdentity,
true
).returnValue
}.transpose().getOrThrow()
bobVaultUpdates.expectEvents { bobVaultUpdates.expectEvents {
parallel( parallel(

View File

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

View File

@ -1,9 +1,6 @@
package net.corda.docs 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.Amount
import net.corda.core.contracts.USD
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultQueryBy 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.serialization.SerializationCustomization
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.flows.CashExitFlow import net.corda.finance.USD
import net.corda.flows.CashIssueFlow import net.corda.finance.contracts.asset.Cash
import net.corda.flows.CashPaymentFlow import net.corda.finance.flows.CashExitFlow
import net.corda.node.services.startFlowPermission 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.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.ALICE import net.corda.testing.ALICE
@ -128,7 +127,7 @@ fun generateTransactions(proxy: CordaRPCOps) {
proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me) proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me)
} else { } else {
val quantity = Math.abs(random.nextLong() % 1000) 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 ownedQuantity += quantity
} }
} }

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