mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge open master
This commit is contained in:
commit
a4ba8e4f2f
14
build.gradle
14
build.gradle
@ -14,7 +14,7 @@ buildscript {
|
||||
//
|
||||
// TODO: Sort this alphabetically.
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
||||
ext.quasar_version = '0.7.9'
|
||||
|
||||
// gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
|
||||
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3
|
||||
@ -42,7 +42,6 @@ buildscript {
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.h2_version = '1.4.194'
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.3.1'
|
||||
ext.dokka_version = '0.9.14'
|
||||
ext.eddsa_version = '0.2.0'
|
||||
|
||||
@ -128,6 +127,13 @@ allprojects {
|
||||
tasks.withType(Test) {
|
||||
// Prevent the project from creating temporary files outside of the build directory.
|
||||
systemProperties['java.io.tmpdir'] = buildDir
|
||||
|
||||
// Ensures that "net.corda.testing.amqp.enable" is passed correctly from Gradle command line
|
||||
// down to JVM executing unit test. It looks like we are running unit tests in the forked mode
|
||||
// and all the "-D" parameters passed to Gradle not making it to unit test level
|
||||
// TODO: Remove once we fully switched to AMQP
|
||||
final AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable"
|
||||
systemProperty(AMQP_ENABLE_PROP_NAME, System.getProperty(AMQP_ENABLE_PROP_NAME))
|
||||
}
|
||||
|
||||
group 'com.r3.corda.enterprise'
|
||||
@ -250,7 +256,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
|
||||
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
@ -285,7 +291,7 @@ artifactory {
|
||||
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
||||
}
|
||||
defaults {
|
||||
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver')
|
||||
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
compile project(':finance')
|
||||
testCompile project(':test-utils')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
@ -17,6 +16,7 @@ dependencies {
|
||||
compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version"
|
||||
// This adds support for java.time types.
|
||||
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
testCompile project(path: ':core', configuration: 'testArtifacts')
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
@ -5,11 +5,9 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.core.*
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import net.corda.contracts.BusinessCalendar
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
@ -33,7 +31,6 @@ import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -83,8 +80,6 @@ object JacksonSupport {
|
||||
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
|
||||
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
|
||||
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
|
||||
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
// For ed25519 pubkeys
|
||||
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
|
||||
@ -276,36 +271,6 @@ object JacksonSupport {
|
||||
}
|
||||
}
|
||||
|
||||
data class BusinessCalendarWrapper(val holidayDates: List<LocalDate>) {
|
||||
fun toCalendar() = BusinessCalendar(holidayDates)
|
||||
}
|
||||
|
||||
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
|
||||
override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) {
|
||||
val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj }
|
||||
if (calendarName != null) {
|
||||
generator.writeString(calendarName)
|
||||
} else {
|
||||
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
|
||||
return try {
|
||||
try {
|
||||
val array = StringArrayDeserializer.instance.deserialize(parser, context)
|
||||
BusinessCalendar.getInstance(*array)
|
||||
} catch (e: Exception) {
|
||||
parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
|
||||
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
|
||||
|
@ -2,7 +2,7 @@ package net.corda.jackson
|
||||
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.finance.USD
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
|
@ -53,7 +53,7 @@ dependencies {
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,13 @@ package net.corda.client.jfx
|
||||
|
||||
import net.corda.client.jfx.model.NodeMonitorModel
|
||||
import net.corda.client.jfx.model.ProgressTrackingEvent
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
@ -22,11 +20,13 @@ import net.corda.core.node.services.Vault
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashExitFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.flows.CashExitFlow
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
@ -108,13 +108,10 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `cash issue works end to end`() {
|
||||
val anonymous = false
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
aliceNode.legalIdentity,
|
||||
notaryNode.notaryIdentity,
|
||||
anonymous
|
||||
notaryNode.notaryIdentity
|
||||
)
|
||||
|
||||
vaultUpdates.expectEvents(isStrict = false) {
|
||||
@ -136,7 +133,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
val anonymous = false
|
||||
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
|
@ -4,10 +4,10 @@ import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import net.corda.client.jfx.utils.fold
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import rx.Observable
|
||||
|
||||
data class Diff<out T : ContractState>(
|
||||
|
@ -1,12 +1,14 @@
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.flows.CashFlowCommand
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.USD
|
||||
import java.util.*
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest
|
||||
import net.corda.finance.flows.CashExitFlow.ExitRequest
|
||||
import net.corda.finance.flows.CashPaymentFlow.PaymentRequest
|
||||
|
||||
/**
|
||||
* [Generator]s for incoming/outgoing cash flow events between parties. It doesn't necessarily generate correct events!
|
||||
@ -26,16 +28,16 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
|
||||
|
||||
protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
|
||||
addToMap(ccy, amount)
|
||||
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary, anonymous = true)
|
||||
IssueAndPaymentRequest(Amount(amount, ccy), issueRef, to, notary, anonymous = true)
|
||||
}
|
||||
|
||||
protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
|
||||
addToMap(ccy, -amount)
|
||||
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
|
||||
ExitRequest(Amount(amount, ccy), issueRef)
|
||||
}
|
||||
|
||||
open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
|
||||
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
|
||||
PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true)
|
||||
}
|
||||
|
||||
open val issuerGenerator = Generator.frequency(listOf(
|
||||
@ -54,28 +56,28 @@ class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>,
|
||||
EXIT_ERROR
|
||||
}
|
||||
|
||||
val errorGenerator = Generator.pickOne(IssuerEvents.values().toList())
|
||||
private val errorGenerator = Generator.pickOne(IssuerEvents.values().toList())
|
||||
|
||||
val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType ->
|
||||
private val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType ->
|
||||
when (errorType) {
|
||||
IssuerEvents.NORMAL_EXIT -> {
|
||||
println("Normal exit")
|
||||
if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount)
|
||||
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care.
|
||||
ExitRequest(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care.
|
||||
}
|
||||
IssuerEvents.EXIT_ERROR -> {
|
||||
println("Exit error")
|
||||
CashFlowCommand.ExitCash(Amount(currencyMap[ccy]!! * 2, ccy), issueRef)
|
||||
ExitRequest(Amount(currencyMap[ccy]!! * 2, ccy), issueRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
|
||||
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
|
||||
private val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
|
||||
PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true)
|
||||
}
|
||||
|
||||
val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
|
||||
CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
|
||||
private val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
|
||||
PaymentRequest(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
|
||||
}
|
||||
|
||||
override val moveCashGenerator = Generator.frequency(listOf(
|
||||
|
@ -50,6 +50,9 @@ processSmokeTestResources {
|
||||
from(project(':node:capsule').tasks['buildCordaJAR']) {
|
||||
rename 'corda-(.*)', 'corda.jar'
|
||||
}
|
||||
from(project(':finance').tasks['jar']) {
|
||||
rename 'finance-(.*)', 'finance.jar'
|
||||
}
|
||||
}
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
@ -78,12 +81,12 @@ dependencies {
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
task smokeTest(type: Test) {
|
||||
testClassesDir = sourceSets.smokeTest.output.classesDir
|
||||
testClassesDirs = sourceSets.smokeTest.output.classesDirs
|
||||
classpath = sourceSets.smokeTest.runtimeClasspath
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
package net.corda.client.rpc;
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.client.rpc.internal.RPCClient;
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.core.contracts.Amount;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.messaging.FlowHandle;
|
||||
import net.corda.core.node.services.ServiceInfo;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.flows.AbstractCashFlow;
|
||||
import net.corda.flows.CashIssueFlow;
|
||||
import net.corda.flows.CashPaymentFlow;
|
||||
import net.corda.finance.flows.AbstractCashFlow;
|
||||
import net.corda.finance.flows.CashIssueFlow;
|
||||
import net.corda.finance.flows.CashPaymentFlow;
|
||||
import net.corda.node.internal.Node;
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService;
|
||||
import net.corda.nodeapi.User;
|
||||
@ -22,10 +22,14 @@ import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static kotlin.test.AssertionsKt.assertEquals;
|
||||
import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault;
|
||||
import static net.corda.contracts.GetBalances.getCashBalance;
|
||||
import static net.corda.node.services.RPCUserServiceKt.startFlowPermission;
|
||||
import static net.corda.finance.CurrencyUtils.DOLLARS;
|
||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||
import static net.corda.node.services.FlowPermissions.startFlowPermission;
|
||||
import static net.corda.testing.TestConstants.getALICE;
|
||||
|
||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
@ -45,10 +49,10 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws ExecutionException, InterruptedException {
|
||||
Set<ServiceInfo> services = new HashSet<>(Collections.singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
|
||||
CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, Arrays.asList(rpcUser), Collections.emptyMap());
|
||||
Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
|
||||
CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
|
||||
node = nodeFuture.get();
|
||||
client = new CordaRPCClient(node.getConfiguration().getRpcAddress(), null, getDefault(), false);
|
||||
client = new CordaRPCClient(requireNonNull(node.getConfiguration().getRpcAddress()), null, getDefault(), false);
|
||||
}
|
||||
|
||||
@After
|
||||
@ -65,17 +69,15 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
|
||||
login(rpcUser.getUsername(), rpcUser.getPassword());
|
||||
|
||||
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
|
||||
|
||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||
dollars123, OpaqueBytes.of("1".getBytes()),
|
||||
node.info.getLegalIdentity(), node.info.getLegalIdentity());
|
||||
DOLLARS(123), OpaqueBytes.of("1".getBytes()),
|
||||
node.info.getLegalIdentity());
|
||||
System.out.println("Started issuing cash, waiting on result");
|
||||
flowHandle.getReturnValue().get();
|
||||
|
||||
Amount<Currency> balance = getCashBalance(rpcProxy, Currency.getInstance("USD"));
|
||||
System.out.print("Balance: " + balance + "\n");
|
||||
|
||||
assertEquals(dollars123, balance, "matching");
|
||||
assertEquals(DOLLARS(123), balance, "matching");
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.contracts.getCashBalance
|
||||
import net.corda.contracts.getCashBalances
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashException
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.contracts.getCashBalance
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashException
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
@ -77,9 +77,9 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
login(rpcUser.username, rpcUser.password)
|
||||
println("Creating proxy")
|
||||
println("Starting flow")
|
||||
val flowHandle = connection!!.proxy.startTrackedFlow(
|
||||
::CashIssueFlow,
|
||||
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
|
||||
val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
|
||||
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
|
||||
)
|
||||
println("Started flow, waiting on result")
|
||||
flowHandle.progress.subscribe {
|
||||
println("PROGRESS $it")
|
||||
@ -113,8 +113,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||
|
||||
val flowHandle = proxy.startFlow(::CashIssueFlow,
|
||||
123.DOLLARS, OpaqueBytes.of(0),
|
||||
node.info.legalIdentity, node.info.legalIdentity
|
||||
123.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
|
||||
)
|
||||
println("Started issuing cash, waiting on result")
|
||||
flowHandle.returnValue.get()
|
||||
@ -140,14 +139,16 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
}
|
||||
}
|
||||
val nodeIdentity = node.info.legalIdentity
|
||||
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
|
||||
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
|
||||
proxy.startFlow(::CashIssueFlow,
|
||||
123.DOLLARS, OpaqueBytes.of(0),
|
||||
nodeIdentity, nodeIdentity
|
||||
123.DOLLARS,
|
||||
OpaqueBytes.of(0),
|
||||
nodeIdentity
|
||||
).returnValue.getOrThrow()
|
||||
proxy.startFlowDynamic(CashIssueFlow::class.java,
|
||||
1000.DOLLARS, OpaqueBytes.of(0),
|
||||
nodeIdentity, nodeIdentity).returnValue.getOrThrow()
|
||||
1000.DOLLARS,
|
||||
OpaqueBytes.of(0),
|
||||
nodeIdentity).returnValue.getOrThrow()
|
||||
assertEquals(2, countRpcFlows)
|
||||
assertEquals(1, countShellFlows)
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ class CordaRPCClient(
|
||||
fun initialiseSerialization() {
|
||||
try {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme(this))
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
|
@ -5,10 +5,10 @@ import net.corda.core.internal.logElapsedTime
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.RPCApi
|
||||
@ -110,6 +110,21 @@ class RPCClient<I : RPCOps>(
|
||||
val proxy: I
|
||||
/** The RPC protocol version reported by the server */
|
||||
val serverProtocolVersion: Int
|
||||
|
||||
/**
|
||||
* Closes this client without notifying the server.
|
||||
* The server will eventually clear out the RPC message queue and disconnect subscribed observers,
|
||||
* but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose].
|
||||
* This method is helpful when the node may be shutting down or
|
||||
* have already shut down and you don't want to block waiting for it to come back.
|
||||
*/
|
||||
fun forceClose()
|
||||
|
||||
/**
|
||||
* Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources.
|
||||
* If the server is not available this method may block for a short period until it's clear the server is not coming back.
|
||||
*/
|
||||
fun notifyServerAndClose()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,13 +183,30 @@ class RPCClient<I : RPCOps>(
|
||||
object : RPCConnection<I> {
|
||||
override val proxy = ops
|
||||
override val serverProtocolVersion = serverProtocolVersion
|
||||
override fun close() {
|
||||
proxyHandler.close()
|
||||
|
||||
private fun close(notify: Boolean) {
|
||||
if (notify) {
|
||||
proxyHandler.notifyServerAndClose()
|
||||
} else {
|
||||
proxyHandler.forceClose()
|
||||
}
|
||||
serverLocator.close()
|
||||
}
|
||||
|
||||
override fun notifyServerAndClose() {
|
||||
close(true)
|
||||
}
|
||||
|
||||
override fun forceClose() {
|
||||
close(false)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
close(true)
|
||||
}
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
proxyHandler.close()
|
||||
proxyHandler.notifyServerAndClose()
|
||||
serverLocator.close()
|
||||
throw exception
|
||||
}
|
||||
|
@ -10,14 +10,17 @@ import com.google.common.cache.RemovalCause
|
||||
import com.google.common.cache.RemovalListener
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.core.internal.LazyStickyPool
|
||||
import net.corda.core.internal.LifeCycle
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.*
|
||||
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
@ -113,7 +116,8 @@ class RPCClientProxyHandler(
|
||||
|
||||
private fun createRpcObservableMap(): RpcObservableMap {
|
||||
val onObservableRemove = RemovalListener<RPCApi.ObservableId, UnicastSubject<Notification<*>>> {
|
||||
val rpcCallSite = callSiteMap?.remove(it.key.toLong)
|
||||
val observableId = it.key!!
|
||||
val rpcCallSite = callSiteMap?.remove(observableId.toLong)
|
||||
if (it.cause == RemovalCause.COLLECTED) {
|
||||
log.warn(listOf(
|
||||
"A hot observable returned from an RPC was never subscribed to.",
|
||||
@ -123,7 +127,7 @@ class RPCClientProxyHandler(
|
||||
"will appear less frequently in future versions of the platform and you can ignore it",
|
||||
"if you want to.").joinToString(" "), rpcCallSite)
|
||||
}
|
||||
observablesToReap.locked { observables.add(it.key) }
|
||||
observablesToReap.locked { observables.add(observableId) }
|
||||
}
|
||||
return CacheBuilder.newBuilder().
|
||||
weakValues().
|
||||
@ -159,7 +163,7 @@ class RPCClientProxyHandler(
|
||||
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build()
|
||||
)
|
||||
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
|
||||
this::reapObservables,
|
||||
this::reapObservablesAndNotify,
|
||||
rpcConfiguration.reapInterval.toMillis(),
|
||||
rpcConfiguration.reapInterval.toMillis(),
|
||||
TimeUnit.MILLISECONDS
|
||||
@ -268,13 +272,36 @@ class RPCClientProxyHandler(
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
|
||||
* Closes this handler without notifying observables.
|
||||
* This method clears up only local resources and as such does not block on any network resources.
|
||||
*/
|
||||
fun close() {
|
||||
fun forceClose() {
|
||||
close(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this handler and sends notifications to all observables, so it can immediately clean up resources.
|
||||
* Notifications sent to observables are to be acknowledged, therefore this call blocks until all acknowledgements are received.
|
||||
* If this is not convenient see the [forceClose] method.
|
||||
* If an observable is not accessible this method may block for a duration of the message broker timeout.
|
||||
*/
|
||||
fun notifyServerAndClose() {
|
||||
close(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
|
||||
* When observables are to be notified (i.e. the [notify] parameter is true),
|
||||
* the method blocks until all the messages are acknowledged by the observables.
|
||||
* Note: If any of the observables is inaccessible, the method blocks for the duration of the timeout set on the message broker.
|
||||
*
|
||||
* @param notify whether to notify observables or not.
|
||||
*/
|
||||
private fun close(notify: Boolean = true) {
|
||||
sessionAndConsumer?.sessionFactory?.close()
|
||||
reaperScheduledFuture?.cancel(false)
|
||||
observableContext.observableMap.invalidateAll()
|
||||
reapObservables()
|
||||
reapObservables(notify)
|
||||
reaperExecutor?.shutdownNow()
|
||||
sessionAndProducerPool.close().forEach {
|
||||
it.sessionFactory.close()
|
||||
@ -315,8 +342,11 @@ class RPCClientProxyHandler(
|
||||
lifeCycle.transition(State.SERVER_VERSION_NOT_SET, State.STARTED)
|
||||
}
|
||||
|
||||
private fun reapObservables() {
|
||||
private fun reapObservablesAndNotify() = reapObservables()
|
||||
|
||||
private fun reapObservables(notify: Boolean = true) {
|
||||
observableContext.observableMap.cleanUp()
|
||||
if (!notify) return
|
||||
val observableIds = observablesToReap.locked {
|
||||
if (observables.isNotEmpty()) {
|
||||
val temporary = observables
|
||||
|
@ -3,21 +3,20 @@ package net.corda.client.rpc.serialization
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import net.corda.client.rpc.internal.RpcClientObservableSerializer
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.RPCKryo
|
||||
import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer
|
||||
import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1
|
||||
|
||||
class KryoClientSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) {
|
||||
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
|
||||
return KryoPool.Builder {
|
||||
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, serializationFactory, context)).apply { classLoader = context.deserializationClassLoader }
|
||||
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader }
|
||||
}.build()
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,11 @@ package net.corda.java.rpc;
|
||||
import net.corda.client.rpc.CordaRPCConnection;
|
||||
import net.corda.core.contracts.Amount;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.messaging.DataFeed;
|
||||
import net.corda.core.messaging.FlowHandle;
|
||||
import net.corda.core.node.NodeInfo;
|
||||
import net.corda.core.node.services.NetworkMapCache;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.flows.AbstractCashFlow;
|
||||
import net.corda.flows.CashIssueFlow;
|
||||
import net.corda.finance.flows.AbstractCashFlow;
|
||||
import net.corda.finance.flows.CashIssueFlow;
|
||||
import net.corda.nodeapi.User;
|
||||
import net.corda.smoketesting.NodeConfig;
|
||||
import net.corda.smoketesting.NodeProcess;
|
||||
@ -18,12 +16,18 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static kotlin.test.AssertionsKt.assertEquals;
|
||||
import static net.corda.contracts.GetBalances.getCashBalance;
|
||||
import static kotlin.test.AssertionsKt.fail;
|
||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||
|
||||
public class StandaloneCordaRPCJavaClientTest {
|
||||
private List<String> perms = Collections.singletonList("ALL");
|
||||
@ -32,6 +36,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
|
||||
private AtomicInteger port = new AtomicInteger(15000);
|
||||
|
||||
private NodeProcess.Factory factory;
|
||||
private NodeProcess notary;
|
||||
private CordaRPCOps rpcProxy;
|
||||
private CordaRPCConnection connection;
|
||||
@ -49,7 +54,9 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
notary = new NodeProcess.Factory().create(notaryConfig);
|
||||
factory = new NodeProcess.Factory();
|
||||
copyFinanceCordapp();
|
||||
notary = factory.create(notaryConfig);
|
||||
connection = notary.connect();
|
||||
rpcProxy = connection.getProxy();
|
||||
notaryNode = fetchNotaryIdentity();
|
||||
@ -60,7 +67,31 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
try {
|
||||
connection.close();
|
||||
} finally {
|
||||
notary.close();
|
||||
if(notary != null) {
|
||||
notary.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFinanceCordapp() {
|
||||
Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins"));
|
||||
try {
|
||||
Files.createDirectories(pluginsDir);
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to create directories");
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(Paths.get("build", "resources", "smokeTest"))) {
|
||||
paths.forEach(file -> {
|
||||
if (file.toString().contains("corda-finance")) {
|
||||
try {
|
||||
Files.copy(file, pluginsDir.resolve(file.getFileName()));
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to copy finance jar");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
fail("Failed to walk files");
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +106,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
|
||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||
dollars123, OpaqueBytes.of("1".getBytes()),
|
||||
notaryNode.getLegalIdentity(), notaryNode.getLegalIdentity());
|
||||
notaryNode.getLegalIdentity());
|
||||
System.out.println("Started issuing cash, waiting on result");
|
||||
flowHandle.getReturnValue().get();
|
||||
|
||||
|
@ -3,13 +3,8 @@ package net.corda.kotlin.rpc
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.client.rpc.notUsed
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.getCashBalance
|
||||
import net.corda.contracts.getCashBalances
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.InputStreamAndHash
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
@ -18,8 +13,15 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.SWISS_FRANCS
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.contracts.getCashBalance
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.smoketesting.NodeConfig
|
||||
import net.corda.smoketesting.NodeProcess
|
||||
@ -30,8 +32,11 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
@ -41,11 +46,12 @@ class StandaloneCordaRPClientTest {
|
||||
private companion object {
|
||||
val log = loggerFor<StandaloneCordaRPClientTest>()
|
||||
val user = User("user1", "test", permissions = setOf("ALL"))
|
||||
val port = AtomicInteger(15000)
|
||||
val port = AtomicInteger(15200)
|
||||
const val attachmentSize = 2116
|
||||
val timeout = 60.seconds
|
||||
}
|
||||
|
||||
private lateinit var factory: NodeProcess.Factory
|
||||
private lateinit var notary: NodeProcess
|
||||
private lateinit var rpcProxy: CordaRPCOps
|
||||
private lateinit var connection: CordaRPCConnection
|
||||
@ -62,7 +68,9 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
notary = NodeProcess.Factory().create(notaryConfig)
|
||||
factory = NodeProcess.Factory()
|
||||
copyFinanceCordapp()
|
||||
notary = factory.create(notaryConfig)
|
||||
connection = notary.connect()
|
||||
rpcProxy = connection.proxy
|
||||
notaryNode = fetchNotaryIdentity()
|
||||
@ -77,6 +85,15 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFinanceCordapp() {
|
||||
val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories()
|
||||
// Find the finance jar file for the smoke tests of this module
|
||||
val financeJar = Paths.get("build", "resources", "smokeTest").list {
|
||||
it.filter { "corda-finance" in it.toString() }.toList().single()
|
||||
}
|
||||
financeJar.copyToDirectory(pluginsDir)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test attachments`() {
|
||||
val attachment = InputStreamAndHash.createInMemoryTestZip(attachmentSize, 1)
|
||||
@ -93,7 +110,7 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
@Test
|
||||
fun `test starting flow`() {
|
||||
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
|
||||
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
}
|
||||
|
||||
@ -101,13 +118,16 @@ class StandaloneCordaRPClientTest {
|
||||
fun `test starting tracked flow`() {
|
||||
var trackCount = 0
|
||||
val handle = rpcProxy.startTrackedFlow(
|
||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity
|
||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.notaryIdentity
|
||||
)
|
||||
val updateLatch = CountDownLatch(1)
|
||||
handle.progress.subscribe { msg ->
|
||||
log.info("Flow>> $msg")
|
||||
++trackCount
|
||||
updateLatch.countDown()
|
||||
}
|
||||
handle.returnValue.getOrThrow(timeout)
|
||||
updateLatch.await()
|
||||
assertNotEquals(0, trackCount)
|
||||
}
|
||||
|
||||
@ -121,17 +141,20 @@ class StandaloneCordaRPClientTest {
|
||||
val (stateMachines, updates) = rpcProxy.stateMachinesFeed()
|
||||
assertEquals(0, stateMachines.size)
|
||||
|
||||
val updateLatch = CountDownLatch(1)
|
||||
val updateCount = AtomicInteger(0)
|
||||
updates.subscribe { update ->
|
||||
if (update is StateMachineUpdate.Added) {
|
||||
log.info("StateMachine>> Id=${update.id}")
|
||||
updateCount.incrementAndGet()
|
||||
updateLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
|
||||
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
updateLatch.await()
|
||||
assertEquals(1, updateCount.get())
|
||||
}
|
||||
|
||||
@ -140,16 +163,16 @@ class StandaloneCordaRPClientTest {
|
||||
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
|
||||
assertEquals(0, vault.totalStatesAvailable)
|
||||
|
||||
val updateCount = AtomicInteger(0)
|
||||
val updateLatch = CountDownLatch(1)
|
||||
vaultUpdates.subscribe { update ->
|
||||
log.info("Vault>> FlowId=${update.flowId}")
|
||||
updateCount.incrementAndGet()
|
||||
updateLatch.countDown()
|
||||
}
|
||||
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
assertNotEquals(0, updateCount.get())
|
||||
updateLatch.await()
|
||||
|
||||
// Check that this cash exists in the vault
|
||||
val cashBalance = rpcProxy.getCashBalances()
|
||||
@ -161,7 +184,7 @@ class StandaloneCordaRPClientTest {
|
||||
@Test
|
||||
fun `test vault query by`() {
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
|
||||
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
||||
@ -187,12 +210,10 @@ class StandaloneCordaRPClientTest {
|
||||
@Test
|
||||
fun `test cash balances`() {
|
||||
val startCash = rpcProxy.getCashBalances()
|
||||
println(startCash)
|
||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||
|
||||
val flowHandle = rpcProxy.startFlow(::CashIssueFlow,
|
||||
629.DOLLARS, OpaqueBytes.of(0),
|
||||
notaryNode.legalIdentity, notaryNode.legalIdentity
|
||||
)
|
||||
val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 629.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity)
|
||||
println("Started issuing cash, waiting on result")
|
||||
flowHandle.returnValue.get()
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
gradlePluginsVersion=0.13.6
|
||||
kotlinVersion=1.1.1
|
||||
gradlePluginsVersion=0.15.1
|
||||
kotlinVersion=1.1.4
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
typesafeConfigVersion=1.3.1
|
||||
typesafeConfigVersion=1.3.1
|
@ -42,7 +42,7 @@ dependencies {
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
// Guava: Google utilities library.
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
testCompile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// RxJava: observable streams of events.
|
||||
compile "io.reactivex:rxjava:$rxjava_version"
|
||||
@ -63,12 +63,6 @@ dependencies {
|
||||
|
||||
// JPA 2.1 annotations.
|
||||
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
|
||||
|
||||
// Requery: SQL based query & persistence for Kotlin
|
||||
compile "io.requery:requery-kotlin:$requery_version"
|
||||
|
||||
// For AMQP serialisation.
|
||||
compile "org.apache.qpid:proton-j:0.19.0"
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.utilities.exactAdd
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.exactAdd
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.util.*
|
||||
@ -13,6 +13,7 @@ import java.util.*
|
||||
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
|
||||
*/
|
||||
interface TokenizableAssetInfo {
|
||||
/** The nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. */
|
||||
val displayTokenSize: BigDecimal
|
||||
}
|
||||
|
||||
@ -28,16 +29,14 @@ interface TokenizableAssetInfo {
|
||||
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
|
||||
* overflow.
|
||||
*
|
||||
* @param quantity the number of tokens as a Long value.
|
||||
* @param displayTokenSize the nominal display unit size of a single token,
|
||||
* potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @param T the type of the token, for example [Currency].
|
||||
* T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
*
|
||||
* TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
|
||||
* @property quantity the number of tokens as a Long value.
|
||||
* @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @property token an instance of type T, usually a singleton.
|
||||
* @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
|
||||
// TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
|
||||
companion object {
|
||||
/**
|
||||
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
|
||||
@ -73,6 +72,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* For other possible token types the asset token should implement TokenizableAssetInfo to
|
||||
* correctly report the designed nominal amount.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDisplayTokenSize(token: Any): BigDecimal {
|
||||
if (token is TokenizableAssetInfo) {
|
||||
return token.displayTokenSize
|
||||
@ -86,14 +86,39 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
return BigDecimal.ONE
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
|
||||
* any of the token types are mismatched; if the iterator yields no elements, return null.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
|
||||
/**
|
||||
* Sums the amounts yielded by the given iterable, throwing an [IllegalArgumentException] if any of the token
|
||||
* types are mismatched.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
|
||||
/**
|
||||
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
|
||||
* any of the token types are mismatched; if the iterator yields no elements, return a zero amount of the given
|
||||
* token type.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
|
||||
|
||||
private val currencySymbols: Map<String, Currency> = mapOf(
|
||||
"$" to USD,
|
||||
"£" to GBP,
|
||||
"€" to EUR,
|
||||
"¥" to JPY,
|
||||
"₽" to RUB
|
||||
"$" to Currency.getInstance("USD"),
|
||||
"£" to Currency.getInstance("GBP"),
|
||||
"€" to Currency.getInstance("EUR"),
|
||||
"¥" to Currency.getInstance("JPY"),
|
||||
"₽" to Currency.getInstance("RUB")
|
||||
)
|
||||
private val currencyCodes: Map<String, Currency> by lazy { Currency.getAvailableCurrencies().map { it.currencyCode to it }.toMap() }
|
||||
|
||||
private val currencyCodes: Map<String, Currency> by lazy {
|
||||
Currency.getAvailableCurrencies().associateBy { it.currencyCode }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an amount that is equal to the given currency amount in text. Examples of what is supported:
|
||||
@ -120,6 +145,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
*
|
||||
* @throws IllegalArgumentException if the input string was not understood.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parseCurrency(input: String): Amount<Currency> {
|
||||
val i = input.filter { it != ',' }
|
||||
try {
|
||||
@ -127,7 +153,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
for ((symbol, currency) in currencySymbols) {
|
||||
if (i.startsWith(symbol)) {
|
||||
val rest = i.substring(symbol.length)
|
||||
return fromDecimal(BigDecimal(rest), currency)
|
||||
return Amount.fromDecimal(BigDecimal(rest), currency)
|
||||
}
|
||||
}
|
||||
// Now check the codes at the end.
|
||||
@ -136,7 +162,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
val (rest, code) = split
|
||||
for ((cc, currency) in currencyCodes) {
|
||||
if (cc == code) {
|
||||
return fromDecimal(BigDecimal(rest), currency)
|
||||
return Amount.fromDecimal(BigDecimal(rest), currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,8 +192,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify aggregation of Amounts.
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException].
|
||||
*
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
@ -177,8 +204,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
/**
|
||||
* A checked addition operator is supported to simplify netting of Amounts.
|
||||
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException].
|
||||
*
|
||||
* @throws ArithmeticException if there is Numeric underflow
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun minus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
@ -197,6 +225,11 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
*/
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
|
||||
|
||||
/**
|
||||
* The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
|
||||
* Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
|
||||
* N.B. Division is not supported as fractional tokens are not representable by an Amount.
|
||||
*/
|
||||
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
|
||||
|
||||
/**
|
||||
@ -219,7 +252,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize,
|
||||
* which determines the size of a single token and controls the trailing decimal places via it's scale property.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
* @see Amount.fromDecimal
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
|
||||
|
||||
@ -231,29 +264,27 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* The result of fromDecimal is used to control the numerical formatting and
|
||||
* the token specifier appended is taken from token.toString.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
* @see Amount.fromDecimal
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return toDecimal().toPlainString() + " " + token
|
||||
}
|
||||
|
||||
/** @suppress */
|
||||
override fun compareTo(other: Amount<T>): Int {
|
||||
checkToken(other)
|
||||
return quantity.compareTo(other.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
|
||||
|
||||
|
||||
/**
|
||||
* Simple data class to associate the origin, owner, or holder of a particular Amount object.
|
||||
* @param source the holder of the Amount.
|
||||
* @param amount the Amount of asset available.
|
||||
* @param ref is an optional field used for housekeeping in the caller.
|
||||
*
|
||||
* @param P Any class type that can disambiguate where the amount came from.
|
||||
* @param T The token type of the underlying [Amount].
|
||||
* @property source the holder of the Amount.
|
||||
* @property amount the Amount of asset available.
|
||||
* @property ref is an optional field used for housekeeping in the caller.
|
||||
* e.g. to point back at the original Vault state objects.
|
||||
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
|
||||
* and calculates the resulting Amount distribution as a new list of SourceAndAmount objects.
|
||||
@ -263,17 +294,17 @@ data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amou
|
||||
/**
|
||||
* This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date.
|
||||
*
|
||||
* @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
|
||||
* @property quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
|
||||
* it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta)
|
||||
* at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens
|
||||
* and the destination will lose Math.abs(quantityDelta) tokens.
|
||||
* Where possible the source and destination should be coded to ensure a positive quantityDelta,
|
||||
* but in various scenarios it may be more consistent to allow positive and negative values.
|
||||
* For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination.
|
||||
* @param token represents the type of asset token as would be used to construct Amount<T> objects.
|
||||
* @param source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* @property token represents the type of asset token as would be used to construct Amount<T> objects.
|
||||
* @property source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* or the token sink if quantityDelta is negative. The type P should support value equality.
|
||||
* @param destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* @property destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* or the token source if quantityDelta is negative. The type P should support value equality.
|
||||
*/
|
||||
@CordaSerializable
|
||||
@ -305,9 +336,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
return AmountTransfer(deltaTokenCount, token, source, destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to make a zero size AmountTransfer
|
||||
*/
|
||||
/** Helper to make a zero size AmountTransfer. */
|
||||
@JvmStatic
|
||||
fun <T : Any, P : Any> zero(token: T,
|
||||
source: P,
|
||||
@ -344,6 +373,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
|
||||
|
||||
/** @suppress */
|
||||
fun copy(quantityDelta: Long = this.quantityDelta,
|
||||
token: T = this.token,
|
||||
source: P = this.source,
|
||||
@ -373,7 +403,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
}
|
||||
|
||||
/**
|
||||
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
|
||||
* This hash code function ensures that reversed source and destination equivalents will hash to the same value.
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values
|
||||
@ -382,18 +412,20 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
return result
|
||||
}
|
||||
|
||||
/** @suppress */
|
||||
override fun toString(): String {
|
||||
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token"
|
||||
}
|
||||
|
||||
/**
|
||||
* Novation is a common financial operation in which a bilateral exchange is modified so that the same
|
||||
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house.
|
||||
* Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. The net
|
||||
* total exchange is the same as in the original input. Novation is a common financial operation in which a
|
||||
* bilateral exchange is modified so that the same relative asset exchange happens, but with each party exchanging
|
||||
* versus a central counterparty, or clearing house.
|
||||
*
|
||||
* @param centralParty The central party to face the exchange against.
|
||||
* @return Returns a list of two new AmountTransfers each between one of the original parties and the centralParty.
|
||||
* The net total exchange is the same as in the original input.
|
||||
*/
|
||||
@Suppress("UNUSED")
|
||||
fun novate(centralParty: P): List<AmountTransfer<T, P>> = listOf(copy(destination = centralParty), copy(source = centralParty))
|
||||
|
||||
/**
|
||||
@ -403,7 +435,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
* @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange.
|
||||
* @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output.
|
||||
* i.e. To the new payment destination entry and to any residual change output.
|
||||
* @return The returned list is a copy of the original list, except that funds needed to cover the exchange
|
||||
* @return A copy of the original list, except that funds needed to cover the exchange
|
||||
* will have been removed and a new output and possibly residual amount entry will be added at the end of the list.
|
||||
* @throws ArithmeticException if there is underflow in the summations.
|
||||
*/
|
||||
|
@ -4,7 +4,6 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
@ -12,41 +11,10 @@ import java.util.*
|
||||
* Defines a simple domain specific language for the specification of financial contracts. Currently covers:
|
||||
*
|
||||
* - Some utilities for working with commands.
|
||||
* - Code for working with currencies.
|
||||
* - An Amount type that represents a positive quantity of a specific currency.
|
||||
* - An Amount type that represents a positive quantity of a specific token.
|
||||
* - A simple language extension for specifying requirements in English, along with logic to enforce them.
|
||||
*
|
||||
* TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
|
||||
*/
|
||||
|
||||
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
fun currency(code: String) = Currency.getInstance(code)!!
|
||||
|
||||
@JvmField val USD = currency("USD")
|
||||
@JvmField val GBP = currency("GBP")
|
||||
@JvmField val EUR = currency("EUR")
|
||||
@JvmField val CHF = currency("CHF")
|
||||
@JvmField val JPY = currency("JPY")
|
||||
@JvmField val RUB = currency("RUB")
|
||||
|
||||
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
|
||||
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
|
||||
|
||||
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
|
||||
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
|
||||
|
||||
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
|
||||
|
||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
object Requirements {
|
||||
|
@ -14,49 +14,28 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
|
||||
* crude are fungible and countable (oil from two small containers can be poured into one large
|
||||
* container), shares of the same class in a specific company are fungible and countable, and so on.
|
||||
*
|
||||
* See [Cash] for an example contract that implements currency using state objects that implement
|
||||
* An example usage would be a cash transaction contract that implements currency using state objects that implement
|
||||
* this interface.
|
||||
*
|
||||
* @param T a type that represents the asset in question. This should describe the basic type of the asset
|
||||
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
interface FungibleAsset<T : Any> : OwnableState {
|
||||
/**
|
||||
* Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally
|
||||
* anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details.
|
||||
*/
|
||||
val amount: Amount<Issued<T>>
|
||||
|
||||
/**
|
||||
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
|
||||
* owner to sign, some (i.e. cash) also require the issuer.
|
||||
*/
|
||||
val exitKeys: Collection<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: AbstractParty
|
||||
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : CommandData {
|
||||
interface Move : MoveCommand, Commands
|
||||
|
||||
/**
|
||||
* Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction
|
||||
* has a unique ID even when there are no inputs.
|
||||
*/
|
||||
interface Issue : IssueCommand, Commands
|
||||
|
||||
/**
|
||||
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
|
||||
* in some other way.
|
||||
*/
|
||||
interface Exit<T : Any> : Commands {
|
||||
val amount: Amount<Issued<T>>
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Copies the underlying data structure, replacing the amount and owner fields with the new values and leaving the
|
||||
* rest (exitKeys) alone.
|
||||
*/
|
||||
fun withNewOwnerAndAmount(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
||||
|
@ -128,10 +128,24 @@ infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(new
|
||||
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, newNotary)
|
||||
|
||||
/**
|
||||
* Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
|
||||
* quantifiable with integer quantities.
|
||||
* The [Issued] data class holds the details of an on ledger digital asset.
|
||||
* In particular it gives the public credentials of the entity that created these digital tokens
|
||||
* and the particular product represented.
|
||||
*
|
||||
* @param P the type of product underlying the definition, for example [java.util.Currency].
|
||||
* @param P the class type of product underlying the definition, for example [java.util.Currency].
|
||||
* @property issuer The [AbstractParty] details of the entity which issued the asset
|
||||
* and a reference blob, which can contain other details related to the token creation e.g. serial number,
|
||||
* warehouse location, etc.
|
||||
* The issuer is the gatekeeper for creating, or destroying the tokens on the digital ledger and
|
||||
* only their [PrivateKey] signature can authorise transactions that do not conserve the total number
|
||||
* of tokens on the ledger.
|
||||
* Other identities may own the tokens, but they can only create transactions that conserve the total token count.
|
||||
* Typically the issuer is also a well know organisation that can convert digital tokens to external assets
|
||||
* and thus underwrites the digital tokens.
|
||||
* Different issuer values may coexist for a particular product, but these cannot be merged.
|
||||
* @property product The details of the specific product represented by these digital tokens. The value
|
||||
* of product may differentiate different kinds of asset within the same logical class e.g the currency, or
|
||||
* it may just be a type marker for a single custom asset.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
|
||||
@ -156,15 +170,15 @@ data class CommandAndState(val command: CommandData, val ownableState: OwnableSt
|
||||
* A contract state that can have a single owner.
|
||||
*/
|
||||
interface OwnableState : ContractState {
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
/** There must be a MoveCommand signed by this key to claim the amount. */
|
||||
val owner: AbstractParty
|
||||
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone. */
|
||||
fun withNewOwner(newOwner: AbstractParty): CommandAndState
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
/** Something which is scheduled to happen at a point in time */
|
||||
/** Something which is scheduled to happen at a point in time. */
|
||||
interface Scheduled {
|
||||
val scheduledAt: Instant
|
||||
}
|
||||
@ -284,20 +298,14 @@ data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>)
|
||||
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
|
||||
}
|
||||
|
||||
/** A common issue command, to enforce that issue commands have a nonce value. */
|
||||
// TODO: Revisit use of nonce values - should this be part of the TX rather than the command perhaps?
|
||||
interface IssueCommand : CommandData {
|
||||
val nonce: Long
|
||||
}
|
||||
|
||||
/** A common move command for contract states which can change owner. */
|
||||
interface MoveCommand : CommandData {
|
||||
/**
|
||||
* Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in
|
||||
* order to settle an obligation contract's state object(s).
|
||||
*/
|
||||
// TODO: Replace SecureHash here with a general contract constraints object
|
||||
val contractHash: SecureHash?
|
||||
// TODO: Replace Class here with a general contract constraints object
|
||||
val contract: Class<out Contract>?
|
||||
}
|
||||
|
||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||
@ -333,15 +341,14 @@ interface Contract {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun verify(tx: LedgerTransaction)
|
||||
|
||||
/**
|
||||
* Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of
|
||||
* the contract's contents).
|
||||
*/
|
||||
val legalContractReference: SecureHash
|
||||
}
|
||||
// DOCEND 5
|
||||
|
||||
/** The annotated [Contract] implements the legal prose identified by the given URI. */
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@MustBeDocumented
|
||||
annotation class LegalProseReference(val uri: String)
|
||||
|
||||
/**
|
||||
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
|
||||
*
|
||||
@ -427,7 +434,7 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
|
||||
* A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot
|
||||
* use brute force techniques and reveal the content of a Merkle-leaf hashed value.
|
||||
* Because this salt serves the role of the seed to compute nonces, its size and entropy should be equal to the
|
||||
* underlying hash function used for Merkle tree generation, currently [SHA256], which has an output of 32 bytes.
|
||||
* underlying hash function used for Merkle tree generation, currently [SecureHash.SHA256], which has an output of 32 bytes.
|
||||
* There are two constructors, one that generates a new 32-bytes random salt, and another that takes a [ByteArray] input.
|
||||
* The latter is required in cases where the salt value needs to be pre-generated (agreed between transacting parties),
|
||||
* but it is highlighted that one should always ensure it has sufficient entropy.
|
||||
|
@ -13,7 +13,7 @@ import java.security.Signature
|
||||
* This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm.
|
||||
*/
|
||||
object ContentSignerBuilder {
|
||||
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner {
|
||||
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
|
||||
val sigAlgId = signatureScheme.signatureOID
|
||||
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
|
||||
if (random != null) {
|
||||
|
@ -70,6 +70,7 @@ object Crypto {
|
||||
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
|
||||
* Note: Recommended key size >= 3072 bits.
|
||||
*/
|
||||
@JvmField
|
||||
val RSA_SHA256 = SignatureScheme(
|
||||
1,
|
||||
"RSA_SHA256",
|
||||
@ -84,6 +85,7 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
|
||||
@JvmField
|
||||
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
||||
2,
|
||||
"ECDSA_SECP256K1_SHA256",
|
||||
@ -98,6 +100,7 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
|
||||
@JvmField
|
||||
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
||||
3,
|
||||
"ECDSA_SECP256R1_SHA256",
|
||||
@ -112,6 +115,7 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
|
||||
@JvmField
|
||||
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||
4,
|
||||
"EDDSA_ED25519_SHA512",
|
||||
@ -131,7 +135,10 @@ object Crypto {
|
||||
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
|
||||
* at the cost of larger key sizes and loss of compatibility.
|
||||
*/
|
||||
@JvmField
|
||||
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
|
||||
|
||||
@JvmField
|
||||
val SPHINCS256_SHA256 = SignatureScheme(
|
||||
5,
|
||||
"SPHINCS-256_SHA512",
|
||||
@ -146,13 +153,12 @@ object Crypto {
|
||||
"at the cost of larger key sizes and loss of compatibility."
|
||||
)
|
||||
|
||||
/**
|
||||
* Corda composite key type
|
||||
*/
|
||||
/** Corda composite key type */
|
||||
@JvmField
|
||||
val COMPOSITE_KEY = SignatureScheme(
|
||||
6,
|
||||
"COMPOSITE",
|
||||
AlgorithmIdentifier(CordaObjectIdentifier.compositeKey),
|
||||
AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY),
|
||||
emptyList(),
|
||||
CordaSecurityProvider.PROVIDER_NAME,
|
||||
CompositeKey.KEY_ALGORITHM,
|
||||
@ -163,13 +169,14 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
|
||||
@JvmField
|
||||
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
|
||||
|
||||
/**
|
||||
* Supported digital signature schemes.
|
||||
* Note: Only the schemes added in this map will be supported (see [Crypto]).
|
||||
*/
|
||||
val supportedSignatureSchemes = listOf(
|
||||
private val signatureSchemeMap: Map<String, SignatureScheme> = listOf(
|
||||
RSA_SHA256,
|
||||
ECDSA_SECP256K1_SHA256,
|
||||
ECDSA_SECP256R1_SHA256,
|
||||
@ -183,15 +190,15 @@ object Crypto {
|
||||
* algorithm identifiers.
|
||||
*/
|
||||
private val algorithmMap: Map<AlgorithmIdentifier, SignatureScheme>
|
||||
= (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } }
|
||||
+ supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) })
|
||||
= (signatureSchemeMap.values.flatMap { scheme -> scheme.alternativeOIDs.map { Pair(it, scheme) } }
|
||||
+ signatureSchemeMap.values.map { Pair(it.signatureOID, it) })
|
||||
.toMap()
|
||||
|
||||
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
|
||||
// that could cause unexpected and suspicious behaviour.
|
||||
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
||||
// The val is private to avoid any harmful state changes.
|
||||
val providerMap: Map<String, Provider> = mapOf(
|
||||
private val providerMap: Map<String, Provider> = mapOf(
|
||||
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
|
||||
CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(),
|
||||
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
|
||||
@ -201,6 +208,14 @@ object Crypto {
|
||||
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun supportedSignatureSchemes(): List<SignatureScheme> = ArrayList(signatureSchemeMap.values)
|
||||
|
||||
@JvmStatic
|
||||
fun findProvider(name: String): Provider {
|
||||
return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name")
|
||||
}
|
||||
|
||||
init {
|
||||
// This registration is needed for reading back EdDSA key from java keystore.
|
||||
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
|
||||
@ -219,8 +234,10 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme {
|
||||
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
|
||||
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)]
|
||||
?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -231,8 +248,11 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme {
|
||||
return signatureSchemeMap[schemeCodeName]
|
||||
?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
|
||||
@ -242,7 +262,7 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(key: PublicKey): SignatureScheme {
|
||||
val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
|
||||
return findSignatureScheme(keyInfo.algorithm)
|
||||
@ -256,7 +276,7 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(key: PrivateKey): SignatureScheme {
|
||||
val keyInfo = PrivateKeyInfo.getInstance(key.encoded)
|
||||
return findSignatureScheme(keyInfo.privateKeyAlgorithm)
|
||||
@ -269,11 +289,12 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
|
||||
val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
|
||||
val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm)
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,8 +305,11 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey {
|
||||
return decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
@ -295,13 +319,18 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
try {
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that " +
|
||||
"it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,11 +341,12 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
|
||||
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
|
||||
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,8 +358,11 @@ object Crypto {
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey {
|
||||
return decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
@ -340,19 +373,25 @@ object Crypto {
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
try {
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " +
|
||||
"that it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based
|
||||
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
|
||||
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use
|
||||
* doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
|
||||
* @param privateKey the signer's [PrivateKey].
|
||||
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the digital signature (in [ByteArray]) on the input message.
|
||||
@ -360,7 +399,8 @@ object Crypto {
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData)
|
||||
|
||||
/**
|
||||
@ -373,8 +413,11 @@ object Crypto {
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature].
|
||||
@ -386,11 +429,14 @@ object Crypto {
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" }
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
|
||||
signature.initSign(privateKey)
|
||||
signature.update(clearData)
|
||||
return signature.sign()
|
||||
@ -398,20 +444,24 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Generic way to sign [SignableData] objects with a [PrivateKey].
|
||||
* [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as a timestamp or partial and blind signature indicators.
|
||||
* @param privateKey the signer's [PrivateKey].
|
||||
* [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as
|
||||
* a timestamp or partial and blind signature indicators.
|
||||
* @param keyPair the signer's [KeyPair].
|
||||
* @param signableData a [SignableData] object that adds extra information to a transaction.
|
||||
* @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and the signature metadata.
|
||||
* @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and
|
||||
* the signature metadata.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature {
|
||||
val sigKey: SignatureScheme = findSignatureScheme(keyPair.private)
|
||||
val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public)
|
||||
if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${sigMetaData.schemeCodeName}" +
|
||||
" is not aligned with the key type: ${sigKey.schemeCodeName}.")
|
||||
require(sigKey == sigMetaData) {
|
||||
"Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}."
|
||||
}
|
||||
val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes)
|
||||
return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata)
|
||||
}
|
||||
@ -430,8 +480,11 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
return doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
@ -448,8 +501,11 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
return doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to verify a digital signature.
|
||||
@ -465,9 +521,12 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
|
||||
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
|
||||
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
|
||||
@ -490,14 +549,16 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
|
||||
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the
|
||||
* input public key's type.
|
||||
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result.
|
||||
@ -507,14 +568,20 @@ object Crypto {
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
|
||||
return isValid(findSignatureScheme(transactionSignature.by), transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
return isValid(
|
||||
findSignatureScheme(transactionSignature.by),
|
||||
transactionSignature.by,
|
||||
transactionSignature.bytes,
|
||||
signableData.serialize().bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the
|
||||
* input public key's type.
|
||||
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result.
|
||||
@ -527,8 +594,11 @@ object Crypto {
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
return isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to verify a digital signature. In comparison to [doVerify] if the key and signature
|
||||
@ -544,9 +614,12 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
signature.initVerify(publicKey)
|
||||
signature.update(clearData)
|
||||
@ -560,7 +633,7 @@ object Crypto {
|
||||
* @return a KeyPair for the requested signature scheme code name.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName))
|
||||
|
||||
/**
|
||||
@ -570,10 +643,12 @@ object Crypto {
|
||||
* @return a new [KeyPair] for the requested [SignatureScheme].
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
if (signatureScheme.algSpec != null)
|
||||
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
|
||||
@ -638,13 +713,17 @@ object Crypto {
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
when (signatureScheme) {
|
||||
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
|
||||
EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed)
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
return when (signatureScheme) {
|
||||
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
|
||||
EDDSA_ED25519_SHA512 -> deriveKeyPairEdDSA(privateKey, seed)
|
||||
else -> throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is " +
|
||||
"not currently implemented for ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -656,6 +735,7 @@ object Crypto {
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair {
|
||||
return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed)
|
||||
}
|
||||
@ -728,11 +808,13 @@ object Crypto {
|
||||
* @return a new [KeyPair] from an entropy input.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
|
||||
when (signatureScheme) {
|
||||
EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy)
|
||||
return when (signatureScheme) {
|
||||
EDDSA_ED25519_SHA512 -> deriveEdDSAKeyPairFromEntropy(entropy)
|
||||
else -> throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair " +
|
||||
"generation: ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -740,6 +822,7 @@ object Crypto {
|
||||
* @param entropy a [BigInteger] value.
|
||||
* @return a new [KeyPair] from an entropy input.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
|
||||
|
||||
// custom key pair generator from entropy.
|
||||
@ -766,8 +849,12 @@ object Crypto {
|
||||
}
|
||||
|
||||
private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter {
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? {
|
||||
return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? {
|
||||
return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -784,22 +871,29 @@ object Crypto {
|
||||
* @return true if the point lies on the curve or false if it doesn't.
|
||||
* @throws IllegalArgumentException if the requested signature scheme or the key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
when (publicKey) {
|
||||
is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid)
|
||||
is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve)
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
return when (publicKey) {
|
||||
is BCECPublicKey -> publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid
|
||||
is EdDSAPublicKey -> publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// return true if EdDSA publicKey is point at infinity.
|
||||
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean {
|
||||
return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
}
|
||||
|
||||
/** Check if the requested [SignatureScheme] is supported by the system. */
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme
|
||||
@JvmStatic
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean {
|
||||
return signatureScheme.schemeCodeName in signatureSchemeMap
|
||||
}
|
||||
|
||||
// validate a key, by checking its algorithmic params.
|
||||
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
|
||||
@ -812,19 +906,19 @@ object Crypto {
|
||||
|
||||
// check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
|
||||
when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key)
|
||||
is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
return when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key)
|
||||
is BCRSAPublicKey, is BCSphincs256PublicKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// check if a private key satisfies algorithm specs.
|
||||
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
|
||||
when (key) {
|
||||
is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec
|
||||
is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec
|
||||
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
return when (key) {
|
||||
is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
|
||||
is EdDSAPrivateKey -> key.params == signatureScheme.algSpec
|
||||
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
@ -838,21 +932,20 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey {
|
||||
return Crypto.decodePublicKey(key.encoded)
|
||||
}
|
||||
@JvmStatic
|
||||
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey = decodePublicKey(key.encoded)
|
||||
|
||||
/**
|
||||
* Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations.
|
||||
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default
|
||||
* return SUN implementations.
|
||||
* @param key a public key.
|
||||
* @return a supported implementation of the input public key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey {
|
||||
return Crypto.decodePublicKey(key.encoded)
|
||||
}
|
||||
@JvmStatic
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded)
|
||||
|
||||
/**
|
||||
* Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
@ -862,7 +955,6 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
|
||||
return Crypto.decodePrivateKey(key.encoded)
|
||||
}
|
||||
@JvmStatic
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded)
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ import java.security.SignatureException
|
||||
// should be renamed to match.
|
||||
/** A wrapper around a digital signature. */
|
||||
@CordaSerializable
|
||||
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
||||
open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/** A digital signature that identifies who the public key is owned by. */
|
||||
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
|
||||
open class WithKey(val by: PublicKey, bytes: ByteArray) : DigitalSignature(bytes) {
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature.
|
||||
*
|
||||
|
@ -1,6 +1,7 @@
|
||||
@file:JvmName("X500NameUtils")
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||
@ -57,7 +58,7 @@ val X500Name.locationOrNull: String? get() = try {
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject
|
||||
val X509Certificate.subject: X500Name get() = toX509CertHolder().subject
|
||||
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
|
||||
|
||||
/**
|
||||
|
@ -42,7 +42,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
fun getInstance(asn1: ASN1Primitive): PublicKey {
|
||||
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.compositeKey)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY)
|
||||
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
|
||||
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
|
||||
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
|
||||
@ -177,7 +177,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
}
|
||||
keyVector.add(ASN1Integer(threshold.toLong()))
|
||||
keyVector.add(DERSequence(childrenVector))
|
||||
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), DERSequence(keyVector)).encoded
|
||||
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY), DERSequence(keyVector)).encoded
|
||||
}
|
||||
|
||||
override fun getFormat() = ASN1Encoding.DER
|
||||
@ -262,7 +262,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
else if (n == 1) {
|
||||
require(threshold == null || threshold == children.first().weight)
|
||||
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
|
||||
children.first().node // We can assume that this node is a correct CompositeKey.
|
||||
// Returning the only child node which is [PublicKey] itself. We need to avoid single-key [CompositeKey] instances,
|
||||
// as there are scenarios where developers expected the underlying key and its composite versions to be equivalent.
|
||||
children.first().node
|
||||
} else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
*/
|
||||
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
||||
companion object {
|
||||
val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,13 @@ package net.corda.core.crypto.provider
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.crypto.composite.CompositeSignature
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import java.security.AccessController
|
||||
import java.security.PrivilegedAction
|
||||
import java.security.Provider
|
||||
|
||||
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
|
||||
companion object {
|
||||
val PROVIDER_NAME = "Corda"
|
||||
const val PROVIDER_NAME = "Corda"
|
||||
}
|
||||
|
||||
init {
|
||||
@ -21,7 +20,7 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
|
||||
put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory")
|
||||
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature")
|
||||
|
||||
val compositeKeyOID = CordaObjectIdentifier.compositeKey.id
|
||||
val compositeKeyOID = CordaObjectIdentifier.COMPOSITE_KEY.id
|
||||
put("Alg.Alias.KeyFactory.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
|
||||
put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
|
||||
put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
|
||||
@ -32,6 +31,6 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
|
||||
object CordaObjectIdentifier {
|
||||
// UUID-based OID
|
||||
// TODO: Register for an OID space and issue our own shorter OID
|
||||
val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
|
||||
val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||
@JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
|
||||
@JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -56,12 +57,14 @@ import java.security.PublicKey
|
||||
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
*
|
||||
* @param partiallySignedTx Transaction to collect the remaining signatures for
|
||||
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
|
||||
* just in the states. If null, the default well known identity of the node is used.
|
||||
*/
|
||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||
// TODO: Update this flow to handle randomly generated keys when that work is complete.
|
||||
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
|
||||
companion object {
|
||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
||||
@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
// TODO: Revisit when key management is properly fleshed out.
|
||||
// This will break if a party uses anything other than their legalIdentityKey.
|
||||
// Check the signatures which have already been provided and that the transaction is valid.
|
||||
// Usually just the Initiator and possibly an oracle would have signed at this point.
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
val myKeys: Iterable<PublicKey> = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey)
|
||||
val signed = partiallySignedTx.sigs.map { it.by }
|
||||
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
|
||||
|
||||
// One of the signatures collected so far MUST be from the initiator of this flow.
|
||||
require(partiallySignedTx.sigs.any { it.by == myKey }) {
|
||||
require(partiallySignedTx.sigs.any { it.by in myKeys }) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction."
|
||||
}
|
||||
|
||||
@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
if (unsigned.isEmpty()) return partiallySignedTx
|
||||
|
||||
// Collect signatures from all counter-parties and append them to the partially signed transaction.
|
||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) }
|
||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) }
|
||||
val stx = partiallySignedTx + counterpartySignatures
|
||||
|
||||
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
||||
@ -112,23 +113,32 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
|
||||
/**
|
||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
|
||||
*
|
||||
* @return a pair of the well known identity to contact for a signature, and the public key that party should sign
|
||||
* with (this may belong to a confidential identity).
|
||||
*/
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Party> = keys.map {
|
||||
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key.
|
||||
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
|
||||
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
|
||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
||||
partyNode.legalIdentity
|
||||
Pair(party, it)
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param counterparty the party to request a signature from.
|
||||
* @param signingKey the key the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable private fun collectSignature(counterparty: Party): TransactionSignature {
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
@Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
|
||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
|
||||
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
||||
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
||||
// for us to check we have the expected signature returned.
|
||||
send(counterparty, signingKey)
|
||||
return receive<TransactionSignature>(counterparty).unwrap {
|
||||
require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." }
|
||||
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
|
||||
it
|
||||
}
|
||||
}
|
||||
@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
|
||||
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||
// keys we own.
|
||||
val signingKey = receive<PublicKey>(otherParty).unwrap {
|
||||
// TODO: We should have a faster way of verifying we own a single key
|
||||
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
|
||||
}
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// Check that the Responder actually needs to sign.
|
||||
checkMySignatureRequired(stx)
|
||||
checkMySignatureRequired(stx, signingKey)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
// Sign and send back our signature to the Initiator.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mySignature = serviceHub.createSignature(stx)
|
||||
val mySignature = serviceHub.createSignature(stx, signingKey)
|
||||
send(otherParty, mySignature)
|
||||
|
||||
// Return the fully signed transaction once it has been committed.
|
||||
@ -214,8 +231,10 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
|
||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||
require(stx.sigs.any { it.by == otherParty.owningKey }) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction."
|
||||
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
|
||||
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
|
||||
require(otherParty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
|
||||
}
|
||||
val signed = stx.sigs.map { it.by }
|
||||
val allSigners = stx.tx.requiredSigningKeys
|
||||
@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
*/
|
||||
@Suspendable abstract protected fun checkTransaction(stx: SignedTransaction)
|
||||
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) {
|
||||
// TODO: Revisit when key management is properly fleshed out.
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
require(myKey in stx.tx.requiredSigningKeys) {
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
|
||||
require(signingKey in stx.tx.requiredSigningKeys) {
|
||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ abstract class FlowLogic<out T> {
|
||||
* network's event horizon time.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
|
||||
/**
|
||||
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
|
||||
@ -239,7 +239,7 @@ abstract class FlowLogic<out T> {
|
||||
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
|
||||
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
|
||||
* Note: This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementationdoes nothing.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java)
|
||||
@ -256,7 +256,7 @@ abstract class FlowLogic<out T> {
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
fun persistFlowStackSnapshot() = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -1,69 +1,21 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
interface FlowStackSnapshotFactory {
|
||||
private object Holder {
|
||||
val INSTANCE: FlowStackSnapshotFactory
|
||||
|
||||
init {
|
||||
val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull()
|
||||
INSTANCE = serviceFactory ?: FlowStackSnapshotDefaultFactory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns flow stack data snapshot extracted from Quasar stack.
|
||||
* It is designed to be used in the debug mode of the flow execution.
|
||||
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
|
||||
|
||||
/** Stores flow stack snapshot as a json file. The stored shapshot is only partial and consists
|
||||
* only data (i.e. stack traces and local variables values) relevant to the flow. It does not
|
||||
* persist corda internal data (e.g. FlowStateMachine). Instead it uses [StackFrameDataToken] to indicate
|
||||
* the class of the element on the stack.
|
||||
* The flow stack snapshot is stored in a file located in
|
||||
* {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
|
||||
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
|
||||
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String): Unit
|
||||
}
|
||||
|
||||
private class FlowStackSnapshotDefaultFactory : FlowStackSnapshotFactory {
|
||||
val log = loggerFor<FlowStackSnapshotDefaultFactory>()
|
||||
|
||||
override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
}
|
||||
}
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Main data object representing snapshot of the flow stack, extracted from the Quasar stack.
|
||||
*/
|
||||
data class FlowStackSnapshot constructor(
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val flowClass: String? = null,
|
||||
val stackFrames: List<Frame> = listOf()
|
||||
data class FlowStackSnapshot(
|
||||
val time: Instant,
|
||||
val flowClass: String,
|
||||
val stackFrames: List<Frame>
|
||||
) {
|
||||
data class Frame(
|
||||
val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?> = listOf()
|
||||
)
|
||||
val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?>
|
||||
) {
|
||||
override fun toString(): String = stackTraceElement.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,8 +13,7 @@ import java.security.PublicKey
|
||||
@CordaSerializable
|
||||
abstract class AbstractParty(val owningKey: PublicKey) {
|
||||
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
||||
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
|
||||
override fun hashCode(): Int = owningKey.hashCode()
|
||||
abstract fun nameOrNull(): X500Name?
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
@ -12,11 +11,7 @@ import java.security.PublicKey
|
||||
* information such as name. It is intended to represent a party on the distributed ledger.
|
||||
*/
|
||||
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
// Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party]
|
||||
// can put in the key and actual name
|
||||
override fun toString() = "${owningKey.toStringShort()} <Anonymous>"
|
||||
|
||||
override fun nameOrNull(): X500Name? = null
|
||||
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.CertificateAndKeyPair
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -26,10 +28,9 @@ import java.security.PublicKey
|
||||
* @see CompositeKey
|
||||
*/
|
||||
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public)
|
||||
override fun toString() = name.toString()
|
||||
override fun nameOrNull(): X500Name? = name
|
||||
|
||||
constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo))
|
||||
override fun nameOrNull(): X500Name = name
|
||||
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
override fun toString() = name.toString()
|
||||
}
|
||||
|
@ -1,49 +1,42 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of
|
||||
* [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party,
|
||||
* not part of the identifier themselves. While party and certificate can both be derived from the certificate path,
|
||||
* this class exists in order to ensure the implementation classes of certificates and party public keys are kept stable.
|
||||
* not part of the identifier themselves.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class PartyAndCertificate(val party: Party,
|
||||
val certificate: X509CertificateHolder,
|
||||
val certPath: CertPath) {
|
||||
constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath)
|
||||
val name: X500Name
|
||||
get() = party.name
|
||||
val owningKey: PublicKey
|
||||
get() = party.owningKey
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is PartyAndCertificate)
|
||||
party == other.party
|
||||
else
|
||||
false
|
||||
//TODO Is VerifiableIdentity a better name?
|
||||
class PartyAndCertificate(val certPath: CertPath) {
|
||||
@Transient val certificate: X509CertificateHolder
|
||||
init {
|
||||
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
|
||||
val certs = certPath.certificates
|
||||
require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
|
||||
certificate = certs[0].toX509CertHolder()
|
||||
}
|
||||
|
||||
@Transient val party: Party = Party(certificate)
|
||||
|
||||
val owningKey: PublicKey get() = party.owningKey
|
||||
val name: X500Name get() = party.name
|
||||
|
||||
operator fun component1(): Party = party
|
||||
operator fun component2(): X509CertificateHolder = certificate
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party
|
||||
override fun hashCode(): Int = party.hashCode()
|
||||
override fun toString(): String = party.toString()
|
||||
|
||||
/**
|
||||
* Verify that the given certificate path is valid and leads to the owning key of the party.
|
||||
*/
|
||||
/** Verify the certificate path is valid. */
|
||||
fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult {
|
||||
require(certPath.certificates.first() is X509Certificate) { "Subject certificate must be an X.509 certificate" }
|
||||
require(Arrays.equals(party.owningKey.encoded, certificate.subjectPublicKeyInfo.encoded)) { "Certificate public key must match party owning key" }
|
||||
require(Arrays.equals(certPath.certificates.first().encoded, certificate.encoded)) { "Certificate path must link to certificate" }
|
||||
|
||||
val validatorParameters = PKIXParameters(setOf(trustAnchor))
|
||||
val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false }
|
||||
val validator = CertPathValidator.getInstance("PKIX")
|
||||
validatorParameters.isRevocationEnabled = false
|
||||
return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult
|
||||
return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,7 @@ package net.corda.core.internal
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowContext
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -39,24 +35,11 @@ interface FlowStateMachine<R> {
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
|
||||
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
|
||||
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
|
||||
|
||||
/**
|
||||
* Persists a shallow copy of the Quasar stack frames at the time of call to [persistFlowStackSnapshot].
|
||||
* Use this to track the monitor evolution of the quasar stack values during the flow execution.
|
||||
* The flow stack snapshot is stored in a file located in {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
|
||||
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
|
||||
*
|
||||
* Note: With respect to the [flowStackSnapshot], the snapshot being persisted by this method is partial,
|
||||
* meaning that only flow relevant traces and local variables are persisted.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(flowClass: Class<*>): Unit
|
||||
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
|
||||
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.internal
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
@ -165,6 +166,9 @@ fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T
|
||||
}
|
||||
}
|
||||
|
||||
fun java.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||
fun javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||
|
||||
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
||||
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
|
||||
val bytes = toByteArray()
|
||||
|
@ -92,7 +92,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
// half way through, it's no big deal, although it might result in us attempting to re-download data
|
||||
// redundantly next time we attempt verification.
|
||||
it.verify(serviceHub)
|
||||
serviceHub.recordTransactions(it)
|
||||
serviceHub.recordTransactions(false, it)
|
||||
}
|
||||
|
||||
return signedTransaction?.let {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.core.internal.concurrent
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.concurrent.match
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
|
@ -421,6 +421,29 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
|
||||
arg3: D
|
||||
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
||||
@Suppress("unused_parameter")
|
||||
flowConstructor: (A, B, C, D, E) -> R,
|
||||
arg0: A,
|
||||
arg1: B,
|
||||
arg2: C,
|
||||
arg3: D,
|
||||
arg4: E
|
||||
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun <T : Any, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
||||
@Suppress("unused_parameter")
|
||||
flowConstructor: (A, B, C, D, E, F) -> R,
|
||||
arg0: A,
|
||||
arg1: B,
|
||||
arg2: C,
|
||||
arg3: D,
|
||||
arg4: E,
|
||||
arg5: F
|
||||
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
|
||||
|
||||
/**
|
||||
* The Data feed contains a snapshot of the requested data and an [Observable] of future updates.
|
||||
*/
|
||||
|
@ -1,7 +1,10 @@
|
||||
package net.corda.core.messaging
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
|
||||
/** The interface for a group of message recipients (which may contain only one recipient) */
|
||||
@CordaSerializable
|
||||
interface MessageRecipients
|
||||
|
||||
/** A base class for the case of point-to-point messages */
|
||||
|
@ -24,16 +24,17 @@ data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
||||
val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services.
|
||||
val legalIdentitiesAndCerts: NonEmptySet<PartyAndCertificate>,
|
||||
val platformVersion: Int,
|
||||
var advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val worldMapLocation: WorldMapLocation? = null) {
|
||||
init {
|
||||
require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" }
|
||||
require(advertisedServices.none { it.identity == legalIdentityAndCert }) {
|
||||
"Service identities must be different from node legal identity"
|
||||
}
|
||||
}
|
||||
val legalIdentity: Party
|
||||
get() = legalIdentityAndCert.party
|
||||
val notaryIdentity: Party
|
||||
get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
|
||||
|
||||
val legalIdentity: Party get() = legalIdentityAndCert.party
|
||||
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
|
||||
fun serviceIdentities(type: ServiceType): List<Party> {
|
||||
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party }
|
||||
return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null }
|
||||
}
|
||||
}
|
||||
|
@ -68,18 +68,35 @@ interface ServiceHub : ServicesForResolution {
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing. This is expected to be run within a database transaction.
|
||||
* further processing if [notifyVault] is true. This is expected to be run within a database transaction.
|
||||
*
|
||||
* @param txs The transactions to record.
|
||||
* @param notifyVault indicate if the vault should be notified for the update.
|
||||
*/
|
||||
fun recordTransactions(txs: Iterable<SignedTransaction>)
|
||||
fun recordTransactions(notifyVault: Boolean, txs: Iterable<SignedTransaction>)
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing if [notifyVault] is true. This is expected to be run within a database transaction.
|
||||
*/
|
||||
fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) {
|
||||
recordTransactions(notifyVault, listOf(first, *remaining))
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing. This is expected to be run within a database transaction.
|
||||
*/
|
||||
fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) {
|
||||
recordTransactions(listOf(first, *remaining))
|
||||
recordTransactions(true, first, *remaining)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing. This is expected to be run within a database transaction.
|
||||
*/
|
||||
fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
recordTransactions(true, txs)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,8 +109,7 @@ interface ServiceHub : ServicesForResolution {
|
||||
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index]
|
||||
}
|
||||
else stx.tx.outputs[stateRef.index]
|
||||
} else stx.tx.outputs[stateRef.index]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,8 +122,7 @@ interface ServiceHub : ServicesForResolution {
|
||||
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
stx.tx.outRef<T>(stateRef.index)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
@ -114,6 +117,6 @@ interface IdentityService {
|
||||
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
|
||||
*/
|
||||
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
|
||||
}
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.toFuture
|
||||
@ -135,8 +136,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val recordedTime: Instant,
|
||||
val consumedTime: Instant?,
|
||||
val status: Vault.StateStatus,
|
||||
val notaryName: String,
|
||||
val notaryKey: String,
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?)
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ package net.corda.core.node.services.vault
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.criteria.Predicate
|
||||
@ -40,6 +40,7 @@ sealed class QueryCriteria {
|
||||
|
||||
abstract class CommonQueryCriteria : QueryCriteria() {
|
||||
abstract val status: Vault.StateStatus
|
||||
abstract val contractStateTypes: Set<Class<out ContractState>>?
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
@ -49,13 +50,14 @@ sealed class QueryCriteria {
|
||||
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
|
||||
*/
|
||||
data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val notaryName: List<X500Name>? = null,
|
||||
val notary: List<AbstractParty>? = null,
|
||||
val softLockingCondition: SoftLockingCondition? = null,
|
||||
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,9 +67,16 @@ sealed class QueryCriteria {
|
||||
data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
|
||||
val uuid: List<UUID>? = null,
|
||||
val externalId: List<String>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
constructor(participants: List<AbstractParty>? = null,
|
||||
linearId: List<UniqueIdentifier>? = null,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,11 +90,13 @@ sealed class QueryCriteria {
|
||||
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
|
||||
val owner: List<AbstractParty>? = null,
|
||||
val quantity: ColumnPredicate<Long>? = null,
|
||||
val issuerPartyName: List<AbstractParty>? = null,
|
||||
val issuer: List<AbstractParty>? = null,
|
||||
val issuerRef: List<OpaqueBytes>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,9 +112,11 @@ sealed class QueryCriteria {
|
||||
*/
|
||||
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
|
||||
(val expression: CriteriaExpression<L, Boolean>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,39 +9,36 @@ import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
|
||||
@CordaSerializable
|
||||
enum class BinaryLogicalOperator {
|
||||
interface Operator
|
||||
|
||||
enum class BinaryLogicalOperator : Operator {
|
||||
AND,
|
||||
OR
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class EqualityComparisonOperator {
|
||||
enum class EqualityComparisonOperator : Operator {
|
||||
EQUAL,
|
||||
NOT_EQUAL
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class BinaryComparisonOperator {
|
||||
enum class BinaryComparisonOperator : Operator {
|
||||
LESS_THAN,
|
||||
LESS_THAN_OR_EQUAL,
|
||||
GREATER_THAN,
|
||||
GREATER_THAN_OR_EQUAL,
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class NullOperator {
|
||||
enum class NullOperator : Operator {
|
||||
IS_NULL,
|
||||
NOT_NULL
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class LikenessOperator {
|
||||
enum class LikenessOperator : Operator {
|
||||
LIKE,
|
||||
NOT_LIKE
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class CollectionOperator {
|
||||
enum class CollectionOperator : Operator {
|
||||
IN,
|
||||
NOT_IN
|
||||
}
|
||||
@ -151,7 +148,7 @@ data class Sort(val columns: Collection<SortColumn>) {
|
||||
|
||||
enum class VaultStateAttribute(val attributeName: String) : Attribute {
|
||||
/** Vault States */
|
||||
NOTARY_NAME("notaryName"),
|
||||
NOTARY_NAME("notary"),
|
||||
CONTRACT_TYPE("contractStateClassName"),
|
||||
STATE_STATUS("stateStatus"),
|
||||
RECORDED_TIME("recordedTime"),
|
||||
|
@ -17,10 +17,17 @@ object CommonSchema
|
||||
/**
|
||||
* First version of the Vault ORM schema
|
||||
*/
|
||||
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) {
|
||||
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = emptyList()) {
|
||||
|
||||
@MappedSuperclass
|
||||
open class LinearState(
|
||||
/** [ContractState] attributes */
|
||||
|
||||
/** X500Name of participant parties **/
|
||||
@ElementCollection
|
||||
@Column(name = "participants")
|
||||
var participants: MutableSet<AbstractParty>? = null,
|
||||
|
||||
/**
|
||||
* Represents a [LinearState] [UniqueIdentifier]
|
||||
*/
|
||||
@ -31,18 +38,26 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
|
||||
var uuid: UUID
|
||||
|
||||
) : PersistentState() {
|
||||
constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id)
|
||||
constructor(uid: UniqueIdentifier, _participants: Set<AbstractParty>)
|
||||
: this(participants = _participants.toMutableSet(),
|
||||
externalId = uid.externalId,
|
||||
uuid = uid.id)
|
||||
}
|
||||
|
||||
@MappedSuperclass
|
||||
open class FungibleState(
|
||||
/** [ContractState] attributes */
|
||||
@OneToMany(cascade = arrayOf(CascadeType.ALL))
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
/** X500Name of participant parties **/
|
||||
@ElementCollection
|
||||
@Column(name = "participants")
|
||||
var participants: MutableSet<AbstractParty>? = null,
|
||||
|
||||
/** [OwnableState] attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var ownerKey: CommonSchemaV1.Party,
|
||||
|
||||
/** X500Name of owner party **/
|
||||
@Column(name = "owner_name")
|
||||
var owner: AbstractParty,
|
||||
|
||||
/** [FungibleAsset] attributes
|
||||
*
|
||||
@ -55,42 +70,12 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
|
||||
var quantity: Long,
|
||||
|
||||
/** Issuer attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var issuerParty: CommonSchemaV1.Party,
|
||||
|
||||
/** X500Name of issuer party **/
|
||||
@Column(name = "issuer_name")
|
||||
var issuer: AbstractParty,
|
||||
|
||||
@Column(name = "issuer_reference")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState() {
|
||||
constructor(_participants: Set<AbstractParty>, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray)
|
||||
: this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(),
|
||||
ownerKey = CommonSchemaV1.Party(_ownerKey),
|
||||
quantity = _quantity,
|
||||
issuerParty = CommonSchemaV1.Party(_issuerParty),
|
||||
issuerRef = _issuerRef)
|
||||
}
|
||||
|
||||
/**
|
||||
* Party entity (to be replaced by referencing final Identity Schema)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "vault_party",
|
||||
indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name")))
|
||||
class Party(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "party_id")
|
||||
var id: Int,
|
||||
|
||||
/**
|
||||
* [Party] attributes
|
||||
*/
|
||||
@Column(name = "party_name")
|
||||
var name: String,
|
||||
|
||||
@Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?)
|
||||
var key: String
|
||||
) {
|
||||
constructor(party: AbstractParty)
|
||||
: this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String())
|
||||
}
|
||||
) : PersistentState()
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
import io.requery.Persistable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -69,4 +68,4 @@ data class PersistentStateRef(
|
||||
/**
|
||||
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
|
||||
*/
|
||||
interface StatePersistable : Persistable
|
||||
interface StatePersistable
|
@ -2,6 +2,7 @@ package net.corda.core.schemas.converters
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Converter
|
||||
@ -17,9 +18,15 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
|
||||
identitySvc()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val log = loggerFor<AbstractPartyToX500NameAsStringConverter>()
|
||||
}
|
||||
|
||||
override fun convertToDatabaseColumn(party: AbstractParty?): String? {
|
||||
party?.let {
|
||||
return identityService.partyFromAnonymous(party)?.toString()
|
||||
val partyName = identityService.partyFromAnonymous(party)?.toString()
|
||||
if (partyName != null) return partyName
|
||||
else log.warn ("Identity service unable to resolve AbstractParty: $party")
|
||||
}
|
||||
return null // non resolvable anonymous parties
|
||||
}
|
||||
@ -27,7 +34,8 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
|
||||
override fun convertToEntityAttribute(dbData: String?): AbstractParty? {
|
||||
dbData?.let {
|
||||
val party = identityService.partyFromX500Name(X500Name(dbData))
|
||||
return party as AbstractParty
|
||||
if (party != null) return party
|
||||
else log.warn ("Identity service unable to resolve X500name: $dbData")
|
||||
}
|
||||
return null // non resolvable anonymous parties are stored as nulls
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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)
|
@ -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)
|
||||
|
||||
|
@ -39,7 +39,7 @@ interface SerializationContext {
|
||||
/**
|
||||
* When serializing, use the format this header sequence represents.
|
||||
*/
|
||||
val preferedSerializationVersion: ByteSequence
|
||||
val preferredSerializationVersion: ByteSequence
|
||||
/**
|
||||
* The class loader to use for deserialization.
|
||||
*/
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
@ -17,6 +18,7 @@ import java.util.function.Predicate
|
||||
* by a [SignedTransaction] that carries the signatures over this payload.
|
||||
* The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class WireTransaction(
|
||||
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
||||
override val inputs: List<StateRef>,
|
||||
|
@ -139,7 +139,10 @@ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of
|
||||
fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this)
|
||||
fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
|
||||
private class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() {
|
||||
/**
|
||||
* Class is public for serialization purposes
|
||||
*/
|
||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() {
|
||||
init {
|
||||
require(offset >= 0 && offset < bytes.size)
|
||||
require(size >= 0 && size <= bytes.size)
|
||||
|
@ -43,31 +43,31 @@ inline fun Logger.debug(msg: () -> String) {
|
||||
* Extension method for easier construction of [Duration]s in terms of integer days: `val twoDays = 2.days`.
|
||||
* @see Duration.ofDays
|
||||
*/
|
||||
inline val Int.days: Duration get() = Duration.ofDays(toLong())
|
||||
val Int.days: Duration get() = Duration.ofDays(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer hours: `val twoHours = 2.hours`.
|
||||
* @see Duration.ofHours
|
||||
*/
|
||||
inline val Int.hours: Duration get() = Duration.ofHours(toLong())
|
||||
val Int.hours: Duration get() = Duration.ofHours(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer minutes: `val twoMinutes = 2.minutes`.
|
||||
* @see Duration.ofMinutes
|
||||
*/
|
||||
inline val Int.minutes: Duration get() = Duration.ofMinutes(toLong())
|
||||
val Int.minutes: Duration get() = Duration.ofMinutes(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer seconds: `val twoSeconds = 2.seconds`.
|
||||
* @see Duration.ofSeconds
|
||||
*/
|
||||
inline val Int.seconds: Duration get() = Duration.ofSeconds(toLong())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer milliseconds: `val twoMillis = 2.millis`.
|
||||
* @see Duration.ofMillis
|
||||
*/
|
||||
inline val Int.millis: Duration get() = Duration.ofMillis(toLong())
|
||||
val Int.millis: Duration get() = Duration.ofMillis(toLong())
|
||||
|
||||
/**
|
||||
* A simple wrapper that enables the use of Kotlin's `val x by transient { ... }` syntax. Such a property
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
@ -7,6 +8,7 @@ import java.net.URI
|
||||
* @param host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets.
|
||||
* @param port a valid port number.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NetworkHostAndPort(val host: String, val port: Int) {
|
||||
companion object {
|
||||
internal val invalidPortFormat = "Invalid port: %s"
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.finance.*
|
||||
import net.corda.core.contracts.Amount.Companion.sumOrZero
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
@ -13,13 +15,6 @@ import kotlin.test.assertTrue
|
||||
* Tests of the [Amount] class.
|
||||
*/
|
||||
class AmountTests {
|
||||
@Test
|
||||
fun basicCurrency() {
|
||||
val expected = 1000L
|
||||
val amount = Amount(expected, GBP)
|
||||
assertEquals(expected, amount.quantity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `make sure Amount has decimal places`() {
|
||||
val x = Amount(1, Currency.getInstance("USD"))
|
||||
@ -27,7 +22,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decimalConversion() {
|
||||
fun `decimal conversion`() {
|
||||
val quantity = 1234L
|
||||
val amountGBP = Amount(quantity, GBP)
|
||||
val expectedGBP = BigDecimal("12.34")
|
||||
@ -49,22 +44,6 @@ class AmountTests {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsing() {
|
||||
assertEquals(Amount(1234L, GBP), Amount.parseCurrency("£12.34"))
|
||||
assertEquals(Amount(1200L, GBP), Amount.parseCurrency("£12"))
|
||||
assertEquals(Amount(1000L, USD), Amount.parseCurrency("$10"))
|
||||
assertEquals(Amount(5000L, JPY), Amount.parseCurrency("¥5000"))
|
||||
assertEquals(Amount(500000L, RUB), Amount.parseCurrency("₽5000"))
|
||||
assertEquals(Amount(1500000000L, CHF), Amount.parseCurrency("15,000,000 CHF"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rendering() {
|
||||
assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString())
|
||||
assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun split() {
|
||||
for (baseQuantity in 0..1000) {
|
||||
@ -81,7 +60,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransfersEquality() {
|
||||
fun `amount transfers equality`() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
@ -106,7 +85,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransferAggregation() {
|
||||
fun `amount transfer aggregation`() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
@ -137,7 +116,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransferApply() {
|
||||
fun `amount transfer apply`() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
@ -182,6 +161,5 @@ class AmountTests {
|
||||
assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)])
|
||||
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)])
|
||||
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, GBP)])
|
||||
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.dummyCommand
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -27,6 +28,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
|
||||
interface Commands {
|
||||
data class Cmd1(val id: Int) : CommandData, Commands
|
||||
data class Cmd2(val id: Int) : CommandData, Commands
|
||||
data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks.
|
||||
}
|
||||
|
||||
|
||||
@ -50,7 +52,11 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
|
||||
|
||||
private fun makeDummyStateAndRef(data: Any): StateAndRef<*> {
|
||||
val dummyState = makeDummyState(data)
|
||||
val fakeIssueTx = services.signInitialTransaction(TransactionBuilder(notary = DUMMY_NOTARY).addOutputState(dummyState))
|
||||
val fakeIssueTx = services.signInitialTransaction(
|
||||
TransactionBuilder(notary = DUMMY_NOTARY)
|
||||
.addOutputState(dummyState)
|
||||
.addCommand(dummyCommand())
|
||||
)
|
||||
services.recordTransactions(fakeIssueTx)
|
||||
val dummyStateRef = StateRef(fakeIssueTx.id, 0)
|
||||
return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef)
|
||||
@ -182,7 +188,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
|
||||
val intCmd2 = ltx.commandsOfType<Commands.Cmd2>()
|
||||
assertEquals(5, intCmd2.size)
|
||||
assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { it.value.id })
|
||||
val notPresentQuery = ltx.commandsOfType(FungibleAsset.Commands.Exit::class.java)
|
||||
val notPresentQuery = ltx.commandsOfType(Commands.Cmd3::class.java)
|
||||
assertEquals(emptyList(), notPresentQuery)
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.ledger
|
||||
@ -27,7 +28,6 @@ class TransactionEncumbranceTests {
|
||||
val timeLock = DummyTimeLock.State(FIVE_PM)
|
||||
|
||||
class DummyTimeLock : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("DummyTimeLock")
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return
|
||||
val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window")
|
||||
|
@ -34,14 +34,15 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() {
|
||||
val notaryServices = MockServices(DUMMY_NOTARY_KEY)
|
||||
|
||||
val originBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
originBuilder.addOutputState(DummyState(random31BitValue()))
|
||||
originBuilder.addCommand(command, MEGA_CORP_PUBKEY)
|
||||
.addOutputState(DummyState(random31BitValue()))
|
||||
.addCommand(command, MEGA_CORP_PUBKEY)
|
||||
|
||||
val originPtx = megaCorpServices.signInitialTransaction(originBuilder)
|
||||
val originTx = notaryServices.addSignature(originPtx)
|
||||
|
||||
val inputBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
inputBuilder.addInputState(originTx.tx.outRef<DummyState>(0))
|
||||
.addInputState(originTx.tx.outRef<DummyState>(0))
|
||||
.addCommand(dummyCommand(MEGA_CORP_PUBKEY))
|
||||
|
||||
val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder)
|
||||
val inputTx = megaCorpServices.addSignature(inputPtx)
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import org.junit.Test
|
||||
|
@ -25,16 +25,16 @@ class CompositeKeyTests : TestDependencyInjectionBase() {
|
||||
@JvmField
|
||||
val tempFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
val aliceKey = generateKeyPair()
|
||||
val bobKey = generateKeyPair()
|
||||
val charlieKey = generateKeyPair()
|
||||
private val aliceKey = generateKeyPair()
|
||||
private val bobKey = generateKeyPair()
|
||||
private val charlieKey = generateKeyPair()
|
||||
|
||||
val alicePublicKey: PublicKey = aliceKey.public
|
||||
val bobPublicKey: PublicKey = bobKey.public
|
||||
val charliePublicKey: PublicKey = charlieKey.public
|
||||
private val alicePublicKey: PublicKey = aliceKey.public
|
||||
private val bobPublicKey: PublicKey = bobKey.public
|
||||
private val charliePublicKey: PublicKey = charlieKey.public
|
||||
|
||||
val message = OpaqueBytes("Transaction".toByteArray())
|
||||
val secureHash = message.sha256()
|
||||
private val message = OpaqueBytes("Transaction".toByteArray())
|
||||
private val secureHash = message.sha256()
|
||||
|
||||
// By lazy is required so that the serialisers are configured before vals initialisation takes place (they internally invoke serialise).
|
||||
val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) }
|
||||
@ -43,7 +43,6 @@ class CompositeKeyTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `(Alice) fulfilled by Alice signature`() {
|
||||
println(aliceKey.serialize().hash)
|
||||
assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) }
|
||||
assertFalse { alicePublicKey.isFulfilledBy(charlieSignature.by) }
|
||||
}
|
||||
|
@ -343,7 +343,7 @@ class CryptoUtilsTest {
|
||||
// test list of supported algorithms
|
||||
@Test
|
||||
fun `Check supported algorithms`() {
|
||||
val algList: List<String> = Crypto.supportedSignatureSchemes.keys.toList()
|
||||
val algList: List<String> = Crypto.supportedSignatureSchemes().map { it.schemeCodeName }
|
||||
val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE")
|
||||
assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); }
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
|
@ -4,20 +4,18 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -32,13 +30,10 @@ import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var configuration: RequeryConfiguration
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockNet = MockNetwork()
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
|
||||
}
|
||||
|
||||
@After
|
||||
@ -137,11 +132,9 @@ class AttachmentTests {
|
||||
val corruptBytes = "arggghhhh".toByteArray()
|
||||
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
|
||||
|
||||
val corruptAttachment = AttachmentEntity()
|
||||
corruptAttachment.attId = id
|
||||
corruptAttachment.content = attachment
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
|
||||
n0.database.transaction {
|
||||
n0.attachments.session.update(corruptAttachment)
|
||||
DatabaseTransactionManager.current().session.update(corruptAttachment)
|
||||
}
|
||||
|
||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||
|
@ -3,13 +3,14 @@ package net.corda.core.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.MINI_CORP_KEY
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
@ -77,16 +78,18 @@ class CollectSignaturesFlowTests {
|
||||
}
|
||||
|
||||
@InitiatedBy(TestFlow.Initiator::class)
|
||||
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
|
||||
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
|
||||
|
||||
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
|
||||
val myInputKeys = state.participants.map { it.owningKey }
|
||||
val myKeys = myInputKeys + (identities[serviceHub.myInfo.legalIdentity] ?: serviceHub.myInfo.legalIdentity).owningKey
|
||||
val command = Command(DummyContract.Commands.Create(), myInputKeys)
|
||||
val builder = TransactionBuilder(notary).withItems(state, command)
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, myKeys))
|
||||
val ftx = subFlow(FinalityFlow(stx)).single()
|
||||
|
||||
return ftx
|
||||
@ -103,10 +106,11 @@ class CollectSignaturesFlowTests {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
|
||||
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
|
||||
val myInputKeys = state.participants.map { it.owningKey }
|
||||
val command = Command(DummyContract.Commands.Create(), myInputKeys)
|
||||
val builder = TransactionBuilder(notary).withItems(state, command)
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys))
|
||||
val ftx = subFlow(FinalityFlow(stx)).single()
|
||||
|
||||
return ftx
|
||||
@ -136,9 +140,12 @@ class CollectSignaturesFlowTests {
|
||||
|
||||
@Test
|
||||
fun `successfully collects two signatures`() {
|
||||
val bConfidentialIdentity = b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false)
|
||||
// Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity
|
||||
a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity)
|
||||
registerFlowOnAllNodes(TestFlowTwo.Responder::class)
|
||||
val magicNumber = 1337
|
||||
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
|
||||
val parties = listOf(a.info.legalIdentity, bConfidentialIdentity.party, c.info.legalIdentity)
|
||||
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
||||
val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
|
||||
mockNet.runNetwork()
|
||||
|
@ -1,22 +1,23 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.internal.CordaRPCOpsImpl
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -42,11 +43,14 @@ class ContractUpgradeFlowTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork()
|
||||
val nodes = mockNet.createSomeNodes()
|
||||
val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override
|
||||
a = nodes.partyNodes[0]
|
||||
b = nodes.partyNodes[1]
|
||||
notary = nodes.notaryNode.info.notaryIdentity
|
||||
mockNet.runNetwork()
|
||||
|
||||
val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity }
|
||||
a.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
|
||||
b.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
|
||||
}
|
||||
|
||||
@After
|
||||
@ -173,8 +177,7 @@ class ContractUpgradeFlowTest {
|
||||
@Test
|
||||
fun `upgrade Cash to v2`() {
|
||||
// Create some cash.
|
||||
val anonymous = false
|
||||
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture
|
||||
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val stx = result.getOrThrow().stx
|
||||
val stateAndRef = stx.tx.outRef<Cash.State>(0)
|
||||
@ -200,7 +203,7 @@ class ContractUpgradeFlowTest {
|
||||
override val contract = CashV2()
|
||||
override val participants = owners
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
|
||||
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
|
||||
override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)"
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
|
||||
}
|
||||
@ -208,9 +211,6 @@ class ContractUpgradeFlowTest {
|
||||
override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {}
|
||||
|
||||
// Dummy Cash contract for testing.
|
||||
override val legalContractReference = SecureHash.sha256("")
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -17,8 +17,6 @@ class VaultUpdateTests {
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
}
|
||||
|
||||
private class DummyState : ContractState {
|
||||
|
@ -18,7 +18,7 @@ import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -53,13 +53,11 @@ private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, conten
|
||||
* @see NodeAttachmentService.importAttachment
|
||||
*/
|
||||
private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) {
|
||||
with(session) {
|
||||
withTransaction {
|
||||
update(AttachmentEntity().apply {
|
||||
attId = attachmentId
|
||||
content = data
|
||||
})
|
||||
}
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val attachment = session.get<NodeAttachmentService.DBAttachment>(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString())
|
||||
attachment?.let {
|
||||
attachment.content = data
|
||||
session.save(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.Before
|
||||
@ -19,8 +19,6 @@ val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash()
|
||||
|
||||
class TransactionSerializationTests : TestDependencyInjectionBase() {
|
||||
class TestCash : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("TestCash")
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||
outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc")
|
||||
processConfigurations = ['compile']
|
||||
sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin')
|
||||
includes = ['packages.md']
|
||||
}
|
||||
|
||||
task buildDocs(dependsOn: ['apidocs', 'makeDocs'])
|
||||
|
@ -7,11 +7,15 @@ set -xeo pipefail
|
||||
# Install the virtualenv
|
||||
if [ ! -d "virtualenv" ]
|
||||
then
|
||||
# If the canonical working directory contains whitespace, virtualenv installs broken scripts.
|
||||
# But if we pass in an absolute path that uses symlinks to avoid whitespace, that fixes the problem.
|
||||
# If you run this script manually (not via gradle) from such a path alias, it's available in PWD:
|
||||
absolutevirtualenv="$PWD/virtualenv"
|
||||
# Check if python2.7 is installed explicitly otherwise fall back to the default python
|
||||
if type "python2.7" > /dev/null; then
|
||||
virtualenv -p python2.7 virtualenv
|
||||
virtualenv -p python2.7 "$absolutevirtualenv"
|
||||
else
|
||||
virtualenv virtualenv
|
||||
virtualenv "$absolutevirtualenv"
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -29,4 +33,4 @@ if [ ! -d "virtualenv/lib/python2.7/site-packages/sphinx" ]
|
||||
then
|
||||
echo "Installing pip dependencies ... "
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
fi
|
||||
|
3
docs/packages.md
Normal file
3
docs/packages.md
Normal 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.
|
@ -22,9 +22,7 @@ The ``Contract`` interface is defined as follows:
|
||||
|
||||
Where:
|
||||
|
||||
* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this
|
||||
contract type are valid
|
||||
* ``legalContractReference`` is the hash of the legal prose contract that ``verify`` seeks to express in code
|
||||
* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this contract type are valid
|
||||
|
||||
verify()
|
||||
--------
|
||||
@ -187,8 +185,6 @@ execution of ``verify()``:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash")
|
||||
}
|
||||
|
||||
.. sourcecode:: java
|
||||
@ -209,9 +205,6 @@ execution of ``verify()``:
|
||||
// Transfer verification logic.
|
||||
}
|
||||
}
|
||||
|
||||
private final SecureHash legalContractReference = SecureHash.sha256("X contract hash");
|
||||
@Override public final SecureHash getLegalContractReference() { return legalContractReference; }
|
||||
}
|
||||
|
||||
Grouping states
|
||||
@ -297,13 +290,5 @@ We can now verify these groups individually:
|
||||
Legal prose
|
||||
-----------
|
||||
|
||||
Current, ``legalContractReference`` is simply the SHA-256 hash of a contract:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
In the future, a contract's legal prose will be included as an attachment instead.
|
||||
Currently, a ``Contract`` subtype may refer to the legal prose it implements via a ``LegalProseReference`` annotation.
|
||||
In the future, a contract's legal prose will be included as an attachment.
|
||||
|
@ -90,7 +90,7 @@ in order to initialise the ORM layer.
|
||||
Several examples of entities and mappings are provided in the codebase, including ``Cash.State`` and
|
||||
``CommercialPaper.State``. For example, here's the first version of the cash schema.
|
||||
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt
|
||||
:language: kotlin
|
||||
|
||||
Identity mapping
|
||||
|
@ -129,7 +129,7 @@ For example, here is a relatively complex state definition, for a state represen
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
@ -11,12 +11,11 @@ API: Transactions
|
||||
|
||||
Transaction workflow
|
||||
--------------------
|
||||
There are four states the transaction can occupy:
|
||||
At any time, a transaction can occupy one of three states:
|
||||
|
||||
* ``TransactionBuilder``, a builder for a transaction in construction
|
||||
* ``WireTransaction``, an immutable transaction
|
||||
* ``TransactionBuilder``, a builder for an in-construction transaction
|
||||
* ``SignedTransaction``, an immutable transaction with 1+ associated signatures
|
||||
* ``LedgerTransaction``, a transaction that can be checked for validity
|
||||
* ``LedgerTransaction``, an immutable transaction that can be checked for validity
|
||||
|
||||
Here are the possible transitions between transaction states:
|
||||
|
||||
@ -26,8 +25,8 @@ TransactionBuilder
|
||||
------------------
|
||||
Creating a builder
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
The first step when creating a transaction is to instantiate a ``TransactionBuilder``. We can create a builder for each
|
||||
transaction type as follows:
|
||||
The first step when creating a new transaction is to instantiate a ``TransactionBuilder``. We create a builder for a
|
||||
transaction as follows:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -342,7 +341,7 @@ SignedTransaction
|
||||
-----------------
|
||||
A ``SignedTransaction`` is a combination of:
|
||||
|
||||
* An immutable ``WireTransaction``
|
||||
* An immutable transaction
|
||||
* A list of signatures over that transaction
|
||||
|
||||
.. container:: codeset
|
||||
@ -357,45 +356,9 @@ transaction's signatures.
|
||||
|
||||
Verifying the transaction's contents
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't
|
||||
currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow
|
||||
called ``ReceiveTransactionFlow``. See :doc:`api-flows` for more details.
|
||||
|
||||
When verifying a ``SignedTransaction``, we don't verify the ``SignedTransaction`` *per se*, but rather the
|
||||
``WireTransaction`` it contains. We extract this ``WireTransaction`` as follows:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 31
|
||||
:end-before: DOCEND 31
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 31
|
||||
:end-before: DOCEND 31
|
||||
:dedent: 12
|
||||
|
||||
However, this still isn't enough. The ``WireTransaction`` holds its inputs as ``StateRef`` instances, and its
|
||||
attachments as hashes. These do not provide enough information to properly validate the transaction's contents. To
|
||||
resolve these into actual ``ContractState`` and ``Attachment`` instances, we need to use the ``ServiceHub`` to convert
|
||||
the ``WireTransaction`` into a ``LedgerTransaction``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 32
|
||||
:end-before: DOCEND 32
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 32
|
||||
:end-before: DOCEND 32
|
||||
:dedent: 12
|
||||
To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't currently have
|
||||
in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow called
|
||||
``ReceiveTransactionFlow``. See :doc:`api-flows` for more details.
|
||||
|
||||
We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output
|
||||
states:
|
||||
@ -414,8 +377,27 @@ states:
|
||||
:end-before: DOCEND 33
|
||||
:dedent: 12
|
||||
|
||||
We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in
|
||||
the contract. Here's an example of how we might do this:
|
||||
We can also conduct additional validation of the transaction, beyond what is performed by its contracts. However, the
|
||||
``SignedTransaction`` holds its inputs as ``StateRef`` instances, and its attachments as hashes. These do not provide
|
||||
enough information to properly validate the transaction's contents. To resolve these into actual ``ContractState`` and
|
||||
``Attachment`` instances, we need to use the ``ServiceHub`` to convert the ``SignedTransaction`` into a
|
||||
``LedgerTransaction``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 32
|
||||
:end-before: DOCEND 32
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 32
|
||||
:end-before: DOCEND 32
|
||||
:dedent: 12
|
||||
|
||||
We can now perform additional verification. Here's a simple example:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -558,8 +540,8 @@ Or using another one of our public keys, as follows:
|
||||
|
||||
Notarising and recording
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See
|
||||
:doc:`api-flows` for more details.
|
||||
Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See :doc:`api-flows` for
|
||||
more details.
|
||||
|
||||
Notary-change transactions
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -70,6 +70,15 @@ There are four implementations of this interface which can be chained together t
|
||||
|
||||
.. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`)
|
||||
|
||||
All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above.
|
||||
|
||||
All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes:
|
||||
|
||||
1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states.
|
||||
When chaining several criterias using AND / OR, the last value of this attribute will override any previous.
|
||||
2. Contract state types (``<Set<Class<out ContractState>>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all types).
|
||||
When chaining several criteria using AND / OR, all specified contract state types are combined into a single set.
|
||||
|
||||
An example of a custom query is illustrated here:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
@ -77,10 +86,6 @@ An example of a custom query is illustrated here:
|
||||
:start-after: DOCSTART VaultQueryExample20
|
||||
:end-before: DOCEND VaultQueryExample20
|
||||
|
||||
All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above.
|
||||
|
||||
All ``QueryCriteria`` implementations provide an explicitly specifiable ``StateStatus`` attribute which defaults to filtering on UNCONSUMED states.
|
||||
|
||||
.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples.
|
||||
|
||||
Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java.
|
||||
@ -317,22 +322,22 @@ Query for consumed deal states or linear ids, specify a paging specification and
|
||||
|
||||
Aggregations on cash using various functions:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
|
||||
:language: kotlin
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample21
|
||||
:end-before: DOCEND VaultJavaQueryExample21
|
||||
|
||||
Aggregations on cash grouped by currency for various functions:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
|
||||
:language: kotlin
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample22
|
||||
:end-before: DOCEND VaultJavaQueryExample22
|
||||
|
||||
Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
|
||||
:language: kotlin
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample23
|
||||
:end-before: DOCEND VaultJavaQueryExample23
|
||||
|
||||
|
@ -6,6 +6,15 @@ from the previous milestone release.
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
* Vault query common attributes (state status and contract state types) are now handled correctly when using composite
|
||||
criteria specifications. State status is overridable. Contract states types are aggregatable.
|
||||
|
||||
* Cash selection algorithm is now pluggable (with H2 being the default implementation)
|
||||
|
||||
* Removed usage of Requery ORM library (repalced with JPA/Hibernate)
|
||||
|
||||
* Vault Query performance improvement (replaced expensive per query SQL statement to obtain concrete state types
|
||||
with single query on start-up followed by dynamic updates using vault state observable))
|
||||
|
||||
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
|
||||
|
||||
@ -40,6 +49,25 @@ UNRELEASED
|
||||
If you specifically need well known identities, use the network map, which is the authoritative source of current well
|
||||
known identities.
|
||||
|
||||
* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`.
|
||||
|
||||
* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use
|
||||
`CashIssueFlow` instead.
|
||||
|
||||
* Some utility/extension functions (``sumOrThrow``, ``sumOrNull``, ``sumOrZero`` on ``Amount`` and ``Commodity``)
|
||||
have moved to be static methods on the classes themselves. This improves the API for Java users who no longer
|
||||
have to see or known about file-level FooKt style classes generated by the Kotlin compile, but means that IntelliJ
|
||||
no longer auto-suggests these extension functions in completion unless you add import lines for them yourself
|
||||
(this is Kotlin IDE bug KT-15286).
|
||||
|
||||
* ``:finance`` module now acting as a CorDapp with regard to flow registration, schemas and serializable types.
|
||||
|
||||
* ``WebServerPluginRegistry`` now has a ``customizeJSONSerialization`` which can be overridden to extend the REST JSON
|
||||
serializers. In particular the IRS demos must now register the ``BusinessCalendar`` serializers.
|
||||
|
||||
* Moved ``:finance`` gradle project files into a ``net.corda.finance`` package namespace.
|
||||
This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files.
|
||||
|
||||
Milestone 14
|
||||
------------
|
||||
|
||||
|
@ -60,7 +60,7 @@ Currently, users need special permissions to start flows via RPC. These permissi
|
||||
]
|
||||
|
||||
.. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without
|
||||
logging in. This will be changed in a future release.
|
||||
logging in. This will be changed in a future release.
|
||||
|
||||
Observables
|
||||
-----------
|
||||
|
@ -23,6 +23,11 @@ Cash shares a common superclass, ``OnLedgerAsset``, with the Commodity contract.
|
||||
assets which can be issued, moved and exited on chain, with the subclasses handling asset-specific data types and
|
||||
behaviour.
|
||||
|
||||
.. note:: Corda supports a pluggable cash selection algorithm by implementing the ``CashSelection`` interface.
|
||||
The default implementation uses an H2 specific query that can be overridden for different database providers.
|
||||
Please see ``CashSelectionH2Impl`` and its associated declaration in
|
||||
``META-INF\services\net.corda.finance.contracts.asset.CashSelection``
|
||||
|
||||
Commodity
|
||||
---------
|
||||
|
||||
|
@ -20,7 +20,6 @@ The Corda repository comprises the following folders:
|
||||
* **lib** contains some dependencies
|
||||
* **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence)
|
||||
* **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC
|
||||
* **node-schemas** contains entity classes used to represent relational database tables
|
||||
* **samples** contains all our Corda demos and code samples
|
||||
* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the
|
||||
mock network) implementation
|
||||
|
@ -64,7 +64,7 @@ applicationDistribution.into("bin") {
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
rpcUsers = [
|
||||
['username' : "user",
|
||||
'password' : "password",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
|
||||
'permissions' : ["StartFlow.net.corda.finance.flows.CashFlow"]]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,22 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultTrackBy
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.parallel
|
||||
import net.corda.testing.sequence
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class IntegrationTestingTutorial {
|
||||
@ -33,7 +25,8 @@ class IntegrationTestingTutorial {
|
||||
// START 1
|
||||
driver {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>()
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
))
|
||||
val bobUser = User("bobUser", "testPassword2", permissions = setOf(
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
@ -63,18 +56,21 @@ class IntegrationTestingTutorial {
|
||||
|
||||
// START 4
|
||||
val issueRef = OpaqueBytes.of(0)
|
||||
val futures = Stack<CordaFuture<*>>()
|
||||
(1..10).map { i ->
|
||||
thread {
|
||||
futures.push(aliceProxy.startFlow(::CashIssueFlow,
|
||||
i.DOLLARS,
|
||||
issueRef,
|
||||
bob.nodeInfo.legalIdentity,
|
||||
notary.nodeInfo.notaryIdentity
|
||||
).returnValue)
|
||||
}
|
||||
}.forEach(Thread::join) // Ensure the stack of futures is populated.
|
||||
futures.forEach { it.getOrThrow() }
|
||||
aliceProxy.startFlow(::CashIssueFlow,
|
||||
i.DOLLARS,
|
||||
issueRef,
|
||||
notary.nodeInfo.notaryIdentity
|
||||
).returnValue
|
||||
}.transpose().getOrThrow()
|
||||
// We wait for all of the issuances to run before we start making payments
|
||||
(1..10).map { i ->
|
||||
aliceProxy.startFlow(::CashPaymentFlow,
|
||||
i.DOLLARS,
|
||||
bob.nodeInfo.legalIdentity,
|
||||
true
|
||||
).returnValue
|
||||
}.transpose().getOrThrow()
|
||||
|
||||
bobVaultUpdates.expectEvents {
|
||||
parallel(
|
||||
|
@ -3,7 +3,6 @@ package net.corda.docs;
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.crypto.TransactionSignature;
|
||||
@ -21,6 +20,7 @@ import net.corda.core.transactions.WireTransaction;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
import net.corda.core.utilities.ProgressTracker.Step;
|
||||
import net.corda.core.utilities.UntrustworthyData;
|
||||
import net.corda.finance.contracts.asset.Cash;
|
||||
import net.corda.testing.contracts.DummyContract;
|
||||
import net.corda.testing.contracts.DummyState;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
@ -28,6 +28,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@ -280,7 +281,7 @@ public class FlowCookbookJava {
|
||||
TypeOnlyCommandData typeOnlyCommandData = new DummyContract.Commands.Create();
|
||||
// 2. Include additional data which can be used by the contract
|
||||
// during verification, alongside fulfilling the roles above
|
||||
CommandData commandDataWithData = new Cash.Commands.Issue(12345678);
|
||||
CommandData commandDataWithData = new Cash.Commands.Issue();
|
||||
|
||||
// Attachments are identified by their hash.
|
||||
// The attachment with the corresponding hash must have been
|
||||
@ -394,15 +395,18 @@ public class FlowCookbookJava {
|
||||
----------------------------*/
|
||||
progressTracker.setCurrentStep(TX_VERIFICATION);
|
||||
|
||||
// Verifying a transaction will also verify every transaction in the transaction's dependency chain, which will require
|
||||
// transaction data access on counterparty's node. The ``SendTransactionFlow`` can be used to automate the sending
|
||||
// and data vending process. The ``SendTransactionFlow`` will listen for data request until the transaction
|
||||
// is resolved and verified on the other side:
|
||||
// Verifying a transaction will also verify every transaction in
|
||||
// the transaction's dependency chain, which will require
|
||||
// transaction data access on counterparty's node. The
|
||||
// ``SendTransactionFlow`` can be used to automate the sending and
|
||||
// data vending process. The ``SendTransactionFlow`` will listen
|
||||
// for data request until the transaction is resolved and verified
|
||||
// on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx));
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx){
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx) {
|
||||
@Override
|
||||
protected void verifyDataRequest(@NotNull FetchDataFlow.Request.Data dataRequest) {
|
||||
// Extra request verification.
|
||||
@ -425,41 +429,43 @@ public class FlowCookbookJava {
|
||||
List<StateAndRef<DummyState>> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow<DummyState>(counterparty));
|
||||
// DOCEND 14
|
||||
|
||||
// A ``SignedTransaction`` is a pairing of a ``WireTransaction``
|
||||
// with signatures over this ``WireTransaction``. We don't verify
|
||||
// a signed transaction per se, but rather the ``WireTransaction``
|
||||
// it contains.
|
||||
// DOCSTART 31
|
||||
WireTransaction wireTx = twiceSignedTx.getTx();
|
||||
// DOCEND 31
|
||||
// Before we can verify the transaction, we need the
|
||||
// ``ServiceHub`` to use our node's local storage to resolve the
|
||||
// transaction's inputs and attachments into actual objects,
|
||||
// rather than just references. We do this by converting the
|
||||
// ``WireTransaction`` into a ``LedgerTransaction``.
|
||||
// DOCSTART 32
|
||||
LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub());
|
||||
// DOCEND 32
|
||||
// We can now verify the transaction.
|
||||
// DOCSTART 33
|
||||
ledgerTx.verify();
|
||||
// DOCEND 33
|
||||
try {
|
||||
|
||||
// We'll often want to perform our own additional verification
|
||||
// too. Just because a transaction is valid based on the contract
|
||||
// rules and requires our signature doesn't mean we have to
|
||||
// sign it! We need to make sure the transaction represents an
|
||||
// agreement we actually want to enter into.
|
||||
// DOCSTART 34
|
||||
DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData();
|
||||
if (outputState.getMagicNumber() != 777) {
|
||||
// ``FlowException`` is a special exception type. It will be
|
||||
// propagated back to any counterparty flows waiting for a
|
||||
// message from this flow, notifying them that the flow has
|
||||
// failed.
|
||||
throw new FlowException("We expected a magic number of 777.");
|
||||
// We can now verify the transaction to ensure that it satisfies
|
||||
// the contracts of all the transaction's input and output states.
|
||||
// DOCSTART 33
|
||||
twiceSignedTx.verify(getServiceHub());
|
||||
// DOCEND 33
|
||||
|
||||
// We'll often want to perform our own additional verification
|
||||
// too. Just because a transaction is valid based on the contract
|
||||
// rules and requires our signature doesn't mean we have to
|
||||
// sign it! We need to make sure the transaction represents an
|
||||
// agreement we actually want to enter into.
|
||||
|
||||
// To do this, we need to convert our ``SignedTransaction``
|
||||
// into a ``LedgerTransaction``. This will use our ServiceHub
|
||||
// to resolve the transaction's inputs and attachments into
|
||||
// actual objects, rather than just references.
|
||||
// DOCSTART 32
|
||||
LedgerTransaction ledgerTx = twiceSignedTx.toLedgerTransaction(getServiceHub());
|
||||
// DOCEND 32
|
||||
|
||||
// We can now perform our additional verification.
|
||||
// DOCSTART 34
|
||||
DummyState outputState = ledgerTx.outputsOfType(DummyState.class).get(0);
|
||||
if (outputState.getMagicNumber() != 777) {
|
||||
// ``FlowException`` is a special exception type. It will be
|
||||
// propagated back to any counterparty flows waiting for a
|
||||
// message from this flow, notifying them that the flow has
|
||||
// failed.
|
||||
throw new FlowException("We expected a magic number of 777.");
|
||||
}
|
||||
// DOCEND 34
|
||||
|
||||
} catch (GeneralSecurityException e) {
|
||||
// Handle this as required.
|
||||
}
|
||||
// DOCEND 34
|
||||
|
||||
// Of course, if you are not a required signer on the transaction,
|
||||
// you have no power to decide whether it is valid or not. If it
|
||||
|
@ -1,9 +1,6 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.client.rpc.notUsed
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultQueryBy
|
||||
@ -13,10 +10,12 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationCustomization
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.flows.CashExitFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashExitFlow
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
@ -128,7 +127,7 @@ fun generateTransactions(proxy: CordaRPCOps) {
|
||||
proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me)
|
||||
} else {
|
||||
val quantity = Math.abs(random.nextLong() % 1000)
|
||||
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, me, notary)
|
||||
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, notary)
|
||||
ownedQuantity += quantity
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user