mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge community-master
This commit is contained in:
commit
b6fd5ede58
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,6 +32,7 @@ lib/dokka.jar
|
||||
.idea/libraries
|
||||
.idea/shelf
|
||||
.idea/dataSources
|
||||
.idea/markdown-navigator
|
||||
/gradle-plugins/.idea/
|
||||
|
||||
# Include the -parameters compiler option by default in IntelliJ required for serialization.
|
||||
@ -66,7 +67,7 @@ lib/dokka.jar
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
**/out/
|
||||
/classes/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -40,6 +40,8 @@
|
||||
<module name="finance_test" target="1.8" />
|
||||
<module name="intellij-plugin_main" target="1.8" />
|
||||
<module name="intellij-plugin_test" target="1.8" />
|
||||
<module name="graphs_main" target="1.8" />
|
||||
<module name="graphs_test" target="1.8" />
|
||||
<module name="irs-demo_integrationTest" target="1.8" />
|
||||
<module name="irs-demo_main" target="1.8" />
|
||||
<module name="irs-demo_test" target="1.8" />
|
||||
|
26
build.gradle
26
build.gradle
@ -4,7 +4,7 @@ buildscript {
|
||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_release_version = "0.14-SNAPSHOT"
|
||||
ext.corda_release_version = "0.15-SNAPSHOT"
|
||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
||||
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
|
||||
ext.corda_platform_version = 1
|
||||
@ -31,7 +31,6 @@ buildscript {
|
||||
ext.log4j_version = '2.7'
|
||||
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
||||
ext.guava_version = constants.getProperty("guavaVersion")
|
||||
ext.quickcheck_version = '0.7'
|
||||
ext.okhttp_version = '3.5.0'
|
||||
ext.netty_version = '4.1.9.Final'
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
@ -168,22 +167,23 @@ repositories {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Corda root project currently produces a dummy cordapp when it shouldn't.
|
||||
// Required for building out the fat JAR.
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
cordaCompile project(':node')
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// Set to compile to ensure it exists now deploy nodes no longer relies on build
|
||||
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
// Set to corda compile to ensure it exists now deploy nodes no longer relies on build
|
||||
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
|
||||
// For the buildCordappDependenciesJar task
|
||||
runtime project(':client:jfx')
|
||||
runtime project(':client:mock')
|
||||
runtime project(':client:rpc')
|
||||
runtime project(':core')
|
||||
runtime project(':finance')
|
||||
runtime project(':webserver')
|
||||
cordaRuntime project(':client:jfx')
|
||||
cordaRuntime project(':client:mock')
|
||||
cordaRuntime project(':client:rpc')
|
||||
cordaRuntime project(':core')
|
||||
cordaRuntime project(':finance')
|
||||
cordaRuntime project(':webserver')
|
||||
testCompile project(':test-utils')
|
||||
}
|
||||
|
||||
@ -285,7 +285,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-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-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ 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"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
@ -18,12 +20,6 @@ dependencies {
|
||||
|
||||
testCompile project(path: ':core', configuration: 'testArtifacts')
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
||||
// TODO: Upgrade to junit-quickcheck 0.8, once it is released,
|
||||
// because it depends on org.javassist:javassist instead
|
||||
// of javassist:javassist.
|
||||
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||
testCompile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
|
||||
}
|
||||
|
||||
jar {
|
||||
@ -31,5 +27,5 @@ jar {
|
||||
}
|
||||
|
||||
publish {
|
||||
name = jar.baseName
|
||||
name jar.baseName
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package net.corda.jackson
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.core.*
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
@ -9,6 +11,8 @@ 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
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -17,9 +21,14 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigDecimal
|
||||
@ -38,32 +47,24 @@ object JacksonSupport {
|
||||
// If you change this API please update the docs in the docsite (json.rst)
|
||||
|
||||
interface PartyObjectMapper {
|
||||
@Deprecated("Use partyFromX500Name instead")
|
||||
fun partyFromName(partyName: String): Party?
|
||||
fun partyFromX500Name(name: X500Name): Party?
|
||||
fun partyFromKey(owningKey: PublicKey): Party?
|
||||
fun partiesFromName(query: String): Set<Party>
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
||||
override fun partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
|
||||
}
|
||||
|
||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
|
||||
override fun partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
||||
override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
|
||||
}
|
||||
|
||||
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
||||
override fun partiesFromName(query: String) = throw UnsupportedOperationException()
|
||||
@ -109,6 +110,10 @@ object JacksonSupport {
|
||||
// For X.500 distinguished names
|
||||
addDeserializer(X500Name::class.java, X500NameDeserializer)
|
||||
addSerializer(X500Name::class.java, X500NameSerializer)
|
||||
|
||||
// Mixins for transaction types to prevent some properties from being serialized
|
||||
setMixInAnnotation(SignedTransaction::class.java, SignedTransactionMixin::class.java)
|
||||
setMixInAnnotation(WireTransaction::class.java, WireTransactionMixin::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,7 +283,7 @@ object JacksonSupport {
|
||||
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) {
|
||||
if (calendarName != null) {
|
||||
generator.writeString(calendarName)
|
||||
} else {
|
||||
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
|
||||
@ -371,5 +376,24 @@ object JacksonSupport {
|
||||
gen.writeBinary(value.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SignedTransactionMixin {
|
||||
@JsonIgnore abstract fun getTxBits(): SerializedBytes<CoreTransaction>
|
||||
@JsonProperty("signatures") protected abstract fun getSigs(): List<TransactionSignature>
|
||||
@JsonProperty protected abstract fun getTransaction(): CoreTransaction
|
||||
@JsonIgnore abstract fun getTx(): WireTransaction
|
||||
@JsonIgnore abstract fun getNotaryChangeTx(): NotaryChangeWireTransaction
|
||||
@JsonIgnore abstract fun getInputs(): List<StateRef>
|
||||
@JsonIgnore abstract fun getNotary(): Party?
|
||||
@JsonIgnore abstract fun getId(): SecureHash
|
||||
@JsonIgnore abstract fun getRequiredSigningKeys(): Set<PublicKey>
|
||||
}
|
||||
|
||||
abstract class WireTransactionMixin {
|
||||
@JsonIgnore abstract fun getMerkleTree(): MerkleTree
|
||||
@JsonIgnore abstract fun getAvailableComponents(): List<Any>
|
||||
@JsonIgnore abstract fun getAvailableComponentHashes(): List<SecureHash>
|
||||
@JsonIgnore abstract fun getOutputStates(): List<ContractState>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,31 @@
|
||||
package net.corda.jackson
|
||||
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.pholser.junit.quickcheck.From
|
||||
import com.pholser.junit.quickcheck.Property
|
||||
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.testing.PublicKeyGenerator
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(JUnitQuickcheck::class)
|
||||
class JacksonSupportTest {
|
||||
class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
companion object {
|
||||
val mapper = JacksonSupport.createNonRpcMapper()
|
||||
}
|
||||
|
||||
@Property
|
||||
fun publicKeySerializingWorks(@From(PublicKeyGenerator::class) publicKey: PublicKey) {
|
||||
@Test
|
||||
fun publicKeySerializingWorks() {
|
||||
val publicKey = generateKeyPair().public
|
||||
val serialized = mapper.writeValueAsString(publicKey)
|
||||
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
|
||||
assertEquals(publicKey, parsedKey)
|
||||
@ -50,4 +54,24 @@ class JacksonSupportTest {
|
||||
val writer = mapper.writer().without(SerializationFeature.INDENT_OUTPUT)
|
||||
assertEquals("""{"notional":"25000000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$25000000"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeTransaction() {
|
||||
fun makeDummyTx(): SignedTransaction {
|
||||
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)).toWireTransaction()
|
||||
val signatures = TransactionSignature(
|
||||
ByteArray(1),
|
||||
ALICE_PUBKEY,
|
||||
SignatureMetadata(
|
||||
1,
|
||||
Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID
|
||||
)
|
||||
)
|
||||
return SignedTransaction(wtx, listOf(signatures))
|
||||
}
|
||||
|
||||
val writer = mapper.writer()
|
||||
// We don't particularly care about the serialized format, just need to make sure it completes successfully.
|
||||
writer.writeValueAsString(makeDummyTx())
|
||||
}
|
||||
}
|
||||
|
@ -62,5 +62,5 @@ jar {
|
||||
}
|
||||
|
||||
publish {
|
||||
name = jar.baseName
|
||||
name jar.baseName
|
||||
}
|
@ -2,15 +2,15 @@ package net.corda.client.jfx
|
||||
|
||||
import net.corda.client.jfx.model.NodeMonitorModel
|
||||
import net.corda.client.jfx.model.ProgressTrackingEvent
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
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.getOrThrow
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
@ -19,12 +19,9 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.CHARLIE
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
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
|
||||
@ -32,11 +29,9 @@ import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
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.node.DriverBasedTest
|
||||
import net.corda.testing.sequence
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
@ -53,7 +48,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var stateMachineUpdatesBob: Observable<StateMachineUpdate>
|
||||
lateinit var progressTracking: Observable<ProgressTrackingEvent>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update<ContractState>>
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var newNode: (X500Name) -> NodeInfo
|
||||
|
||||
@ -78,14 +73,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
|
||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
|
||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false)
|
||||
rpc = monitor.proxyObservable.value!!
|
||||
|
||||
val bobNodeHandle = startNode(BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
|
||||
bobNode = bobNodeHandle.nodeInfo
|
||||
val monitorBob = NodeMonitorModel()
|
||||
stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed()
|
||||
monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
|
||||
monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false)
|
||||
rpcBob = monitorBob.proxyObservable.value!!
|
||||
runTest()
|
||||
}
|
||||
@ -148,7 +143,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
var issueTx: SignedTransaction? = null
|
||||
var moveTx: SignedTransaction? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
stateMachineUpdates.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
@ -159,14 +154,13 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
// MOVE - N.B. There are other framework flows that happen in parallel for the remote resolve transactions flow
|
||||
expect(match = { it is StateMachineUpdate.Added && it.stateMachineInfo.flowLogicClassName == CashPaymentFlow::class.java.name }) { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
expect(match = { it is StateMachineUpdate.Removed && it.id == moveSmId }) {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ data class Diff<out T : ContractState>(
|
||||
* This model exposes the list of owned contract states.
|
||||
*/
|
||||
class ContractStateModel {
|
||||
private val vaultUpdates: Observable<Vault.Update> by observable(NodeMonitorModel::vaultUpdates)
|
||||
private val vaultUpdates: Observable<Vault.Update<ContractState>> by observable(NodeMonitorModel::vaultUpdates)
|
||||
|
||||
private val contractStatesDiff: Observable<Diff<ContractState>> = vaultUpdates.map {
|
||||
Diff(it.produced, it.consumed)
|
||||
|
@ -3,14 +3,13 @@ package net.corda.client.jfx.model
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineInfo
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import rx.Observable
|
||||
@ -32,14 +31,14 @@ data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val mess
|
||||
class NodeMonitorModel {
|
||||
|
||||
private val stateMachineUpdatesSubject = PublishSubject.create<StateMachineUpdate>()
|
||||
private val vaultUpdatesSubject = PublishSubject.create<Vault.Update>()
|
||||
private val vaultUpdatesSubject = PublishSubject.create<Vault.Update<ContractState>>()
|
||||
private val transactionsSubject = PublishSubject.create<SignedTransaction>()
|
||||
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
|
||||
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
|
||||
private val networkMapSubject = PublishSubject.create<MapChange>()
|
||||
|
||||
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
|
||||
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
|
||||
val vaultUpdates: Observable<Vault.Update<ContractState>> = vaultUpdatesSubject
|
||||
val transactions: Observable<SignedTransaction> = transactionsSubject
|
||||
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
|
||||
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
||||
@ -51,17 +50,18 @@ class NodeMonitorModel {
|
||||
* Register for updates to/from a given vault.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) {
|
||||
fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, initialiseSerialization: Boolean = true) {
|
||||
val client = CordaRPCClient(
|
||||
hostAndPort = nodeHostAndPort,
|
||||
configuration = CordaRPCClientConfiguration.default.copy(
|
||||
connectionMaxRetryInterval = 10.seconds
|
||||
)
|
||||
),
|
||||
initialiseSerialization = initialiseSerialization
|
||||
)
|
||||
val connection = client.start(username, password)
|
||||
val proxy = connection.proxy
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
|
||||
// Extract the flow tracking stream
|
||||
// TODO is there a nicer way of doing this? Stream of streams in general results in code like this...
|
||||
val currentProgressTrackerUpdates = stateMachines.mapNotNull { stateMachine ->
|
||||
@ -82,21 +82,22 @@ class NodeMonitorModel {
|
||||
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
|
||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
||||
|
||||
// Vault updates
|
||||
val (vault, vaultUpdates) = proxy.vaultAndUpdates()
|
||||
val initialVaultUpdate = Vault.Update(setOf(), vault.toSet())
|
||||
// Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates
|
||||
val (vaultSnapshot, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
||||
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||
val initialVaultUpdate = Vault.Update(setOf(), vaultSnapshot.states.toSet())
|
||||
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
|
||||
|
||||
// Transactions
|
||||
val (transactions, newTransactions) = proxy.verifiedTransactions()
|
||||
val (transactions, newTransactions) = proxy.verifiedTransactionsFeed()
|
||||
newTransactions.startWith(transactions).subscribe(transactionsSubject)
|
||||
|
||||
// SM -> TX mapping
|
||||
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
|
||||
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMappingFeed()
|
||||
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject)
|
||||
|
||||
// Parties on network
|
||||
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
|
||||
val (parties, futurePartyUpdate) = proxy.networkMapFeed()
|
||||
futurePartyUpdate.startWith(parties.map { MapChange.Added(it) }).subscribe(networkMapSubject)
|
||||
|
||||
proxyObservable.set(proxy)
|
||||
|
@ -23,13 +23,14 @@ object AmountBindings {
|
||||
) { sum -> Amount(sum.toLong(), token) }
|
||||
|
||||
fun exchange(
|
||||
currency: ObservableValue<Currency>,
|
||||
exchangeRate: ObservableValue<ExchangeRate>
|
||||
observableCurrency: ObservableValue<Currency>,
|
||||
observableExchangeRate: ObservableValue<ExchangeRate>
|
||||
): ObservableValue<Pair<Currency, (Amount<Currency>) -> Long>> {
|
||||
return EasyBind.combine(currency, exchangeRate) { currency, exchangeRate ->
|
||||
Pair(currency) { amount: Amount<Currency> ->
|
||||
(exchangeRate.rate(amount.token, currency) * amount.quantity).toLong()
|
||||
}
|
||||
return EasyBind.combine(observableCurrency, observableExchangeRate) { currency, exchangeRate ->
|
||||
Pair<Currency, (Amount<Currency>) -> Long>(
|
||||
currency,
|
||||
{ (quantity, _, token) -> (exchangeRate.rate(token, currency) * quantity).toLong() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.client.jfx.utils
|
||||
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.binding.BooleanBinding
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||
@ -10,7 +11,13 @@ import javafx.collections.MapChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import javafx.collections.transformation.FilteredList
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.services.Vault
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import java.util.function.Predicate
|
||||
|
||||
/**
|
||||
@ -313,3 +320,36 @@ fun <A> ObservableList<A>.firstOrDefault(default: ObservableValue<A?>, predicate
|
||||
fun <A> ObservableList<A>.firstOrNullObservable(predicate: (A) -> Boolean): ObservableValue<A?> {
|
||||
return Bindings.createObjectBinding({ this.firstOrNull(predicate) }, arrayOf(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the given Rx observable such that emissions are run on the JavaFX GUI thread. Use this when you have an Rx
|
||||
* observable that may emit in the background e.g. from the network and you wish to link it to the user interface.
|
||||
*
|
||||
* Note: you should use the returned observable, not the original one this method is called on.
|
||||
*/
|
||||
fun <T> Observable<T>.observeOnFXThread(): Observable<T> = observeOn(Schedulers.from(Platform::runLater))
|
||||
|
||||
/**
|
||||
* Given a [DataFeed] that contains the results of a vault query and a subsequent stream of changes, returns a JavaFX
|
||||
* [ObservableList] that mirrors the streamed results on the UI thread. Note that the paging is *not* respected by this
|
||||
* function: if a state is added that would not have appeared in the page in the initial query, it will still be added
|
||||
* to the observable list.
|
||||
*
|
||||
* @see toFXListOfStates if you want just the state objects and not the ledger pointers too.
|
||||
*/
|
||||
fun <T : ContractState> DataFeed<Vault.Page<T>, Vault.Update<T>>.toFXListOfStateRefs(): ObservableList<StateAndRef<T>> {
|
||||
val list = FXCollections.observableArrayList(snapshot.states)
|
||||
updates.observeOnFXThread().subscribe { (consumed, produced) ->
|
||||
list.removeAll(consumed)
|
||||
list.addAll(produced)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the same list as [toFXListOfStateRefs] but which contains the states instead of [StateAndRef] wrappers.
|
||||
* The same notes apply as with that function.
|
||||
*/
|
||||
fun <T : ContractState> DataFeed<Vault.Page<T>, Vault.Update<T>>.toFXListOfStates(): ObservableList<T> {
|
||||
return toFXListOfStateRefs().map { it.state.data }
|
||||
}
|
@ -30,5 +30,5 @@ jar {
|
||||
}
|
||||
|
||||
publish {
|
||||
name = jar.baseName
|
||||
name jar.baseName
|
||||
}
|
@ -167,21 +167,23 @@ fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Gen
|
||||
}
|
||||
|
||||
|
||||
fun <A> Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator<A>) = Generator<List<A>> {
|
||||
fun <A> Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator<A>, atLeastOne: Boolean = false) = Generator<List<A>> {
|
||||
val chance = (meanSize - 1) / meanSize
|
||||
val result = mutableListOf<A>()
|
||||
var finish = false
|
||||
while (!finish) {
|
||||
val result = Generator.doubleRange(0.0, 1.0).generate(it).flatMap { value ->
|
||||
val res = Generator.doubleRange(0.0, 1.0).generate(it).flatMap { value ->
|
||||
if (value < chance) {
|
||||
generator.generate(it).map { result.add(it) }
|
||||
} else {
|
||||
finish = true
|
||||
Try.Success(Unit)
|
||||
if (result.isEmpty() && atLeastOne) {
|
||||
generator.generate(it).map { result.add(it) }
|
||||
} else Try.Success(Unit)
|
||||
}
|
||||
}
|
||||
if (result is Try.Failure) {
|
||||
return@Generator result
|
||||
if (res is Try.Failure) {
|
||||
return@Generator res
|
||||
}
|
||||
}
|
||||
Try.Success(result)
|
||||
|
@ -24,6 +24,11 @@ sourceSets {
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
java {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/java')
|
||||
}
|
||||
}
|
||||
smokeTest {
|
||||
kotlin {
|
||||
@ -33,6 +38,11 @@ sourceSets {
|
||||
runtimeClasspath += main.output
|
||||
srcDir file('src/smoke-test/kotlin')
|
||||
}
|
||||
java {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
srcDir file('src/smoke-test/java')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,5 +92,5 @@ jar {
|
||||
}
|
||||
|
||||
publish {
|
||||
name = jar.baseName
|
||||
name jar.baseName
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
package net.corda.client.rpc;
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.client.rpc.internal.RPCClient;
|
||||
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.node.internal.Node;
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService;
|
||||
import net.corda.nodeapi.User;
|
||||
import net.corda.testing.node.NodeBasedTest;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
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.testing.TestConstants.getALICE;
|
||||
|
||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
private List<String> perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class));
|
||||
private Set<String> permSet = new HashSet<>(perms);
|
||||
private User rpcUser = new User("user1", "test", permSet);
|
||||
|
||||
private Node node;
|
||||
private CordaRPCClient client;
|
||||
private RPCClient.RPCConnection<CordaRPCOps> connection = null;
|
||||
private CordaRPCOps rpcProxy;
|
||||
|
||||
private void login(String username, String password) {
|
||||
connection = client.start(username, password);
|
||||
rpcProxy = connection.getProxy();
|
||||
}
|
||||
|
||||
@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());
|
||||
node = nodeFuture.get();
|
||||
client = new CordaRPCClient(node.getConfiguration().getRpcAddress(), null, getDefault(), false);
|
||||
}
|
||||
|
||||
@After
|
||||
public void done() throws IOException {
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogin() {
|
||||
login(rpcUser.getUsername(), rpcUser.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
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());
|
||||
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");
|
||||
}
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
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.getOrThrow
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashException
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
@ -15,13 +17,13 @@ import net.corda.node.internal.Node
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
@ -42,7 +44,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||
client = CordaRPCClient(node.configuration.rpcAddress!!)
|
||||
client = CordaRPCClient(node.configuration.rpcAddress!!, initialiseSerialization = false)
|
||||
}
|
||||
|
||||
@After
|
||||
@ -117,20 +119,18 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
println("Started issuing cash, waiting on result")
|
||||
flowHandle.returnValue.get()
|
||||
|
||||
val finishCash = proxy.getCashBalances()
|
||||
println("Cash Balances: $finishCash")
|
||||
assertEquals(1, finishCash.size)
|
||||
assertEquals(123.DOLLARS, finishCash.get(Currency.getInstance("USD")))
|
||||
val cashDollars = proxy.getCashBalance(USD)
|
||||
println("Balance: $cashDollars")
|
||||
assertEquals(123.DOLLARS, cashDollars)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow initiator via RPC`() {
|
||||
login(rpcUser.username, rpcUser.password)
|
||||
val proxy = connection!!.proxy
|
||||
val smUpdates = proxy.stateMachinesAndUpdates()
|
||||
var countRpcFlows = 0
|
||||
var countShellFlows = 0
|
||||
smUpdates.second.subscribe {
|
||||
proxy.stateMachinesFeed().updates.subscribe {
|
||||
if (it is StateMachineUpdate.Added) {
|
||||
val initiator = it.stateMachineInfo.initiator
|
||||
if (initiator is FlowInitiator.RPC)
|
||||
|
@ -1,24 +1,15 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.future
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.millis
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.RPCKryo
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.poll
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
@ -29,10 +20,7 @@ import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class RPCStabilityTests {
|
||||
@ -238,9 +226,7 @@ class RPCStabilityTests {
|
||||
assertEquals("pong", client.ping())
|
||||
serverFollower.shutdown()
|
||||
startRpcServer<ReconnectOps>(ops = ops, customPort = serverPort).getOrThrow()
|
||||
val pingFuture = future {
|
||||
client.ping()
|
||||
}
|
||||
val pingFuture = ForkJoinPool.commonPool().fork(client::ping)
|
||||
assertEquals("pong", pingFuture.getOrThrow(10.seconds))
|
||||
clientFollower.shutdown() // Driver would do this after the new server, causing hang.
|
||||
}
|
||||
@ -274,9 +260,9 @@ class RPCStabilityTests {
|
||||
).get()
|
||||
|
||||
val numberOfClients = 4
|
||||
val clients = Futures.allAsList((1 .. numberOfClients).map {
|
||||
val clients = (1 .. numberOfClients).map {
|
||||
startRandomRpcClient<TrackSubscriberOps>(server.broker.hostAndPort!!)
|
||||
}).get()
|
||||
}.transpose().get()
|
||||
|
||||
// Poll until all clients connect
|
||||
pollUntilClientNumber(server, numberOfClients)
|
||||
@ -305,16 +291,8 @@ class RPCStabilityTests {
|
||||
return Observable.interval(interval.toMillis(), TimeUnit.MILLISECONDS).map { chunk }
|
||||
}
|
||||
}
|
||||
val dummyObservableSerialiser = object : Serializer<Observable<Any>>() {
|
||||
override fun write(kryo: Kryo?, output: Output?, `object`: Observable<Any>?) {
|
||||
}
|
||||
override fun read(kryo: Kryo?, input: Input?, type: Class<Observable<Any>>?): Observable<Any> {
|
||||
return Observable.empty()
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun `slow consumers are kicked`() {
|
||||
val kryoPool = KryoPool.Builder { RPCKryo(dummyObservableSerialiser) }.build()
|
||||
rpcDriver {
|
||||
val server = startRpcServer(maxBufferedBytesPerClient = 10 * 1024 * 1024, ops = SlowConsumerRPCOpsImpl()).get()
|
||||
|
||||
@ -339,7 +317,7 @@ class RPCStabilityTests {
|
||||
methodName = SlowConsumerRPCOps::streamAtInterval.name,
|
||||
arguments = listOf(10.millis, 123456)
|
||||
)
|
||||
request.writeToClientMessage(kryoPool, message)
|
||||
request.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message)
|
||||
producer.send(message)
|
||||
session.commit()
|
||||
|
||||
|
@ -2,11 +2,17 @@ package net.corda.client.rpc
|
||||
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.client.rpc.serialization.KryoClientSerializationScheme
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.serialization.AMQPClientSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import java.time.Duration
|
||||
|
||||
/** @see RPCClient.RPCConnection */
|
||||
@ -35,11 +41,22 @@ data class CordaRPCClientConfiguration(
|
||||
class CordaRPCClient(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default,
|
||||
initialiseSerialization: Boolean = true
|
||||
) {
|
||||
init {
|
||||
// Init serialization. It's plausible there are multiple clients in a single JVM, so be tolerant of
|
||||
// others having registered first.
|
||||
// TODO: allow clients to have serialization factory etc injected and align with RPC protocol version?
|
||||
if (initialiseSerialization) {
|
||||
initialiseSerialization()
|
||||
}
|
||||
}
|
||||
|
||||
private val rpcClient = RPCClient<CordaRPCOps>(
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
|
||||
configuration.toRpcClientConfiguration()
|
||||
configuration.toRpcClientConfiguration(),
|
||||
KRYO_RPC_CLIENT_CONTEXT
|
||||
)
|
||||
|
||||
fun start(username: String, password: String): CordaRPCConnection {
|
||||
@ -49,4 +66,21 @@ class CordaRPCClient(
|
||||
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
|
||||
return start(username, password).use(block)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun initialiseSerialization() {
|
||||
try {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme(this))
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
||||
} catch(e: IllegalStateException) {
|
||||
// Check that it's registered as we expect
|
||||
check(SerializationDefaults.SERIALIZATION_FACTORY is SerializationFactoryImpl) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
||||
check((SerializationDefaults.SERIALIZATION_FACTORY as SerializationFactoryImpl).alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package net.corda.client.rpc.internal
|
||||
|
||||
import net.corda.core.logElapsedTime
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.minutes
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.seconds
|
||||
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.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
@ -85,13 +87,15 @@ data class RPCClientConfiguration(
|
||||
*/
|
||||
class RPCClient<I : RPCOps>(
|
||||
val transport: TransportConfiguration,
|
||||
val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default
|
||||
val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default,
|
||||
val serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
|
||||
) {
|
||||
constructor(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
configuration: RPCClientConfiguration = RPCClientConfiguration.default
|
||||
) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration)
|
||||
configuration: RPCClientConfiguration = RPCClientConfiguration.default,
|
||||
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
|
||||
) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration, serializationContext)
|
||||
|
||||
companion object {
|
||||
private val log = loggerFor<RPCClient<*>>()
|
||||
@ -146,7 +150,7 @@ class RPCClient<I : RPCOps>(
|
||||
minLargeMessageSize = rpcConfiguration.maxFileSize
|
||||
}
|
||||
|
||||
val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass)
|
||||
val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass, serializationContext)
|
||||
try {
|
||||
proxyHandler.start()
|
||||
|
||||
|
@ -4,18 +4,19 @@ import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import com.google.common.cache.Cache
|
||||
import com.google.common.cache.CacheBuilder
|
||||
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.ThreadBox
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.core.internal.LazyStickyPool
|
||||
import net.corda.core.internal.LifeCycle
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.KryoPoolWithContext
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.nodeapi.*
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
@ -61,7 +62,8 @@ class RPCClientProxyHandler(
|
||||
private val rpcPassword: String,
|
||||
private val serverLocator: ServerLocator,
|
||||
private val clientAddress: SimpleString,
|
||||
private val rpcOpsClass: Class<out RPCOps>
|
||||
private val rpcOpsClass: Class<out RPCOps>,
|
||||
serializationContext: SerializationContext
|
||||
) : InvocationHandler {
|
||||
|
||||
private enum class State {
|
||||
@ -74,9 +76,6 @@ class RPCClientProxyHandler(
|
||||
|
||||
private companion object {
|
||||
val log = loggerFor<RPCClientProxyHandler>()
|
||||
// Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context
|
||||
// to do that. However it may still be used for serialisation of RPC requests and related messages.
|
||||
val kryoPool: KryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build()
|
||||
// To check whether toString() is being invoked
|
||||
val toStringMethod: Method = Object::toString.javaMethod!!
|
||||
}
|
||||
@ -85,7 +84,7 @@ class RPCClientProxyHandler(
|
||||
private var reaperExecutor: ScheduledExecutorService? = null
|
||||
|
||||
// A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering.
|
||||
private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build()
|
||||
private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").setDaemon(true).build()
|
||||
private val observationExecutorPool = LazyStickyPool(rpcConfiguration.observationExecutorPoolSize) {
|
||||
Executors.newFixedThreadPool(1, observationExecutorThreadFactory)
|
||||
}
|
||||
@ -109,11 +108,10 @@ class RPCClientProxyHandler(
|
||||
private val observablesToReap = ThreadBox(object {
|
||||
var observables = ArrayList<RPCApi.ObservableId>()
|
||||
})
|
||||
// A Kryo pool that automatically adds the observable context when an instance is requested.
|
||||
private val kryoPoolWithObservableContext = RpcClientObservableSerializer.createPoolWithContext(kryoPool, observableContext)
|
||||
private val serializationContextWithObservableContext = RpcClientObservableSerializer.createContext(serializationContext, observableContext)
|
||||
|
||||
private fun createRpcObservableMap(): RpcObservableMap {
|
||||
val onObservableRemove = RemovalListener<RPCApi.ObservableId, UnicastSubject<Notification<Any>>> {
|
||||
val onObservableRemove = RemovalListener<RPCApi.ObservableId, UnicastSubject<Notification<*>>> {
|
||||
val rpcCallSite = callSiteMap?.remove(it.key.toLong)
|
||||
if (it.cause == RemovalCause.COLLECTED) {
|
||||
log.warn(listOf(
|
||||
@ -194,7 +192,7 @@ class RPCClientProxyHandler(
|
||||
val replyFuture = SettableFuture.create<Any>()
|
||||
sessionAndProducerPool.run {
|
||||
val message = it.session.createMessage(false)
|
||||
request.writeToClientMessage(kryoPool, message)
|
||||
request.writeToClientMessage(serializationContextWithObservableContext, message)
|
||||
|
||||
log.debug {
|
||||
val argumentsString = arguments?.joinToString() ?: ""
|
||||
@ -221,7 +219,7 @@ class RPCClientProxyHandler(
|
||||
|
||||
// The handler for Artemis messages.
|
||||
private fun artemisMessageHandler(message: ClientMessage) {
|
||||
val serverToClient = RPCApi.ServerToClient.fromClientMessage(kryoPoolWithObservableContext, message)
|
||||
val serverToClient = RPCApi.ServerToClient.fromClientMessage(serializationContextWithObservableContext, message)
|
||||
log.debug { "Got message from RPC server $serverToClient" }
|
||||
when (serverToClient) {
|
||||
is RPCApi.ServerToClient.RpcReply -> {
|
||||
@ -338,7 +336,7 @@ class RPCClientProxyHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private typealias RpcObservableMap = Cache<RPCApi.ObservableId, UnicastSubject<Notification<Any>>>
|
||||
private typealias RpcObservableMap = Cache<RPCApi.ObservableId, UnicastSubject<Notification<*>>>
|
||||
private typealias RpcReplyMap = ConcurrentHashMap<RPCApi.RpcRequestId, SettableFuture<Any?>>
|
||||
private typealias CallSiteMap = ConcurrentHashMap<Long, Throwable?>
|
||||
|
||||
@ -348,7 +346,7 @@ private typealias CallSiteMap = ConcurrentHashMap<Long, Throwable?>
|
||||
* @param observableMap holds the Observables that are ultimately exposed to the user.
|
||||
* @param hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to.
|
||||
*/
|
||||
private data class ObservableContext(
|
||||
data class ObservableContext(
|
||||
val callSiteMap: CallSiteMap?,
|
||||
val observableMap: RpcObservableMap,
|
||||
val hardReferenceStore: MutableSet<Observable<*>>
|
||||
@ -357,17 +355,17 @@ private data class ObservableContext(
|
||||
/**
|
||||
* A [Serializer] to deserialise Observables once the corresponding Kryo instance has been provided with an [ObservableContext].
|
||||
*/
|
||||
private object RpcClientObservableSerializer : Serializer<Observable<Any>>() {
|
||||
object RpcClientObservableSerializer : Serializer<Observable<*>>() {
|
||||
private object RpcObservableContextKey
|
||||
fun createPoolWithContext(kryoPool: KryoPool, observableContext: ObservableContext): KryoPool {
|
||||
return KryoPoolWithContext(kryoPool, RpcObservableContextKey, observableContext)
|
||||
|
||||
fun createContext(serializationContext: SerializationContext, observableContext: ObservableContext): SerializationContext {
|
||||
return serializationContext.withProperty(RpcObservableContextKey, observableContext)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Observable<Any>>): Observable<Any> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Observable<*>>): Observable<Any> {
|
||||
val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext
|
||||
val observableId = RPCApi.ObservableId(input.readLong(true))
|
||||
val observable = UnicastSubject.create<Notification<Any>>()
|
||||
val observable = UnicastSubject.create<Notification<*>>()
|
||||
require(observableContext.observableMap.getIfPresent(observableId) == null) {
|
||||
"Multiple Observables arrived with the same ID $observableId"
|
||||
}
|
||||
@ -384,7 +382,7 @@ private object RpcClientObservableSerializer : Serializer<Observable<Any>>() {
|
||||
}.dematerialize()
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, observable: Observable<Any>) {
|
||||
override fun write(kryo: Kryo, output: Output, observable: Observable<*>) {
|
||||
throw UnsupportedOperationException("Cannot serialise Observables on the client side")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
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) {
|
||||
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 }
|
||||
}.build()
|
||||
}
|
||||
|
||||
// We're on the client and don't have access to server classes.
|
||||
override fun rpcServerKryoPool(context: SerializationContext): KryoPool {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
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.nodeapi.User;
|
||||
import net.corda.smoketesting.NodeConfig;
|
||||
import net.corda.smoketesting.NodeProcess;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static kotlin.test.AssertionsKt.assertEquals;
|
||||
import static net.corda.contracts.GetBalances.getCashBalance;
|
||||
|
||||
public class StandaloneCordaRPCJavaClientTest {
|
||||
private List<String> perms = Collections.singletonList("ALL");
|
||||
private Set<String> permSet = new HashSet<>(perms);
|
||||
private User rpcUser = new User("user1", "test", permSet);
|
||||
|
||||
private AtomicInteger port = new AtomicInteger(15000);
|
||||
|
||||
private NodeProcess notary;
|
||||
private CordaRPCOps rpcProxy;
|
||||
private CordaRPCConnection connection;
|
||||
private NodeInfo notaryNode;
|
||||
|
||||
private NodeConfig notaryConfig = new NodeConfig(
|
||||
new X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"),
|
||||
port.getAndIncrement(),
|
||||
port.getAndIncrement(),
|
||||
port.getAndIncrement(),
|
||||
Collections.singletonList("corda.notary.validating"),
|
||||
Arrays.asList(rpcUser),
|
||||
null
|
||||
);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
notary = new NodeProcess.Factory().create(notaryConfig);
|
||||
connection = notary.connect();
|
||||
rpcProxy = connection.getProxy();
|
||||
notaryNode = fetchNotaryIdentity();
|
||||
}
|
||||
|
||||
@After
|
||||
public void done() {
|
||||
try {
|
||||
connection.close();
|
||||
} finally {
|
||||
notary.close();
|
||||
}
|
||||
}
|
||||
|
||||
private NodeInfo fetchNotaryIdentity() {
|
||||
DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> nodeDataFeed = rpcProxy.networkMapFeed();
|
||||
return nodeDataFeed.getSnapshot().get(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
|
||||
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
|
||||
|
||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||
dollars123, OpaqueBytes.of("1".getBytes()),
|
||||
notaryNode.getLegalIdentity(), notaryNode.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");
|
||||
}
|
||||
}
|
@ -5,19 +5,19 @@ 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.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.POUNDS
|
||||
import net.corda.core.contracts.SWISS_FRANCS
|
||||
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.getOrThrow
|
||||
import net.corda.core.internal.InputStreamAndHash
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.sizedInputStreamAndHash
|
||||
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.nodeapi.User
|
||||
@ -35,6 +35,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StandaloneCordaRPClientTest {
|
||||
private companion object {
|
||||
@ -78,7 +79,7 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
@Test
|
||||
fun `test attachments`() {
|
||||
val attachment = sizedInputStreamAndHash(attachmentSize)
|
||||
val attachment = InputStreamAndHash.createInMemoryTestZip(attachmentSize, 1)
|
||||
assertFalse(rpcProxy.attachmentExists(attachment.sha256))
|
||||
val id = WrapperStream(attachment.inputStream).use { rpcProxy.uploadAttachment(it) }
|
||||
assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash")
|
||||
@ -117,38 +118,38 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
@Test
|
||||
fun `test state machines`() {
|
||||
val (stateMachines, updates) = rpcProxy.stateMachinesAndUpdates()
|
||||
val (stateMachines, updates) = rpcProxy.stateMachinesFeed()
|
||||
assertEquals(0, stateMachines.size)
|
||||
|
||||
var updateCount = 0
|
||||
val updateCount = AtomicInteger(0)
|
||||
updates.subscribe { update ->
|
||||
if (update is StateMachineUpdate.Added) {
|
||||
log.info("StateMachine>> Id=${update.id}")
|
||||
++updateCount
|
||||
updateCount.incrementAndGet()
|
||||
}
|
||||
}
|
||||
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
assertEquals(1, updateCount)
|
||||
assertEquals(1, updateCount.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test vault track by`() {
|
||||
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>()
|
||||
assertEquals(0, vault.states.size)
|
||||
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>(paging = PageSpecification(DEFAULT_PAGE_NUM))
|
||||
assertEquals(0, vault.totalStatesAvailable)
|
||||
|
||||
var updateCount = 0
|
||||
val updateCount = AtomicInteger(0)
|
||||
vaultUpdates.subscribe { update ->
|
||||
log.info("Vault>> FlowId=${update.flowId}")
|
||||
++updateCount
|
||||
updateCount.incrementAndGet()
|
||||
}
|
||||
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
assertNotEquals(0, updateCount)
|
||||
assertNotEquals(0, updateCount.get())
|
||||
|
||||
// Check that this cash exists in the vault
|
||||
val cashBalance = rpcProxy.getCashBalances()
|
||||
@ -177,10 +178,27 @@ class StandaloneCordaRPClientTest {
|
||||
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
|
||||
|
||||
// Check that this cash exists in the vault
|
||||
val cashBalance = rpcProxy.getCashBalances()
|
||||
log.info("Cash Balances: $cashBalance")
|
||||
assertEquals(1, cashBalance.size)
|
||||
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
|
||||
val cashBalances = rpcProxy.getCashBalances()
|
||||
log.info("Cash Balances: $cashBalances")
|
||||
assertEquals(1, cashBalances.size)
|
||||
assertEquals(629.POUNDS, cashBalances[Currency.getInstance("GBP")])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test cash balances`() {
|
||||
val startCash = rpcProxy.getCashBalances()
|
||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||
|
||||
val flowHandle = rpcProxy.startFlow(::CashIssueFlow,
|
||||
629.DOLLARS, OpaqueBytes.of(0),
|
||||
notaryNode.legalIdentity, notaryNode.legalIdentity
|
||||
)
|
||||
println("Started issuing cash, waiting on result")
|
||||
flowHandle.returnValue.get()
|
||||
|
||||
val balance = rpcProxy.getCashBalance(USD)
|
||||
println("Balance: " + balance)
|
||||
assertEquals(629.DOLLARS, balance)
|
||||
}
|
||||
|
||||
private fun fetchNotaryIdentity(): NodeInfo {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.kotlin.rpc
|
||||
|
||||
import net.corda.core.div
|
||||
import net.corda.core.internal.div
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.flatMap
|
||||
import net.corda.core.map
|
||||
import net.corda.core.internal.concurrent.flatMap
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.User
|
||||
@ -44,13 +44,13 @@ open class AbstractRPCTest {
|
||||
startInVmRpcClient<I>(rpcUser.username, rpcUser.password, clientConfiguration).map {
|
||||
TestProxy(it, { startInVmArtemisSession(rpcUser.username, rpcUser.password) })
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
RPCTestMode.Netty ->
|
||||
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server ->
|
||||
startRpcClient<I>(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
|
||||
TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.concurrent.doneFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.thenMatch
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.messaging.getRpcContext
|
||||
import net.corda.nodeapi.RPCSinceVersion
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
@ -27,7 +27,9 @@ import kotlin.test.assertTrue
|
||||
class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
// TODO: Test that timeouts work
|
||||
|
||||
private fun RPCDriverExposedDSLInterface.testProxy() = testProxy<TestOps>(TestOpsImpl()).ops
|
||||
private fun RPCDriverExposedDSLInterface.testProxy(): TestOps {
|
||||
return testProxy<TestOps>(TestOpsImpl()).ops
|
||||
}
|
||||
|
||||
interface TestOps : RPCOps {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@ -41,9 +43,9 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
|
||||
fun makeComplicatedObservable(): Observable<Pair<String, Observable<String>>>
|
||||
|
||||
fun makeListenableFuture(): ListenableFuture<Int>
|
||||
fun makeListenableFuture(): CordaFuture<Int>
|
||||
|
||||
fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>>
|
||||
fun makeComplicatedListenableFuture(): CordaFuture<Pair<String, CordaFuture<String>>>
|
||||
|
||||
@RPCSinceVersion(2)
|
||||
fun addedLater()
|
||||
@ -52,7 +54,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
}
|
||||
|
||||
private lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
|
||||
private lateinit var complicatedListenableFuturee: ListenableFuture<Pair<String, ListenableFuture<String>>>
|
||||
private lateinit var complicatedListenableFuturee: CordaFuture<Pair<String, CordaFuture<String>>>
|
||||
|
||||
inner class TestOpsImpl : TestOps {
|
||||
override val protocolVersion = 1
|
||||
@ -60,9 +62,9 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
override fun void() {}
|
||||
override fun someCalculation(str: String, num: Int) = "$str $num"
|
||||
override fun makeObservable(): Observable<Int> = Observable.just(1, 2, 3, 4)
|
||||
override fun makeListenableFuture(): ListenableFuture<Int> = Futures.immediateFuture(1)
|
||||
override fun makeListenableFuture() = doneFuture(1)
|
||||
override fun makeComplicatedObservable() = complicatedObservable
|
||||
override fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>> = complicatedListenableFuturee
|
||||
override fun makeComplicatedListenableFuture() = complicatedListenableFuturee
|
||||
override fun addedLater(): Unit = throw IllegalStateException()
|
||||
override fun captureUser(): String = getRpcContext().currentUser.username
|
||||
}
|
||||
@ -150,10 +152,10 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
fun `complex ListenableFuture`() {
|
||||
rpcDriver {
|
||||
val proxy = testProxy()
|
||||
val serverQuote = SettableFuture.create<Pair<String, ListenableFuture<String>>>()
|
||||
val serverQuote = openFuture<Pair<String, CordaFuture<String>>>()
|
||||
complicatedListenableFuturee = serverQuote
|
||||
|
||||
val twainQuote = "Mark Twain" to Futures.immediateFuture("I have never let my schooling interfere with my education.")
|
||||
val twainQuote = "Mark Twain" to doneFuture("I have never let my schooling interfere with my education.")
|
||||
|
||||
val clientQuotes = LinkedBlockingQueue<String>()
|
||||
val clientFuture = proxy.makeComplicatedListenableFuture()
|
||||
|
@ -1,10 +1,10 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.future
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.millis
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
@ -17,6 +17,7 @@ import rx.subjects.UnicastSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
@ -68,7 +69,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
Observable.empty<ObservableRose<Int>>()
|
||||
} else {
|
||||
val publish = UnicastSubject.create<ObservableRose<Int>>()
|
||||
future {
|
||||
ForkJoinPool.commonPool().fork {
|
||||
(1..branchingFactor).toList().parallelStream().forEach {
|
||||
publish.onNext(getParallelObservableTree(depth - 1, branchingFactor))
|
||||
}
|
||||
@ -105,7 +106,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
val done = CountDownLatch(numberOfBlockedCalls)
|
||||
// Start a couple of blocking RPC calls
|
||||
(1..numberOfBlockedCalls).forEach {
|
||||
future {
|
||||
ForkJoinPool.commonPool().fork {
|
||||
proxy.ops.waitLatch(id)
|
||||
done.countDown()
|
||||
}
|
||||
|
@ -3,12 +3,11 @@ package net.corda.client.rpc
|
||||
import com.google.common.base.Stopwatch
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.minutes
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.utilities.div
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.testing.performance.div
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.driver.ShutdownManager
|
||||
import net.corda.testing.measure
|
||||
import net.corda.testing.performance.startPublishingFixedRateInjector
|
||||
import net.corda.testing.performance.startReporter
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.node.services.messaging.getRpcContext
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.nodeapi.PermissionException
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
|
@ -1,4 +1,4 @@
|
||||
gradlePluginsVersion=0.13.2
|
||||
gradlePluginsVersion=0.13.6
|
||||
kotlinVersion=1.1.1
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
|
@ -17,3 +17,7 @@ dependencies {
|
||||
// Bouncy Castle: for X.500 distinguished name manipulation
|
||||
compile "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version"
|
||||
}
|
||||
|
||||
publish {
|
||||
name project.name
|
||||
}
|
@ -4,6 +4,7 @@ import static java.util.Collections.emptyList;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import com.typesafe.config.ConfigValueFactory;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -86,6 +87,6 @@ public class CordformNode implements NodeDefinition {
|
||||
* @param id The (0-based) BFT replica ID.
|
||||
*/
|
||||
public void bftReplicaId(Integer id) {
|
||||
config = config.withValue("bftReplicaId", ConfigValueFactory.fromAnyRef(id));
|
||||
config = config.withValue("bftSMaRt", ConfigValueFactory.fromMap(Collections.singletonMap("replicaId", id)));
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-jpa'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda core'
|
||||
|
||||
@ -40,22 +41,12 @@ dependencies {
|
||||
// AssertJ: for fluent assertions for testing
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
// TODO: Upgrade to junit-quickcheck 0.8, once it is released,
|
||||
// because it depends on org.javassist:javassist instead
|
||||
// of javassist:javassist.
|
||||
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||
testCompile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
|
||||
|
||||
// Guava: Google utilities library.
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// RxJava: observable streams of events.
|
||||
compile "io.reactivex:rxjava:$rxjava_version"
|
||||
|
||||
// Kryo: object graph serialization.
|
||||
compile "com.esotericsoftware:kryo:4.0.0"
|
||||
compile "de.javakaffee:kryo-serializers:0.41"
|
||||
|
||||
// Apache JEXL: An embeddable expression evaluation library.
|
||||
// This may be temporary until we experiment with other ways to do on-the-fly contract specialisation via an API.
|
||||
compile "org.apache.commons:commons-jexl3:3.0"
|
||||
@ -98,5 +89,5 @@ jar {
|
||||
}
|
||||
|
||||
publish {
|
||||
name = jar.baseName
|
||||
name jar.baseName
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* WARNING: This is an internal package and not part of the public API. Do not use anything found here or in any sub-package.
|
||||
*/
|
||||
package net.corda.core.internal;
|
@ -58,9 +58,9 @@ open class CordaException internal constructor(override var originalExceptionCla
|
||||
}
|
||||
}
|
||||
|
||||
open class CordaRuntimeException internal constructor(override var originalExceptionClassName: String?,
|
||||
private var _message: String? = null,
|
||||
private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable {
|
||||
open class CordaRuntimeException(override var originalExceptionClassName: String?,
|
||||
private var _message: String? = null,
|
||||
private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable {
|
||||
constructor(message: String?, cause: Throwable?) : this(null, message, cause)
|
||||
|
||||
override val message: String?
|
||||
|
@ -1,30 +0,0 @@
|
||||
package net.corda.core
|
||||
|
||||
import java.util.*
|
||||
import java.util.Spliterator.*
|
||||
import java.util.stream.IntStream
|
||||
import java.util.stream.Stream
|
||||
import java.util.stream.StreamSupport
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
private fun IntProgression.spliteratorOfInt(): Spliterator.OfInt {
|
||||
val kotlinIterator = iterator()
|
||||
val javaIterator = object : PrimitiveIterator.OfInt {
|
||||
override fun nextInt() = kotlinIterator.nextInt()
|
||||
override fun hasNext() = kotlinIterator.hasNext()
|
||||
override fun remove() = throw UnsupportedOperationException("remove")
|
||||
}
|
||||
val spliterator = Spliterators.spliterator(
|
||||
javaIterator,
|
||||
(1 + (last - first) / step).toLong(),
|
||||
SUBSIZED or IMMUTABLE or NONNULL or SIZED or ORDERED or SORTED or DISTINCT
|
||||
)
|
||||
return if (step > 0) spliterator else object : Spliterator.OfInt by spliterator {
|
||||
override fun getComparator() = Comparator.reverseOrder<Int>()
|
||||
}
|
||||
}
|
||||
|
||||
fun IntProgression.stream(): IntStream = StreamSupport.intStream(spliteratorOfInt(), false)
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable).
|
||||
inline fun <reified T> Stream<out T>.toTypedArray() = toArray { size -> arrayOfNulls<T>(size) } as Array<T>
|
@ -1,102 +1,16 @@
|
||||
// TODO Move out the Kotlin specific stuff into a separate file
|
||||
@file:JvmName("Utils")
|
||||
|
||||
package net.corda.core
|
||||
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.io.ByteStreams
|
||||
import com.google.common.util.concurrent.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.*
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.stream.Stream
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
val Int.days: Duration get() = Duration.ofDays(this.toLong())
|
||||
@Suppress("unused") // It's here for completeness
|
||||
val Int.hours: Duration get() = Duration.ofHours(this.toLong())
|
||||
val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
|
||||
val Int.millis: Duration get() = Duration.ofMillis(this.toLong())
|
||||
// TODO Delete this file once the Future stuff is out of here
|
||||
|
||||
|
||||
// TODO: Review by EOY2016 if we ever found these utilities helpful.
|
||||
val Int.bd: BigDecimal get() = BigDecimal(this)
|
||||
val Double.bd: BigDecimal get() = BigDecimal(this)
|
||||
val String.bd: BigDecimal get() = BigDecimal(this)
|
||||
val Long.bd: BigDecimal get() = BigDecimal(this)
|
||||
|
||||
fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + "…"
|
||||
|
||||
/** Like the + operator but throws an exception in case of integer overflow. */
|
||||
infix fun Int.checkedAdd(b: Int) = Math.addExact(this, b)
|
||||
|
||||
/** Like the + operator but throws an exception in case of integer overflow. */
|
||||
@Suppress("unused")
|
||||
infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b)
|
||||
|
||||
/** Same as [Future.get] but with a more descriptive name, and doesn't throw [ExecutionException], instead throwing its cause */
|
||||
fun <T> Future<T>.getOrThrow(timeout: Duration? = null): T {
|
||||
return try {
|
||||
if (timeout == null) get() else get(timeout.toNanos(), TimeUnit.NANOSECONDS)
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
|
||||
fun <V> future(block: () -> V): Future<V> = CompletableFuture.supplyAsync(block)
|
||||
|
||||
fun <F : ListenableFuture<*>, V> F.then(block: (F) -> V) = addListener(Runnable { block(this) }, MoreExecutors.directExecutor())
|
||||
|
||||
fun <U, V> Future<U>.match(success: (U) -> V, failure: (Throwable) -> V): V {
|
||||
return success(try {
|
||||
getOrThrow()
|
||||
} catch (t: Throwable) {
|
||||
return failure(t)
|
||||
})
|
||||
}
|
||||
|
||||
fun <U, V, W> ListenableFuture<U>.thenMatch(success: (U) -> V, failure: (Throwable) -> W) = then { it.match(success, failure) }
|
||||
fun ListenableFuture<*>.andForget(log: Logger) = then { it.match({}, { log.error("Background task failed:", it) }) }
|
||||
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
|
||||
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) })
|
||||
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
|
||||
|
||||
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
|
||||
inline fun <T> SettableFuture<T>.catch(block: () -> T) {
|
||||
try {
|
||||
set(block())
|
||||
} catch (t: Throwable) {
|
||||
setException(t)
|
||||
}
|
||||
}
|
||||
|
||||
fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
|
||||
fun <A> CordaFuture<out A>.toObservable(): Observable<A> {
|
||||
return Observable.create { subscriber ->
|
||||
thenMatch({
|
||||
subscriber.onNext(it)
|
||||
@ -107,303 +21,26 @@ fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */
|
||||
operator fun Path.div(other: String): Path = resolve(other)
|
||||
operator fun String.div(other: String): Path = Paths.get(this) / other
|
||||
|
||||
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
||||
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
|
||||
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
|
||||
fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path {
|
||||
require(targetDir.isDirectory()) { "$targetDir is not a directory" }
|
||||
val targetFile = targetDir.resolve(fileName)
|
||||
Files.copy(this, targetFile, *options)
|
||||
return targetFile
|
||||
}
|
||||
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
|
||||
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
|
||||
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
|
||||
val Path.size: Long get() = Files.size(this)
|
||||
inline fun <R> Path.list(block: (Stream<Path>) -> R): R = Files.list(this).use(block)
|
||||
fun Path.deleteIfExists(): Boolean = Files.deleteIfExists(this)
|
||||
fun Path.readAll(): ByteArray = Files.readAllBytes(this)
|
||||
inline fun <R> Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = Files.newInputStream(this, *options).use(block)
|
||||
inline fun Path.write(createDirs: Boolean = false, vararg options: OpenOption = emptyArray(), block: (OutputStream) -> Unit) {
|
||||
if (createDirs) {
|
||||
normalize().parent?.createDirectories()
|
||||
}
|
||||
Files.newOutputStream(this, *options).use(block)
|
||||
}
|
||||
|
||||
inline fun <R> Path.readLines(charset: Charset = UTF_8, block: (Stream<String>) -> R): R = Files.lines(this, charset).use(block)
|
||||
fun Path.readAllLines(charset: Charset = UTF_8): List<String> = Files.readAllLines(this, charset)
|
||||
fun Path.writeLines(lines: Iterable<CharSequence>, charset: Charset = UTF_8, vararg options: OpenOption): Path = Files.write(this, lines, charset, *options)
|
||||
|
||||
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
|
||||
|
||||
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
|
||||
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
|
||||
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
val i = indexOf(item)
|
||||
require(i != -1)
|
||||
return i
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single element matching the given [predicate], or `null` if element was not found,
|
||||
* or throws if more than one element was found.
|
||||
*/
|
||||
fun <T> Iterable<T>.noneOrSingle(predicate: (T) -> Boolean): T? {
|
||||
var single: T? = null
|
||||
for (element in this) {
|
||||
if (predicate(element)) {
|
||||
if (single == null) {
|
||||
single = element
|
||||
} else throw IllegalArgumentException("Collection contains more than one matching element.")
|
||||
}
|
||||
}
|
||||
return single
|
||||
}
|
||||
|
||||
/** Returns single element, or `null` if element was not found, or throws if more than one element was found. */
|
||||
fun <T> Iterable<T>.noneOrSingle(): T? {
|
||||
var single: T? = null
|
||||
for (element in this) {
|
||||
if (single == null) {
|
||||
single = element
|
||||
} else throw IllegalArgumentException("Collection contains more than one matching element.")
|
||||
}
|
||||
return single
|
||||
}
|
||||
|
||||
/** Returns a random element in the list, or null if empty */
|
||||
fun <T> List<T>.randomOrNull(): T? {
|
||||
if (size <= 1) return firstOrNull()
|
||||
val randomIndex = (Math.random() * size).toInt()
|
||||
return get(randomIndex)
|
||||
}
|
||||
|
||||
/** Returns a random element in the list matching the given predicate, or null if none found */
|
||||
fun <T> List<T>.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).randomOrNull()
|
||||
|
||||
inline fun elapsedTime(block: () -> Unit): Duration {
|
||||
val start = System.nanoTime()
|
||||
block()
|
||||
val end = System.nanoTime()
|
||||
return Duration.ofNanos(end - start)
|
||||
}
|
||||
|
||||
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
||||
// returns in the IRSSimulationTest. If not, commit the inline back.
|
||||
fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
|
||||
// Use nanoTime as it's monotonic.
|
||||
val now = System.nanoTime()
|
||||
try {
|
||||
return body()
|
||||
} finally {
|
||||
val elapsed = Duration.ofNanos(System.nanoTime() - now).toMillis()
|
||||
if (logger != null)
|
||||
logger.info("$label took $elapsed msec")
|
||||
else
|
||||
println("$label took $elapsed msec")
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body)
|
||||
|
||||
/**
|
||||
* A threadbox is a simple utility that makes it harder to forget to take a lock before accessing some shared state.
|
||||
* Simply define a private class to hold the data that must be grouped under the same lock, and then pass the only
|
||||
* instance to the ThreadBox constructor. You can now use the [locked] method with a lambda to take the lock in a
|
||||
* way that ensures it'll be released if there's an exception.
|
||||
*
|
||||
* Note that this technique is not infallible: if you capture a reference to the fields in another lambda which then
|
||||
* gets stored and invoked later, there may still be unsafe multi-threaded access going on, so watch out for that.
|
||||
* This is just a simple guard rail that makes it harder to slip up.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* private class MutableState { var i = 5 }
|
||||
* private val state = ThreadBox(MutableState())
|
||||
*
|
||||
* val ii = state.locked { i }
|
||||
*/
|
||||
class ThreadBox<out T>(val content: T, val lock: ReentrantLock = ReentrantLock()) {
|
||||
inline fun <R> locked(body: T.() -> R): R = lock.withLock { body(content) }
|
||||
inline fun <R> alreadyLocked(body: T.() -> R): R {
|
||||
check(lock.isHeldByCurrentThread, { "Expected $lock to already be locked." })
|
||||
return body(content)
|
||||
}
|
||||
|
||||
fun checkNotLocked() = check(!lock.isHeldByCurrentThread)
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a transient exception or condition that might no longer be thrown if the operation is re-run or called
|
||||
* again.
|
||||
*
|
||||
* We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization.
|
||||
*/
|
||||
@CordaSerializable
|
||||
abstract class RetryableException(message: String) : FlowException(message)
|
||||
|
||||
/**
|
||||
* A simple wrapper that enables the use of Kotlin's "val x by TransientProperty { ... }" syntax. Such a property
|
||||
* will not be serialized to disk, and if it's missing (or the first time it's accessed), the initializer will be
|
||||
* used to set it up. Note that the initializer will be called with the TransientProperty object locked.
|
||||
*/
|
||||
class TransientProperty<out T>(private val initializer: () -> T) {
|
||||
@Transient private var v: T? = null
|
||||
|
||||
@Synchronized
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: initializer().also { v = it }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a path to a zip file, extracts it to the given directory.
|
||||
*/
|
||||
fun extractZipFile(zipFile: Path, toDirectory: Path) = extractZipFile(Files.newInputStream(zipFile), toDirectory)
|
||||
|
||||
/**
|
||||
* Given a zip file input stream, extracts it to the given directory.
|
||||
*/
|
||||
fun extractZipFile(inputStream: InputStream, toDirectory: Path) {
|
||||
val normalisedDirectory = toDirectory.normalize().createDirectories()
|
||||
ZipInputStream(BufferedInputStream(inputStream)).use {
|
||||
while (true) {
|
||||
val e = it.nextEntry ?: break
|
||||
val outPath = (normalisedDirectory / e.name).normalize()
|
||||
|
||||
// Security checks: we should reject a zip that contains tricksy paths that try to escape toDirectory.
|
||||
check(outPath.startsWith(normalisedDirectory)) { "ZIP contained a path that resolved incorrectly: ${e.name}" }
|
||||
|
||||
if (e.isDirectory) {
|
||||
outPath.createDirectories()
|
||||
continue
|
||||
}
|
||||
outPath.write { out ->
|
||||
ByteStreams.copy(it, out)
|
||||
}
|
||||
it.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid InputStream from an in-memory zip as required for tests.
|
||||
* Note that a slightly bigger than numOfExpectedBytes size is expected.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun sizedInputStreamAndHash(numOfExpectedBytes: Int): InputStreamAndHash {
|
||||
if (numOfExpectedBytes <= 0) throw IllegalArgumentException("A positive number of numOfExpectedBytes is required.")
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use({ zos ->
|
||||
val arraySize = 1024
|
||||
val bytes = ByteArray(arraySize)
|
||||
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
zos.putNextEntry(ZipEntry("z"))
|
||||
for (i in 0 until n) {
|
||||
zos.write(bytes, 0, arraySize)
|
||||
}
|
||||
zos.closeEntry()
|
||||
})
|
||||
return getInputStreamAndHashFromOutputStream(baos)
|
||||
}
|
||||
|
||||
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
||||
fun getInputStreamAndHashFromOutputStream(baos: ByteArrayOutputStream): InputStreamAndHash {
|
||||
// TODO: Consider converting OutputStream to InputStream without creating a ByteArray, probably using piped streams.
|
||||
val bytes = baos.toByteArray()
|
||||
// TODO: Consider calculating sha256 on the fly using a DigestInputStream.
|
||||
return InputStreamAndHash(ByteArrayInputStream(bytes), bytes.sha256())
|
||||
}
|
||||
|
||||
data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256)
|
||||
|
||||
// TODO: Generic csv printing utility for clases.
|
||||
|
||||
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
|
||||
|
||||
/**
|
||||
* Returns an Observable that buffers events until subscribed.
|
||||
* @see UnicastSubject
|
||||
*/
|
||||
fun <T> Observable<T>.bufferUntilSubscribed(): Observable<T> {
|
||||
val subject = UnicastSubject.create<T>()
|
||||
val subscription = subscribe(subject)
|
||||
return subject.doOnUnsubscribe { subscription.unsubscribe() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy an [Observer] to multiple other [Observer]s.
|
||||
*/
|
||||
fun <T> Observer<T>.tee(vararg teeTo: Observer<T>): Observer<T> {
|
||||
val subject = PublishSubject.create<T>()
|
||||
subject.subscribe(this)
|
||||
teeTo.forEach { subject.subscribe(it) }
|
||||
return subject
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [ListenableFuture] bound to the *first* item emitted by this Observable. The future will complete with a
|
||||
* Returns a [CordaFuture] bound to the *first* item emitted by this Observable. The future will complete with a
|
||||
* NoSuchElementException if no items are emitted or any other error thrown by the Observable. If it's cancelled then
|
||||
* it will unsubscribe from the observable.
|
||||
*/
|
||||
fun <T> Observable<T>.toFuture(): ListenableFuture<T> = ObservableToFuture(this)
|
||||
fun <T> Observable<T>.toFuture(): CordaFuture<T> = openFuture<T>().also {
|
||||
val subscription = first().subscribe(object : Observer<T> {
|
||||
override fun onNext(value: T) {
|
||||
it.set(value)
|
||||
}
|
||||
|
||||
private class ObservableToFuture<T>(observable: Observable<T>) : AbstractFuture<T>(), Observer<T> {
|
||||
private val subscription = observable.first().subscribe(this)
|
||||
override fun onNext(value: T) {
|
||||
set(value)
|
||||
}
|
||||
override fun onError(e: Throwable) {
|
||||
it.setException(e)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
setException(e)
|
||||
}
|
||||
|
||||
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
|
||||
subscription.unsubscribe()
|
||||
return super.cancel(mayInterruptIfRunning)
|
||||
}
|
||||
|
||||
override fun onCompleted() {}
|
||||
}
|
||||
|
||||
/** Return the sum of an Iterable of [BigDecimal]s. */
|
||||
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
||||
|
||||
fun codePointsString(vararg codePoints: Int): String {
|
||||
val builder = StringBuilder()
|
||||
codePoints.forEach { builder.append(Character.toChars(it)) }
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun <T> Class<T>.checkNotUnorderedHashMap() {
|
||||
if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) {
|
||||
throw NotSerializableException("Map type $this is unstable under iteration. Suggested fix: use LinkedHashMap instead.")
|
||||
}
|
||||
}
|
||||
|
||||
fun Class<*>.requireExternal(msg: String = "Internal class")
|
||||
= require(!name.startsWith("net.corda.node.") && !name.contains(".internal.")) { "$msg: $name" }
|
||||
|
||||
interface DeclaredField<T> {
|
||||
companion object {
|
||||
inline fun <reified T> Any?.declaredField(clazz: KClass<*>, name: String): DeclaredField<T> = declaredField(clazz.java, name)
|
||||
inline fun <reified T> Any.declaredField(name: String): DeclaredField<T> = declaredField(javaClass, name)
|
||||
inline fun <reified T> Any?.declaredField(clazz: Class<*>, name: String): DeclaredField<T> {
|
||||
val javaField = clazz.getDeclaredField(name).apply { isAccessible = true }
|
||||
val receiver = this
|
||||
return object : DeclaredField<T> {
|
||||
override var value
|
||||
get() = javaField.get(receiver) as T
|
||||
set(value) = javaField.set(receiver, value)
|
||||
}
|
||||
override fun onCompleted() {}
|
||||
})
|
||||
it.then {
|
||||
if (it.isCancelled) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
var value: T
|
||||
}
|
||||
|
@ -1,34 +1,44 @@
|
||||
package net.corda.core.concurrent
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.catch
|
||||
import net.corda.core.match
|
||||
import net.corda.core.then
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/** Invoke [getOrThrow] and pass the value/throwable to success/failure respectively. */
|
||||
fun <V, W> Future<V>.match(success: (V) -> W, failure: (Throwable) -> W): W {
|
||||
val value = try {
|
||||
getOrThrow()
|
||||
} catch (t: Throwable) {
|
||||
return failure(t)
|
||||
}
|
||||
return success(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* As soon as a given future becomes done, the handler is invoked with that future as its argument.
|
||||
* The result of the handler is copied into the result future, and the handler isn't invoked again.
|
||||
* If a given future errors after the result future is done, the error is automatically logged.
|
||||
*/
|
||||
fun <S, T> firstOf(vararg futures: ListenableFuture<out S>, handler: (ListenableFuture<out S>) -> T) = firstOf(futures, defaultLog, handler)
|
||||
fun <V, W> firstOf(vararg futures: CordaFuture<out V>, handler: (CordaFuture<out V>) -> W) = firstOf(futures, defaultLog, handler)
|
||||
|
||||
private val defaultLog = LoggerFactory.getLogger("net.corda.core.concurrent")
|
||||
@VisibleForTesting
|
||||
internal val shortCircuitedTaskFailedMessage = "Short-circuited task failed:"
|
||||
|
||||
internal fun <S, T> firstOf(futures: Array<out ListenableFuture<out S>>, log: Logger, handler: (ListenableFuture<out S>) -> T): ListenableFuture<T> {
|
||||
val resultFuture = SettableFuture.create<T>()
|
||||
internal fun <V, W> firstOf(futures: Array<out CordaFuture<out V>>, log: Logger, handler: (CordaFuture<out V>) -> W): CordaFuture<W> {
|
||||
val resultFuture = openFuture<W>()
|
||||
val winnerChosen = AtomicBoolean()
|
||||
futures.forEach {
|
||||
it.then {
|
||||
if (winnerChosen.compareAndSet(false, true)) {
|
||||
resultFuture.catch { handler(it) }
|
||||
} else if (!it.isCancelled) {
|
||||
resultFuture.capture { handler(it) }
|
||||
} else if (it.isCancelled) {
|
||||
// Do nothing.
|
||||
} else {
|
||||
it.match({}, { log.error(shortCircuitedTaskFailedMessage, it) })
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package net.corda.core.concurrent
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* Same as [Future] with additional methods to provide some of the features of [java.util.concurrent.CompletableFuture] while minimising the API surface area.
|
||||
* In Kotlin, to avoid compile errors, whenever CordaFuture is used in a parameter or extension method receiver type, its type parameter should be specified with out variance.
|
||||
*/
|
||||
interface CordaFuture<V> : Future<V> {
|
||||
/**
|
||||
* Run the given callback when this future is done, on the completion thread.
|
||||
* If the completion thread is problematic for you e.g. deadlock, you can submit to an executor manually.
|
||||
* If callback fails, its throwable is logged.
|
||||
*/
|
||||
fun <W> then(callback: (CordaFuture<V>) -> W): Unit
|
||||
|
||||
/**
|
||||
* @return a new [CompletableFuture] with the same outcome as this Future.
|
||||
*/
|
||||
fun toCompletableFuture(): CompletableFuture<V>
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
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 java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
@ -168,7 +171,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
*/
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
return Amount(Math.addExact(quantity, other.quantity), displayTokenSize, token)
|
||||
return Amount(quantity exactAdd other.quantity, displayTokenSize, token)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,9 +271,9 @@ data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amou
|
||||
* 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], [Account], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* @param 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], [Account], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* @param 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
|
||||
@ -329,7 +332,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
"Only AmountTransfer between the same two parties can be aggregated/netted"
|
||||
}
|
||||
return if (other.source == source) {
|
||||
AmountTransfer(Math.addExact(quantityDelta, other.quantityDelta), token, source, destination)
|
||||
AmountTransfer(quantityDelta exactAdd other.quantityDelta, token, source, destination)
|
||||
} else {
|
||||
AmountTransfer(Math.subtractExact(quantityDelta, other.quantityDelta), token, source, destination)
|
||||
}
|
||||
@ -388,10 +391,10 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
* 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 two new AmountTransfers each between one of the original parties and the centralParty.
|
||||
* @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.
|
||||
*/
|
||||
fun novate(centralParty: P): Pair<AmountTransfer<T, P>, AmountTransfer<T, P>> = Pair(copy(destination = centralParty), copy(source = centralParty))
|
||||
fun novate(centralParty: P): List<AmountTransfer<T, P>> = listOf(copy(destination = centralParty), copy(source = centralParty))
|
||||
|
||||
/**
|
||||
* Applies this AmountTransfer to a list of [SourceAndAmount] objects representing balances.
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
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
|
||||
@ -54,13 +55,6 @@ object Requirements {
|
||||
infix inline fun String.using(expr: Boolean) {
|
||||
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
||||
}
|
||||
// Avoid overloading Kotlin keywords
|
||||
@Deprecated("This function is deprecated, use 'using' instead",
|
||||
ReplaceWith("using (expr)", "net.corda.core.contracts.Requirements.using"))
|
||||
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
|
||||
infix inline fun String.by(expr: Boolean) {
|
||||
using(expr)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
|
||||
@ -71,7 +65,7 @@ inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
|
||||
|
||||
/** Filters the command list by type, party and public key all at once. */
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
|
||||
party: Party? = null) =
|
||||
party: AbstractParty? = null) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signer == null) true else signer in it.signers }.
|
||||
filter { if (party == null) true else party in it.signingParties }.
|
||||
|
@ -1,19 +1,24 @@
|
||||
@file:JvmName("Structures")
|
||||
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.secureRandomBytes
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
@ -76,7 +81,7 @@ interface ContractState {
|
||||
* A _participant_ is any party that is able to consume this state in a valid transaction.
|
||||
*
|
||||
* The list of participants is required for certain types of transactions. For example, when changing the notary
|
||||
* for this state ([TransactionType.NotaryChange]), every participant has to be involved and approve the transaction
|
||||
* for this state, every participant has to be involved and approve the transaction
|
||||
* so that they receive the updated state, and don't end up in a situation where they can no longer use a state
|
||||
* they possess, since someone consumed that state during the notary change process.
|
||||
*
|
||||
@ -141,6 +146,12 @@ data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
|
||||
fun <T : Any> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, token.product)
|
||||
|
||||
// DOCSTART 3
|
||||
|
||||
/**
|
||||
* Return structure for [OwnableState.withNewOwner]
|
||||
*/
|
||||
data class CommandAndState(val command: CommandData, val ownableState: OwnableState)
|
||||
|
||||
/**
|
||||
* A contract state that can have a single owner.
|
||||
*/
|
||||
@ -149,7 +160,7 @@ interface OwnableState : ContractState {
|
||||
val owner: AbstractParty
|
||||
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
||||
fun withNewOwner(newOwner: AbstractParty): Pair<CommandData, OwnableState>
|
||||
fun withNewOwner(newOwner: AbstractParty): CommandAndState
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
@ -199,26 +210,6 @@ interface LinearState : ContractState {
|
||||
* True if this should be tracked by our vault(s).
|
||||
*/
|
||||
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
||||
|
||||
/**
|
||||
* Standard clause to verify the LinearState safety properties.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class ClauseVerifier<in S : LinearState, C : CommandData> : Clause<S, C, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Unit?): Set<C> {
|
||||
val inputIds = inputs.map { it.linearId }.distinct()
|
||||
val outputIds = outputs.map { it.linearId }.distinct()
|
||||
requireThat {
|
||||
"LinearStates are not merged" using (inputIds.count() == inputs.count())
|
||||
"LinearStates are not split" using (outputIds.count() == outputs.count())
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
@ -281,14 +272,13 @@ abstract class TypeOnlyCommandData : CommandData {
|
||||
|
||||
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||
@CordaSerializable
|
||||
// DOCSTART 9
|
||||
data class Command(val value: CommandData, val signers: List<PublicKey>) {
|
||||
// DOCEND 9
|
||||
data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>) {
|
||||
// TODO Introduce NonEmptyList?
|
||||
init {
|
||||
require(signers.isNotEmpty())
|
||||
}
|
||||
|
||||
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
|
||||
constructor(data: T, key: PublicKey) : this(data, listOf(key))
|
||||
|
||||
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
|
||||
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
|
||||
@ -324,63 +314,6 @@ data class AuthenticatedObject<out T : Any>(
|
||||
)
|
||||
// DOCEND 6
|
||||
|
||||
/**
|
||||
* A time-window is required for validation/notarization purposes.
|
||||
* If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be
|
||||
* between (fromTime, untilTime).
|
||||
* Usually, a time-window is required to have both sides set (fromTime, untilTime).
|
||||
* However, some apps may require that a time-window has a start [Instant] (fromTime), but no end [Instant] (untilTime) and vice versa.
|
||||
* TODO: Consider refactoring using TimeWindow abstraction like TimeWindow.From, TimeWindow.Until, TimeWindow.Between.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class TimeWindow private constructor(
|
||||
/** The time at which this transaction is said to have occurred is after this moment. */
|
||||
val fromTime: Instant?,
|
||||
/** The time at which this transaction is said to have occurred is before this moment. */
|
||||
val untilTime: Instant?
|
||||
) {
|
||||
companion object {
|
||||
/** Use when the left-side [fromTime] of a [TimeWindow] is only required and we don't need an end instant (untilTime). */
|
||||
@JvmStatic
|
||||
fun fromOnly(fromTime: Instant) = TimeWindow(fromTime, null)
|
||||
|
||||
/** Use when the right-side [untilTime] of a [TimeWindow] is only required and we don't need a start instant (fromTime). */
|
||||
@JvmStatic
|
||||
fun untilOnly(untilTime: Instant) = TimeWindow(null, untilTime)
|
||||
|
||||
/** Use when both sides of a [TimeWindow] must be set ([fromTime], [untilTime]). */
|
||||
@JvmStatic
|
||||
fun between(fromTime: Instant, untilTime: Instant): TimeWindow {
|
||||
require(fromTime < untilTime) { "fromTime should be earlier than untilTime" }
|
||||
return TimeWindow(fromTime, untilTime)
|
||||
}
|
||||
|
||||
/** Use when we have a start time and a period of validity. */
|
||||
@JvmStatic
|
||||
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
|
||||
|
||||
/**
|
||||
* When we need to create a [TimeWindow] based on a specific time [Instant] and some tolerance in both sides of this instant.
|
||||
* The result will be the following time-window: ([time] - [tolerance], [time] + [tolerance]).
|
||||
*/
|
||||
@JvmStatic
|
||||
fun withTolerance(time: Instant, tolerance: Duration) = between(time - tolerance, time + tolerance)
|
||||
}
|
||||
|
||||
/** The midpoint is calculated as fromTime + (untilTime - fromTime)/2. Note that it can only be computed if both sides are set. */
|
||||
val midpoint: Instant get() = fromTime!! + Duration.between(fromTime, untilTime!!).dividedBy(2)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TimeWindow) return false
|
||||
return (fromTime == other.fromTime && untilTime == other.untilTime)
|
||||
}
|
||||
|
||||
override fun hashCode() = 31 * (fromTime?.hashCode() ?: 0) + (untilTime?.hashCode() ?: 0)
|
||||
|
||||
override fun toString() = "TimeWindow(fromTime=$fromTime, untilTime=$untilTime)"
|
||||
}
|
||||
|
||||
// DOCSTART 5
|
||||
/**
|
||||
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
|
||||
@ -399,7 +332,7 @@ interface Contract {
|
||||
* existing contract code.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun verify(tx: TransactionForContract)
|
||||
fun verify(tx: LedgerTransaction)
|
||||
|
||||
/**
|
||||
* Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of
|
||||
@ -489,3 +422,23 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
|
||||
}
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/** Constructs a salt with a randomly-generated 32 byte value. */
|
||||
constructor() : this(secureRandomBytes(32))
|
||||
|
||||
init {
|
||||
require(bytes.size == 32) { "Privacy salt should be 32 bytes." }
|
||||
require(!bytes.all { it == 0.toByte() }) { "Privacy salt should not be all zeros." }
|
||||
}
|
||||
}
|
||||
|
92
core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt
Normal file
92
core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt
Normal file
@ -0,0 +1,92 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.until
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* An interval on the time-line; not a single instantaneous point.
|
||||
*
|
||||
* There is no such thing as _exact_ time in networked systems due to the underlying physics involved and other issues
|
||||
* such as network latency. The best that can be approximated is "fuzzy time" or an instant of time which has margin of
|
||||
* tolerance around it. This is what [TimeWindow] represents. Time windows can be open-ended (i.e. specify only one of
|
||||
* [fromTime] and [untilTime]) or they can be fully bounded.
|
||||
*
|
||||
* [WireTransaction] has an optional time-window property, which if specified, restricts the validity of the transaction
|
||||
* to that time-interval as the Consensus Service will not sign it if it's received outside of this window.
|
||||
*/
|
||||
@CordaSerializable
|
||||
abstract class TimeWindow {
|
||||
companion object {
|
||||
/** Creates a [TimeWindow] with null [untilTime], i.e. the time interval `[fromTime, ∞)`. [midpoint] will return null. */
|
||||
@JvmStatic
|
||||
fun fromOnly(fromTime: Instant): TimeWindow = From(fromTime)
|
||||
|
||||
/** Creates a [TimeWindow] with null [fromTime], i.e. the time interval `(∞, untilTime)`. [midpoint] will return null. */
|
||||
@JvmStatic
|
||||
fun untilOnly(untilTime: Instant): TimeWindow = Until(untilTime)
|
||||
|
||||
/**
|
||||
* Creates a [TimeWindow] with the time interval `[fromTime, untilTime)`. [midpoint] will return
|
||||
* `fromTime + (untilTime - fromTime) / 2`.
|
||||
* @throws IllegalArgumentException If [fromTime] ≥ [untilTime]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun between(fromTime: Instant, untilTime: Instant): TimeWindow = Between(fromTime, untilTime)
|
||||
|
||||
/**
|
||||
* Creates a [TimeWindow] with the time interval `[fromTime, fromTime + duration)`. [midpoint] will return
|
||||
* `fromTime + duration / 2`
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
|
||||
|
||||
/**
|
||||
* Creates a [TimeWindow] which is centered around [instant] with the given [tolerance] on both sides, i.e the
|
||||
* time interval `[instant - tolerance, instant + tolerance)`. [midpoint] will return [instant].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun withTolerance(instant: Instant, tolerance: Duration) = between(instant - tolerance, instant + tolerance)
|
||||
}
|
||||
|
||||
/** Returns the inclusive lower-bound of this [TimeWindow]'s interval, with null implying infinity. */
|
||||
abstract val fromTime: Instant?
|
||||
|
||||
/** Returns the exclusive upper-bound of this [TimeWindow]'s interval, with null implying infinity. */
|
||||
abstract val untilTime: Instant?
|
||||
|
||||
/**
|
||||
* Returns the midpoint of [fromTime] and [untilTime] if both are non-null, calculated as
|
||||
* `fromTime + (untilTime - fromTime) / 2`, otherwise returns null.
|
||||
*/
|
||||
abstract val midpoint: Instant?
|
||||
|
||||
/** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */
|
||||
abstract operator fun contains(instant: Instant): Boolean
|
||||
|
||||
private data class From(override val fromTime: Instant) : TimeWindow() {
|
||||
override val untilTime: Instant? get() = null
|
||||
override val midpoint: Instant? get() = null
|
||||
override fun contains(instant: Instant): Boolean = instant >= fromTime
|
||||
override fun toString(): String = "[$fromTime, ∞)"
|
||||
}
|
||||
|
||||
private data class Until(override val untilTime: Instant) : TimeWindow() {
|
||||
override val fromTime: Instant? get() = null
|
||||
override val midpoint: Instant? get() = null
|
||||
override fun contains(instant: Instant): Boolean = instant < untilTime
|
||||
override fun toString(): String = "(∞, $untilTime)"
|
||||
}
|
||||
|
||||
private data class Between(override val fromTime: Instant, override val untilTime: Instant) : TimeWindow() {
|
||||
init {
|
||||
require(fromTime < untilTime) { "fromTime must be earlier than untilTime" }
|
||||
}
|
||||
override val midpoint: Instant get() = fromTime + (fromTime until untilTime) / 2
|
||||
override fun contains(instant: Instant): Boolean = instant >= fromTime && instant < untilTime
|
||||
override fun toString(): String = "[$fromTime, $untilTime)"
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
/** Defines transaction build & validation logic for a specific transaction type */
|
||||
@CordaSerializable
|
||||
sealed class TransactionType {
|
||||
/**
|
||||
* Check that the transaction is valid based on:
|
||||
* - General platform rules
|
||||
* - Rules for the specific transaction type
|
||||
*
|
||||
* Note: Presence of _signatures_ is not checked, only the public keys to be signed for.
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
require(tx.notary != null || tx.timeWindow == null) { "Transactions with time-windows must be notarised" }
|
||||
val duplicates = detectDuplicateInputs(tx)
|
||||
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx.id, duplicates)
|
||||
val missing = verifySigners(tx)
|
||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx.id, missing.toList())
|
||||
verifyTransaction(tx)
|
||||
}
|
||||
|
||||
/** Check that the list of signers includes all the necessary keys */
|
||||
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
|
||||
val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet()
|
||||
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx.id)
|
||||
|
||||
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
||||
val missing = requiredKeys - tx.mustSign
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
/** Check that the inputs are unique. */
|
||||
private fun detectDuplicateInputs(tx: LedgerTransaction): Set<StateRef> {
|
||||
var seenInputs = emptySet<StateRef>()
|
||||
var duplicates = emptySet<StateRef>()
|
||||
tx.inputs.forEach { state ->
|
||||
if (seenInputs.contains(state.ref)) {
|
||||
duplicates += state.ref
|
||||
}
|
||||
seenInputs += state.ref
|
||||
}
|
||||
return duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of public keys that that require signatures for the transaction type.
|
||||
* Note: the notary key is checked separately for all transactions and need not be included.
|
||||
*/
|
||||
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
|
||||
|
||||
/** Implement type specific transaction validation logic */
|
||||
abstract fun verifyTransaction(tx: LedgerTransaction)
|
||||
|
||||
/** A general transaction type where transaction validity is determined by custom contract code */
|
||||
object General : TransactionType() {
|
||||
/** Just uses the default [TransactionBuilder] with no special logic */
|
||||
class Builder(notary: Party?) : TransactionBuilder(General, notary)
|
||||
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
verifyNoNotaryChange(tx)
|
||||
verifyEncumbrances(tx)
|
||||
verifyContracts(tx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
* are any inputs, all outputs must have the same notary.
|
||||
*
|
||||
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||
* flexible on output notaries.
|
||||
*/
|
||||
private fun verifyNoNotaryChange(tx: LedgerTransaction) {
|
||||
if (tx.notary != null && tx.inputs.isNotEmpty()) {
|
||||
tx.outputs.forEach {
|
||||
if (it.notary != tx.notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx.id, tx.notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyEncumbrances(tx: LedgerTransaction) {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
|
||||
encumberedInputs.forEach { (state, ref) ->
|
||||
val encumbranceStateExists = tx.inputs.any {
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx.id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
|
||||
// and that the number of outputs can contain the encumbrance.
|
||||
for ((i, output) in tx.outputs.withIndex()) {
|
||||
val encumbranceIndex = output.encumbrance ?: continue
|
||||
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx.id,
|
||||
encumbranceIndex,
|
||||
TransactionVerificationException.Direction.OUTPUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts(tx: LedgerTransaction) {
|
||||
val ctx = tx.toTransactionForContract()
|
||||
// TODO: This will all be replaced in future once the sandbox and contract constraints work is done.
|
||||
val contracts = (ctx.inputs.map { it.contract } + ctx.outputs.map { it.contract }).toSet()
|
||||
for (contract in contracts) {
|
||||
try {
|
||||
contract.verify(ctx)
|
||||
} catch(e: Throwable) {
|
||||
throw TransactionVerificationException.ContractRejection(tx.id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* A special transaction type for reassigning a notary for a state. Validation does not involve running
|
||||
* any contract code, it just checks that the states are unmodified apart from the notary field.
|
||||
*/
|
||||
object NotaryChange : TransactionType() {
|
||||
/**
|
||||
* A transaction builder that automatically sets the transaction type to [NotaryChange]
|
||||
* and adds the list of participants to the signers set for every input state.
|
||||
*/
|
||||
class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) {
|
||||
override fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
|
||||
signers.addAll(stateAndRef.state.data.participants.map { it.owningKey })
|
||||
super.addInputState(stateAndRef)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the difference between inputs and outputs is only the notary field, and that all required signing
|
||||
* public keys are present.
|
||||
*
|
||||
* @throws TransactionVerificationException.InvalidNotaryChange if the validity check fails.
|
||||
*/
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
try {
|
||||
for ((input, output) in tx.inputs.zip(tx.outputs)) {
|
||||
check(input.state.data == output.data)
|
||||
check(input.state.notary != output.notary)
|
||||
}
|
||||
check(tx.commands.isEmpty())
|
||||
} catch (e: IllegalStateException) {
|
||||
throw TransactionVerificationException.InvalidNotaryChange(tx.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
|
||||
|
||||
/**
|
||||
* A transaction to be passed as input to a contract verification function. Defines helper methods to
|
||||
* simplify verification logic in contracts.
|
||||
*/
|
||||
// DOCSTART 1
|
||||
data class TransactionForContract(val inputs: List<ContractState>,
|
||||
val outputs: List<ContractState>,
|
||||
val attachments: List<Attachment>,
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
val origHash: SecureHash,
|
||||
val inputNotary: Party? = null,
|
||||
val timeWindow: TimeWindow? = null) {
|
||||
// DOCEND 1
|
||||
override fun hashCode() = origHash.hashCode()
|
||||
override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash
|
||||
|
||||
/**
|
||||
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
||||
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
|
||||
* equals and hashCode).
|
||||
*
|
||||
* The purpose of this function is to simplify the writing of verification logic for transactions that may contain
|
||||
* similar but unrelated state evolutions which need to be checked independently. Consider a transaction that
|
||||
* simultaneously moves both dollars and euros (e.g. is an atomic FX trade). There may be multiple dollar inputs and
|
||||
* multiple dollar outputs, depending on things like how fragmented the owner's vault is and whether various privacy
|
||||
* techniques are in use. The quantity of dollars on the output side must sum to the same as on the input side, to
|
||||
* ensure no money is being lost track of. This summation and checking must be repeated independently for each
|
||||
* currency. To solve this, you would use groupStates with a type of Cash.State and a selector that returns the
|
||||
* currency field: the resulting list can then be iterated over to perform the per-currency calculation.
|
||||
*/
|
||||
// DOCSTART 2
|
||||
fun <T : ContractState, K : Any> groupStates(ofType: Class<T>, selector: (T) -> K): List<InOutGroup<T, K>> {
|
||||
val inputs = inputs.filterIsInstance(ofType)
|
||||
val outputs = outputs.filterIsInstance(ofType)
|
||||
|
||||
val inGroups: Map<K, List<T>> = inputs.groupBy(selector)
|
||||
val outGroups: Map<K, List<T>> = outputs.groupBy(selector)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
return groupStatesInternal(inGroups, outGroups)
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
/** See the documentation for the reflection-based version of [groupStates] */
|
||||
inline fun <reified T : ContractState, K : Any> groupStates(selector: (T) -> K): List<InOutGroup<T, K>> {
|
||||
val inputs = inputs.filterIsInstance<T>()
|
||||
val outputs = outputs.filterIsInstance<T>()
|
||||
|
||||
val inGroups: Map<K, List<T>> = inputs.groupBy(selector)
|
||||
val outGroups: Map<K, List<T>> = outputs.groupBy(selector)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
return groupStatesInternal(inGroups, outGroups)
|
||||
}
|
||||
|
||||
@Deprecated("Do not use this directly: exposed as public only due to function inlining")
|
||||
fun <T : ContractState, K : Any> groupStatesInternal(inGroups: Map<K, List<T>>, outGroups: Map<K, List<T>>): List<InOutGroup<T, K>> {
|
||||
val result = ArrayList<InOutGroup<T, K>>()
|
||||
|
||||
for ((k, v) in inGroups.entries)
|
||||
result.add(InOutGroup(v, outGroups[k] ?: emptyList(), k))
|
||||
for ((k, v) in outGroups.entries) {
|
||||
if (inGroups[k] == null)
|
||||
result.add(InOutGroup(emptyList(), v, k))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/** Utilities for contract writers to incorporate into their logic. */
|
||||
|
||||
/**
|
||||
* A set of related inputs and outputs that are connected by some common attributes. An InOutGroup is calculated
|
||||
* using [groupStates] and is useful for handling cases where a transaction may contain similar but unrelated
|
||||
* state evolutions, for example, a transaction that moves cash in two different currencies. The numbers must add
|
||||
* up on both sides of the transaction, but the values must be summed independently per currency. Grouping can
|
||||
* be used to simplify this logic.
|
||||
*/
|
||||
// DOCSTART 3
|
||||
data class InOutGroup<out T : ContractState, out K : Any>(val inputs: List<T>, val outputs: List<T>, val groupingKey: K)
|
||||
// DOCEND 3
|
||||
}
|
||||
|
||||
class TransactionResolutionException(val hash: SecureHash) : FlowException() {
|
||||
override fun toString(): String = "Transaction resolution failure for $hash"
|
||||
}
|
||||
|
||||
class AttachmentResolutionException(val hash: SecureHash) : FlowException() {
|
||||
override fun toString(): String = "Attachment resolution failure for $hash"
|
||||
}
|
||||
|
||||
sealed class TransactionVerificationException(val txId: SecureHash, cause: Throwable?) : FlowException(cause) {
|
||||
class ContractRejection(txId: SecureHash, val contract: Contract, cause: Throwable?) : TransactionVerificationException(txId, cause)
|
||||
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, null)
|
||||
class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, null) {
|
||||
override fun toString(): String = "Signers missing: ${missing.joinToString()}"
|
||||
}
|
||||
|
||||
class DuplicateInputStates(txId: SecureHash, val duplicates: Set<StateRef>) : TransactionVerificationException(txId, null) {
|
||||
override fun toString(): String = "Duplicate inputs: ${duplicates.joinToString()}"
|
||||
}
|
||||
|
||||
class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, null)
|
||||
class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party) : TransactionVerificationException(txId, null) {
|
||||
override fun toString(): String {
|
||||
return "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary"
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction) : TransactionVerificationException(txId, null) {
|
||||
override val message: String get() = "Missing required encumbrance $missing in $inOut"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class Direction {
|
||||
INPUT,
|
||||
OUTPUT
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import java.security.PublicKey
|
||||
|
||||
class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash")
|
||||
class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attachment resolution failure for $hash")
|
||||
|
||||
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||
: FlowException("$message, transaction: $txId", cause) {
|
||||
|
||||
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable)
|
||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause)
|
||||
|
||||
class MoreThanOneNotary(txId: SecureHash)
|
||||
: TransactionVerificationException(txId, "More than one notary", null)
|
||||
|
||||
class SignersMissing(txId: SecureHash, missing: List<PublicKey>)
|
||||
: TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null)
|
||||
|
||||
class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet<StateRef>)
|
||||
: TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null)
|
||||
|
||||
class InvalidNotaryChange(txId: SecureHash)
|
||||
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
|
||||
|
||||
class NotaryChangeInWrongTransactionType(txId: SecureHash, txNotary: Party, outputNotary: Party)
|
||||
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
|
||||
|
||||
class TransactionMissingEncumbranceException(txId: SecureHash, missing: Int, inOut: Direction)
|
||||
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
|
||||
|
||||
@CordaSerializable
|
||||
enum class Direction {
|
||||
INPUT,
|
||||
OUTPUT
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
|
||||
*/
|
||||
@Deprecated("Use AllOf")
|
||||
class AllComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : AllOf<S, C, K>(firstClause, *remainingClauses)
|
@ -1,38 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
|
||||
*/
|
||||
open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
|
||||
init {
|
||||
clauses.add(firstClause)
|
||||
clauses.addAll(remainingClauses)
|
||||
}
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||
clauses.forEach { clause ->
|
||||
check(clause.matches(commands)) { "Failed to match clause $clause" }
|
||||
}
|
||||
return clauses
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: K?): Set<C> {
|
||||
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "All: $clauses.toList()"
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that any number of the clauses can run.
|
||||
*/
|
||||
@Deprecated("Use AnyOf instead, although note that any of requires at least one matched clause")
|
||||
class AnyComposition<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : AnyOf<S, C, K>(*rawClauses)
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that one or more of the clauses can run.
|
||||
*/
|
||||
open class AnyOf<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses: List<Clause<S, C, K>> = rawClauses.toList()
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||
val matched = clauses.filter { it.matches(commands) }
|
||||
require(matched.isNotEmpty()) { "At least one clause must match" }
|
||||
return matched
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "Any: ${clauses.toList()}"
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
|
||||
/**
|
||||
* A clause of a contract, containing a chunk of verification logic. That logic may be delegated to other clauses, or
|
||||
* provided directly by this clause.
|
||||
*
|
||||
* @param S the type of contract state this clause operates on.
|
||||
* @param C a common supertype of commands this clause operates on.
|
||||
* @param K the type of the grouping key for states this clause operates on. Use [Unit] if not applicable.
|
||||
*
|
||||
* @see CompositeClause
|
||||
*/
|
||||
abstract class Clause<in S : ContractState, C : CommandData, in K : Any> {
|
||||
companion object {
|
||||
val log: Logger by lazy { loggerFor<Clause<*, *, *>>() }
|
||||
}
|
||||
|
||||
/** Determine whether this clause runs or not */
|
||||
open val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||
|
||||
/**
|
||||
* Determine the subclauses which will be verified as a result of verifying this clause.
|
||||
*
|
||||
* @throws IllegalStateException if the given commands do not result in a valid execution (for example no match
|
||||
* with [FirstOf]).
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
open fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= listOf(this)
|
||||
|
||||
/**
|
||||
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
|
||||
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
|
||||
* if any matched.
|
||||
*
|
||||
* @param tx the full transaction being verified. This is provided for cases where clauses need to access
|
||||
* states or commands outside of their normal scope.
|
||||
* @param inputs input states which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||
* @param outputs output states which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||
* @param commands commands which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||
* @param groupingKey a grouping key applied to states and commands, where applicable. Taken from
|
||||
* [TransactionForContract.InOutGroup].
|
||||
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
|
||||
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
|
||||
* verify() functions may do further filtering on possible matches, and return a subset. This may also include
|
||||
* commands that were not required (for example the Exit command for fungible assets is optional).
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
abstract fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: K?): Set<C>
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given list of commands matches the required commands for a clause to trigger.
|
||||
*/
|
||||
fun <C : CommandData> Clause<*, C, *>.matches(commands: List<AuthenticatedObject<C>>): Boolean {
|
||||
return if (requiredCommands.isEmpty())
|
||||
true
|
||||
else
|
||||
commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands)
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
@file:JvmName("ClauseVerifier")
|
||||
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
|
||||
/**
|
||||
* Verify a transaction against the given list of clauses.
|
||||
*
|
||||
* @param tx transaction to be verified.
|
||||
* @param clauses the clauses to verify.
|
||||
* @param commands commands extracted from the transaction, which are relevant to the
|
||||
* clauses.
|
||||
*/
|
||||
fun <C : CommandData> verifyClause(tx: TransactionForContract,
|
||||
clause: Clause<ContractState, C, Unit>,
|
||||
commands: List<AuthenticatedObject<C>>) {
|
||||
if (Clause.log.isTraceEnabled) {
|
||||
clause.getExecutionPath(commands).forEach {
|
||||
Clause.log.trace("Tx ${tx.origHash} clause: $clause")
|
||||
}
|
||||
}
|
||||
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
|
||||
|
||||
check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) }
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
|
||||
/**
|
||||
* Abstract supertype for clauses which compose other clauses together in some logical manner.
|
||||
*/
|
||||
abstract class CompositeClause<in S : ContractState, C : CommandData, in K : Any> : Clause<S, C, K>() {
|
||||
/** List of clauses under this composite clause */
|
||||
abstract val clauses: List<Clause<S, C, K>>
|
||||
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= matchedClauses(commands).flatMap { it.getExecutionPath(commands) }
|
||||
|
||||
/**
|
||||
* Determine which clauses are matched by the supplied commands.
|
||||
*
|
||||
* @throws IllegalStateException if the given commands do not result in a valid execution (for example no match
|
||||
* with [FirstOf]).
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
abstract fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>>
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
|
||||
/**
|
||||
* Filter the states that are passed through to the wrapped clause, to restrict them to a specific type.
|
||||
*/
|
||||
class FilterOn<S : ContractState, C : CommandData, in K : Any>(val clause: Clause<S, C, K>,
|
||||
val filterStates: (List<ContractState>) -> List<S>) : Clause<ContractState, C, K>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
= clause.requiredCommands
|
||||
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= clause.getExecutionPath(commands)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: K?): Set<C>
|
||||
= clause.verify(tx, filterStates(inputs), filterStates(outputs), commands, groupingKey)
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||
*/
|
||||
@Deprecated("Use FirstOf instead")
|
||||
class FirstComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = listOf(clauses.first { it.matches(commands) })
|
||||
|
||||
init {
|
||||
clauses.add(firstClause)
|
||||
clauses.addAll(remainingClauses)
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||
val clause = matchedClauses(commands).singleOrNull() ?: throw IllegalStateException("No delegate clause matched in first composition")
|
||||
return clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
|
||||
override fun toString() = "First: ${clauses.toList()}"
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||
*/
|
||||
class FirstOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
companion object {
|
||||
val logger = loggerFor<FirstOf<*, *, *>>()
|
||||
}
|
||||
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
|
||||
/**
|
||||
* Get the single matched clause from the set this composes, based on the given commands. This is provided as
|
||||
* helper method for internal use, rather than using the exposed [matchedClauses] function which unnecessarily
|
||||
* wraps the clause in a list.
|
||||
*/
|
||||
private fun matchedClause(commands: List<AuthenticatedObject<C>>): Clause<S, C, K> {
|
||||
return clauses.firstOrNull { it.matches(commands) } ?: throw IllegalStateException("No delegate clause matched in first composition")
|
||||
}
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>) = listOf(matchedClause(commands))
|
||||
|
||||
init {
|
||||
clauses.add(firstClause)
|
||||
clauses.addAll(remainingClauses)
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||
return matchedClause(commands).verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
|
||||
override fun toString() = "First: ${clauses.toList()}"
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
abstract class GroupClauseVerifier<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>) : Clause<ContractState, C, Unit>() {
|
||||
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, K>>
|
||||
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= clause.getExecutionPath(commands)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Unit?): Set<C> {
|
||||
val groups = groupStates(tx)
|
||||
val matchedCommands = HashSet<C>()
|
||||
|
||||
for ((groupInputs, groupOutputs, groupToken) in groups) {
|
||||
matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
|
||||
}
|
||||
|
||||
return matchedCommands
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.crypto.composite.CompositeSignature
|
||||
import net.corda.core.crypto.provider.CordaObjectIdentifier
|
||||
import net.corda.core.crypto.provider.CordaSecurityProvider
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
@ -13,19 +14,19 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import org.bouncycastle.asn1.*
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.DERNull
|
||||
import org.bouncycastle.asn1.DLSequence
|
||||
import org.bouncycastle.asn1.bc.BCObjectIdentifiers
|
||||
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers
|
||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||
import org.bouncycastle.asn1.sec.SECObjectIdentifiers
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey
|
||||
@ -39,10 +40,6 @@ import org.bouncycastle.jce.spec.ECPublicKeySpec
|
||||
import org.bouncycastle.math.ec.ECConstants
|
||||
import org.bouncycastle.math.ec.FixedPointCombMultiplier
|
||||
import org.bouncycastle.math.ec.WNafUtil
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||
@ -52,7 +49,6 @@ import java.security.*
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.util.*
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@ -195,7 +191,7 @@ object Crypto {
|
||||
// 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.
|
||||
private val providerMap: Map<String, Provider> = mapOf(
|
||||
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.
|
||||
@ -282,7 +278,7 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* This should be used when the type key is known, e.g. during deserialisation or with key caches or key managers.
|
||||
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey a PKCS8 encoded private key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
@ -293,7 +289,7 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* This should be used when the type key is known, e.g. during deserialisation or with key caches or key managers.
|
||||
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey a PKCS8 encoded private key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
@ -325,7 +321,7 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* This should be used when the type key is known, e.g. during deserialisation or with key caches or key managers.
|
||||
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported.
|
||||
@ -337,7 +333,7 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* This should be used when the type key is known, e.g. during deserialisation or with key caches or key managers.
|
||||
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported.
|
||||
@ -401,23 +397,23 @@ object Crypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic way to sign [MetaData] objects with a [PrivateKey].
|
||||
* [MetaData] is a wrapper over the transaction's Merkle root in order to attach extra information, such as a timestamp or partial and blind signature indicators.
|
||||
* 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].
|
||||
* @param metaData a [MetaData] object that adds extra information to a transaction.
|
||||
* @return a [TransactionSignature] object than contains the output of a successful signing and the metaData.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or
|
||||
* if metaData.schemeCodeName is not aligned with key type.
|
||||
* @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.
|
||||
* @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)
|
||||
fun doSign(privateKey: PrivateKey, metaData: MetaData): TransactionSignature {
|
||||
val sigKey: SignatureScheme = findSignatureScheme(privateKey)
|
||||
val sigMetaData: SignatureScheme = findSignatureScheme(metaData.schemeCodeName)
|
||||
if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${metaData.schemeCodeName} is not aligned with the key type.")
|
||||
val signatureData = doSign(sigKey.schemeCodeName, privateKey, metaData.bytes())
|
||||
return TransactionSignature(signatureData, metaData)
|
||||
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}.")
|
||||
val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes)
|
||||
return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -434,7 +430,7 @@ 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)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
|
||||
|
||||
/**
|
||||
@ -485,9 +481,9 @@ object Crypto {
|
||||
/**
|
||||
* Utility to simplify the act of verifying a [TransactionSignature].
|
||||
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||
* @param publicKey the signer's [PublicKey].
|
||||
* @param transactionSignature the signatureData on a message.
|
||||
* @return true if verification passes or throws an exception if verification fails.
|
||||
* @param txId transaction's id (Merkle root).
|
||||
* @param transactionSignature the signature on the transaction.
|
||||
* @return true if verification passes or throw exception if verification fails.
|
||||
* @throws InvalidKeyException if the key is invalid.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
@ -495,9 +491,26 @@ object Crypto {
|
||||
* @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, transactionSignature: TransactionSignature): Boolean {
|
||||
if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.toStringShort()} does not match")
|
||||
return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes())
|
||||
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.
|
||||
* 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.
|
||||
* @param txId transaction's id (Merkle root).
|
||||
* @param transactionSignature the signature on the transaction.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* 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.
|
||||
*/
|
||||
@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)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -752,90 +765,6 @@ object Crypto {
|
||||
return mac.doFinal(seed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a partial X.509 certificate ready for signing.
|
||||
*
|
||||
* @param issuer name of the issuing entity.
|
||||
* @param subject name of the certificate subject.
|
||||
* @param subjectPublicKey public key of the certificate subject.
|
||||
* @param validityWindow the time period the certificate is valid for.
|
||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||
*/
|
||||
fun createCertificate(certificateType: CertificateType, issuer: X500Name,
|
||||
subject: X500Name, subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509v3CertificateBuilder {
|
||||
|
||||
val serial = BigInteger.valueOf(random63BitValue())
|
||||
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
|
||||
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded))
|
||||
|
||||
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey)
|
||||
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo))
|
||||
.addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA))
|
||||
.addExtension(Extension.keyUsage, false, certificateType.keyUsage)
|
||||
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
|
||||
|
||||
if (nameConstraints != null) {
|
||||
builder.addExtension(Extension.nameConstraints, true, nameConstraints)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and sign an X.509 certificate with the given signer.
|
||||
*
|
||||
* @param issuer name of the issuing entity.
|
||||
* @param issuerSigner content signer to sign the certificate with.
|
||||
* @param subject name of the certificate subject.
|
||||
* @param subjectPublicKey public key of the certificate subject.
|
||||
* @param validityWindow the time period the certificate is valid for.
|
||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||
*/
|
||||
fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerSigner: ContentSigner,
|
||||
subject: X500Name, subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
|
||||
return builder.build(issuerSigner).apply {
|
||||
require(isValidOn(Date()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and sign an X.509 certificate with CA cert private key.
|
||||
*
|
||||
* @param issuer name of the issuing entity.
|
||||
* @param issuerKeyPair the public & private key to sign the certificate with.
|
||||
* @param subject name of the certificate subject.
|
||||
* @param subjectPublicKey public key of the certificate subject.
|
||||
* @param validityWindow the time period the certificate is valid for.
|
||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||
*/
|
||||
fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair,
|
||||
subject: X500Name, subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
|
||||
val signatureScheme = findSignatureScheme(issuerKeyPair.private)
|
||||
val provider = providerMap[signatureScheme.providerName]
|
||||
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
|
||||
|
||||
val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider)
|
||||
return builder.build(signer).apply {
|
||||
require(isValidOn(Date()))
|
||||
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create certificate signing request using provided information.
|
||||
*/
|
||||
fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest {
|
||||
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, providerMap[signatureScheme.providerName])
|
||||
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer)
|
||||
}
|
||||
|
||||
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) }
|
||||
|
@ -35,7 +35,17 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignat
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes) = private.sign(bytesToSign.bytes, public)
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes)
|
||||
/**
|
||||
* Helper function for signing a [SignableData] object.
|
||||
* @param signableData the object to be signed.
|
||||
* @return a [TransactionSignature] object.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSign(this, signableData)
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature.
|
||||
@ -89,7 +99,7 @@ fun PublicKey.containsAny(otherKeys: Iterable<PublicKey>): Boolean {
|
||||
}
|
||||
|
||||
/** Returns the set of all [PublicKey]s of the signatures */
|
||||
fun Iterable<DigitalSignature.WithKey>.byKeys() = map { it.by }.toSet()
|
||||
fun Iterable<TransactionSignature>.byKeys() = map { it.by }.toSet()
|
||||
|
||||
// Allow Kotlin destructuring: val (private, public) = keyPair
|
||||
operator fun KeyPair.component1(): PrivateKey = this.private
|
||||
@ -106,17 +116,6 @@ fun generateKeyPair(): KeyPair = Crypto.generateKeyPair()
|
||||
*/
|
||||
fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEntropy(entropy)
|
||||
|
||||
/**
|
||||
* Helper function for signing.
|
||||
* @param metaData tha attached MetaData object.
|
||||
* @return a [TransactionSignature ] object.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(this, metaData)
|
||||
|
||||
/**
|
||||
* Helper function to verify a signature.
|
||||
* @param signatureData the signature on a message.
|
||||
@ -130,21 +129,6 @@ fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(th
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData)
|
||||
|
||||
/**
|
||||
* Helper function to verify a metadata attached signature. It is noted that the transactionSignature contains
|
||||
* signatureData and a [MetaData] object that contains the signer's public key and the transaction's Merkle root.
|
||||
* @param transactionSignature a [TransactionSignature] object that .
|
||||
* @throws InvalidKeyException if the key is invalid.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PublicKey.verify(transactionSignature: TransactionSignature): Boolean {
|
||||
return Crypto.doVerify(this, transactionSignature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for the signers to verify their own signature.
|
||||
* @param signatureData the signature on a message.
|
||||
|
@ -60,7 +60,7 @@ fun String.hexToBase58(): String = hexToByteArray().toBase58()
|
||||
/** Encoding changer. Hex-[String] to Base64-[String], i.e. "48656C6C6F20576F726C64" -> "SGVsbG8gV29ybGQ=" */
|
||||
fun String.hexToBase64(): String = hexToByteArray().toBase64()
|
||||
|
||||
// TODO We use for both CompositeKeys and EdDSAPublicKey custom Kryo serializers and deserializers. We need to specify encoding.
|
||||
// TODO We use for both CompositeKeys and EdDSAPublicKey custom serializers and deserializers. We need to specify encoding.
|
||||
// TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition
|
||||
// structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
|
||||
fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>()
|
||||
|
@ -23,8 +23,10 @@ sealed class MerkleTree {
|
||||
/**
|
||||
* Merkle tree building using hashes, with zero hash padding to full power of 2.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@Throws(MerkleTreeException::class)
|
||||
fun getMerkleTree(allLeavesHashes: List<SecureHash>): MerkleTree {
|
||||
if (allLeavesHashes.isEmpty())
|
||||
throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.")
|
||||
val leaves = padWithZeros(allLeavesHashes).map { Leaf(it) }
|
||||
return buildMerkleTree(leaves)
|
||||
}
|
||||
@ -46,8 +48,6 @@ sealed class MerkleTree {
|
||||
* @return Tree root.
|
||||
*/
|
||||
private tailrec fun buildMerkleTree(lastNodesList: List<MerkleTree>): MerkleTree {
|
||||
if (lastNodesList.isEmpty())
|
||||
throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.")
|
||||
if (lastNodesList.size == 1) {
|
||||
return lastNodesList[0] //Root reached.
|
||||
} else {
|
||||
|
@ -1,71 +0,0 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.opaque
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A [MetaData] object adds extra information to a transaction. MetaData is used to support a universal
|
||||
* digital signature model enabling full, partial, fully or partially blind and metaData attached signatures,
|
||||
* (such as an attached timestamp). A MetaData object contains both the merkle root of the transaction and the signer's public key.
|
||||
* When signatureType is set to FULL, then visibleInputs and signedInputs can be ignored.
|
||||
* Note: We could omit signatureType as it can always be defined by combining visibleInputs and signedInputs,
|
||||
* but it helps to speed up the process when FULL is used, and thus we can bypass the extra check on boolean arrays.
|
||||
*
|
||||
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param versionID DLT's version.
|
||||
* @param signatureType type of the signature, see [SignatureType] (e.g. FULL, PARTIAL, BLIND, PARTIAL_AND_BLIND).
|
||||
* @param timestamp the signature's timestamp as provided by the signer.
|
||||
* @param visibleInputs for partially/fully blind signatures. We use Merkle tree boolean index flags (from left to right)
|
||||
* indicating what parts of the transaction were visible when the signature was calculated.
|
||||
* @param signedInputs for partial signatures. We use Merkle tree boolean index flags (from left to right)
|
||||
* indicating what parts of the Merkle tree are actually signed.
|
||||
* @param merkleRoot the Merkle root of the transaction.
|
||||
* @param publicKey the signer's public key.
|
||||
*/
|
||||
@CordaSerializable
|
||||
open class MetaData(
|
||||
val schemeCodeName: String,
|
||||
val versionID: String,
|
||||
val signatureType: SignatureType = SignatureType.FULL,
|
||||
val timestamp: Instant?,
|
||||
val visibleInputs: BitSet?,
|
||||
val signedInputs: BitSet?,
|
||||
val merkleRoot: ByteArray,
|
||||
val publicKey: PublicKey) {
|
||||
|
||||
fun bytes() = this.serialize().bytes
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as MetaData
|
||||
|
||||
if (schemeCodeName != other.schemeCodeName) return false
|
||||
if (versionID != other.versionID) return false
|
||||
if (signatureType != other.signatureType) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (visibleInputs != other.visibleInputs) return false
|
||||
if (signedInputs != other.signedInputs) return false
|
||||
if (merkleRoot.opaque() != other.merkleRoot.opaque()) return false
|
||||
if (publicKey != other.publicKey) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = schemeCodeName.hashCode()
|
||||
result = 31 * result + versionID.hashCode()
|
||||
result = 31 * result + signatureType.hashCode()
|
||||
result = 31 * result + (timestamp?.hashCode() ?: 0)
|
||||
result = 31 * result + (visibleInputs?.hashCode() ?: 0)
|
||||
result = 31 * result + (signedInputs?.hashCode() ?: 0)
|
||||
result = 31 * result + Arrays.hashCode(merkleRoot)
|
||||
result = 31 * result + publicKey.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
||||
|
||||
@CordaSerializable
|
||||
class MerkleTreeException(val reason: String) : Exception() {
|
||||
override fun toString() = "Partial Merkle Tree exception. Reason: $reason"
|
||||
}
|
||||
class MerkleTreeException(val reason: String) : Exception("Partial Merkle Tree exception. Reason: $reason")
|
||||
|
||||
/**
|
||||
* Building and verification of Partial Merkle Tree.
|
||||
|
@ -1,8 +1,9 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import com.google.common.io.BaseEncoding
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.parseAsHex
|
||||
import net.corda.core.utilities.toHexString
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
@ -18,7 +19,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = BaseEncoding.base16().encode(bytes)
|
||||
override fun toString(): String = bytes.toHexString()
|
||||
|
||||
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
|
||||
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
|
||||
@ -26,7 +27,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parse(str: String) = BaseEncoding.base16().decode(str.toUpperCase()).let {
|
||||
fun parse(str: String) = str.toUpperCase().parseAsHex().let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
|
14
core/src/main/kotlin/net/corda/core/crypto/SignableData.kt
Normal file
14
core/src/main/kotlin/net/corda/core/crypto/SignableData.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* A [SignableData] object is the packet actually signed.
|
||||
* It works as a wrapper over transaction id and signature metadata.
|
||||
*
|
||||
* @param txId transaction's id.
|
||||
* @param signatureMetadata meta data required.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class SignableData(val txId: SecureHash, val signatureMetadata: SignatureMetadata)
|
||||
|
@ -0,0 +1,15 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* SignatureMeta is required to add extra meta-data to a transaction's signature.
|
||||
* It currently supports platformVersion only, but it can be extended to support a universal digital
|
||||
* signature model enabling partial signatures and attaching extra information, such as a user's timestamp or other
|
||||
* application-specific fields.
|
||||
*
|
||||
* @param platformVersion current DLT version.
|
||||
* @param schemeNumberID number id of the signature scheme used based on signer's key-pair, see [SignatureScheme.schemeNumberID].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class SignatureMetadata(val platformVersion: Int, val schemeNumberID: Int)
|
@ -6,7 +6,7 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
|
||||
/**
|
||||
* This class is used to define a digital signature scheme.
|
||||
* @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes.
|
||||
* @param schemeNumberID we assign a number ID for better efficiency on-wire serialisation. Please ensure uniqueness between schemes.
|
||||
* @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
|
||||
* @param signatureOID ASN.1 algorithm identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA)
|
||||
* @param alternativeOIDs ASN.1 algorithm identifiers for keys of the signature, where we want to map multiple keys to
|
||||
|
@ -1,17 +0,0 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* Supported Signature types:
|
||||
* <p><ul>
|
||||
* <li>FULL = signature covers whole transaction, by the convention that signing the Merkle root, it is equivalent to signing all parts of the transaction.
|
||||
* <li>PARTIAL = signature covers only a part of the transaction, see [MetaData].
|
||||
* <li>BLIND = when an entity blindly signs without having full knowledge on the content, see [MetaData].
|
||||
* <li>PARTIAL_AND_BLIND = combined PARTIAL and BLIND in the same time.
|
||||
* </ul>
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class SignatureType {
|
||||
FULL, PARTIAL, BLIND, PARTIAL_AND_BLIND
|
||||
}
|
@ -23,7 +23,8 @@ open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSign
|
||||
@Throws(SignatureException::class)
|
||||
fun verified(): T {
|
||||
sig.by.verify(raw.bytes, sig)
|
||||
val data = raw.deserialize()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val data = raw.deserialize<Any>() as T
|
||||
verifyData(data)
|
||||
return data
|
||||
}
|
||||
|
@ -1,22 +1,57 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A wrapper around a digital signature accompanied with metadata, see [MetaData.Full] and [DigitalSignature].
|
||||
* The signature protocol works as follows: s = sign(MetaData.hashBytes).
|
||||
* A wrapper over the signature output accompanied by signer's public key and signature metadata.
|
||||
* This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction signatures.
|
||||
*/
|
||||
open class TransactionSignature(val signatureData: ByteArray, val metaData: MetaData) : DigitalSignature(signatureData) {
|
||||
@CordaSerializable
|
||||
class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes) {
|
||||
/**
|
||||
* Function to auto-verify a [MetaData] object's signature.
|
||||
* Note that [MetaData] contains both public key and merkle root of the transaction.
|
||||
* Function to verify a [SignableData] object's signature.
|
||||
* Note that [SignableData] contains the id of the transaction and extra metadata, such as DLT's platform version.
|
||||
*
|
||||
* @param txId transaction's id (Merkle root), which along with [signatureMetadata] will be used to construct the [SignableData] object to be signed.
|
||||
* @throws InvalidKeyException if the key is invalid.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun verify(): Boolean = Crypto.doVerify(metaData.publicKey, signatureData, metaData.bytes())
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun verify(txId: SecureHash) = Crypto.doVerify(txId, this)
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an
|
||||
* exception, making it more suitable where a boolean is required, but normally you should use the function
|
||||
* which throws, as it avoids the risk of failing to test the result.
|
||||
*
|
||||
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
|
||||
* signature).
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged).
|
||||
* @return whether the signature is correct for this key.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun isValid(txId: SecureHash) = Crypto.isValid(txId, this)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TransactionSignature) return false
|
||||
|
||||
return (Arrays.equals(bytes, other.bytes)
|
||||
&& by == other.by
|
||||
&& signatureMetadata == other.signatureMetadata)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + by.hashCode()
|
||||
result = 31 * result + signatureMetadata.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
77
core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt
Normal file
77
core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt
Normal file
@ -0,0 +1,77 @@
|
||||
@file:JvmName("X500NameUtils")
|
||||
package net.corda.core.crypto
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Rebuild the distinguished name, adding a postfix to the common name. If no common name is present.
|
||||
* @throws IllegalArgumentException if the distinguished name does not contain a common name element.
|
||||
*/
|
||||
fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName }
|
||||
|
||||
/**
|
||||
* Rebuild the distinguished name, replacing the common name with the given value. If no common name is present, this
|
||||
* adds one.
|
||||
* @throws IllegalArgumentException if the distinguished name does not contain a common name element.
|
||||
*/
|
||||
fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName }
|
||||
|
||||
/**
|
||||
* Rebuild the distinguished name, replacing the common name with a value generated from the provided function.
|
||||
*
|
||||
* @param mutator a function to generate the new value from the previous one.
|
||||
* @throws IllegalArgumentException if the distinguished name does not contain a common name element.
|
||||
*/
|
||||
private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500Name {
|
||||
val builder = X500NameBuilder(BCStyle.INSTANCE)
|
||||
var matched = false
|
||||
this.rdNs.forEach { rdn ->
|
||||
rdn.typesAndValues.forEach { typeAndValue ->
|
||||
when (typeAndValue.type) {
|
||||
BCStyle.CN -> {
|
||||
matched = true
|
||||
builder.addRDN(typeAndValue.type, mutator(typeAndValue.value))
|
||||
}
|
||||
else -> {
|
||||
builder.addRDN(typeAndValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require(matched) { "Input X.500 name must include a common name (CN) attribute: ${this}" }
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString()
|
||||
val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString()
|
||||
val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString()
|
||||
val X500Name.locationOrNull: String? get() = try {
|
||||
location
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject
|
||||
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
|
||||
|
||||
/**
|
||||
* Generate a distinguished name from the provided values.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun getX509Name(myLegalName: String, nearestCity: String, email: String, country: String? = null): X500Name {
|
||||
return X500NameBuilder(BCStyle.INSTANCE).let { builder ->
|
||||
builder.addRDN(BCStyle.CN, myLegalName)
|
||||
builder.addRDN(BCStyle.L, nearestCity)
|
||||
country?.let { builder.addRDN(BCStyle.C, it) }
|
||||
builder.addRDN(BCStyle.E, email)
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)
|
@ -4,13 +4,13 @@ import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.composite.CompositeKey.NodeAndWeight
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.crypto.provider.CordaObjectIdentifier
|
||||
import net.corda.core.crypto.toSHA256Bytes
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.utilities.exactAdd
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.sequence
|
||||
import org.bouncycastle.asn1.*
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
@ -59,7 +59,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
}
|
||||
}
|
||||
|
||||
val children = children.sorted()
|
||||
val children: List<NodeAndWeight> = children.sorted()
|
||||
|
||||
init {
|
||||
// TODO: replace with the more extensive, but slower, checkValidity() test.
|
||||
@ -127,7 +127,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
var sum = 0
|
||||
for ((_, weight) in children) {
|
||||
require(weight > 0) { "Non-positive weight: $weight detected." }
|
||||
sum = Math.addExact(sum, weight) // Add and check for integer overflow.
|
||||
sum = sum exactAdd weight // Add and check for integer overflow.
|
||||
}
|
||||
return sum
|
||||
}
|
||||
@ -145,7 +145,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
override fun compareTo(other: NodeAndWeight): Int {
|
||||
return if (weight == other.weight)
|
||||
ByteBuffer.wrap(node.toSHA256Bytes()).compareTo(ByteBuffer.wrap(other.node.toSHA256Bytes()))
|
||||
node.encoded.sequence().compareTo(other.node.encoded.sequence())
|
||||
else
|
||||
weight.compareTo(other.weight)
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package net.corda.core.crypto.composite
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.serialization.deserialize
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.*
|
||||
import java.security.spec.AlgorithmParameterSpec
|
||||
@ -77,7 +76,7 @@ class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
||||
fun engineVerify(sigBytes: ByteArray): Boolean {
|
||||
val sig = sigBytes.deserialize<CompositeSignaturesWithKeys>()
|
||||
return if (verifyKey.isFulfilledBy(sig.sigs.map { it.by })) {
|
||||
val clearData = buffer.toByteArray()
|
||||
val clearData = SecureHash.SHA256(buffer.toByteArray())
|
||||
sig.sigs.all { it.isValid(clearData) }
|
||||
} else {
|
||||
false
|
||||
|
@ -1,14 +1,14 @@
|
||||
package net.corda.core.crypto.composite
|
||||
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* Custom class for holding signature data. This exists for later extension work to provide a standardised cross-platform
|
||||
* serialization format (i.e. not Kryo).
|
||||
* serialization format.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class CompositeSignaturesWithKeys(val sigs: List<DigitalSignature.WithKey>) {
|
||||
data class CompositeSignaturesWithKeys(val sigs: List<TransactionSignature>) {
|
||||
companion object {
|
||||
val EMPTY = CompositeSignaturesWithKeys(emptyList())
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
package net.corda.core.crypto.testing
|
||||
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
|
||||
@CordaSerializable
|
||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "NULL"
|
||||
override fun getEncoded() = byteArrayOf(0)
|
||||
override fun getFormat() = "NULL"
|
||||
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
||||
override fun toString() = "NULL_KEY"
|
||||
}
|
||||
|
||||
val NULL_PARTY = AnonymousParty(NullPublicKey)
|
||||
|
||||
// TODO: Clean up this duplication between Null and Dummy public key
|
||||
@CordaSerializable
|
||||
@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead")
|
||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "DUMMY"
|
||||
override fun getEncoded() = s.toByteArray()
|
||||
override fun getFormat() = "ASN.1"
|
||||
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
|
||||
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
|
||||
override fun hashCode(): Int = s.hashCode()
|
||||
override fun toString() = "PUBKEY[$s]"
|
||||
}
|
||||
|
||||
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
|
||||
@CordaSerializable
|
||||
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
|
@ -0,0 +1,21 @@
|
||||
package net.corda.core.crypto.testing
|
||||
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
|
||||
@CordaSerializable
|
||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "NULL"
|
||||
override fun getEncoded() = byteArrayOf(0)
|
||||
override fun getFormat() = "NULL"
|
||||
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
||||
override fun toString() = "NULL_KEY"
|
||||
}
|
||||
|
||||
val NULL_PARTY = AnonymousParty(NullPublicKey)
|
||||
|
||||
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
|
||||
val NULL_SIGNATURE = TransactionSignature(ByteArray(32), NullPublicKey, SignatureMetadata(1, -1))
|
@ -1,19 +1,14 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -32,7 +27,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
* @param M the type of a class representing proposed modification by the instigator.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Proposal<out M>(val stateRef: StateRef, val modification: M, val stx: SignedTransaction)
|
||||
data class Proposal<out M>(val stateRef: StateRef, val modification: M)
|
||||
|
||||
/**
|
||||
* The assembled transaction for upgrading a contract.
|
||||
@ -56,7 +51,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
abstract class Instigator<out S : ContractState, out T : ContractState, out M>(
|
||||
val originalState: StateAndRef<S>,
|
||||
val modification: M,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<StateAndRef<T>>() {
|
||||
override val progressTracker: ProgressTracker = Instigator.tracker()) : FlowLogic<StateAndRef<T>>() {
|
||||
companion object {
|
||||
object SIGNING : ProgressTracker.Step("Requesting signatures from other parties")
|
||||
object NOTARY : ProgressTracker.Step("Requesting notary signature")
|
||||
@ -79,7 +74,16 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
val finalTx = stx + signatures
|
||||
serviceHub.recordTransactions(finalTx)
|
||||
return finalTx.tx.outRef(0)
|
||||
|
||||
val newOutput = run {
|
||||
if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub).outRef<T>(0)
|
||||
} else {
|
||||
stx.tx.outRef<T>(0)
|
||||
}
|
||||
}
|
||||
|
||||
return newOutput
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,7 +95,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
abstract protected fun assembleTx(): UpgradeTx
|
||||
|
||||
@Suspendable
|
||||
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<TransactionSignature> {
|
||||
val parties = participants.map {
|
||||
val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?:
|
||||
throw IllegalStateException("Participant $it to state $originalState not found on the network")
|
||||
@ -109,10 +113,10 @@ abstract class AbstractStateReplacementFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getParticipantSignature(party: Party, stx: SignedTransaction): DigitalSignature.WithKey {
|
||||
val proposal = Proposal(originalState.ref, modification, stx)
|
||||
val response = sendAndReceive<DigitalSignature.WithKey>(party, proposal)
|
||||
return response.unwrap {
|
||||
private fun getParticipantSignature(party: Party, stx: SignedTransaction): TransactionSignature {
|
||||
val proposal = Proposal(originalState.ref, modification)
|
||||
subFlow(SendTransactionFlow(party, stx))
|
||||
return sendAndReceive<TransactionSignature>(party, proposal).unwrap {
|
||||
check(party.owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
it.verify(stx.id)
|
||||
it
|
||||
@ -120,7 +124,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||
private fun getNotarySignatures(stx: SignedTransaction): List<TransactionSignature> {
|
||||
progressTracker.currentStep = NOTARY
|
||||
try {
|
||||
return subFlow(NotaryFlow.Client(stx))
|
||||
@ -133,7 +137,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
// Type parameter should ideally be Unit but that prevents Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
||||
// We use Void? instead of Unit? as that's what you'd use in Java.
|
||||
abstract class Acceptor<in T>(val otherSide: Party,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<Void?>() {
|
||||
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
|
||||
companion object {
|
||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||
@ -145,63 +149,61 @@ abstract class AbstractStateReplacementFlow {
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call(): Void? {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// We expect stx to have insufficient signatures here
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
checkMySignatureRequired(stx)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
||||
val stx: SignedTransaction = maybeProposal.unwrap {
|
||||
verifyProposal(it)
|
||||
verifyTx(it.stx)
|
||||
it.stx
|
||||
maybeProposal.unwrap {
|
||||
verifyProposal(stx, it)
|
||||
}
|
||||
approve(stx)
|
||||
return null
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun verifyTx(stx: SignedTransaction) {
|
||||
checkMySignatureRequired(stx.tx)
|
||||
checkDependenciesValid(stx)
|
||||
// We expect stx to have insufficient signatures, so we convert the WireTransaction to the LedgerTransaction
|
||||
// here, thus bypassing the sufficient-signatures check.
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun approve(stx: SignedTransaction) {
|
||||
progressTracker.currentStep = APPROVING
|
||||
|
||||
val mySignature = sign(stx)
|
||||
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, mySignature)
|
||||
val swapSignatures = sendAndReceive<List<TransactionSignature>>(otherSide, mySignature)
|
||||
|
||||
// TODO: This step should not be necessary, as signatures are re-checked in verifySignatures.
|
||||
// TODO: This step should not be necessary, as signatures are re-checked in verifyRequiredSignatures.
|
||||
val allSignatures = swapSignatures.unwrap { signatures ->
|
||||
signatures.forEach { it.verify(stx.id) }
|
||||
signatures
|
||||
}
|
||||
|
||||
val finalTx = stx + allSignatures
|
||||
finalTx.verifySignatures()
|
||||
if (finalTx.isNotaryChangeTransaction()) {
|
||||
finalTx.resolveNotaryChangeTransaction(serviceHub).verifyRequiredSignatures()
|
||||
} else {
|
||||
finalTx.verifyRequiredSignatures()
|
||||
}
|
||||
serviceHub.recordTransactions(finalTx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state change proposal to confirm that it's acceptable to this node. Rules for verification depend
|
||||
* on the change proposed, and may further depend on the node itself (for example configuration). The
|
||||
* proposal is returned if acceptable, otherwise a [StateReplacementException] is thrown.
|
||||
* Check the state change proposal and the signed transaction to confirm that it's acceptable to this node.
|
||||
* Rules for verification depend on the change proposed, and may further depend on the node itself (for example configuration).
|
||||
* The proposal is returned if acceptable, otherwise a [StateReplacementException] is thrown.
|
||||
*/
|
||||
@Throws(StateReplacementException::class)
|
||||
abstract protected fun verifyProposal(proposal: Proposal<T>)
|
||||
abstract protected fun verifyProposal(stx: SignedTransaction, proposal: Proposal<T>)
|
||||
|
||||
private fun checkMySignatureRequired(tx: WireTransaction) {
|
||||
private fun checkMySignatureRequired(stx: SignedTransaction) {
|
||||
// TODO: use keys from the keyManagementService instead
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
require(myKey in tx.mustSign) { "Party is not a participant for any of the input states of transaction ${tx.id}" }
|
||||
|
||||
val requiredKeys = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys
|
||||
} else {
|
||||
stx.tx.requiredSigningKeys
|
||||
}
|
||||
|
||||
require(myKey in requiredKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" }
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependenciesValid(stx: SignedTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(stx.tx, otherSide))
|
||||
}
|
||||
|
||||
private fun sign(stx: SignedTransaction): DigitalSignature.WithKey {
|
||||
private fun sign(stx: SignedTransaction): TransactionSignature {
|
||||
return serviceHub.createSignature(stx)
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
|
||||
/**
|
||||
* Notify the specified parties about a transaction. The remote peers will download this transaction and its
|
||||
@ -18,17 +16,13 @@ import net.corda.core.transactions.SignedTransaction
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
||||
val participants: Set<Party>) : FlowLogic<Unit>() {
|
||||
@CordaSerializable
|
||||
data class NotifyTxRequest(val tx: SignedTransaction)
|
||||
|
||||
val participants: NonEmptySet<Party>) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// TODO: Messaging layer should handle this broadcast for us
|
||||
val msg = NotifyTxRequest(notarisedTransaction)
|
||||
participants.filter { it != serviceHub.myInfo.legalIdentity }.forEach { participant ->
|
||||
// This pops out the other side in NotifyTransactionHandler
|
||||
send(participant, msg)
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(participant, notarisedTransaction))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -19,7 +17,7 @@ import java.security.PublicKey
|
||||
*
|
||||
* You would typically use this flow after you have built a transaction with the TransactionBuilder and signed it with
|
||||
* your key pair. If there are additional signatures to collect then they can be collected using this flow. Signatures
|
||||
* are collected based upon the [WireTransaction.mustSign] property which contains the union of all the PublicKeys
|
||||
* are collected based upon the [WireTransaction.requiredSigningKeys] property which contains the union of all the PublicKeys
|
||||
* listed in the transaction's commands as well as a notary's public key, if required. This flow returns a
|
||||
* [SignedTransaction] which can then be passed to the [FinalityFlow] for notarisation. The other side of this flow is
|
||||
* the [SignTransactionFlow].
|
||||
@ -44,7 +42,7 @@ import java.security.PublicKey
|
||||
*
|
||||
* Example - issuing a multi-lateral agreement which requires N signatures:
|
||||
*
|
||||
* val builder = TransactionType.General.Builder(notaryRef)
|
||||
* val builder = TransactionBuilder(notaryRef)
|
||||
* val issueCommand = Command(Agreement.Commands.Issue(), state.participants)
|
||||
*
|
||||
* builder.withItems(state, issueCommand)
|
||||
@ -62,7 +60,7 @@ import java.security.PublicKey
|
||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||
// TODO: Update this flow to handle randomly generated keys when that works is complete.
|
||||
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
override val progressTracker: ProgressTracker = tracker()): FlowLogic<SignedTransaction>() {
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||
@ -80,7 +78,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
// Usually just the Initiator and possibly an oracle would have signed at this point.
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
val signed = partiallySignedTx.sigs.map { it.by }
|
||||
val notSigned = partiallySignedTx.tx.mustSign - signed
|
||||
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 }) {
|
||||
@ -88,7 +86,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
}
|
||||
|
||||
// The signatures must be valid and the transaction must be valid.
|
||||
partiallySignedTx.verifySignatures(*notSigned.toTypedArray())
|
||||
partiallySignedTx.verifySignaturesExcept(*notSigned.toTypedArray())
|
||||
partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
// Determine who still needs to sign.
|
||||
@ -107,7 +105,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
|
||||
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
||||
progressTracker.currentStep = VERIFYING
|
||||
if (notaryKey != null) stx.verifySignatures(notaryKey) else stx.verifySignatures()
|
||||
if (notaryKey != null) stx.verifySignaturesExcept(notaryKey) else stx.verifyRequiredSignatures()
|
||||
|
||||
return stx
|
||||
}
|
||||
@ -115,7 +113,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
/**
|
||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
|
||||
*/
|
||||
@Suspendable private fun keysToParties(keys: List<PublicKey>): List<Party> = keys.map {
|
||||
@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)
|
||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
||||
@ -126,8 +124,10 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*/
|
||||
@Suspendable private fun collectSignature(counterparty: Party): DigitalSignature.WithKey {
|
||||
return sendAndReceive<DigitalSignature.WithKey>(counterparty, partiallySignedTx).unwrap {
|
||||
@Suspendable private fun collectSignature(counterparty: Party): TransactionSignature {
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
|
||||
return receive<TransactionSignature>(counterparty).unwrap {
|
||||
require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." }
|
||||
it
|
||||
}
|
||||
@ -175,7 +175,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
* @param otherParty The counter-party which is providing you a transaction to sign.
|
||||
*/
|
||||
abstract class SignTransactionFlow(val otherParty: Party,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SignedTransaction>() {
|
||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
|
||||
@ -187,35 +187,30 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
val checkedProposal = receive<SignedTransaction>(otherParty).unwrap { proposal ->
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// Check that the Responder actually needs to sign.
|
||||
checkMySignatureRequired(proposal)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(proposal)
|
||||
// Resolve dependencies and verify, pass in the WireTransaction as we don't have all signatures.
|
||||
subFlow(ResolveTransactionsFlow(proposal.tx, otherParty))
|
||||
proposal.tx.toLedgerTransaction(serviceHub).verify()
|
||||
// Perform some custom verification over the transaction.
|
||||
try {
|
||||
checkTransaction(proposal)
|
||||
} catch(e: Exception) {
|
||||
if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError)
|
||||
throw FlowException(e)
|
||||
else
|
||||
throw e
|
||||
}
|
||||
// All good. Unwrap the proposal.
|
||||
proposal
|
||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// Check that the Responder actually needs to sign.
|
||||
checkMySignatureRequired(stx)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
// Perform some custom verification over the transaction.
|
||||
try {
|
||||
checkTransaction(stx)
|
||||
} catch(e: Exception) {
|
||||
if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError)
|
||||
throw FlowException(e)
|
||||
else
|
||||
throw e
|
||||
}
|
||||
|
||||
// Sign and send back our signature to the Initiator.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mySignature = serviceHub.createSignature(checkedProposal)
|
||||
val mySignature = serviceHub.createSignature(stx)
|
||||
send(otherParty, mySignature)
|
||||
|
||||
// Return the fully signed transaction once it has been committed.
|
||||
return waitForLedgerCommit(checkedProposal.id)
|
||||
return waitForLedgerCommit(stx.id)
|
||||
}
|
||||
|
||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||
@ -223,9 +218,9 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction."
|
||||
}
|
||||
val signed = stx.sigs.map { it.by }
|
||||
val allSigners = stx.tx.mustSign
|
||||
val allSigners = stx.tx.requiredSigningKeys
|
||||
val notSigned = allSigners - signed
|
||||
stx.verifySignatures(*notSigned.toTypedArray())
|
||||
stx.verifySignaturesExcept(*notSigned.toTypedArray())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -253,7 +248,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
@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.mustSign) {
|
||||
require(myKey in stx.tx.requiredSigningKeys) {
|
||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -25,14 +22,17 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun verify(tx: TransactionForContract) {
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
|
||||
verify(tx.inputs.single(), tx.outputs.single(), tx.commands.map { Command(it.value, it.signers) }.single())
|
||||
verify(
|
||||
tx.inputStates.single(),
|
||||
tx.outputStates.single(),
|
||||
tx.commandsOfType<UpgradeCommand>().single())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun verify(input: ContractState, output: ContractState, commandData: Command) {
|
||||
val command = commandData.value as UpgradeCommand
|
||||
fun verify(input: ContractState, output: ContractState, commandData: Command<UpgradeCommand>) {
|
||||
val command = commandData.value
|
||||
val participantKeys: Set<PublicKey> = input.participants.map { it.owningKey }.toSet()
|
||||
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -47,19 +47,22 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
|
||||
|
||||
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||
stateRef: StateAndRef<OldState>,
|
||||
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
|
||||
privacySalt: PrivacySalt
|
||||
): TransactionBuilder {
|
||||
val contractUpgrade = upgradedContractClass.newInstance()
|
||||
return TransactionType.General.Builder(stateRef.state.notary)
|
||||
return TransactionBuilder(stateRef.state.notary)
|
||||
.withItems(
|
||||
stateRef,
|
||||
contractUpgrade.upgrade(stateRef.state.data),
|
||||
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }))
|
||||
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }),
|
||||
privacySalt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||
val baseTx = assembleBareTx(originalState, modification)
|
||||
val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
|
||||
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
@ -1,16 +1,18 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
|
||||
/**
|
||||
* Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions
|
||||
@ -32,9 +34,10 @@ import net.corda.core.utilities.ProgressTracker
|
||||
* @param transactions What to commit.
|
||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||
*/
|
||||
class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
|
||||
val extraParticipants: Set<Participant> = extraRecipients.map { it -> Participant(it, it) }.toSet()
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
|
||||
@ -50,6 +53,9 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
||||
}
|
||||
|
||||
open protected val me
|
||||
get() = serviceHub.myInfo.legalIdentity
|
||||
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): List<SignedTransaction> {
|
||||
@ -59,31 +65,43 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
// Lookup the resolved transactions and use them to map each signed transaction to the list of participants.
|
||||
// Then send to the notary if needed, record locally and distribute.
|
||||
progressTracker.currentStep = NOTARISING
|
||||
val notarisedTxns = notariseAndRecord(lookupParties(resolveDependenciesOf(transactions)))
|
||||
val notarisedTxns: List<Pair<SignedTransaction, Set<Participant>>> = resolveDependenciesOf(transactions)
|
||||
.map { (stx, ltx) -> Pair(notariseAndRecord(stx), lookupParties(ltx)) }
|
||||
|
||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||
progressTracker.currentStep = BROADCASTING
|
||||
val me = serviceHub.myInfo.legalIdentity
|
||||
for ((stx, parties) in notarisedTxns) {
|
||||
subFlow(BroadcastTransactionFlow(stx, parties + extraRecipients - me))
|
||||
broadcastTransaction(stx, (parties + extraParticipants).filter { it.wellKnown != me })
|
||||
}
|
||||
return notarisedTxns.map { it.first }
|
||||
}
|
||||
|
||||
// TODO: API: Make some of these protected?
|
||||
/**
|
||||
* Broadcast a transaction to the participants. By default calls [BroadcastTransactionFlow], however can be
|
||||
* overridden for more complex transaction delivery protocols (for example where not all parties know each other).
|
||||
* This implementation will filter out any participants for whom there is no well known identity.
|
||||
*
|
||||
* @param participants the participants to send the transaction to. This is expected to include extra participants
|
||||
* and exclude the local node.
|
||||
*/
|
||||
@Suspendable
|
||||
open protected fun broadcastTransaction(stx: SignedTransaction, participants: Iterable<Participant>) {
|
||||
val wellKnownParticipants = participants.map { it.wellKnown }.filterNotNull()
|
||||
if (wellKnownParticipants.isNotEmpty()) {
|
||||
subFlow(BroadcastTransactionFlow(stx, wellKnownParticipants.toNonEmptySet()))
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun notariseAndRecord(stxnsAndParties: List<Pair<SignedTransaction, Set<Party>>>): List<Pair<SignedTransaction, Set<Party>>> {
|
||||
return stxnsAndParties.map { (stx, parties) ->
|
||||
val notarised = if (needsNotarySignature(stx)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySignatures
|
||||
} else {
|
||||
stx
|
||||
}
|
||||
serviceHub.recordTransactions(notarised)
|
||||
Pair(notarised, parties)
|
||||
private fun notariseAndRecord(stx: SignedTransaction): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(stx)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySignatures
|
||||
} else {
|
||||
stx
|
||||
}
|
||||
serviceHub.recordTransactions(notarised)
|
||||
return notarised
|
||||
}
|
||||
|
||||
private fun needsNotarySignature(stx: SignedTransaction): Boolean {
|
||||
@ -99,14 +117,31 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
return !(notaryKey?.isFulfilledBy(signers) ?: false)
|
||||
}
|
||||
|
||||
private fun lookupParties(ltxns: List<Pair<SignedTransaction, LedgerTransaction>>): List<Pair<SignedTransaction, Set<Party>>> {
|
||||
return ltxns.map { (stx, ltx) ->
|
||||
// Calculate who is meant to see the results based on the participants involved.
|
||||
val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants }
|
||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail?
|
||||
val parties = keys.mapNotNull { serviceHub.identityService.partyFromAnonymous(it) }.toSet()
|
||||
Pair(stx, parties)
|
||||
}
|
||||
/**
|
||||
* Resolve the parties involved in a transaction.
|
||||
*
|
||||
* @return the set of participants and their resolved well known identities (where known).
|
||||
*/
|
||||
open protected fun lookupParties(ltx: LedgerTransaction): Set<Participant> {
|
||||
// Calculate who is meant to see the results based on the participants involved.
|
||||
return extractParticipants(ltx)
|
||||
.map(this::partyFromAnonymous)
|
||||
.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract all participants from a ledger transaction. Intended to help implement [lookupParties]
|
||||
* overriding functions.
|
||||
*/
|
||||
protected fun extractParticipants(ltx: LedgerTransaction): List<AbstractParty> {
|
||||
return ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function which wraps [IdentityService.partyFromAnonymous] so it can be called as a lambda function.
|
||||
*/
|
||||
protected fun partyFromAnonymous(anon: AbstractParty): Participant {
|
||||
return Participant(anon, serviceHub.identityService.partyFromAnonymous(anon))
|
||||
}
|
||||
|
||||
private fun resolveDependenciesOf(signedTransactions: Iterable<SignedTransaction>): List<Pair<SignedTransaction, LedgerTransaction>> {
|
||||
@ -125,10 +160,12 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
return sorted.map { stx ->
|
||||
val notary = stx.tx.notary
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
val wtx = if (notary != null) stx.verifySignatures(notary.owningKey) else stx.verifySignatures()
|
||||
val ltx = wtx.toLedgerTransaction(augmentedLookup)
|
||||
if (notary != null) stx.verifySignaturesExcept(notary.owningKey) else stx.verifyRequiredSignatures()
|
||||
val ltx = stx.toLedgerTransaction(augmentedLookup, false)
|
||||
ltx.verify()
|
||||
stx to ltx
|
||||
}
|
||||
}
|
||||
|
||||
data class Participant(val participant: AbstractParty, val wellKnown: Party?)
|
||||
}
|
@ -25,6 +25,6 @@ open class FlowException(message: String?, cause: Throwable?) : CordaException(m
|
||||
* that we were not expecting), or the other side had an internal error, or the other side terminated when we
|
||||
* were waiting for a response.
|
||||
*/
|
||||
class FlowSessionException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
|
||||
class UnexpectedFlowEndException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
|
||||
constructor(msg: String) : this(msg, null)
|
||||
}
|
@ -4,8 +4,10 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.abbreviate
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
@ -51,10 +53,15 @@ abstract class FlowLogic<out T> {
|
||||
*/
|
||||
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
||||
|
||||
@Deprecated("This is no longer used and will be removed in a future release. If you are using this to communicate " +
|
||||
"with the same party but for two different message streams, then the correct way of doing that is to use sub-flows",
|
||||
level = DeprecationLevel.ERROR)
|
||||
open fun getCounterpartyMarker(party: Party): Class<*> = javaClass
|
||||
/**
|
||||
* Returns a [FlowContext] object describing the flow [otherParty] is using. With [FlowContext.flowVersion] it
|
||||
* provides the necessary information needed for the evolution of flows and enabling backwards compatibility.
|
||||
*
|
||||
* This method can be called before any send or receive has been done with [otherParty]. In such a case this will force
|
||||
* them to start their flow.
|
||||
*/
|
||||
@Suspendable
|
||||
fun getFlowContext(otherParty: Party): FlowContext = stateMachine.getFlowContext(otherParty, flowUsedForSessions)
|
||||
|
||||
/**
|
||||
* Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
|
||||
@ -89,11 +96,6 @@ abstract class FlowLogic<out T> {
|
||||
return stateMachine.sendAndReceive(receiveType, otherParty, payload, flowUsedForSessions)
|
||||
}
|
||||
|
||||
/** @see sendAndReceiveWithRetry */
|
||||
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
return sendAndReceiveWithRetry(R::class.java, otherParty, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to [sendAndReceive] but also instructs the `payload` to be redelivered until the expected message is received.
|
||||
*
|
||||
@ -103,9 +105,8 @@ abstract class FlowLogic<out T> {
|
||||
* oracle services. If one or more nodes in the service cluster go down mid-session, the message will be redelivered
|
||||
* to a different one, so there is no need to wait until the initial node comes back up to obtain a response.
|
||||
*/
|
||||
@Suspendable
|
||||
internal open fun <R : Any> sendAndReceiveWithRetry(receiveType: Class<R>, otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(receiveType, otherParty, payload, flowUsedForSessions, true)
|
||||
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, true)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,7 +140,7 @@ abstract class FlowLogic<out T> {
|
||||
* network's event horizon time.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
|
||||
/**
|
||||
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
|
||||
@ -163,7 +164,7 @@ abstract class FlowLogic<out T> {
|
||||
}
|
||||
logger.debug { "Calling subflow: $subLogic" }
|
||||
val result = subLogic.call()
|
||||
logger.debug { "Subflow finished with result $result" }
|
||||
logger.debug { "Subflow finished with result ${result.toString().abbreviate(300)}" }
|
||||
// It's easy to forget this when writing flows so we just step it to the DONE state when it completes.
|
||||
subLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
return result
|
||||
@ -180,7 +181,9 @@ abstract class FlowLogic<out T> {
|
||||
* @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded.
|
||||
*/
|
||||
@Throws(FlowException::class)
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) = stateMachine.checkFlowPermission(permissionName, extraAuditData)
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) {
|
||||
stateMachine.checkFlowPermission(permissionName, extraAuditData)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -189,7 +192,9 @@ abstract class FlowLogic<out T> {
|
||||
* @param comment a general human readable summary of the event.
|
||||
* @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded.
|
||||
*/
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData)
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>) {
|
||||
stateMachine.recordAuditEvent(eventType, comment, extraAuditData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
|
||||
@ -230,6 +235,29 @@ abstract class FlowLogic<out T> {
|
||||
@Suspendable
|
||||
fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Also, this logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private var _stateMachine: FlowStateMachine<*>? = null
|
||||
@ -261,3 +289,20 @@ abstract class FlowLogic<out T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Version and name of the CorDapp hosting the other side of the flow.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class FlowContext(
|
||||
/**
|
||||
* The integer flow version the other side is using.
|
||||
* @see InitiatingFlow
|
||||
*/
|
||||
val flowVersion: Int,
|
||||
/**
|
||||
* Name of the CorDapp jar hosting the flow, without the .jar extension. It will include a unique identifier
|
||||
* to deduplicate it from other releases of the same CorDapp, typically a version string. See the
|
||||
* [CorDapp JAR format](https://docs.corda.net/cordapp-build-systems.html#cordapp-jar-format) for more details.
|
||||
*/
|
||||
val appName: String)
|
||||
|
@ -0,0 +1,73 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: Class<*>? = null,
|
||||
val stackFrames: List<Frame> = listOf()
|
||||
) {
|
||||
data class Frame(
|
||||
val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?> = listOf()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Token class, used to indicate stack presence of the corda internal data. Since this data is of no use for
|
||||
* a CordApp developer, it is skipped from serialisation and its presence is only marked by this token.
|
||||
*/
|
||||
data class StackFrameDataToken(val className: String)
|
@ -0,0 +1,20 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* Alternative finality flow which only does not attempt to take participants from the transaction, but instead all
|
||||
* participating parties must be provided manually.
|
||||
*
|
||||
* @param transactions What to commit.
|
||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||
*/
|
||||
class ManualFinalityFlow(transactions: Iterable<SignedTransaction>,
|
||||
recipients: Set<Party>,
|
||||
progressTracker: ProgressTracker) : FinalityFlow(transactions, recipients, progressTracker) {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
override fun lookupParties(ltx: LedgerTransaction): Set<Participant> = emptySet()
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* A flow to be used for changing a state's Notary. This is required since all input states to a transaction
|
||||
* must point to the same notary.
|
||||
*
|
||||
* This assembles the transaction for notary replacement and sends out change proposals to all participants
|
||||
* of that state. If participants agree to the proposed change, they each sign the transaction.
|
||||
* Finally, the transaction containing all signatures is sent back to each participant so they can record it and
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class NotaryChangeFlow<out T : ContractState>(
|
||||
originalState: StateAndRef<T>,
|
||||
newNotary: Party,
|
||||
progressTracker: ProgressTracker = AbstractStateReplacementFlow.Instigator.tracker())
|
||||
: AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
|
||||
|
||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||
val inputs = resolveEncumbrances(originalState)
|
||||
|
||||
val tx = NotaryChangeWireTransaction(
|
||||
inputs.map { it.ref },
|
||||
originalState.state.notary,
|
||||
modification
|
||||
)
|
||||
|
||||
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||
val signableData = SignableData(tx.id, SignatureMetadata(serviceHub.myInfo.platformVersion, Crypto.findSignatureScheme(myKey).schemeNumberID))
|
||||
val mySignature = serviceHub.keyManagementService.sign(signableData, myKey)
|
||||
val stx = SignedTransaction(tx, listOf(mySignature))
|
||||
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
}
|
||||
|
||||
/** Resolves the encumbrance state chain for the given [state] */
|
||||
private fun resolveEncumbrances(state: StateAndRef<T>): List<StateAndRef<T>> {
|
||||
val states = mutableListOf(state)
|
||||
while (states.last().state.encumbrance != null) {
|
||||
val encumbranceStateRef = StateRef(states.last().ref.txhash, states.last().state.encumbrance!!)
|
||||
val encumbranceState = serviceHub.toStateAndRef<T>(encumbranceStateRef)
|
||||
states.add(encumbranceState)
|
||||
}
|
||||
return states
|
||||
}
|
||||
}
|
@ -1,21 +1,22 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
|
||||
object NotaryFlow {
|
||||
@ -31,8 +32,8 @@ object NotaryFlow {
|
||||
*/
|
||||
@InitiatingFlow
|
||||
open class Client(private val stx: SignedTransaction,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<DigitalSignature.WithKey>>() {
|
||||
constructor(stx: SignedTransaction) : this(stx, Client.tracker())
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<TransactionSignature>>() {
|
||||
constructor(stx: SignedTransaction) : this(stx, tracker())
|
||||
|
||||
companion object {
|
||||
object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service")
|
||||
@ -45,27 +46,36 @@ object NotaryFlow {
|
||||
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): List<DigitalSignature.WithKey> {
|
||||
override fun call(): List<TransactionSignature> {
|
||||
progressTracker.currentStep = REQUESTING
|
||||
val wtx = stx.tx
|
||||
notaryParty = wtx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||
check(wtx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) {
|
||||
|
||||
notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||
check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) {
|
||||
"Input states must have the same Notary"
|
||||
}
|
||||
try {
|
||||
stx.verifySignatures(notaryParty.owningKey)
|
||||
} catch (ex: SignedTransaction.SignaturesMissingException) {
|
||||
throw NotaryException(NotaryError.SignaturesMissing(ex))
|
||||
}
|
||||
|
||||
val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
stx
|
||||
} else {
|
||||
wtx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
|
||||
try {
|
||||
if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
|
||||
} else {
|
||||
stx.verifySignaturesExcept(notaryParty.owningKey)
|
||||
}
|
||||
} catch (ex: SignatureException) {
|
||||
throw NotaryException(NotaryError.TransactionInvalid(ex))
|
||||
}
|
||||
|
||||
val response = try {
|
||||
sendAndReceiveWithRetry<List<DigitalSignature.WithKey>>(notaryParty, payload)
|
||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
subFlow(SendTransactionWithRetry(notaryParty, stx))
|
||||
receive<List<TransactionSignature>>(notaryParty)
|
||||
} else {
|
||||
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.notaryChangeTx
|
||||
} else {
|
||||
stx.tx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
|
||||
}
|
||||
sendAndReceiveWithRetry(notaryParty, tx)
|
||||
}
|
||||
} catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
e.error.conflict.verified()
|
||||
@ -74,14 +84,14 @@ object NotaryFlow {
|
||||
}
|
||||
|
||||
return response.unwrap { signatures ->
|
||||
signatures.forEach { validateSignature(it, stx.id.bytes) }
|
||||
signatures.forEach { validateSignature(it, stx.id) }
|
||||
signatures
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSignature(sig: DigitalSignature.WithKey, data: ByteArray) {
|
||||
private fun validateSignature(sig: TransactionSignature, txId: SecureHash) {
|
||||
check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" }
|
||||
sig.verify(data)
|
||||
sig.verify(txId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +124,7 @@ object NotaryFlow {
|
||||
|
||||
@Suspendable
|
||||
private fun signAndSendResponse(txId: SecureHash) {
|
||||
val signature = service.sign(txId.bytes)
|
||||
val signature = service.sign(txId)
|
||||
send(otherSide, listOf(signature))
|
||||
}
|
||||
}
|
||||
@ -137,10 +147,16 @@ sealed class NotaryError {
|
||||
/** Thrown if the time specified in the [TimeWindow] command is outside the allowed tolerance. */
|
||||
object TimeWindowInvalid : NotaryError()
|
||||
|
||||
data class TransactionInvalid(val msg: String) : NotaryError()
|
||||
data class SignaturesInvalid(val msg: String) : NotaryError()
|
||||
|
||||
data class SignaturesMissing(val cause: SignedTransaction.SignaturesMissingException) : NotaryError() {
|
||||
data class TransactionInvalid(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry]
|
||||
* instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only.
|
||||
*/
|
||||
private class SendTransactionWithRetry(otherSide: Party, stx: SignedTransaction) : SendTransactionFlow(otherSide, stx) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any) = sendAndReceiveWithRetry<FetchDataFlow.Request>(otherSide, payload)
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* The [ReceiveTransactionFlow] should be called in response to the [SendTransactionFlow].
|
||||
*
|
||||
* This flow is a combination of [receive], resolve and [SignedTransaction.verify]. This flow will receive the [SignedTransaction]
|
||||
* and perform the resolution back-and-forth required to check the dependencies and download any missing attachments.
|
||||
* The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
||||
*/
|
||||
class ReceiveTransactionFlow
|
||||
@JvmOverloads
|
||||
constructor(private val otherParty: Party, private val checkSufficientSignatures: Boolean = true) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
return receive<SignedTransaction>(otherParty).unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it, otherParty))
|
||||
it.verify(serviceHub, checkSufficientSignatures)
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The [ReceiveStateAndRefFlow] should be called in response to the [SendStateAndRefFlow].
|
||||
*
|
||||
* This flow is a combination of [receive] and resolve. This flow will receive a list of [StateAndRef]
|
||||
* and perform the resolution back-and-forth required to check the dependencies.
|
||||
* The flow will return the list of [StateAndRef] after it is resolved.
|
||||
*/
|
||||
// @JvmSuppressWildcards is used to suppress wildcards in return type when calling `subFlow(new ReceiveStateAndRef<T>(otherParty))` in java.
|
||||
class ReceiveStateAndRefFlow<out T : ContractState>(private val otherParty: Party) : FlowLogic<@JvmSuppressWildcards List<StateAndRef<T>>>() {
|
||||
@Suspendable
|
||||
override fun call(): List<StateAndRef<T>> {
|
||||
return receive<List<StateAndRef<T>>>(otherParty).unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherParty))
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
/**
|
||||
* The [SendTransactionFlow] should be used to send a transaction to another peer that wishes to verify that transaction's
|
||||
* integrity by resolving and checking the dependencies as well. The other side should invoke [ReceiveTransactionFlow] at
|
||||
* the right point in the conversation to receive the sent transaction and perform the resolution back-and-forth required
|
||||
* to check the dependencies and download any missing attachments.
|
||||
*
|
||||
* @param otherSide the target party.
|
||||
* @param stx the [SignedTransaction] being sent to the [otherSide].
|
||||
*/
|
||||
open class SendTransactionFlow(otherSide: Party, stx: SignedTransaction) : DataVendingFlow(otherSide, stx)
|
||||
|
||||
/**
|
||||
* The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify
|
||||
* the input's integrity by resolving and checking the dependencies as well. The other side should invoke [ReceiveStateAndRefFlow]
|
||||
* at the right point in the conversation to receive the input state and ref and perform the resolution back-and-forth
|
||||
* required to check the dependencies.
|
||||
*
|
||||
* @param otherSide the target party.
|
||||
* @param stateAndRefs the list of [StateAndRef] being sent to the [otherSide].
|
||||
*/
|
||||
open class SendStateAndRefFlow(otherSide: Party, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSide, stateAndRefs)
|
||||
|
||||
sealed class DataVendingFlow(val otherSide: Party, val payload: Any) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
protected open fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any) = sendAndReceive<FetchDataFlow.Request>(otherSide, payload)
|
||||
|
||||
@Suspendable
|
||||
protected open fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
||||
// User can override this method to perform custom request verification.
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
|
||||
var payload = payload
|
||||
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSide` has all the data they need
|
||||
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSide` to indicate end of
|
||||
// data request.
|
||||
while (true) {
|
||||
val dataRequest = sendPayloadAndReceiveDataRequest(otherSide, payload).unwrap { request ->
|
||||
when (request) {
|
||||
is FetchDataFlow.Request.Data -> {
|
||||
// Security TODO: Check for abnormally large or malformed data requests
|
||||
verifyDataRequest(request)
|
||||
request
|
||||
}
|
||||
FetchDataFlow.Request.End -> return null
|
||||
}
|
||||
}
|
||||
payload = when (dataRequest.dataType) {
|
||||
FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map {
|
||||
serviceHub.validatedTransactions.getTransaction(it) ?: throw FetchDataFlow.HashNotFound(it)
|
||||
}
|
||||
FetchDataFlow.DataType.ATTACHMENT -> dataRequest.hashes.map {
|
||||
serviceHub.attachments.openAttachment(it)?.open()?.readBytes() ?: throw FetchDataFlow.HashNotFound(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package net.corda.flows
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
@ -16,33 +16,37 @@ import net.corda.core.utilities.unwrap
|
||||
@InitiatingFlow
|
||||
class TransactionKeyFlow(val otherSide: Party,
|
||||
val revocationEnabled: Boolean,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymisedIdentity>>() {
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||
constructor(otherSide: Party) : this(otherSide, false, tracker())
|
||||
|
||||
companion object {
|
||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_KEY)
|
||||
fun validateIdentity(otherSide: Party, anonymousOtherSide: AnonymisedIdentity): AnonymisedIdentity {
|
||||
require(anonymousOtherSide.certificate.subject == otherSide.name)
|
||||
fun validateAndRegisterIdentity(identityService: IdentityService, otherSide: Party, anonymousOtherSide: PartyAndCertificate): PartyAndCertificate {
|
||||
require(anonymousOtherSide.name == otherSide.name)
|
||||
// Validate then store their identity so that we can prove the key in the transaction is owned by the
|
||||
// counterparty.
|
||||
identityService.verifyAndRegisterIdentity(anonymousOtherSide)
|
||||
return anonymousOtherSide
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): LinkedHashMap<Party, AnonymisedIdentity> {
|
||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||
progressTracker.currentStep = AWAITING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
|
||||
serviceHub.identityService.registerAnonymousIdentity(legalIdentityAnonymous.identity, serviceHub.myInfo.legalIdentity, legalIdentityAnonymous.certPath)
|
||||
|
||||
// Special case that if we're both parties, a single identity is generated
|
||||
val identities = LinkedHashMap<Party, AnonymisedIdentity>()
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
if (otherSide == serviceHub.myInfo.legalIdentity) {
|
||||
identities.put(otherSide, legalIdentityAnonymous)
|
||||
identities.put(otherSide, legalIdentityAnonymous.party.anonymise())
|
||||
} else {
|
||||
val otherSideAnonymous = sendAndReceive<AnonymisedIdentity>(otherSide, legalIdentityAnonymous).unwrap { validateIdentity(otherSide, it) }
|
||||
identities.put(serviceHub.myInfo.legalIdentity, legalIdentityAnonymous)
|
||||
identities.put(otherSide, otherSideAnonymous)
|
||||
val anonymousOtherSide = sendAndReceive<PartyAndCertificate>(otherSide, legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSide, confidentialIdentity)
|
||||
}
|
||||
identities.put(serviceHub.myInfo.legalIdentity, legalIdentityAnonymous.party.anonymise())
|
||||
identities.put(otherSide, anonymousOtherSide.party.anonymise())
|
||||
}
|
||||
return identities
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.flows
|
||||
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
|
||||
@CordaSerializable
|
||||
data class AnonymisedIdentity(
|
||||
val certPath: CertPath,
|
||||
val certificate: X509CertificateHolder,
|
||||
val identity: AnonymousParty) {
|
||||
constructor(certPath: CertPath, certificate: X509CertificateHolder, identity: PublicKey)
|
||||
: this(certPath, certificate, AnonymousParty(identity))
|
||||
}
|
@ -2,6 +2,7 @@ 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
|
||||
import java.security.PublicKey
|
||||
@ -13,7 +14,7 @@ import java.security.PublicKey
|
||||
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.toBase58String()} <Anonymous>"
|
||||
override fun toString() = "${owningKey.toStringShort()} <Anonymous>"
|
||||
|
||||
override fun nameOrNull(): X500Name? = null
|
||||
|
||||
|
@ -30,5 +30,6 @@ class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey)
|
||||
override fun toString() = name.toString()
|
||||
override fun nameOrNull(): X500Name? = name
|
||||
|
||||
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
}
|
||||
|
@ -4,12 +4,14 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
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.
|
||||
* 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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class PartyAndCertificate(val party: Party,
|
||||
@ -30,4 +32,18 @@ data class PartyAndCertificate(val 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.
|
||||
*/
|
||||
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 validator = CertPathValidator.getInstance("PKIX")
|
||||
validatorParameters.isRevocationEnabled = false
|
||||
return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.codePointsString
|
||||
package net.corda.core.internal
|
||||
|
||||
/**
|
||||
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
|
||||
@ -11,8 +9,8 @@ object Emoji {
|
||||
// Check for that here. DemoBench sets TERM_PROGRAM appropriately.
|
||||
val hasEmojiTerminal by lazy {
|
||||
System.getenv("CORDA_FORCE_EMOJI") != null ||
|
||||
System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app") ||
|
||||
(System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o")
|
||||
System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app") ||
|
||||
(System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o")
|
||||
}
|
||||
|
||||
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
|
||||
@ -29,6 +27,9 @@ object Emoji {
|
||||
@JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA)
|
||||
@JvmStatic val CODE_SLEEPING_FACE: String = codePointsString(0x1F634)
|
||||
@JvmStatic val CODE_LIGHTBULB: String = codePointsString(0x1F4A1)
|
||||
@JvmStatic val CODE_FREE: String = codePointsString(0x1F193)
|
||||
@JvmStatic val CODE_SOON: String = codePointsString(0x1F51C)
|
||||
|
||||
|
||||
/**
|
||||
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
|
||||
@ -46,6 +47,8 @@ object Emoji {
|
||||
val books: String get() = if (emojiMode.get() != null) "$CODE_BOOKS " else ""
|
||||
val sleepingFace: String get() = if (emojiMode.get() != null) "$CODE_SLEEPING_FACE " else ""
|
||||
val lightBulb: String get() = if (emojiMode.get() != null) "$CODE_LIGHTBULB " else ""
|
||||
val free: String get() = if (emojiMode.get() != null) "$CODE_FREE " else ""
|
||||
val soon: String get() = if (emojiMode.get() != null) "$CODE_SOON " else ""
|
||||
|
||||
// These have old/non-emoji symbols with better platform support.
|
||||
val greenTick: String get() = if (emojiMode.get() != null) "$CODE_GREEN_TICK " else "✓"
|
||||
@ -79,4 +82,9 @@ object Emoji {
|
||||
}
|
||||
}
|
||||
|
||||
private fun codePointsString(vararg codePoints: Int): String {
|
||||
val builder = StringBuilder()
|
||||
codePoints.forEach { builder.append(Character.toChars(it)) }
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
179
core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt
Normal file
179
core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt
Normal file
@ -0,0 +1,179 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.AbstractAttachment
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch
|
||||
import net.corda.core.internal.FetchDataFlow.HashNotFound
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationToken
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* An abstract flow for fetching typed data from a remote peer.
|
||||
*
|
||||
* Given a set of hashes (IDs), either loads them from local disk or asks the remote peer to provide them.
|
||||
*
|
||||
* A malicious response in which the data provided by the remote peer does not hash to the requested hash results in
|
||||
* [DownloadedVsRequestedDataMismatch] being thrown. If the remote peer doesn't have an entry, it results in a
|
||||
* [HashNotFound] exception being thrown.
|
||||
*
|
||||
* By default this class does not insert data into any local database, if you want to do that after missing items were
|
||||
* fetched then override [maybeWriteToDisk]. You *must* override [load]. If the wire type is not the same as the
|
||||
* ultimate type, you must also override [convert].
|
||||
*
|
||||
* @param T The ultimate type of the data being fetched.
|
||||
* @param W The wire type of the data being fetched, for when it isn't the same as the ultimate type.
|
||||
*/
|
||||
sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
protected val requests: Set<SecureHash>,
|
||||
protected val otherSide: Party,
|
||||
protected val dataType: DataType) : FlowLogic<FetchDataFlow.Result<T>>() {
|
||||
|
||||
@CordaSerializable
|
||||
class DownloadedVsRequestedDataMismatch(val requested: SecureHash, val got: SecureHash) : IllegalArgumentException()
|
||||
|
||||
@CordaSerializable
|
||||
class DownloadedVsRequestedSizeMismatch(val requested: Int, val got: Int) : IllegalArgumentException()
|
||||
|
||||
class HashNotFound(val requested: SecureHash) : FlowException()
|
||||
|
||||
@CordaSerializable
|
||||
data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>)
|
||||
|
||||
@CordaSerializable
|
||||
sealed class Request {
|
||||
data class Data(val hashes: NonEmptySet<SecureHash>, val dataType: DataType) : Request()
|
||||
object End : Request()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class DataType {
|
||||
TRANSACTION, ATTACHMENT
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(HashNotFound::class)
|
||||
override fun call(): Result<T> {
|
||||
// Load the items we have from disk and figure out which we're missing.
|
||||
val (fromDisk, toFetch) = loadWhatWeHave()
|
||||
|
||||
return if (toFetch.isEmpty()) {
|
||||
Result(fromDisk, emptyList())
|
||||
} else {
|
||||
logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSide.name}")
|
||||
|
||||
// TODO: Support "large message" response streaming so response sizes are not limited by RAM.
|
||||
// We can then switch to requesting items in large batches to minimise the latency penalty.
|
||||
// This is blocked by bugs ARTEMIS-1278 and ARTEMIS-1279. For now we limit attachments and txns to 10mb each
|
||||
// and don't request items in batch, which is a performance loss, but works around the issue. We have
|
||||
// configured Artemis to not fragment messages up to 10mb so we can send 10mb messages without problems.
|
||||
// Above that, we start losing authentication data on the message fragments and take exceptions in the
|
||||
// network layer.
|
||||
val maybeItems = ArrayList<W>(toFetch.size)
|
||||
for (hash in toFetch) {
|
||||
// We skip the validation here (with unwrap { it }) because we will do it below in validateFetchResponse.
|
||||
// The only thing checked is the object type. It is a protocol violation to send results out of order.
|
||||
maybeItems += sendAndReceive<List<W>>(otherSide, Request.Data(NonEmptySet.of(hash), dataType)).unwrap { it }
|
||||
}
|
||||
// Check for a buggy/malicious peer answering with something that we didn't ask for.
|
||||
val downloaded = validateFetchResponse(UntrustworthyData(maybeItems), toFetch)
|
||||
logger.info("Fetched ${downloaded.size} elements from ${otherSide.name}")
|
||||
maybeWriteToDisk(downloaded)
|
||||
Result(fromDisk, downloaded)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun maybeWriteToDisk(downloaded: List<T>) {
|
||||
// Do nothing by default.
|
||||
}
|
||||
|
||||
private fun loadWhatWeHave(): Pair<List<T>, List<SecureHash>> {
|
||||
val fromDisk = ArrayList<T>()
|
||||
val toFetch = ArrayList<SecureHash>()
|
||||
for (txid in requests) {
|
||||
val stx = load(txid)
|
||||
if (stx == null)
|
||||
toFetch += txid
|
||||
else
|
||||
fromDisk += stx
|
||||
}
|
||||
return Pair(fromDisk, toFetch)
|
||||
}
|
||||
|
||||
protected abstract fun load(txid: SecureHash): T?
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
protected open fun convert(wire: W): T = wire as T
|
||||
|
||||
private fun validateFetchResponse(maybeItems: UntrustworthyData<ArrayList<W>>,
|
||||
requests: List<SecureHash>): List<T> {
|
||||
return maybeItems.unwrap { response ->
|
||||
if (response.size != requests.size)
|
||||
throw DownloadedVsRequestedSizeMismatch(requests.size, response.size)
|
||||
val answers = response.map { convert(it) }
|
||||
// Check transactions actually hash to what we requested, if this fails the remote node
|
||||
// is a malicious flow violator or buggy.
|
||||
for ((index, item) in answers.withIndex()) {
|
||||
if (item.id != requests[index])
|
||||
throw DownloadedVsRequestedDataMismatch(requests[index], item.id)
|
||||
}
|
||||
answers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a set of hashes either loads from from local storage or requests them from the other peer. Downloaded
|
||||
* attachments are saved to local storage automatically.
|
||||
*/
|
||||
class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||
otherSide: Party) : FetchDataFlow<Attachment, ByteArray>(requests, otherSide, DataType.ATTACHMENT) {
|
||||
|
||||
override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid)
|
||||
|
||||
override fun convert(wire: ByteArray): Attachment = FetchedAttachment({ wire })
|
||||
|
||||
override fun maybeWriteToDisk(downloaded: List<Attachment>) {
|
||||
for (attachment in downloaded) {
|
||||
serviceHub.attachments.importAttachment(attachment.open())
|
||||
}
|
||||
}
|
||||
|
||||
private class FetchedAttachment(dataLoader: () -> ByteArray) : AbstractAttachment(dataLoader), SerializeAsToken {
|
||||
override val id: SecureHash by lazy { attachmentData.sha256() }
|
||||
|
||||
private class Token(private val id: SecureHash) : SerializationToken {
|
||||
override fun fromToken(context: SerializeAsTokenContext) = FetchedAttachment(context.attachmentDataLoader(id))
|
||||
}
|
||||
|
||||
override fun toToken(context: SerializeAsTokenContext) = Token(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of tx hashes (IDs), either loads them from local disk or asks the remote peer to provide them.
|
||||
*
|
||||
* A malicious response in which the data provided by the remote peer does not hash to the requested hash results in
|
||||
* [FetchDataFlow.DownloadedVsRequestedDataMismatch] being thrown. If the remote peer doesn't have an entry, it
|
||||
* results in a [FetchDataFlow.HashNotFound] exception. Note that returned transactions are not inserted into
|
||||
* the database, because it's up to the caller to actually verify the transactions are valid.
|
||||
*/
|
||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: Party) :
|
||||
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {
|
||||
|
||||
override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid)
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
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.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
@ -14,6 +16,9 @@ import org.slf4j.Logger
|
||||
|
||||
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
||||
interface FlowStateMachine<R> {
|
||||
@Suspendable
|
||||
fun getFlowContext(otherParty: Party, sessionFlow: FlowLogic<*>): FlowContext
|
||||
|
||||
@Suspendable
|
||||
fun <T : Any> sendAndReceive(receiveType: Class<T>,
|
||||
otherParty: Party,
|
||||
@ -25,18 +30,37 @@ interface FlowStateMachine<R> {
|
||||
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
|
||||
|
||||
@Suspendable
|
||||
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
|
||||
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>): Unit
|
||||
|
||||
@Suspendable
|
||||
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction
|
||||
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String,String>)
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>): Unit
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>)
|
||||
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?
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
val id: StateMachineRunId
|
||||
val resultFuture: ListenableFuture<R>
|
||||
val resultFuture: CordaFuture<R>
|
||||
val flowInitiator: FlowInitiator
|
||||
}
|
||||
|
258
core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt
Normal file
258
core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt
Normal file
@ -0,0 +1,258 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.*
|
||||
import java.lang.reflect.Field
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.*
|
||||
import java.util.Spliterator.*
|
||||
import java.util.stream.IntStream
|
||||
import java.util.stream.Stream
|
||||
import java.util.stream.StreamSupport
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this
|
||||
fun Throwable.getStackTraceAsString() = StringWriter().also { printStackTrace(PrintWriter(it)) }.toString()
|
||||
|
||||
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
|
||||
|
||||
operator fun Duration.div(divider: Long): Duration = dividedBy(divider)
|
||||
operator fun Duration.times(multiplicand: Long): Duration = multipliedBy(multiplicand)
|
||||
|
||||
/**
|
||||
* Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform
|
||||
* separator problems.
|
||||
*/
|
||||
operator fun Path.div(other: String): Path = resolve(other)
|
||||
operator fun String.div(other: String): Path = Paths.get(this) / other
|
||||
|
||||
/**
|
||||
* Returns the single element matching the given [predicate], or `null` if the collection is empty, or throws exception
|
||||
* if more than one element was found.
|
||||
*/
|
||||
inline fun <T> Iterable<T>.noneOrSingle(predicate: (T) -> Boolean): T? {
|
||||
val iterator = iterator()
|
||||
for (single in iterator) {
|
||||
if (predicate(single)) {
|
||||
while (iterator.hasNext()) {
|
||||
if (predicate(iterator.next())) throw IllegalArgumentException("Collection contains more than one matching element.")
|
||||
}
|
||||
return single
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single element, or `null` if the list is empty, or throws an exception if it has more than one element.
|
||||
*/
|
||||
fun <T> List<T>.noneOrSingle(): T? {
|
||||
return when (size) {
|
||||
0 -> null
|
||||
1 -> this[0]
|
||||
else -> throw IllegalArgumentException("List has more than one element.")
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a random element in the list, or `null` if empty */
|
||||
fun <T> List<T>.randomOrNull(): T? {
|
||||
return when (size) {
|
||||
0 -> null
|
||||
1 -> this[0]
|
||||
else -> this[(Math.random() * size).toInt()]
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
val i = indexOf(item)
|
||||
require(i != -1)
|
||||
return i
|
||||
}
|
||||
|
||||
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
||||
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
|
||||
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
|
||||
fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path {
|
||||
require(targetDir.isDirectory()) { "$targetDir is not a directory" }
|
||||
val targetFile = targetDir.resolve(fileName)
|
||||
Files.copy(this, targetFile, *options)
|
||||
return targetFile
|
||||
}
|
||||
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
|
||||
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
|
||||
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
|
||||
inline val Path.size: Long get() = Files.size(this)
|
||||
inline fun <R> Path.list(block: (Stream<Path>) -> R): R = Files.list(this).use(block)
|
||||
fun Path.deleteIfExists(): Boolean = Files.deleteIfExists(this)
|
||||
fun Path.readAll(): ByteArray = Files.readAllBytes(this)
|
||||
inline fun <R> Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = Files.newInputStream(this, *options).use(block)
|
||||
inline fun Path.write(createDirs: Boolean = false, vararg options: OpenOption = emptyArray(), block: (OutputStream) -> Unit) {
|
||||
if (createDirs) {
|
||||
normalize().parent?.createDirectories()
|
||||
}
|
||||
Files.newOutputStream(this, *options).use(block)
|
||||
}
|
||||
|
||||
inline fun <R> Path.readLines(charset: Charset = UTF_8, block: (Stream<String>) -> R): R = Files.lines(this, charset).use(block)
|
||||
fun Path.readAllLines(charset: Charset = UTF_8): List<String> = Files.readAllLines(this, charset)
|
||||
fun Path.writeLines(lines: Iterable<CharSequence>, charset: Charset = UTF_8, vararg options: OpenOption): Path = Files.write(this, lines, charset, *options)
|
||||
|
||||
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
|
||||
|
||||
fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + "…"
|
||||
|
||||
/** Return the sum of an Iterable of [BigDecimal]s. */
|
||||
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
||||
|
||||
/**
|
||||
* Returns an Observable that buffers events until subscribed.
|
||||
* @see UnicastSubject
|
||||
*/
|
||||
fun <T> Observable<T>.bufferUntilSubscribed(): Observable<T> {
|
||||
val subject = UnicastSubject.create<T>()
|
||||
val subscription = subscribe(subject)
|
||||
return subject.doOnUnsubscribe { subscription.unsubscribe() }
|
||||
}
|
||||
|
||||
/** Copy an [Observer] to multiple other [Observer]s. */
|
||||
fun <T> Observer<T>.tee(vararg teeTo: Observer<T>): Observer<T> {
|
||||
val subject = PublishSubject.create<T>()
|
||||
subject.subscribe(this)
|
||||
teeTo.forEach { subject.subscribe(it) }
|
||||
return subject
|
||||
}
|
||||
|
||||
/** Executes the given code block and returns a [Duration] of how long it took to execute in nanosecond precision. */
|
||||
inline fun elapsedTime(block: () -> Unit): Duration {
|
||||
val start = System.nanoTime()
|
||||
block()
|
||||
val end = System.nanoTime()
|
||||
return Duration.ofNanos(end - start)
|
||||
}
|
||||
|
||||
|
||||
fun <T> Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body)
|
||||
|
||||
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
||||
// returns in the IRSSimulationTest. If not, commit the inline back.
|
||||
fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
|
||||
// Use nanoTime as it's monotonic.
|
||||
val now = System.nanoTime()
|
||||
try {
|
||||
return body()
|
||||
} finally {
|
||||
val elapsed = Duration.ofNanos(System.nanoTime() - now).toMillis()
|
||||
if (logger != null)
|
||||
logger.info("$label took $elapsed msec")
|
||||
else
|
||||
println("$label took $elapsed msec")
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
||||
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
|
||||
val bytes = toByteArray()
|
||||
return InputStreamAndHash(ByteArrayInputStream(bytes), bytes.sha256())
|
||||
}
|
||||
|
||||
data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256) {
|
||||
companion object {
|
||||
/**
|
||||
* Get a valid InputStream from an in-memory zip as required for some tests. The zip consists of a single file
|
||||
* called "z" that contains the given content byte repeated the given number of times.
|
||||
* Note that a slightly bigger than numOfExpectedBytes size is expected.
|
||||
*/
|
||||
fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte): InputStreamAndHash {
|
||||
require(numOfExpectedBytes > 0)
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use { zos ->
|
||||
val arraySize = 1024
|
||||
val bytes = ByteArray(arraySize) { content }
|
||||
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
zos.putNextEntry(ZipEntry("z"))
|
||||
for (i in 0 until n) {
|
||||
zos.write(bytes, 0, arraySize)
|
||||
}
|
||||
zos.closeEntry()
|
||||
}
|
||||
return baos.toInputStreamAndHash()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun IntIterator.toJavaIterator(): PrimitiveIterator.OfInt {
|
||||
return object : PrimitiveIterator.OfInt {
|
||||
override fun nextInt() = this@toJavaIterator.nextInt()
|
||||
override fun hasNext() = this@toJavaIterator.hasNext()
|
||||
override fun remove() = throw UnsupportedOperationException("remove")
|
||||
}
|
||||
}
|
||||
|
||||
private fun IntProgression.toSpliterator(): Spliterator.OfInt {
|
||||
val spliterator = Spliterators.spliterator(
|
||||
iterator().toJavaIterator(),
|
||||
(1 + (last - first) / step).toLong(),
|
||||
SUBSIZED or IMMUTABLE or NONNULL or SIZED or ORDERED or SORTED or DISTINCT
|
||||
)
|
||||
return if (step > 0) spliterator else object : Spliterator.OfInt by spliterator {
|
||||
override fun getComparator() = Comparator.reverseOrder<Int>()
|
||||
}
|
||||
}
|
||||
|
||||
fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel)
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable).
|
||||
inline fun <reified T> Stream<out T>.toTypedArray() = toArray { size -> arrayOfNulls<T>(size) } as Array<T>
|
||||
|
||||
fun <T> Class<T>.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null
|
||||
|
||||
/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */
|
||||
fun <T> Class<*>.staticField(name: String): DeclaredField<T> = DeclaredField(this, name, null)
|
||||
/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */
|
||||
fun <T> KClass<*>.staticField(name: String): DeclaredField<T> = DeclaredField(java, name, null)
|
||||
/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */
|
||||
fun <T> Any.declaredField(name: String): DeclaredField<T> = DeclaredField(javaClass, name, this)
|
||||
/**
|
||||
* Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared
|
||||
* in its superclass [clazz].
|
||||
*/
|
||||
fun <T> Any.declaredField(clazz: KClass<*>, name: String): DeclaredField<T> = DeclaredField(clazz.java, name, this)
|
||||
|
||||
/**
|
||||
* A simple wrapper around a [Field] object providing type safe read and write access using [value], ignoring the field's
|
||||
* visibility.
|
||||
*/
|
||||
class DeclaredField<T>(clazz: Class<*>, name: String, private val receiver: Any?) {
|
||||
private val javaField = clazz.getDeclaredField(name).apply { isAccessible = true }
|
||||
var value: T
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
get() = javaField.get(receiver) as T
|
||||
set(value) = javaField.set(receiver, value)
|
||||
}
|
||||
|
||||
/** The annotated object would have a more restricted visibility were it not needed in tests. */
|
||||
@Target(AnnotationTarget.CLASS,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPEALIAS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@MustBeDocumented
|
||||
annotation class VisibleForTesting
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user